2.4.1 Pwntools

    1. 安装binutils:

    2. 安装capstone:

      1. cd capstone
      2. make
      3. sudo make install
    3. 安装pwntools:

      1. sudo apt-get install libssl-dev
      2. sudo pip install pwntools

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

    1. $ yaourt -S python2-pwntools
    2. 或者
    3. $ yaourt -S python2-pwntools-git

    但是由于 Arch 没有 PPA 源,如果想要支持更多的体系结构(如 arm, aarch64 等),只能手动编译安装相应的 binutils,使用下面的脚本,注意将变量 VARCH 换成你需要的。源码

    1. #!/usr/bin/env bash
    2. V = 2.29 # binutils version
    3. ARCH = arm # target architecture
    4. cd /tmp
    5. wget -nc https://ftp.gnu.org/gnu/binutils/binutils-$V.tar.xz
    6. wget -nc https://ftp.gnu.org/gnu/binutils/binutils-$V.tar.xz.sig
    7. # gpg --keyserver keys.gnupg.net --recv-keys C3126D3B4AE55E93
    8. # gpg --verify binutils-$V.tar.xz.sig
    9. tar xf binutils-$V.tar.xz
    10. mkdir binutils-build
    11. cd binutils-build
    12. export AR=ar
    13. export AS=as
    14. ../binutils-$V/configure \
    15. --prefix=/usr/local \
    16. --target=$ARCH-unknown-linux-gnu \
    17. --disable-static \
    18. --disable-multilib \
    19. --disable-werror \
    20. --disable-nls
    21. make
    22. sudo make install

    测试安装是否成功:

    1. >>> from pwn import *
    2. >>> asm('nop')
    3. '\x90'
    4. >>> asm('nop', arch='arm')
    5. '\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:一些实用小工具

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

    在一次漏洞利用中,首先当然要与二进制文件或者目标服务器进行交互,这就要用到 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 开启一个套接字管道与之交互:

    1. >>> from pwn import *
    2. >>> l = listen()
    3. [x] Trying to bind to 0.0.0.0 on port 0
    4. [+] Trying to bind to 0.0.0.0 on port 0: Done
    5. [x] Waiting for connections on 0.0.0.0:46147
    6. >>> r = remote('localhost', l.lport)
    7. [x] Opening connection to localhost on port 46147
    8. [x] Opening connection to localhost on port 46147: Trying ::1
    9. [x] Opening connection to localhost on port 46147: Trying 127.0.0.1
    10. [+] Opening connection to localhost on port 46147: Done
    11. >>> [+] Waiting for connections on 0.0.0.0:46147: Got connection from 127.0.0.1 on port 38684
    12. >>> c = l.wait_for_connection()
    13. >>> r.send('hello\n')
    14. >>> c.recv()
    15. 'hello\n'
    16. >>> r.send('hello\n')
    17. >>> c.recvline()
    18. 'hello\n'
    19. >>> r.sendline('hello')
    20. >>> c.recv()
    21. 'hello\n'
    22. >>> r.sendline('hello')
    23. >>> c.recvline()
    24. >>> r.sendline('hello')
    25. >>> c.recvline(keepends=False)
    26. 'hello'
    27. >>> r.send('hello world')
    28. >>> c.recvuntil('hello')
    29. 'hello'
    30. >>> c.recv()
    31. ' world'
    32. >>> c.close()
    33. [*] Closed connection to 127.0.0.1 port 38684
    34. >>> r.close()
    35. [*] Closed connection to localhost port 46147

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

    shellcraft

    1. >>> print shellcraft.i386.nop().strip('\n')
    2. nop
    3. >>> print shellcraft.i386.linux.sh()
    4. /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    5. /* push '/bin///sh\x00' */
    6. push 0x68
    7. push 0x732f2f2f
    8. push 0x6e69622f
    9. mov ebx, esp
    10. /* push argument array ['sh\x00'] */
    11. /* push 'sh\x00\x00' */
    12. push 0x1010101
    13. xor dword ptr [esp], 0x1016972
    14. xor ecx, ecx
    15. push ecx /* null terminate */
    16. push 4
    17. pop ecx
    18. add ecx, esp
    19. push ecx /* 'sh\x00' */
    20. mov ecx, esp
    21. xor edx, edx
    22. /* call execve() */
    23. push SYS_execve /* 0xb */
    24. pop eax
    25. int 0x80

    asm

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

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

    汇编:(pwnlib.asm.asm)

    1. >>> asm('nop')
    2. '\x90'
    3. >>> asm(shellcraft.nop())
    4. '\x90'
    5. >>> asm('nop', arch='arm')
    6. '\x00\xf0 \xe3'
    7. >>> context.arch = 'arm'
    8. >>> context.os = 'linux'
    9. >>> context.endian = 'little'
    10. >>> context.word_size = 32
    11. >>> context
    12. ContextType(arch = 'arm', bits = 32, endian = 'little', os = 'linux')
    13. >>> asm('nop')
    14. '\x00\xf0 \xe3'
    1. >>> asm('mov eax, 1')
    2. '\xb8\x01\x00\x00\x00'
    3. >>> asm('mov eax, 1').encode('hex')
    4. 'b801000000'

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

    反汇编:(pwnlib.asm.disasm)

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

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

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

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

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

    1. >>> asm_sh = shellcraft.amd64.linux.sh()
    2. >>> print asm_sh
    3. /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    4. /* push '/bin///sh\x00' */
    5. push 0x68
    6. mov rax, 0x732f2f2f6e69622f
    7. push rax
    8. mov rdi, rsp
    9. /* push argument array ['sh\x00'] */
    10. /* push 'sh\x00' */
    11. push 0x1010101 ^ 0x6873
    12. xor dword ptr [rsp], 0x1010101
    13. xor esi, esi /* 0 */
    14. push rsi /* null terminate */
    15. pop rsi
    16. add rsi, rsp
    17. push rsi /* 'sh\x00' */
    18. mov rsi, rsp
    19. xor edx, edx /* 0 */
    20. /* call execve() */
    21. push SYS_execve /* 0x3b */
    22. pop rax
    23. syscall
    24. >>> filename = make_elf_from_assembly(asm_sh)
    25. >>> filename
    26. '/tmp/pwn-asm-ApZ4_p/step3'
    27. >>> p = process(filename)
    28. [x] Starting local process '/tmp/pwn-asm-ApZ4_p/step3'
    29. [+] Starting local process '/tmp/pwn-asm-ApZ4_p/step3': pid 28429
    30. >>> p.sendline('echo hello')
    31. >>> p.recv()

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

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

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

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

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

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

    1. >>> e = ELF('/bin/cat')
    2. >>> e.read(e.address+1, 3)
    3. 'ELF'
    4. >>> e.asm(e.address, 'ret')
    5. >>> e.save('/tmp/quiet-cat')
    6. >>> disasm(file('/tmp/quiet-cat','rb').read(1))
    7. ' 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() 进行调试
    1. >>> core = Corefile('/tmp/core-a.out-30555-1507796886')
    2. [x] Parsing corefile...
    3. [*] '/tmp/core-a.out-30555-1507796886'
    4. Arch: i386-32-little
    5. EIP: 0x565cd57b
    6. ESP: 0x4141413d
    7. Exe: '/home/firmy/a.out' (0x565cd000)
    8. Fault: 0x4141413d
    9. [+] Parsing corefile...: Done
    10. >>> core.registers
    11. {'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}
    12. >>> print core.maps
    13. 565cd000-565ce000 r-xp 1000 /home/firmy/a.out
    14. 565ce000-565cf000 r--p 1000 /home/firmy/a.out
    15. 565cf000-565d0000 rw-p 1000 /home/firmy/a.out
    16. 57b3c000-57b5e000 rw-p 22000
    17. f7510000-f76df000 r-xp 1cf000 /usr/lib32/libc-2.26.so
    18. f76df000-f76e0000 ---p 1000 /usr/lib32/libc-2.26.so
    19. f76e0000-f76e2000 r--p 2000 /usr/lib32/libc-2.26.so
    20. f76e2000-f76e3000 rw-p 1000 /usr/lib32/libc-2.26.so
    21. f76e3000-f76e6000 rw-p 3000
    22. f7722000-f7724000 rw-p 2000
    23. f7724000-f7726000 r--p 2000 [vvar]
    24. f7726000-f7728000 r-xp 2000 [vdso]
    25. f7728000-f774d000 r-xp 25000 /usr/lib32/ld-2.26.so
    26. f774d000-f774e000 r--p 1000 /usr/lib32/ld-2.26.so
    27. f774e000-f774f000 rw-p 1000 /usr/lib32/ld-2.26.so
    28. ffe37000-ffe58000 rw-p 21000 [stack]
    29. >>> print hex(core.fault_addr)
    30. 0x4141413d
    31. >>> print hex(core.pc)
    32. 0x565cd57b
    33. >>> print core.libc
    34. f7510000-f76df000 r-xp 1cf000 /usr/lib32/libc-2.26.so

    dynelf

    pwnlib.dynelf.DynELF

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

    fmtstr

    pwnlib.fmtstr.FmtStrpwnlib.fmtstr.fmtstr_payload

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

    pwnlib.gdb

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

    两个常用函数:

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

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

    1. # attach to pid 1234
    2. gdb.attach(1234)
    3. # attach to a process
    4. bash = process('bash')
    5. gdb.attach(bash, '''
    6. set follow-fork-mode child
    7. continue
    8. ''')
    9. bash.sendline('whoami')
    1. # Create a new process, and stop it at 'main'
    2. io = gdb.debug('bash', '''
    3. # Wait until we hit the main executable's entry point
    4. break _start
    5. continue
    6. # Now set breakpoint on shared library routines
    7. break malloc
    8. break free
    9. continue
    10. ''')

    memleak

    pwnlib.memleak

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

    rop

    pwnlib.util.packing, pwnlib.util.cyclic

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

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

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

    1. >>> p32(0xdeadbeef)
    2. '\xef\xbe\xad\xde'
    3. >>> p64(0xdeadbeef).encode('hex')
    4. 'efbeadde00000000'
    5. >>> p32(0xdeadbeef, endian='big', sign='unsigned')
    6. '\xde\xad\xbe\xef'
    1. >>> cyclic(20)
    2. 'aaaabaaacaaadaaaeaaa'
    3. >>> cyclic_find(0x61616162)
    4. 4

    可以在下面的仓库中找到大量使用 pwntools 的 write-up: