# 2.7 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/) ```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)`:接收一行,可选择是否保留行尾的 `\n` - `recvrepeat(timeout=default)`:接收数据直到 EOF 或 timeout - `recvuntil(delims, timeout=default)`:接收数据直到 delims 出现 - `send(data)`:发送数据 - `sendline(data)`:发送一行,默认在行尾加 `\n` - `close()`:关闭管道 下面是一个例子,先使用 listen 开启一个本地的监听端口,然后使用 remote 开启一个套接字管道与之交互: ```text >>> 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 ``` 下面是一个与进程交互的例子: ```text >>> 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`) ```text >>> 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`) ```text >>> 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`) ```text >>> 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 文件: ```text >>> 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`) ```text >>> 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 中是非常有用的: ```text >>> 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 文件的代码: ```text >>> 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)