created: 2022-10-02T20:32:50 (UTC +08:00)
tags: [pwntools手册]
source: https://blog.csdn.net/kelxLZ/article/details/123152529?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_utm_term~default-4-123152529-blog-113980922.pc_relevant_multi_platform_whitelistv3&spm=1001.2101.3001.4242.3&utm_relevant_index=7
author:https://blog.csdn.net/kelxLZ/article/details/123152529?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_utm_term~default-4-123152529-blog-113980922.pc_relevant_multi_platform_whitelistv3&spm=1001.2101.3001.4242.3&utm_relevant_index=7

Pwntools 2022简明手册_ZERO-A-ONE的博客-CSDN博客_pwntools手册

Excerpt

Author:ZERO-A-ONEDate:2022-02-24本文翻译自:https://github.com/Gallopsled/pwntools-tutorial,主要是考虑到目前中文互联网中关于系统介绍pwntools使用方法的文章都比较老和杂乱,且转换为Python3后又有许多零零散散的问题,看到这个仓库中包含了很多使用技巧和调试问题的解决方案,感到可以翻译一下这个资源库包含了一些开始使用pwntools(和pwntools)的基本教程。这些教程并不致力于解释逆向工程或利用,而是.


  • Author:ZERO-A-ONE
  • Date:2022-02-24

本文翻译自:https://[github](https://so.csdn.net/so/search?q=github&spm=1001.2101.3001.7020).com/Gallopsled/pwntools-tutorial,主要是考虑到目前中文互联网中关于系统介绍pwntools使用方法的文章都比较老和杂乱,且转换为Python3后又有许多零零散散的问题,看到这个仓库中包含了很多使用技巧和调试问题的解决方案,感到可以翻译一下

这个资源库包含了一些开始使用pwntools(和pwntools)的基本教程。

这些教程并不致力于解释逆向工程或利用,而是假定读者有这方面的知识。

一、简介

Pwntools是一个工具包,使选手们在CTF期间的尽可能容易的编写EXP,并使EXP尽可能的容易阅读。

有些代码每个人都写过无数次,而且每个人都有自己的方法。Pwntools的目标是以半标准的方式提供所有这些,这样你就可以停止复制粘贴相同的struct.unpack('>I', x)代码,而是使用更多稍微清晰的包装器,如pack或p32甚至p64(..., endian='big', sign=True)

除了对日常的功能进行方便的包装外,它还提供了一套非常丰富的IO管道,将所有你曾经执行过的IO封装在一个统一的界面中。从本地攻击切换到远程攻击,或者通过SSH进行本地攻击,都只是修改一行代码的工作。

最后但并非最不重要的是,它还包括一系列用于中级到高级使用情况的开发协助工具。这些工具包括给定内存泄露基元的远程符号解析(MemLeakDynELF),ELF解析和修补(ELF),以及ROP小工具发现和调用链构建(ROP)。

二、目录

  • Installing Pwntools
  • Tubes
    • Basic Tubes
    • Interactive Shells
    • Processes
    • Networking
    • Secure Shell
    • Serial Ports
  • Utility
    • Encoding and Hashing
    • Packing / unpacking integers
    • Pattern generation
    • Safe evaluation
  • Bytes vs. Strings
    • Python2
    • Python3
      • Gotchas
  • Context
    • Architecture
    • Endianness
    • Log verbosity
    • Timeout
  • ELFs
    • Reading and writing
    • Patching
    • Symbols
  • Assembly
    • Assembling shellcode
    • Disassembling bytes
    • Shellcraft library
    • Constants
  • Debugging
    • Debugging local processes
    • Breaking at the entry point
    • Debugging shellcode
  • ROP
    • Dumping gadgets
    • Searching for gadgets
    • ROP stack generation
    • Helper functions
  • Logging
    • Basic logging
    • Log verbosity
    • Progress spinners
  • Leaking Remote Memory
    • Declaring a leak function
    • Leaking arbitrary memory
    • Remote symbol resolution

三、安装Pwntools

这个过程可以说是简单明了,Ubuntu 18.04和20.04是唯一 “官方支持 “的平台,因为它们是官方对软件进行自动化测试的唯二平台。

  1. $ apt-get update
  2. $ apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
  3. $ python3 -m pip install --upgrade pip
  4. $ python3 -m pip install --upgrade pwntools

3.1 验证安装

如果以下命令成功,一切都应该是OK的

  1. $ python -c 'from pwn import *'

3.2 其它架构

如果你想为其它的架构组装或反汇编代码,你需要安装一个合适的binutils。对于Ubuntu和Mac OS X用户,安装说明可在docs.pwntools.com上找到。

  1. $ apt-get install binutils-*

四、管道

管道是方便高校的I/O包装器,里面包含了你需要执行的大多数类型的I/O。

  • Local processes
  • Remote TCP or UDP connections
  • Processes running on a remote server over SSH
  • Serial port I/O

本介绍提供了一些所提供功能的例子,但更复杂的组合是可能的。关于如何进行正则表达式匹配,以及将管道连接在一起的更多信息,请参阅完整的文档

4.1 基础IO

下面介绍一些IO中的基本功能:

接收数据

  • recv(n) - 接收任何数量的可用字节
  • recvline() - 接收数据,直到遇到换行
  • recvuntil(delim) - 接收数据,直到找到一个分隔符
  • recvregex(pattern) - 接收数据,直到满足一个与pattern重合的内容为止
  • recvrepeat(timeout) - 继续接收数据,直到发生超时
  • clean() - 丢弃所有缓冲的数据

发送数据

  • send(data) - 发送数据
  • sendline(line) - 发送数据加一个换行

操作整数

  • pack(int) - 打包发送一个字(word)大小的整数
  • unpack() - 接收并解包一个字(word)大小的整数

4.2 进程和基本功能

为了创建一个与进程对话的管道,你只需创建一个进程对象并给它一个目标二进制的名字。

  1. from pwn import *
  2. io = process('sh')
  3. io.sendline('echo Hello, world')
  4. io.recvline()
  5. # 'Hello, world\n'

如果你需要提供命令行参数,或设置环境,可以使用额外的选项。更多信息请参见完整的文档

  1. from pwn import *
  2. io = process(['sh', '-c', 'echo $MYENV'], env={'MYENV': 'MYVAL'})
  3. io.recvline()
  4. # 'MYVAL\n'

读取二进制数据也不是一个问题。你可以用recv接收多达若干字节的数据,或者用recvn接受精确的字节数。

  1. from pwn import *
  2. io = process(['sh', '-c', 'echo A; sleep 1; echo B; sleep 1; echo C; sleep 1; echo DDD'])
  3. io.recv()
  4. # 'A\n'
  5. io.recvn(4)
  6. # 'B\nC\n'
  7. hex(io.unpack())
  8. # 0xa444444

4.3 会话互动

你在游戏服务器中获取了一个shell吗?赶快!互动地使用它是很容易的。

  1. from pwn import *
  2. # Let's pretend we're uber 1337 and landed a shell.
  3. io = process('sh')
  4. # <exploit goes here>
  5. io.interactive()

4.4 网络

创建一个网络连接也很容易,而且有完全相同的接口。一个remote对象连接到其他地方,而一个listen对象则在等待连接。

  1. from pwn import *
  2. io = remote('google.com', 80)
  3. io.send('GET /\r\n\r\n')
  4. io.recvline()
  5. # 'HTTP/1.0 200 OK\r\n'

如果你需要指定协议信息,也是很直接方便的。

  1. from pwn import *
  2. dns = remote('8.8.8.8', 53, typ='udp')
  3. tcp6 = remote('google.com', 80, fam='ipv6')

侦听连接并没有多复杂。请注意,这正好是在监听一个连接,然后停止监听。

  1. from pwn import *
  2. client = listen(8080).wait_for_connection()

4.5 安全的Shell

SSH连接也同样简单。可以将下面的代码与上面 “Hello Process “中的代码进行比较。

你还可以用SSH做更复杂的事情,如端口转发和文件上传/下载。更多信息请参见SSH教程。

  1. from pwn import *
  2. session = ssh('bandit0', 'bandit.labs.overthewire.org', password='bandit0')
  3. io = session.process('sh', env={"PS1":""})
  4. io.sendline('echo Hello, world!')
  5. io.recvline()
  6. # 'Hello, world!\n'

4.6 串行端口

如果你需要在本地进行一些黑客攻击,也有一个串行管道。一如既往,在完整的在线文档中有更多信息。

  1. from pwn import *
  2. io = serialtube('/dev/ttyUSB0', baudrate=115200)

五、实用功能

Pwntools大约有一半的内容是实用功能,这样你就不再需要到处复制粘贴这样的东西。

  1. import struct
  2. def p(x):
  3. return struct.pack('I', x)
  4. def u(x):
  5. return struct.unpack('I', x)[0]
  6. 1234 == u(p(1234))

此外,你不仅得到了漂亮的小包装,作为额外的奖励,在阅读别人的漏洞代码时,一切都更清晰,更容易理解。

  1. from pwn import *
  2. 1234 == unpack(pack(1234))

5.1 打包和解包整数

这可能是你最常做的事情,所以它在最前面。主要的packunpack函数都知道context中的全局设置,如endianbitssign

你也可以在函数调用中明确指定它们。

  1. pack(1)
  2. # '\x01\x00\x00\x00'
  3. pack(-1)
  4. # '\xff\xff\xff\xff'
  5. pack(2**32 - 1)
  6. # '\xff\xff\xff\xff'
  7. pack(1, endian='big')
  8. # '\x00\x00\x00\x01'
  9. p16(1)
  10. # '\x01\x00'
  11. hex(unpack('AAAA'))
  12. # '0x41414141'
  13. hex(u16('AA'))
  14. # '0x4141'

5.2 文件I/O

只需调用一个函数,它就能做你想做的事。

  1. from pwn import *
  2. write('filename', 'data')
  3. read('filename')
  4. # 'data'
  5. read('filename', 1)
  6. # 'd'

5.3 哈希和编码

能够快速的将你的数据转换成你需要的任何格式。

Base64

  1. 'hello' == b64d(b64e('hello'))

Hashes

  1. md5sumhex('hello') == '5d41402abc4b2a76b9719d911017c592'
  2. write('file', 'hello')
  3. md5filehex('file') == '5d41402abc4b2a76b9719d911017c592'
  4. sha1sumhex('hello') == 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d'

URL Encoding

  1. urlencode("Hello, World!") == '%48%65%6c%6c%6f%2c%20%57%6f%72%6c%64%21'

Hex Encoding

  1. enhex('hello')
  2. # '68656c6c6f'
  3. unhex('776f726c64')
  4. # 'world'

Bit Manipulation and Hex Dumping

  1. bits(0b1000001) == bits('A')
  2. # [0, 0, 0, 1, 0, 1, 0, 1]
  3. unbits([0,1,0,1,0,1,0,1])
  4. # 'U'

Hex Dumping

  1. print hexdump(read('/dev/urandom', 32))
  2. # 00000000 65 4c b6 62 da 4f 1d 1b d8 44 a6 59 a3 e8 69 2c │eL·b│·O··│·D·Y│··i,│
  3. # 00000010 09 d8 1c f2 9b 4a 9e 94 14 2b 55 7c 4e a8 52 a5 │····│·J··│·+U|│N·R·│
  4. # 00000020

5.4 样例生成

样例生成是一种非常方便的方法,可以在不需要进行数学计算的情况下找到偏移量。

假设我们有一个直接的缓冲区溢出,我们生成一个样例并提供给目标应用程序。

  1. io = process(...)
  2. io.send(cyclic(512))

在核心转储中,我们可能看到崩溃发生在0x61616178。我们可以不用对崩溃帧做任何分析,只需把这个数字打回去,得到一个偏移量。

  1. cyclic_find(0x61616178)
  2. # 92

六、Bytes vs. Strings

当Pwntools最初(重新)编写时,大约在十年前,Python2是最受欢迎的。

  1. commit e692277db8533eaf62dd3d2072144ccf0f673b2e
  2. Author: Morten Brøns-Pedersen <mortenbp@gmail.com>
  3. Date: Thu Jun 7 17:34:48 2012 +0200
  4. ALL THE THINGS

多年来在Python中编写的许多EXP都假定str对象与bytes对象有1:1的映射,因为这是Python2上的工作原理。 在这一节中,我们讨论在Python3上编写EXP所需的一些变化,并阐述与Python2的对应关系。

6.1 Python2

在Python2中,str类和bytes类是一样的,而且有一个1:1的映射。从来不需要对任何东西调用encodedecode – 文本就是字节,字节就是文本。

这对编写EXP来说是非常方便的,因为你只需写”\x90\x90\x90\x90 “就可以得到一个NOP滑块。Python2上所有的Pwntools管道和数据操作都支持字符串或字节。

从来没有人使用unicode对象来编写漏洞,所以unicode到字节的转换极其罕见。

6.2 Python3

在 Python3 中,unicode类实际上就是str类。这有一些直接和明显的影响。

乍一看,Python3似乎让事情变得更难了,因为bytes声明的是单个的八位数(正如名字bytes所暗示的),而str用于任何基于文本的数据表示。

Pwntools花了很大力气来遵循 “最小惊喜原则”——也就是说,事情会按照你预期的方式进行。

  1. >>> r.send('❤️')
  2. [DEBUG] Sent 0x6 bytes:
  3. 00000000 e2 9d a4 ef b8 8f │····│··│
  4. 00000006
  5. >>> r.send('\x00\xff\x7f\x41\x41\x41\x41')
  6. [DEBUG] Sent 0x7 bytes:
  7. 00000000 00 ff 7f 41 41 41 41 │···AAAA
  8. 00000007

然而,有时事情会出现一些故障。注意这里99f7e2如何被转换为c299c3b7c3a2。

  1. >>> shellcode = "\x99\xf7\xe2"
  2. >>> print(hexdump(flat("padding\x00", shellcode)))
  3. 00000000 70 61 64 64 69 6e 67 00 c2 99 c3 b7 c3 a2 padding·│····│··│
  4. 0000000e

这是因为文本字符串”\x99\xf7\xe2 “被自动转换为UTF-8代码。这不可能是用户想要的。

作为解决方案,我们只需要以b为前缀:

  1. >>> shellcode = b"\x99\xf7\xe2"
  2. >>> print(hexdump(flat(b"padding\x00", shellcode)))
  3. 00000000 70 61 64 64 69 6e 67 00 99 f7 e2 padding·│···│
  4. 0000000b

好极了!

一般来说,Python3上的Pwntools的修复方法是确保你所有的字符串都有一个b前缀。这就解决了歧义,并使一切变得简单明了。

6.3 麻烦

关于Python3的bytes对象,有一个值得一提的 “麻烦”。当对它们进行迭代时,你会得到整数,而不是bytes对象。这是与Python2的巨大差异,也是一个主要的烦恼。

  1. >>> x=b'123'
  2. >>> for i in x:
  3. ... print(i)
  4. ...
  5. 49
  6. 50
  7. 51

为了解决这个问题,我们建议使用切片,它产生长度为1bytes的对象。

  1. >>> for i in range(len(x)):
  2. ... print(x[i:i+1])
  3. ...
  4. b'1'
  5. b'2'
  6. b'3'

七、环境

context对象是一个全局的、线程感知的对象,包含了pwntools使用的各种设置。

一般来说,在一个EXP的首部,你会发现类似的东西:

  1. from pwn import *
  2. context.arch = 'amd64'

这通知pwntools生成的shellcode将用于amd64,并且默认字大小为64位。

7.1 环境设置

arch

目标架构。有效值是"arch64""arm""i386""amd64",等等。默认是 "i386"

第一次设置时,它会自动将默认的context.bits和context.endian设置为最可能的值。

bits

在目标二进制中,有多少位组成一个字,如3264

binary

从ELF文件中获取配置。例如:context.binary='/bin/sh'

log_file

将所有的日志输出送入的文件。

log_level

日志的详细程度。有效值是整数(越小越详细),以及"debug""info ""error "等字符串值。

sign

设置整数打包/解包的是否有符号。默认为 "unsigned"

terminal

用来打开新窗口的首选终端程序。默认情况下,使用x-terminal-emulatortmux

timeout

管道操作的默认超时范围。

update

一次设置多个值,例如context.update(arch='mips', bits=64, endian='big')

八、ELFs

Pwntools通过ELF类使与ELF文件的交互变得相对简单。你可以在RTD上找到完整的文档。

8.1 加载ELF文件

ELF文件是按路径加载的。在被加载后,一些与安全有关的文件属性被打印出来。

  1. from pwn import *
  2. e = ELF('/bin/bash')
  3. # [*] '/bin/bash'
  4. # Arch: amd64-64-little
  5. # RELRO: Partial RELRO
  6. # Stack: Canary found
  7. # NX: NX enabled
  8. # PIE: No PIE
  9. # FORTIFY: Enabled

8.2 使用符号表

ELF文件有几组不同的符号表可用,每组都包含在{name: data}的字典中。

  • ELF.symbols 列出所有已知的符号,包括下面的符号。优先考虑PLT条目,而不是GOT条目。
  • ELF.got 只包含GOT表
  • ELF.plt 只包含PLT表
  • ELF.functions 只包含函数符号表(需要DWARF符号表)

这对于保持漏洞的稳健性非常有用,因为它消除了对硬编码地址的需要。

  1. from pwn import *
  2. e = ELF('/bin/bash')
  3. print "%#x -> license" % e.symbols['bash_license']
  4. print "%#x -> execve" % e.symbols['execve']
  5. print "%#x -> got.execve" % e.got['execve']
  6. print "%#x -> plt.execve" % e.plt['execve']
  7. print "%#x -> list_all_jobs" % e.functions['list_all_jobs'].address

这将打印出类似下面的内容:

  1. 0x4ba738 -> license
  2. 0x41db60 -> execve
  3. 0x6f0318 -> got.execve
  4. 0x41db60 -> plt.execve
  5. 0x446420 -> list_all_jobs

8.3 改变基本地址

使用pwntools改变ELF文件的基址(比如为ASLR做调整)是非常直接和简单的。让我们改变bash的基址,看看所有的符号都有什么变化。

  1. from pwn import *
  2. e = ELF('/bin/bash')
  3. print "%#x -> base address" % e.address
  4. print "%#x -> entry point" % e.entry
  5. print "%#x -> execve" % e.symbols['execve']
  6. print "---"
  7. e.address = 0x12340000
  8. print "%#x -> base address" % e.address
  9. print "%#x -> entry point" % e.entry
  10. print "%#x -> execve" % e.symbols['execve']

这应该打印出类似的内容:

  1. 0x400000 -> base address
  2. 0x42020b -> entry point
  3. 0x41db60 -> execve
  4. ---
  5. 0x12340000 -> base address
  6. 0x1236020b -> entry point
  7. 0x1235db60 -> execve

8.4 读取ELF文件

我们可以通过pwntools直接与ELF互动,就像它被加载到内存中一样,使用readwrite和与packing模块中的函数命名相同。此外,你可以通过disasm方法看到反汇编。

  1. from pwn import *
  2. e = ELF('/bin/bash')
  3. print repr(e.read(e.address, 4))
  4. p_license = e.symbols['bash_license']
  5. license = e.unpack(p_license)
  6. print "%#x -> %#x" % (p_license, license)
  7. print e.read(license, 14)
  8. print e.disasm(e.symbols['main'], 12)

打印出来的东西应该如下:

  1. '\x7fELF'
  2. 0x4ba738 -> 0x4ba640
  3. License GPLv3+
  4. 41eab0: 41 57 push r15
  5. 41eab2: 41 56 push r14
  6. 41eab4: 41 55 push r13

8.5 对ELF文件进行修补

对ELF文件的修补也同样简单。

  1. from pwn import *
  2. e = ELF('/bin/bash')
  3. # Cause a debug break on the 'exit' command
  4. e.asm(e.symbols['exit_builtin'], 'int3')
  5. # Disable chdir and just print it out instead
  6. e.pack(e.got['chdir'], e.plt['puts'])
  7. # Change the license
  8. p_license = e.symbols['bash_license']
  9. license = e.unpack(p_license)
  10. e.write(license, 'Hello, world!\n\x00')
  11. e.save('./bash-modified')

然后我们可以运行我们修改过的bash版本。

  1. $ chmod +x ./bash-modified
  2. $ ./bash-modified -c 'exit'
  3. Trace/breakpoint trap (core dumped)
  4. $ ./bash-modified --version | grep "Hello"
  5. Hello, world!
  6. $ ./bash-modified -c 'cd "No chdir for you!"'
  7. /home/user/No chdir for you!
  8. No chdir for you!
  9. ./bash-modified: line 0: cd: No chdir for you!: No such file or directory

8.6 搜索ELF文件

在编写EXP的时候,你经常需要找到一些字节序列。最常见的例子是搜索例如"/bin/sh\x00 "execve调用。search方法返回一个迭代器,允许你选择第一个结果,或者如果你需要一些特殊的东西(比如地址中没有坏字符),可以继续搜索。你可以选择传递一个writable参数给search,表示它应该只返回可写段的地址。

  1. from pwn import *
  2. e = ELF('/bin/bash')
  3. for address in e.search('/bin/sh\x00'):
  4. print hex(address)

上面的例子打印的内容应该如下:

  1. 0x420b82
  2. 0x420c5e

8.7 构建ELF文件

通过pwntools我们可以很方便地从头开始创建一个ELF文件。所有这些功能都是上下文感知的。相关的函数是from_bytesfrom_assembly。每一个都返回一个ELF对象,它可以很容易地被保存到文件中。

  1. from pwn import *
  2. ELF.from_bytes('\xcc').save('int3-1')
  3. ELF.from_assembly('int3').save('int3-2')
  4. ELF.from_assembly('nop', arch='powerpc').save('powerpc-nop')

8.8 运行和调试ELF文件

如果你有一个ELF对象,你可以直接运行或调试它。以下两个代码是等同的:

  1. >>> io = elf.process()
  2. # vs
  3. >>> io = process(elf.path)

同样地,你可以启动一个调试器,并将其连接到ELF上。这在测试shellcode时是非常有用的,不需要用C语言包装器来加载和调试它。

  1. >>> io = elf.debug()
  2. # vs
  3. >>> io = gdb.debug(elf.path)

九、汇编

Pwntools使得用户在几乎所有的架构中进行汇编变得非常容易,并带有各种可以开箱即用已经生成好且依然可定制的shellcode。

walkthrough目录中,有几个较长的shellcode教程。本页为您提供了基础知识。

9.1 基础汇编

最基本的例子,是将汇编代码转换成shellcode。

  1. from pwn import *
  2. print repr(asm('xor edi, edi'))
  3. # '1\xff'
  4. print enhex(asm('xor edi, edi'))
  5. # 31ff

9.2 现成的汇编(shellcraft

shellcraft模块会提供给你一些现成的汇编代码。它通常是可定制的。找出存在哪些shellcraft模板的最简单方法是查看RTD上的文档。

  1. from pwn import *
  2. help(shellcraft.sh)
  3. print '---'
  4. print shellcraft.sh()
  5. print '---'
  6. print enhex(asm(shellcraft.sh()))
  1. Help on function sh in module pwnlib.shellcraft.internal:
  2. sh()
  3. Execute /bin/sh
  4. ---
  5. /* push '/bin///sh\x00' */
  6. push 0x68
  7. push 0x732f2f2f
  8. push 0x6e69622f
  9. /* call execve('esp', 0, 0) */
  10. push (SYS_execve) /* 0xb */
  11. pop eax
  12. mov ebx, esp
  13. xor ecx, ecx
  14. cdq /* edx=0 */
  15. int 0x80
  16. ---
  17. 6a68682f2f2f73682f62696e6a0b5889e331c999cd80

9.3 命令行工具

有三个命令行工具用于与汇编进行交互。

  • asm
  • disasm
  • shellcraft

asm

asm工具的功能正如其名,它将汇编码转换为机器码,它为汇编指令输出的格式化提供了几个选项,当输出是一个终端时,它默认为十六进制编码。

  1. $ asm nop
  2. 90

当输出是其他东西时,它显示的是原始数据。

  1. $ asm nop | xxd
  2. 0000000: 90 .

如果在命令行上没有提供指令,它将在stdin上获取数据。

  1. $ echo 'push ebx; pop edi' | asm
  2. 535f

最后,它支持一些不同的选项,通过--format选项来指定输出格式。支持的参数有rawhexstringelf

  1. $ asm --format=elf 'int3' > ./int3
  2. $ ./halt
  3. Trace/breakpoint trap (core dumped)

disasm

Disasm是asm的反义词,也就是将16进制的机器码反汇编成汇编指令。

  1. $ disasm cd80
  2. 0: cd 80 int 0x80
  3. $ asm nop | disasm
  4. 0: 90 nop

shellcraft

shellcraft命令是内部shellcraft模块的命令行接口。在命令行中,必须按arch.os.template的顺序指定完整的环境信息。

  1. $ shellcraft i386.linux.sh
  2. 6a68682f2f2f73682f62696e6a0b5889e331c999cd80

9.4 异构架构

为其它非X86架构进行汇编交互,你需要自行安装适当版本的binutils。你应该看看installing.md以了解更多这方面的信息。我们唯一需要改变的是在全局环境变量中设置架构。你可以在 context.md 中看到更多关于context的信息。

  1. from pwn import *
  2. context.arch = 'arm'
  3. print repr(asm('mov r0, r1'))
  4. # '\x01\x00\xa0\xe1'
  5. print enhex(asm('mov r0, r1'))
  6. # 0100a0e1

9.4.1 现成汇编

shellcraft模块会自动切换到相应的架构。

  1. from pwn import *
  2. context.arch = 'arm'
  3. print shellcraft.sh()
  4. print enhex(asm(shellcraft.sh()))
  1. adr r0, bin_sh
  2. mov r2, #0
  3. mov r1, r2
  4. svc SYS_execve
  5. bin_sh: .asciz "/bin/sh"
  6. 08008fe20020a0e30210a0e10b0000ef2f62696e2f736800

9.4.2 命令行工具

你也可以通过使用--context命令行选项,使用命令行来汇编生成其它架构的shellcode

  1. $ asm --context=arm 'mov r0, r1'
  2. 0100a0e1
  3. $ shellcraft arm.linux.sh
  4. 08008fe20020a0e30210a0e10b0000ef2f62696e2f736800

十、调试

Pwntools对在你的漏洞工作流程中使用调试器有丰富的支持,在开发EXP的问题出现时,调试器非常有用。

除了这里的调试资源外,你可能想通过以下项目来增强你的GDB经验:

10.1 先前条件

你的机器上应该同时安装了gdbgdbserver。你可以用which gdbwhich gdbserver来轻松检查。

如果你发现你没有安装它们,它们可以很容易地从大多数软件包管理器中安装。

  1. $ sudo apt-get install gdb gdbserver

10.2 在GDB下启动一个进程

在GDB下启动一个进程,同时还能从pwntools与该进程进行交互,这在之前是一个棘手的过程,但幸运的是,这一切都已经被解决了,而且这个过程是相当无感和便捷的。

要在GDB下从第一条指令开始启动一个进程,只需使用gdb.debug

  1. >>> io = gdb.debug("/bin/bash", gdbscript='continue')
  2. >>> io.sendline('echo hello')
  3. >>> io.recvline()
  4. # b'hello\n'
  5. >>> io.interactive()

这应该会自动在一个新的窗口中启动调试器,以便你进行交互。如果不是这样,或者你看到关于context.terminal的错误,请查看指定终端窗口的章节。

在这个例子中,我们传入了gdbscript='continue',以使调试器恢复执行,但是你可以传入任何有效的GDB脚本命令,它们将在调试进程启动时被执行。

10.3 附加到一个正在运行的进程

有时你不想在调试器下启动你的目标,但想在开发过程的某个阶段附加到它。这也已经被Pwntools便捷无缝的实现了。

10.3.1 本地进程

一般来说,你会创建一个process()管道,以便与目标可执行文件交互。你可以简单地把它传递给gdb.attach(),它将神奇地打开一个新的终端窗口,在调试器中运行目标二进制文件。

  1. >>> io = process('/bin/sh')
  2. >>> gdb.attach(io, gdbscript='continue')

一个新的窗口应该出现,你可以继续与进程进行互动,就像你通常在Pwntools中做的一样。

10.3.2 远程服务器

有时你想调试的二进制文件运行在一个远程服务器上,你想调试你所连接的进程(而不是服务器本身)。只要服务器在当前机器上运行,这也可以无缝地完成。

让我们用socat伪造一个服务器!

  1. >>> socat = process(['socat', 'TCP-LISTEN:4141,reuseaddr,fork', 'EXEC:/bin/bash -i'])

然后我们像往常一样用远程管道连接到远程进程。

  1. >>> io = remote('localhost', 4141)
  2. [x] Opening connection to localhost on port 4141
  3. [x] Opening connection to localhost on port 4141: Trying 127.0.0.1
  4. [+] Opening connection to localhost on port 4141: Done
  5. >>> io.sendline('echo hello')
  6. >>> io.recvline()
  7. b'hello\n'
  8. >>> io.lport, io.rport

它是有效的!为了调试特定的bash进程,只要把它我们的远程对象传给gdb.attach()。Pwntools将查找连接的远程端的PID,并尝试自动连接到它。

  1. >>> gdb.attach(io)

调试器应该自动出现,你可以与进程进行交互。

10.3 调试异构架构

从基于英特尔的系统中在pwntools下调试异构架构(如ARM或PowerPC)是十分容易的。

  1. >>> context.arch = 'arm'
  2. >>> elf = ELF.from_assembly(shellcraft.echo("Hello, world!\n") + shellcraft.exit())
  3. >>> process(elf.path).recvall()
  4. b'Hello, world!\n'

gdb.debug(...)来代替调用process(...)

  1. >>> gdb.debug(elf.path).recvall()
  2. b'Hello, world!\n'

10.3.1 提示和限制

运行异构架构的进程必须用gdb.debug启动,以便对其进行调试,由于QEMU的工作方式,不可能附加到一个正在运行的进程上。

需要注意的是,QEMU有一个非常有限的用来通知GDB各种库的位置存根,所以调试可能会更加困难,一些命令也无法工作。

Pwntools推荐使用Pwndbg来处理这种情况,因为它拥有专门处理QEMU存根下调试程序的能力。

10.4 故障排除(Pwntools自身)

10.4.1 幕后花絮(工作详情)

有时程序就是不正常工作,你需要看看Pwntools内部在调试器的设置下发生了什么。

你可以在全局范围内设置日志上下文(例如通过context.log_level='debug'),也可以通过传递相同的参数,只为GDB会话设置。

你应该看到在幕后为你处理的一切操作。比如说:

  1. >>> io = gdb.debug('/bin/sh', log_level='debug')
  2. [x] Starting local process '/home/user/bin/gdbserver' argv=[b'/home/user/bin/gdbserver', b'--multi', b'--no-disable-randomization', b'localhost:0', b'/bin/sh']
  3. [+] Starting local process '/home/user/bin/gdbserver' argv=[b'/home/user/bin/gdbserver', b'--multi', b'--no-disable-randomization', b'localhost:0', b'/bin/sh'] : pid 34282
  4. [DEBUG] Received 0x25 bytes:
  5. b'Process /bin/sh created; pid = 34286\n'
  6. [DEBUG] Received 0x18 bytes:
  7. b'Listening on port 45145\n'
  8. [DEBUG] Wrote gdb script to '/tmp/user/pwnxcd1zbyx.gdb'
  9. target remote 127.0.0.1:45145
  10. [*] running in new terminal: /usr/bin/gdb -q "/bin/sh" -x /tmp/user/pwnxcd1zbyx.gdb
  11. [DEBUG] Launching a new terminal: ['/usr/local/bin/tmux', 'splitw', '/usr/bin/gdb -q "/bin/sh" -x /tmp/user/pwnxcd1zbyx.gdb']
  12. [DEBUG] Received 0x25 bytes:
  13. b'Remote debugging from host 127.0.0.1\n'

10.4.2 指定一个终端窗口

Pwntools[attempts to launch a new window][run_in_new_terminal],根据你当前使用的任何窗口系统来展示你的调试器。

默认情况下,它是自动检测的:

  • tmux or screen
  • X11-based terminals like GNOME Terminal

如果你没有使用支持的终端环境,或者它没有以你想要的方式工作(例如,水平与垂直分割),你可以通过设置context.terminal环境变量来增加支持。

例如,下面将使用TMUX进行水平分割,而不是默认设置。

  1. >>> context.terminal = ['tmux', 'splitw', '-h']

也许你是一个GNOME终端的用户,而默认的设置并不工作?

  1. >>> context.terminal = ['gnome-terminal', '-x', 'sh', '-c']

你可以指定任何你喜欢的终端,甚至可以把设置放在~/.pwn.conf里面,这样它就会被用于你的所有脚本了

  1. [context]
  2. terminal=['x-terminal-emulator', '-e']

10.4.3 环境变量

Pwntools允许你通过process()指定任何你喜欢的环境变量,对于gdb.debug()也是如此。

  1. >>> io = gdb.debug(['bash', '-c', 'echo $HELLO'], env={'HELLO': 'WORLD'})
  2. >>> io.recvline()
  3. b'WORLD\n'

CWD

不幸的是,当使用gdb.debug()时,该进程是在gdbserver下启动的,它增加了自己的环境变量。当环境必须被非常仔细地控制时,这可能会带来复杂的情况。

  1. >>> io = gdb.debug(['env'], env={'FOO':'BAR'}, gdbscript='continue')
  2. >>> print(io.recvallS())
  3. =/home/user/bin/gdbserver
  4. FOO=BAR
  5. Child exited with status 0
  6. GDBserver exiting

这只在你用gdb.debug()在调试器下启动进程时发生。如果你能够启动你的进程,然后用gdb.attach()附加,你就可以避免这个问题。

环境变量排序

一些漏洞可能需要某些环境变量以特定的顺序出现。但是Python2的字典是没有顺序的,这可能会加剧这个问题。

为了让你的环境变量有一个特定的顺序,我们建议使用Python3(它基于插入顺序对字典进行排序),或者使用collection.OrderedDict

10.4.4 无法附加到进程中

现代的Linux系统有一个叫做trace_scope的设置,它可以阻止非子进程的进程被调试。Pwntools对于它自己启动的任何进程都能解决这个问题,但是如果你必须在Pwntools之外启动一个进程,并试图通过pid附加到它(例如gdb.attach(1234)),你可能被阻止附加。

你可以通过禁用安全设置和重启机器来解决这个问题:

  1. sudo tee /etc/sysctl.d/10-ptrace.conf <<EOF
  2. kernel.yama.ptrace_scope = 0
  3. EOF

10.4.5 argv0 and argc==0

有些题目要求在启动时将argv[0]设置为一个特定的值,甚至要求它是NULL(即argc==0)。

通过gdb.debug()不可能用这种配置启动一个processs,但你可以使用gdb.attach()。这是因为在gdbserver下启动二进制文件的限制。

十一、ROP

11.1 背景

面向返回的编程(ROP)是一种绕过NX(no-execute,也称为预防数据执行(DEP))的技术。

Pwntools有几个特点,使ROP的利用更简单,但只适用于i386和amd64架构。

11.2 加载一个ELF

要创建一个ROP对象,只需向它传递一个ELF文件。

  1. elf = ELF('/bin/sh')
  2. rop = ROP(elf)

这将自动加载二进制文件,并从其中提取大多数简单的gadgets。例如,如果你想加载rbx寄存器。

  1. rop.rbx
  2. # Gadget(0x5fd5, ['pop rbx', 'ret'], ['rbx'], 0x8)

11.2.1 修复地址

在这里,我们可以看到gadgets的地址,它的反汇编内容,它加载了什么寄存器,以及gadgets执行时堆栈被调整了多少。

由于在我们的例子中,/bin/sh是地址无关的(即使用ASLR),我们可以先调整ELF对象上的加载地址。

  1. elf.address = 0xff000000
  2. rop = ROP(elf)
  3. rop.rbx
  4. # Gadget(0xff005fd5, ['pop rbx', 'ret'], ['rbx'], 0x8)

11.3 检查gadgets

你可以通过魔法访问器询问ROP对象如何加载你想要的任何寄存器。我们在上面使用了rbx,但是我们也可以寻找其他的寄存器。

  1. rop.rbx
  2. # Gadget(0xff005fd5, ['pop rbx', 'ret'], ['rbx'], 0x8)

如果寄存器不能被加载,返回值为None。在我们的例子中,假如没有pop rcx; ret的gadgets:

  1. rop.rcx
  2. # None

11.3.1 查看所有gadgets

Pwntools有意排除了大多数非实质性的gadgets,但你可以通过查看ROP.gadgets属性看到它已经加载的列表,该属性将一个gadgets的地址映射到gadgets本身。

  1. rop.gadgets
  2. # {4278225723: Gadget(0xff008b3b, ['add esp, 0x10', 'pop rbx', 'pop rbp', 'pop r12', 'ret'], ['rbx', 'rbp', 'r12'], 0x20),
  3. # 4278278088: Gadget(0xff0157c8, ['add esp, 0x130', 'pop rbp', 'ret'], ['rbp'], 0x138),
  4. # 4278284789: Gadget(0xff0171f5, ['add esp, 0x138', 'pop rbx', 'pop rbp', 'ret'], ['rbx', 'rbp'], 0x144),
  5. # 4278272966: Gadget(0xff0143c6, ['add esp, 0x18', 'ret'], [], 0x1c),
  6. # 4278239612: Gadget(0xff00c17c, ['add esp, 0x20', 'pop rbx', 'pop rbp', 'pop r12', 'ret'], ['rbx', 'rbp', 'r12'], 0x30),
  7. # 4278259611: Gadget(0xff010f9b, ['add esp, 0x28', 'pop rbp', 'pop r12', 'ret'], ['rbp', 'r12'], 0x34),
  8. # ...
  9. # 4278216828: Gadget(0xff00687c, ['pop rsp', 'pop r13', 'ret'], ['rsp', 'r13'], 0xc),
  10. # 4278214225: Gadget(0xff005e51, ['pop rsp', 'ret'], ['rsp'], 0x8),
  11. # 4278210586: Gadget(0xff00501a, ['ret'], [], 0x4)}

11.3.2 真正查看所有的gadgets

Pwntools的ROP过滤掉了非实质性的小工具,所以如果它没有你想要的东西,我们建议使用ROPGadget来检查二进制文件。

11.4 添加原始数据

为了将原始数据添加到ROP栈中,只需调用ROP.raw()

  1. rop.raw(0xdeadbeef)
  2. rop.raw(0xcafebabe)
  3. rop.raw('asdf')

11.5 导出ROP栈

现在我们有了一些gadgets,让我们看看ROP栈上有什么:

  1. print(rop.dump())
  2. # 0x0000: 0xdeadbeef
  3. # 0x0004: 0xcafebabe
  4. # 0x0008: b'asdf' 'asdf'

11.6 提取原始字节

现在我们有了一个ROP栈,我们想从它那里得到原始字节。我们可以使用byte()方法来实现这个功能。

  1. print(hexdump(bytes(rop)))
  2. # 00000000 ef be ad de be ba fe ca 61 73 64 66 │····│····│asdf│
  3. # 0000000c

11.7 神奇地调用函数

Pwntools的ROP工具的真正威力在于能够调用任意的函数,无论是通过神奇的访问器还是通过ROP.call()例程。

  1. elf = ELF('/bin/sh')
  2. rop = ROP(elf)
  3. rop.call(0xdeadbeef, [0, 1])
  4. print(rop.dump())
  5. # 0x0000: 0xdeadbeef 0xdeadbeef(0, 1, 2, 3)
  6. # 0x0004: b'baaa' <return address>
  7. # 0x0008: 0x0 arg0
  8. # 0x000c: 0x1 arg1

注意这里它使用的是32位ABI,这是不正确的。我们也可以对64位二进制文件进行ROP,但我们需要相应地设置context.arch。我们可以使用context.binary来自动完成这个工作。

  1. context.binary = elf = ELF('/bin/sh')
  2. rop = ROP(elf)
  3. rop.call(0xdeadbeef, [0, 1])
  4. print(rop.dump())
  5. # 0x0000: 0x61aa pop rdi; ret
  6. # 0x0008: 0x0 [arg0] rdi = 0
  7. # 0x0010: 0x5f73 pop rsi; ret
  8. # 0x0018: 0x1 [arg1] rsi = 1
  9. # 0x0020: 0xdeadbeef

11.8 使用函数名来调用函数

如果你的库在其GOT/PLT中有你想调用的函数,或者有二进制的符号,你可以直接调用函数名。

  1. context.binary = elf = ELF('/bin/sh')
  2. rop = ROP(elf)
  3. rop.execve(0xdeadbeef)
  4. print(rop.dump())
  5. # 0x0000: 0x61aa pop rdi; ret
  6. # 0x0008: 0xdeadbeef [arg0] rdi = 3735928559
  7. # 0x0010: 0x5824 execve

11.9 多重ELF

一般来说,在你的进程的地址空间中,一次有一个以上的ELF可用。让我们看一个使用/bin/sh以及其libc的例子。最初,我们看了rop.rcx,这个gadgets是不存在的,因为bash中没有pop rcx; ret这个gadgets。然后,现在我们也有来自libc的所有gadgets了。

  1. context.binary = elf = ELF('/bin/sh')
  2. libc = elf.libc
  3. elf.address = 0xAA000000
  4. libc.address = 0xBB000000
  5. rop.rax
  6. # Gadget(0xaa00eb87, ['pop rax', 'ret'], ['rax'], 0x10)
  7. rop.rbx
  8. # Gadget(0xaa005fd5, ['pop rbx', 'ret'], ['rbx'], 0x10)
  9. rop.rcx
  10. # Gadget(0xbb09f822, ['pop rcx', 'ret'], ['rcx'], 0x10)
  11. rop.rdx
  12. # Gadget(0xbb117960, ['pop rdx', 'add rsp, 0x38', 'ret'], ['rdx'], 0x48)

注意raxrbx的gadgets是在主二进制文件中(0xAA…),而后两个是在libc(0xBB…)。

现在,让我们做一个更复杂的函数调用吧!

  1. rop.memcpy(0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc)
  2. print(rop.dump())
  3. # 0x0000: 0xbb11c1e1 pop rdx; pop r12; ret
  4. # 0x0008: 0xcccccccc [arg2] rdx = 3435973836
  5. # 0x0010: b'eaaafaaa' <pad r12>
  6. # 0x0018: 0xaa0061aa pop rdi; ret
  7. # 0x0020: 0xaaaaaaaa [arg0] rdi = 2863311530
  8. # 0x0028: 0xaa005f73 pop rsi; ret
  9. # 0x0030: 0xbbbbbbbb [arg1] rsi = 3149642683
  10. # 0x0038: 0xaa0058a4 memcpy

请注意,Pwntools能够使用pop rdx; pop r12; retgadgets,并说明堆栈上需要的额外值。还要注意的是,每个项目的符号值都在rop.dump()中获取。例如,它显示我们正在设置rdx=3435973836

11.10 获取一个shell

当我们了解了pwntools的ROP功能时,获得一个shell是很容易的!我们直接调用execve,并从内存中的某个地方找到一个"/bin/sh/x00 "的实例作为第一个参数传递进去。

  1. context.binary = elf = ELF('/bin/sh')
  2. libc = elf.libc
  3. elf.address = 0xAA000000
  4. libc.address = 0xBB000000
  5. rop = ROP([elf, libc])
  6. binsh = next(libc.search(b"/bin/sh\x00"))
  7. rop.execve(binsh, 0, 0)

显示我们的ROP栈

  1. print(rop.dump())
  2. # 0x0000: 0xbb11c1e1 pop rdx; pop r12; ret
  3. # 0x0008: 0x0 [arg2] rdx = 0
  4. # 0x0010: b'eaaafaaa' <pad r12>
  5. # 0x0018: 0xaa0061aa pop rdi; ret
  6. # 0x0020: 0xbb1b75aa [arg0] rdi = 3139138986
  7. # 0x0028: 0xaa005f73 pop rsi; ret
  8. # 0x0030: 0x0 [arg1] rsi = 0
  9. # 0x0038: 0xaa005824 execve

提取ROP的原始字节

  1. print(hexdump(bytes(rop)))
  2. # 00000000 e1 c1 11 bb 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
  3. # 00000010 65 61 61 61 66 61 61 61 aa 61 00 aa 00 00 00 00 │eaaa│faaa│·a··│····│
  4. # 00000020 aa 75 1b bb 00 00 00 00 73 5f 00 aa 00 00 00 00 │·u··│····│s_··│····│
  5. # 00000030 00 00 00 00 00 00 00 00 24 58 00 aa 00 00 00 00 │····│····│$X··│····│
  6. # 00000040

十二、日志

Pwntools有一个丰富的内部调试系统,可用于你自己的调试,以及弄清Pwntools幕后发生的事情。

12.1 功能

当你从pwn导入*时,日志功能就导入了。这些功能如下:

  • error
  • warn
  • info
  • debug

例如:

  1. >>> warn('Warning!')
  2. [!] Warning!
  3. >>> info('Info!')
  4. [*] Info!
  5. >>> debug('Debug!')

注意,最后一行默认不显示,因为默认的日志级别是 “info”。

你可以在你的开发脚本中使用这些,而不是打印,这可以让你准确地调控你看到的调试信息量

你可以通过各种方式控制哪些日志信息是可见的,所有这些都将在下面解释。

12.2 命令行

最简单的方法是在运行你的脚本时加入神奇的参数DEBUG,例如:打开最大限度的日志记录功能:

  1. $ python exploit.py DEBUG

这对于查看正在发送/接收的确切字节,以及在pwntools内部发生的事情,以使你的EXP发挥作用是很有用的。

12.3 环境

你也可以通过context.log_level来设置日志的粗略程度,就像你设置目标架构等的方式一样。这与在命令行中控制所有的日志语句的方式相同。

  1. >>> context.log_level = 'debug'

log_console

默认情况下,所有的日志都转到STDOUT。如果你想把它改成一个不同的文件,例如STDERR,你可以通过log_console设置来实现。

  1. >>> context.log_console = sys.stderr

log_file

有时你想让你的日志转到一个特定的文件,例如log.txt,以便以后查看。你可以通过设置context.log_file来添加一个日志文件。

  1. >>> context.log_file = './log.txt'

12.4 管道

每个管子在创建时都可以单独控制其日志的粗略程度。只需将level='...'传递给对象的构造。

  1. >>> io = process('sh', level='debug')
  2. [x] Starting local process '/usr/bin/sh' argv=[b'sh']
  3. [+] Starting local process '/usr/bin/sh' argv=[b'sh'] : pid 34475
  4. >>> io.sendline('echo hello')
  5. [DEBUG] Sent 0xb bytes:
  6. b'echo hello\n'
  7. >>> io.recvline()
  8. [DEBUG] Received 0x6 bytes:
  9. b'hello\n'
  10. b'hello\n'

这适用于所有的管子(processremote等),也适用于类似管子的东西(如gdb.attachgdb.debug)以及其他许多例程。

例如,如果你想确切地看到一些shellcode是如何组装的。

  1. >>> asm('nop', log_level='debug')
  2. [DEBUG] cpp -C -nostdinc -undef -P -I/home/user/pwntools/pwnlib/data/includes /dev/stdin
  3. [DEBUG] Assembling
  4. .section .shellcode,"awx"
  5. .global _start
  6. .global __start
  7. _start:
  8. __start:
  9. .intel_syntax noprefix
  10. nop
  11. [DEBUG] /usr/bin/x86_64-linux-gnu-as -32 -o /tmp/user/pwn-asm-0yy12n6i/step2 /tmp/user/pwn-asm-0yy12n6i/step1
  12. [DEBUG] /usr/bin/x86_64-linux-gnu-objcopy -j .shellcode -Obinary /tmp/user/pwn-asm-0yy12n6i/step3 /tmp/user/pwn-asm-0yy12n6i/step4
  13. b'\x90'

12.5 范围

有时你希望所有的日志都被启用,但只针对部分漏洞脚本。你可以手动切换context.log_level,或者你可以使用一个范围内的助手。

  1. io = process(...)
  2. with context.local(log_level='debug'):
  3. # Things inside the 'with' block are logged verbosely
  4. io.recvall()