# 2.4.1 pwntools

* [安装](#安装)
* [模块简介](#模块简介)
* [使用 Pwntools](#使用-pwntools)
* [Pwntools 在 CTF 中的运用](#pwntools-在-ctf-中的运用)
* [参考资料](#参考资料)

Pwntools 是一个 CTF 框架和漏洞利用开发库，用 Python 开发，由 rapid 设计，旨在让使用者简单快速的编写 exp 脚本。包含了本地执行、远程连接读写、shellcode 生成、ROP 链的构建、ELF 解析、符号泄露众多强大功能。

## 安装

1. 安装binutils：

   ```shell
   git clone https://github.com/Gallopsled/pwntools-binutils
   sudo apt-get install software-properties-common
   sudo apt-add-repository ppa:pwntools/binutils
   sudo apt-get update
   sudo apt-get install binutils-arm-linux-gnu
   ```
2. 安装capstone：

   ```shell
   git clone https://github.com/aquynh/capstone
   cd capstone
   make
   sudo make install
   ```
3. 安装pwntools:

   ```shell
   sudo apt-get install libssl-dev
   sudo pip install pwntools
   ```

如果你在使用 Arch Linux，则可以通过 AUR 直接安装，这个包目前是由我维护的，如果有什么问题，欢迎与我交流：

```
$ yaourt -S python2-pwntools

或者

$ yaourt -S python2-pwntools-git
```

但是由于 Arch 没有 PPA 源，如果想要支持更多的体系结构（如 arm, aarch64 等），只能手动编译安装相应的 binutils，使用下面的脚本，注意将变量 `V` 和 `ARCH` 换成你需要的。[binutils](https://ftp.gnu.org/gnu/binutils/)[源码](https://github.com/firmianay/CTF-All-In-One/blob/master/src/others/2.4.1_pwntools/binutils.sh)

```bash
#!/usr/bin/env bash

V = 2.29   # binutils version
ARCH = arm # target architecture

cd /tmp
wget -nc https://ftp.gnu.org/gnu/binutils/binutils-$V.tar.xz
wget -nc https://ftp.gnu.org/gnu/binutils/binutils-$V.tar.xz.sig

# gpg --keyserver keys.gnupg.net --recv-keys C3126D3B4AE55E93
# gpg --verify binutils-$V.tar.xz.sig

tar xf binutils-$V.tar.xz

mkdir binutils-build
cd binutils-build

export AR=ar
export AS=as

../binutils-$V/configure \
    --prefix=/usr/local \
    --target=$ARCH-unknown-linux-gnu \
    --disable-static \
    --disable-multilib \
    --disable-werror \
    --disable-nls

make
sudo make install
```

测试安装是否成功：

```python
>>> from pwn import *
>>> asm('nop')
'\x90'
>>> asm('nop', arch='arm')
'\x00\xf0 \xe3'
```

## 模块简介

Pwntools 分为两个模块，一个是 `pwn`，简单地使用 `from pwn import *` 即可将所有子模块和一些常用的系统库导入到当前命名空间中，是专门针对 CTF 比赛的；而另一个模块是 `pwnlib`，它更推荐你仅仅导入需要的子模块，常用于基于 pwntools 的开发。

下面是 pwnlib 的一些子模块（常用模块和函数加粗显示）：

* `adb`：安卓调试桥
* `args`：命令行魔法参数
* **`asm`**：汇编和反汇编，支持 i386/i686/amd64/thumb 等
* `constants`：对不同架构和操作系统的常量的快速访问
* `config`：配置文件
* `context`：设置运行时变量
* **`dynelf`**：用于远程函数泄露
* `encoders`：对 shellcode 进行编码
* **`elf`**：用于操作 ELF 可执行文件和库
* `flag`：提交 flag 到服务器
* **`fmtstr`**：格式化字符串利用工具
* **`gdb`**：与 gdb 配合使用
* `libcdb`：libc 数据库
* `log`：日志记录
* **`memleak`**：用于内存泄露
* **`rop`**：ROP 利用模块，包括 rop 和 srop
* `runner`：运行 shellcode
* **`shellcraft`**：shellcode 生成器
* `term`：终端处理
* `timeout`：超时处理
* **`tubes`**：能与 sockets, processes, ssh 等进行连接
* `ui`：与用户交互
* `useragents`：useragent 字符串数据库
* **`util`**：一些实用小工具

## 使用 Pwntools

下面我们对常用模块和函数做详细的介绍。

### tubes

在一次漏洞利用中，首先当然要与二进制文件或者目标服务器进行交互，这就要用到 tubes 模块。

主要函数在 `pwnlib.tubes.tube` 中实现，子模块只实现某管道特殊的地方。四种管道和相对应的子模块如下：

* `pwnlib.tubes.process`：进程
  * `>>> p = process('/bin/sh')`
* `pwnlib.tubes.serialtube`：串口
* `pwnlib.tubes.sock`：套接字
  * `>>> r = remote('127.0.0.1', 1080)`
  * `>>> l = listen(1080)`
* `pwnlib.tubes.ssh`：SSH
  * `>>> s = ssh(host='example.com`, user='name', password='passwd')\`

`pwnlib.tubes.tube` 中的主要函数：

* `interactive()`：可同时读写管道，相当于回到 shell 模式进行交互，在取得 shell 之后调用
* `recv(numb=1096, timeout=default)`：接收指定字节数的数据
* `recvall()`：接收数据直到 EOF
* `recvline(keepends=True)`：接收一行，可选择是否保留行尾的&#x20;
* `recvrepeat(timeout=default)`：接收数据直到 EOF 或 timeout
* `recvuntil(delims, timeout=default)`：接收数据直到 delims 出现
* `send(data)`：发送数据
* `sendline(data)`：发送一行，默认在行尾加&#x20;
* `close()`：关闭管道

下面是一个例子，先使用 listen 开启一个本地的监听端口，然后使用 remote 开启一个套接字管道与之交互：

```
>>> from pwn import *
>>> l = listen()
[x] Trying to bind to 0.0.0.0 on port 0
[x] Trying to bind to 0.0.0.0 on port 0: Trying 0.0.0.0
[+] Trying to bind to 0.0.0.0 on port 0: Done
[x] Waiting for connections on 0.0.0.0:46147
>>> r = remote('localhost', l.lport)
[x] Opening connection to localhost on port 46147
[x] Opening connection to localhost on port 46147: Trying ::1
[x] Opening connection to localhost on port 46147: Trying 127.0.0.1
[+] Opening connection to localhost on port 46147: Done
>>> [+] Waiting for connections on 0.0.0.0:46147: Got connection from 127.0.0.1 on port 38684

>>> c = l.wait_for_connection()
>>> r.send('hello\n')
>>> c.recv()
'hello\n'
>>> r.send('hello\n')
>>> c.recvline()
'hello\n'
>>> r.sendline('hello')
>>> c.recv()
'hello\n'
>>> r.sendline('hello')
>>> c.recvline()
'hello\n'
>>> r.sendline('hello')
>>> c.recvline(keepends=False)
'hello'
>>> r.send('hello world')
>>> c.recvuntil('hello')
'hello'
>>> c.recv()
' world'
>>> c.close()
[*] Closed connection to 127.0.0.1 port 38684
>>> r.close()
[*] Closed connection to localhost port 46147
```

下面是一个与进程交互的例子：

```
>>> p = process('/bin/sh')
[x] Starting local process '/bin/sh'
[+] Starting local process '/bin/sh': pid 26481
>>> p.sendline('sleep 3; echo hello world;')
>>> p.recvline(timeout=1)
'hello world\n'
>>> p.sendline('sleep 3; echo hello world;')
>>> p.recvline(timeout=1)
''
>>> p.recvline(timeout=5)
'hello world\n'
>>> p.interactive()
[*] Switching to interactive mode
whoami
firmy
^C[*] Interrupted
>>> p.close()
[*] Stopped process '/bin/sh' (pid 26481)
```

### shellcraft

使用 shellcraft 模块可以生成对应架构和 shellcode 代码，直接使用链式调用的方法就可以得到，首先指定体系结构，再指定操作系统：

```
>>> print shellcraft.i386.nop().strip('\n')
    nop
>>> print shellcraft.i386.linux.sh()
    /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push '/bin///sh\x00' */
    push 0x68
    push 0x732f2f2f
    push 0x6e69622f
    mov ebx, esp
    /* push argument array ['sh\x00'] */
    /* push 'sh\x00\x00' */
    push 0x1010101
    xor dword ptr [esp], 0x1016972
    xor ecx, ecx
    push ecx /* null terminate */
    push 4
    pop ecx
    add ecx, esp
    push ecx /* 'sh\x00' */
    mov ecx, esp
    xor edx, edx
    /* call execve() */
    push SYS_execve /* 0xb */
    pop eax
    int 0x80
```

### asm

该模块用于汇编和反汇编代码。

体系结构，端序和字长需要在 `asm()` 和 `disasm()` 中设置，但为了避免重复，运行时变量最好使用 `pwnlib.context` 来设置。

汇编：(`pwnlib.asm.asm`)

```
>>> asm('nop')
'\x90'
>>> asm(shellcraft.nop())
'\x90'
>>> asm('nop', arch='arm')
'\x00\xf0 \xe3'
>>> context.arch = 'arm'
>>> context.os = 'linux'
>>> context.endian = 'little'
>>> context.word_size = 32
>>> context
ContextType(arch = 'arm', bits = 32, endian = 'little', os = 'linux')
>>> asm('nop')
'\x00\xf0 \xe3'
```

```
>>> asm('mov eax, 1')
'\xb8\x01\x00\x00\x00'
>>> asm('mov eax, 1').encode('hex')
'b801000000'
```

请注意，这里我们生成了 i386 和 arm 两种不同体系结构的 `nop`，当你使用不同与本机平台的汇编时，需要安装该平台的 binutils，方法在上面已经介绍过了。

反汇编：(`pwnlib.asm.disasm`)

```
>>> print disasm('\xb8\x01\x00\x00\x00')
   0:   b8 01 00 00 00          mov    eax,0x1
>>> print disasm('6a0258cd80ebf9'.decode('hex'))
   0:   6a 02                   push   0x2
   2:   58                      pop    eax
   3:   cd 80                   int    0x80
   5:   eb f9                   jmp    0x0
```

构建具有指定二进制数据的 ELF 文件：(`pwnlib.asm.make_elf`)

```
>>> context.clear(arch='amd64')
>>> context
ContextType(arch = 'amd64', bits = 64, endian = 'little')
>>> bin_sh = asm(shellcraft.amd64.linux.sh())
>>> bin_sh
'jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05'
>>> filename = make_elf(bin_sh, extract=False)
>>> filename
'/tmp/pwn-asm-V4GWGN/step3-elf'
>>> p = process(filename)
[x] Starting local process '/tmp/pwn-asm-V4GWGN/step3-elf'
[+] Starting local process '/tmp/pwn-asm-V4GWGN/step3-elf': pid 28323
>>> p.sendline('echo hello')
>>> p.recv()
'hello\n'
```

这里我们生成了 amd64，即 64 位 `/bin/sh` 的 shellcode，配合上 asm 函数，即可通过 `make_elf` 得到 ELF 文件。

另一个函数 `pwnlib.asm.make_elf_from_assembly` 允许你构建具有指定汇编代码的 ELF 文件：

```
>>> asm_sh = shellcraft.amd64.linux.sh()
>>> print asm_sh
    /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push '/bin///sh\x00' */
    push 0x68
    mov rax, 0x732f2f2f6e69622f
    push rax
    mov rdi, rsp
    /* push argument array ['sh\x00'] */
    /* push 'sh\x00' */
    push 0x1010101 ^ 0x6873
    xor dword ptr [rsp], 0x1010101
    xor esi, esi /* 0 */
    push rsi /* null terminate */
    push 8
    pop rsi
    add rsi, rsp
    push rsi /* 'sh\x00' */
    mov rsi, rsp
    xor edx, edx /* 0 */
    /* call execve() */
    push SYS_execve /* 0x3b */
    pop rax
    syscall

>>> filename = make_elf_from_assembly(asm_sh)
>>> filename
'/tmp/pwn-asm-ApZ4_p/step3'
>>> p = process(filename)
[x] Starting local process '/tmp/pwn-asm-ApZ4_p/step3'
[+] Starting local process '/tmp/pwn-asm-ApZ4_p/step3': pid 28429
>>> p.sendline('echo hello')
>>> p.recv()
'hello\n'
```

与上一个函数不同的是，`make_elf_from_assembly` 直接从汇编生成 ELF 文件，并且保留了所有的符号，例如标签和局部变量等。

### elf

该模块用于 ELF 二进制文件的操作，包括符号查找、虚拟内存、文件偏移，以及修改和保存二进制文件等功能。(`pwnlib.elf.elf.ELF`)

```
>>> e = ELF('/bin/cat')
[*] '/bin/cat'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
>>> print hex(e.address)
0x400000
>>> print hex(e.symbols['write'])
0x401680
>>> print hex(e.got['write'])
0x60b070
>>> print hex(e.plt['write'])
0x401680
```

上面的代码分别获得了 ELF 文件装载的基地址、函数地址、GOT 表地址和 PLT 表地址。

我们常常用它打开一个 libc.so，从而得到 system 函数的位置，这在 CTF 中是非常有用的：

```
>>> e = ELF('/usr/lib/libc.so.6')
[*] '/usr/lib/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
>>> print hex(e.symbols['system'])
0x42010
```

我们甚至可以修改 ELF 文件的代码：

```
>>> e = ELF('/bin/cat')
>>> e.read(e.address+1, 3)
'ELF'
>>> e.asm(e.address, 'ret')
>>> e.save('/tmp/quiet-cat')
>>> disasm(file('/tmp/quiet-cat','rb').read(1))
'   0:   c3                      ret'
```

下面是一些常用函数：

* `asm(address, assembly)`：汇编指定指令并插入到 ELF 的指定地址处，需要使用 ELF.save() 保存
* `bss(offset)`：返回 `.bss` 段加上 `offset` 后的地址
* `checksec()`：打印出文件使用的安全保护
* `disable_nx()`：关闭 NX
* `disasm(address, n_bytes)`：返回对指定虚拟地址进行反汇编后的字符串
* `offset_to_vaddr(offset)`：将指定偏移转换为虚拟地址
* `vaddr_to_offset(address)`：将指定虚拟地址转换为文件偏移
* `read(address, count)`：从指定虚拟地址读取 `count` 个字节的数据
* `write(address, data)`：在指定虚拟地址处写入 `data`
* `section(name)`：获取 `name` 段的数据
* `debug()`：使用 `gdb.debug()` 进行调试

最后还要注意一下 `pwnlib.elf.corefile`，它用于处理核心转储文件（Core Dump），当我们在写利用代码时，核心转储文件是非常有用的，关于它更详细的内容已经在前面 Linux基础一章中讲过，这里我们还是使用那一章中的示例代码，但使用 pwntools 来操作。

```
>>> core = Corefile('/tmp/core-a.out-30555-1507796886')
[x] Parsing corefile...
[*] '/tmp/core-a.out-30555-1507796886'
    Arch:      i386-32-little
    EIP:       0x565cd57b
    ESP:       0x4141413d
    Exe:       '/home/firmy/a.out' (0x565cd000)
    Fault:     0x4141413d
[+] Parsing corefile...: Done
>>> core.registers
{'xds': 43, 'eip': 1448924539, 'xss': 43, 'esp': 1094795581, 'xgs': 99, 'edi': 0, 'orig_eax': 4294967295, 'xcs': 35, 'eax': 1, 'ebp': 1094795585, 'xes': 43, 'eflags': 66182, 'edx': 4151195744, 'ebx': 1094795585, 'xfs': 0, 'esi': 4151189032, 'ecx': 1094795585}
>>> print core.maps
565cd000-565ce000 r-xp 1000 /home/firmy/a.out
565ce000-565cf000 r--p 1000 /home/firmy/a.out
565cf000-565d0000 rw-p 1000 /home/firmy/a.out
57b3c000-57b5e000 rw-p 22000
f7510000-f76df000 r-xp 1cf000 /usr/lib32/libc-2.26.so
f76df000-f76e0000 ---p 1000 /usr/lib32/libc-2.26.so
f76e0000-f76e2000 r--p 2000 /usr/lib32/libc-2.26.so
f76e2000-f76e3000 rw-p 1000 /usr/lib32/libc-2.26.so
f76e3000-f76e6000 rw-p 3000
f7722000-f7724000 rw-p 2000
f7724000-f7726000 r--p 2000 [vvar]
f7726000-f7728000 r-xp 2000 [vdso]
f7728000-f774d000 r-xp 25000 /usr/lib32/ld-2.26.so
f774d000-f774e000 r--p 1000 /usr/lib32/ld-2.26.so
f774e000-f774f000 rw-p 1000 /usr/lib32/ld-2.26.so
ffe37000-ffe58000 rw-p 21000 [stack]
>>> print hex(core.fault_addr)
0x4141413d
>>> print hex(core.pc)
0x565cd57b
>>> print core.libc
f7510000-f76df000 r-xp 1cf000 /usr/lib32/libc-2.26.so
```

### dynelf

`pwnlib.dynelf.DynELF`

该模块是专门用来应对无 libc 情况下的漏洞利用。它首先找到 glibc 的基地址，然后使用符号表和字符串表对所有符号进行解析，直到找到我们需要的函数的符号。这是一个有趣的话题，我们会专门开一个章节去讲解它。详见 *4.4 使用 DynELF 泄露函数地址*

### fmtstr

`pwnlib.fmtstr.FmtStr`，`pwnlib.fmtstr.fmtstr_payload`

该模块用于格式化字符串漏洞的利用，格式化字符串漏洞是 CTF 中一种常见的题型，我们会在后面的章节中详细讲述，关于该模块的使用也会留到那儿。详见 *3.3.1 格式化字符串漏洞*

### gdb

`pwnlib.gdb`

在写漏洞利用的时候，常常需要使用 gdb 动态调试，该模块就提供了这方面的支持。

两个常用函数：

* `gdb.attach(target, gdbscript=None)`：在一个新终端打开 gdb 并 attach 到指定 PID 的进程，或是一个 `pwnlib.tubes` 对象。
* `gdb.debug(args, gdbscript=None)`：在新终端中使用 gdb 加载一个二进制文件。

上面两种方法都可以在开启的时候传递一个脚本到 gdb，可以很方便地做一些操作，如自动设置断点。

```python
# attach to pid 1234
gdb.attach(1234)

# attach to a process
bash = process('bash')
gdb.attach(bash, '''
set follow-fork-mode child
continue
''')
bash.sendline('whoami')
```

```
# Create a new process, and stop it at 'main'
io = gdb.debug('bash', '''
# Wait until we hit the main executable's entry point
break _start
continue

# Now set breakpoint on shared library routines
break malloc
break free
continue
''')
```

### memleak

`pwnlib.memleak`

该模块用于内存泄露的利用。可用作装饰器。它会将泄露的内存缓存起来，在漏洞利用过程中可能会用到。

### rop

### util

`pwnlib.util.packing`, `pwnlib.util.cyclic`

util 其实是一些模块的集合，包含了一些实用的小工具。这里主要介绍两个，packing 和 cyclic。

packing 模块用于将整数打包和解包，它简化了标准库中的 `struct.pack` 和 `struct.unpack` 函数，同时增加了对任意宽度整数的支持。

使用 `p32`, `p64`, `u32`, `u64` 函数分别对 32 位和 64 位整数打包和解包，也可以使用 `pack()` 自己定义长度，另外添加参数 `endian` 和 `signed` 设置端序和是否带符号。

```
>>> p32(0xdeadbeef)
'\xef\xbe\xad\xde'
>>> p64(0xdeadbeef).encode('hex')
'efbeadde00000000'
>>> p32(0xdeadbeef, endian='big', sign='unsigned')
'\xde\xad\xbe\xef'
```

```
>>> u32('1234')
875770417
>>> u32('1234', endian='big', sign='signed')
825373492
>>> u32('\xef\xbe\xad\xde')
3735928559
```

cyclic 模块在缓冲区溢出中很有用，它帮助生成模式字符串，然后查找偏移，以确定返回地址。

```
>>> cyclic(20)
'aaaabaaacaaadaaaeaaa'
>>> cyclic_find(0x61616162)
4
```

## Pwntools 在 CTF 中的运用

可以在下面的仓库中找到大量使用 pwntools 的 write-up： [pwntools-write-ups](https://github.com/Gallopsled/pwntools-write-ups)

## 参考资料

* [docs.pwntools.com](https://docs.pwntools.com/en/stable/index.html)
