0


Pwntools 2022简明手册

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

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

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

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

一、简介

Pwntools

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

有些代码每个人都写过无数次,而且每个人都有自己的方法。Pwntools的目标是以半标准的方式提供所有这些,这样你就可以停止复制粘贴相同的

struct.unpack('>I', x)

代码,而是使用更多稍微清晰的包装器,如

pac

k或

p32

甚至

p64(..., endian='big', sign=True)

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

最后但并非最不重要的是,它还包括一系列用于中级到高级使用情况的开发协助工具。这些工具包括给定内存泄露基元的远程符号解析(

MemLeak

DynELF

),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是唯一 "官方支持 "的平台,因为它们是官方对软件进行自动化测试的唯二平台。

$ apt-get update
$ apt-getinstall python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
$ python3 -m pip install --upgrade pip
$ python3 -m pip install --upgrade pwntools

3.1 验证安装

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

$ python -c 'from pwn import *'

3.2 其它架构

如果你想为其它的架构组装或反汇编代码,你需要安装一个合适的

binutils

。对于Ubuntu和Mac OS X用户,安装说明可在docs.pwntools.com上找到。

$ apt-getinstall 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 进程和基本功能

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

from pwn import*

io = process('sh')
io.sendline('echo Hello, world')
io.recvline()# 'Hello, world\n'

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

from pwn import*

io = process(['sh','-c','echo $MYENV'], env={'MYENV':'MYVAL'})
io.recvline()# 'MYVAL\n'

读取二进制数据也不是一个问题。你可以用

recv

接收多达若干字节的数据,或者用

recvn

接受精确的字节数。

from pwn import*

io = process(['sh','-c','echo A; sleep 1; echo B; sleep 1; echo C; sleep 1; echo DDD'])

io.recv()# 'A\n'

io.recvn(4)# 'B\nC\n'hex(io.unpack())# 0xa444444

4.3 会话互动

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

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

io.interactive()

4.4 网络

创建一个网络连接也很容易,而且有完全相同的接口。一个

remote

对象连接到其他地方,而一个

listen

对象则在等待连接。

from pwn import*

io = remote('google.com',80)
io.send('GET /\r\n\r\n')
io.recvline()# 'HTTP/1.0 200 OK\r\n'

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

from pwn import*

dns  = remote('8.8.8.8',53, typ='udp')
tcp6 = remote('google.com',80, fam='ipv6')

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

from pwn import*

client = listen(8080).wait_for_connection()

4.5 安全的Shell

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

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

from pwn import*

session = ssh('bandit0','bandit.labs.overthewire.org', password='bandit0')

io = session.process('sh', env={"PS1":""})
io.sendline('echo Hello, world!')
io.recvline()# 'Hello, world!\n'

4.6 串行端口

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

from pwn import*

io = serialtube('/dev/ttyUSB0', baudrate=115200)

五、实用功能

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

import struct

defp(x):return struct.pack('I', x)defu(x):return struct.unpack('I', x)[0]1234== u(p(1234))

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

from pwn import*1234== unpack(pack(1234))

5.1 打包和解包整数

这可能是你最常做的事情,所以它在最前面。主要的

pack

unpack

函数都知道

context

中的全局设置,如

endian

bits

sign

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

pack(1)# '\x01\x00\x00\x00'

pack(-1)# '\xff\xff\xff\xff'

pack(2**32-1)# '\xff\xff\xff\xff'

pack(1, endian='big')# '\x00\x00\x00\x01'

p16(1)# '\x01\x00'hex(unpack('AAAA'))# '0x41414141'hex(u16('AA'))# '0x4141'

5.2 文件I/O

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

from pwn import*

write('filename','data')
read('filename')# 'data'
read('filename',1)# 'd'

5.3 哈希和编码

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

Base64

'hello'== b64d(b64e('hello'))

Hashes

md5sumhex('hello')=='5d41402abc4b2a76b9719d911017c592'
write('file','hello')
md5filehex('file')=='5d41402abc4b2a76b9719d911017c592'
sha1sumhex('hello')=='aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d'

URL Encoding

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

Hex Encoding

enhex('hello')# '68656c6c6f'
unhex('776f726c64')# 'world'

Bit Manipulation and Hex Dumping

bits(0b1000001)== bits('A')# [0, 0, 0, 1, 0, 1, 0, 1]
unbits([0,1,0,1,0,1,0,1])# 'U'

Hex Dumping

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

5.4 样例生成

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

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

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

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

cyclic_find(0x61616178)# 92

六、Bytes vs. Strings

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

commit e692277db8533eaf62dd3d2072144ccf0f673b2e
Author: Morten Brøns-Pedersen <[email protected]>
Date:   Thu Jun 717:34:482012+0200

    ALL THE THINGS

多年来在Python中编写的许多EXP都假定

str

对象与

bytes

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

6.1 Python2

在Python2中,

str

类和

bytes

类是一样的,而且有一个1:1的映射。从来不需要对任何东西调用

encode

decode

– 文本就是字节,字节就是文本。

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

从来没有人使用

unicode

对象来编写漏洞,所以

unicode

到字节的转换极其罕见。

6.2 Python3

在 Python3 中,

unicode

类实际上就是

str

类。这有一些直接和明显的影响。

乍一看,Python3似乎让事情变得更难了,因为

bytes

声明的是单个的八位数(正如名字

bytes

所暗示的),而

str

用于任何基于文本的数据表示。

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

>>> r.send('❤️')[DEBUG] Sent 0x6bytes:00000000  e2 9d a4 ef  b8 8f                                  │····│··│
    00000006>>> r.send('\x00\xff\x7f\x41\x41\x41\x41')[DEBUG] Sent 0x7bytes:0000000000 ff 7f41414141                               │···A│AAA│
    00000007

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

>>> shellcode ="\x99\xf7\xe2">>>print(hexdump(flat("padding\x00", shellcode)))0000000070616464696e6700  c2 99 c3 b7  c3 a2        │padd│ing·│····│··│
0000000e

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

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

>>> shellcode =b"\x99\xf7\xe2">>>print(hexdump(flat(b"padding\x00", shellcode)))0000000070616464696e670099 f7 e2                  │padd│ing·│···│
0000000b

好极了!

一般来说,Python3上的Pwntools的修复方法是确保你所有的字符串都有一个

b

前缀。这就解决了歧义,并使一切变得简单明了。

6.3 麻烦

关于Python3的

bytes

对象,有一个值得一提的 “麻烦”。当对它们进行迭代时,你会得到整数,而不是

bytes

对象。这是与Python2的巨大差异,也是一个主要的烦恼。

>>> x=b'123'>>>for i in x:...print(i)...495051

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

bytes

的对象。

>>>for i inrange(len(x)):...print(x[i:i+1])...b'1'b'2'b'3'

七、环境

context

对象是一个全局的、线程感知的对象,包含了

pwntools

使用的各种设置。

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

from pwn import*
context.arch ='amd64'

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

7.1 环境设置

arch

目标架构。有效值是

"arch64"

"arm"

"i386"

"amd64"

,等等。默认是

"i386"

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

bits

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

32

64

binary

从ELF文件中获取配置。例如:

context.binary='/bin/sh'

log_file

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

log_level

日志的详细程度。有效值是整数(越小越详细),以及

"debug"

"info "

"error "

等字符串值。

sign

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

"unsigned"

terminal

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

x-terminal-emulator

tmux

timeout

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

update

一次设置多个值,例如

context.update(arch='mips', bits=64, endian='big')

八、ELFs

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

8.1 加载ELF文件

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

from pwn import*

e = ELF('/bin/bash')# [*] '/bin/bash'#     Arch:     amd64-64-little#     RELRO:    Partial RELRO#     Stack:    Canary found#     NX:       NX enabled#     PIE:      No PIE#     FORTIFY:  Enabled

8.2 使用符号表

ELF文件有几组不同的符号表可用,每组都包含在

{name: data}

的字典中。

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

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

from pwn import*

e = ELF('/bin/bash')print"%#x -> license"% e.symbols['bash_license']print"%#x -> execve"% e.symbols['execve']print"%#x -> got.execve"% e.got['execve']print"%#x -> plt.execve"% e.plt['execve']print"%#x -> list_all_jobs"% e.functions['list_all_jobs'].address

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

0x4ba738-> license
0x41db60-> execve
0x6f0318-> got.execve
0x41db60-> plt.execve
0x446420-> list_all_jobs

8.3 改变基本地址

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

from pwn import*

e = ELF('/bin/bash')print"%#x -> base address"% e.address
print"%#x -> entry point"% e.entry
print"%#x -> execve"% e.symbols['execve']print"---"
e.address =0x12340000print"%#x -> base address"% e.address
print"%#x -> entry point"% e.entry
print"%#x -> execve"% e.symbols['execve']

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

0x400000-> base address
0x42020b-> entry point
0x41db60-> execve
---0x12340000-> base address
0x1236020b-> entry point
0x1235db60-> execve

8.4 读取ELF文件

我们可以通过pwntools直接与ELF互动,就像它被加载到内存中一样,使用

read

write

和与

packing

模块中的函数命名相同。此外,你可以通过

disasm

方法看到反汇编。

from pwn import*

e = ELF('/bin/bash')printrepr(e.read(e.address,4))

p_license = e.symbols['bash_license']
license   = e.unpack(p_license)print"%#x -> %#x"%(p_license, license)print e.read(license,14)print e.disasm(e.symbols['main'],12)

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

'\x7fELF'
0x4ba738 -> 0x4ba640
License GPLv3+
  41eab0:       41 57                   push   r15
  41eab2:       41 56                   push   r14
  41eab4:       41 55                   push   r13

8.5 对ELF文件进行修补

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

from pwn import*

e = ELF('/bin/bash')# Cause a debug break on the 'exit' command
e.asm(e.symbols['exit_builtin'],'int3')# Disable chdir and just print it out instead
e.pack(e.got['chdir'], e.plt['puts'])# Change the license
p_license = e.symbols['bash_license']
license = e.unpack(p_license)
e.write(license,'Hello, world!\n\x00')

e.save('./bash-modified')

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

$ chmod +x ./bash-modified
$ ./bash-modified -c 'exit'
Trace/breakpoint trap(core dumped)
$ ./bash-modified --version |grep"Hello"
Hello, world!
$ ./bash-modified -c 'cd "No chdir for you!"'
/home/user/No chdir for you!
No chdir for you!
./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

,表示它应该只返回可写段的地址。

from pwn import*

e = ELF('/bin/bash')for address in e.search('/bin/sh\x00'):printhex(address)

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

0x420b82
0x420c5e

8.7 构建ELF文件

通过pwntools我们可以很方便地从头开始创建一个ELF文件。所有这些功能都是上下文感知的。相关的函数是

from_bytes

from_assembly

。每一个都返回一个

ELF

对象,它可以很容易地被保存到文件中。

from pwn import*

ELF.from_bytes('\xcc').save('int3-1')
ELF.from_assembly('int3').save('int3-2')
ELF.from_assembly('nop', arch='powerpc').save('powerpc-nop')

8.8 运行和调试ELF文件

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

>>> io = elf.process()# vs>>> io = process(elf.path)

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

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

九、汇编

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

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

9.1 基础汇编

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

from pwn import*printrepr(asm('xor edi, edi'))# '1\xff'print enhex(asm('xor edi, edi'))# 31ff

9.2 现成的汇编(

shellcraft

shellcraft

模块会提供给你一些现成的汇编代码。它通常是可定制的。找出存在哪些

shellcraft

模板的最简单方法是查看RTD上的文档。

from pwn import*help(shellcraft.sh)print'---'print shellcraft.sh()print'---'print enhex(asm(shellcraft.sh()))
Help on function sh in module pwnlib.shellcraft.internal:

sh()
    Execute /bin/sh
---
    /* push '/bin///sh\x00' */
    push 0x68
    push 0x732f2f2f
    push 0x6e69622f

    /* call execve('esp', 0, 0) */
    push (SYS_execve) /* 0xb */
    pop eax
    mov ebx, esp
    xor ecx, ecx
    cdq /* edx=0 */
    int 0x80
---
6a68682f2f2f73682f62696e6a0b5889e331c999cd80

9.3 命令行工具

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

  • asm
  • disasm
  • shellcraft

asm

asm

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

$ asm nop
90

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

$ asm nop | xxd
0000000: 90.

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

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

最后,它支持一些不同的选项,通过

--format

选项来指定输出格式。支持的参数有

raw

hex

string

elf

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

disasm

Disasm是

asm

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

$ disasm cd80
   0:    cd80                    int    0x80
$ asm nop | disasm
   0:    90                       nop

shellcraft

shellcraft

命令是内部

shellcraft

模块的命令行接口。在命令行中,必须按

arch.os.template

的顺序指定完整的环境信息。

$ shellcraft i386.linux.sh
6a68682f2f2f73682f62696e6a0b5889e331c999cd80

9.4 异构架构

为其它非X86架构进行汇编交互,你需要自行安装适当版本的

binutils

。你应该看看installing.md以了解更多这方面的信息。我们唯一需要改变的是在全局环境变量中设置架构。你可以在 context.md 中看到更多关于

context

的信息。

from pwn import*

context.arch ='arm'printrepr(asm('mov r0, r1'))# '\x01\x00\xa0\xe1'print enhex(asm('mov r0, r1'))# 0100a0e1
9.4.1 现成汇编
shellcraft

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

from pwn import*

context.arch ='arm'print shellcraft.sh()print enhex(asm(shellcraft.sh()))
    adr r0, bin_sh
    mov r2, #0
    mov r1, r2
    svc SYS_execve
bin_sh: .asciz "/bin/sh"

08008fe20020a0e30210a0e10b0000ef2f62696e2f736800
9.4.2 命令行工具

你也可以通过使用

--context

命令行选项,使用命令行来汇编生成其它架构的

shellcode

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

十、调试

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

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

  • Pwndbg
  • GDB Enhanced Features (GEF)

10.1 先前条件

你的机器上应该同时安装了

gdb

gdbserver

。你可以用

which gdb

which gdbserver

来轻松检查。

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

$ sudoapt-getinstall gdb gdbserver

10.2 在GDB下启动一个进程

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

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

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

这应该会自动在一个新的窗口中启动调试器,以便你进行交互。如果不是这样,或者你看到关于

context.terminal

的错误,请查看指定终端窗口的章节。

在这个例子中,我们传入了

gdbscript='continue'

,以使调试器恢复执行,但是你可以传入任何有效的GDB脚本命令,它们将在调试进程启动时被执行。

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

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

10.3.1 本地进程

一般来说,你会创建一个

process()

管道,以便与目标可执行文件交互。你可以简单地把它传递给

gdb.attach()

,它将神奇地打开一个新的终端窗口,在调试器中运行目标二进制文件。

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

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

10.3.2 远程服务器

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

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

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

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

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

它是有效的!为了调试特定的

bash

进程,只要把它我们的远程对象传给

gdb.attach()

。Pwntools将查找连接的远程端的PID,并尝试自动连接到它。

>>> gdb.attach(io)

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

10.3 调试异构架构

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

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

gdb.debug(...)

来代替调用

process(...)
>>> gdb.debug(elf.path).recvall()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会话设置。

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

>>> io = gdb.debug('/bin/sh', log_level='debug')[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'][+] 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[DEBUG] Received 0x25bytes:b'Process /bin/sh created; pid = 34286\n'[DEBUG] Received 0x18bytes:b'Listening on port 45145\n'[DEBUG] Wrote gdb script to '/tmp/user/pwnxcd1zbyx.gdb'
    target remote 127.0.0.1:45145[*] running in new terminal:/usr/bin/gdb -q  "/bin/sh"-x /tmp/user/pwnxcd1zbyx.gdb
[DEBUG] Launching a new terminal:['/usr/local/bin/tmux','splitw','/usr/bin/gdb -q  "/bin/sh" -x /tmp/user/pwnxcd1zbyx.gdb'][DEBUG] Received 0x25bytes: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进行水平分割,而不是默认设置。

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

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

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

你可以指定任何你喜欢的终端,甚至可以把设置放在

~/.pwn.conf

里面,这样它就会被用于你的所有脚本了

[context]
terminal=['x-terminal-emulator','-e']
10.4.3 环境变量

Pwntools允许你通过

process()

指定任何你喜欢的环境变量,对于

gdb.debug()

也是如此。

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

CWD

不幸的是,当使用

gdb.debug()

时,该进程是在

gdbserver

下启动的,它增加了自己的环境变量。当环境必须被非常仔细地控制时,这可能会带来复杂的情况。

>>> io = gdb.debug(['env'], env={'FOO':'BAR'}, gdbscript='continue')>>>print(io.recvallS())=/home/user/bin/gdbserver
FOO=BAR

Child exited with status 0
GDBserver exiting

这只在你用

gdb.debug()

在调试器下启动进程时发生。如果你能够启动你的进程,然后用

gdb.attach()

附加,你就可以避免这个问题。

环境变量排序

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

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

collection.OrderedDict

10.4.4 无法附加到进程中

现代的Linux系统有一个叫做

trace_scope

的设置,它可以阻止非子进程的进程被调试。Pwntools对于它自己启动的任何进程都能解决这个问题,但是如果你必须在Pwntools之外启动一个进程,并试图通过pid附加到它(例如

gdb.attach(1234)

),你可能被阻止附加。

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

sudotee /etc/sysctl.d/10-ptrace.conf <<EOF
kernel.yama.ptrace_scope = 0
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

文件。

elf = ELF('/bin/sh')
rop = ROP(elf)

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

rbx

寄存器。

rop.rbx
# Gadget(0x5fd5, ['pop rbx', 'ret'], ['rbx'], 0x8)
11.2.1 修复地址

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

由于在我们的例子中,

/bin/sh

是地址无关的(即使用ASLR),我们可以先调整ELF对象上的加载地址。

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

11.3 检查gadgets

你可以通过魔法访问器询问ROP对象如何加载你想要的任何寄存器。我们在上面使用了

rbx

,但是我们也可以寻找其他的寄存器。

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

如果寄存器不能被加载,返回值为

None

。在我们的例子中,假如没有

pop rcx; ret

的gadgets:

rop.rcx
# None
11.3.1 查看所有gadgets

Pwntools有意排除了大多数非实质性的gadgets,但你可以通过查看

ROP.gadgets

属性看到它已经加载的列表,该属性将一个gadgets的地址映射到gadgets本身。

rop.gadgets
# {4278225723: Gadget(0xff008b3b, ['add esp, 0x10', 'pop rbx', 'pop rbp', 'pop r12', 'ret'], ['rbx', 'rbp', 'r12'], 0x20),#  4278278088: Gadget(0xff0157c8, ['add esp, 0x130', 'pop rbp', 'ret'], ['rbp'], 0x138),#  4278284789: Gadget(0xff0171f5, ['add esp, 0x138', 'pop rbx', 'pop rbp', 'ret'], ['rbx', 'rbp'], 0x144),#  4278272966: Gadget(0xff0143c6, ['add esp, 0x18', 'ret'], [], 0x1c),#  4278239612: Gadget(0xff00c17c, ['add esp, 0x20', 'pop rbx', 'pop rbp', 'pop r12', 'ret'], ['rbx', 'rbp', 'r12'], 0x30),#  4278259611: Gadget(0xff010f9b, ['add esp, 0x28', 'pop rbp', 'pop r12', 'ret'], ['rbp', 'r12'], 0x34),# ...#  4278216828: Gadget(0xff00687c, ['pop rsp', 'pop r13', 'ret'], ['rsp', 'r13'], 0xc),#  4278214225: Gadget(0xff005e51, ['pop rsp', 'ret'], ['rsp'], 0x8),#  4278210586: Gadget(0xff00501a, ['ret'], [], 0x4)}
11.3.2 真正查看所有的gadgets

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

11.4 添加原始数据

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

ROP.raw()

rop.raw(0xdeadbeef)
rop.raw(0xcafebabe)
rop.raw('asdf')

11.5 导出ROP栈

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

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

11.6 提取原始字节

现在我们有了一个ROP栈,我们想从它那里得到原始字节。我们可以使用

byte()

方法来实现这个功能。

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

11.7 神奇地调用函数

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

ROP.call()

例程。

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

注意这里它使用的是32位ABI,这是不正确的。我们也可以对64位二进制文件进行ROP,但我们需要相应地设置

context.arch

。我们可以使用

context.binary

来自动完成这个工作。

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

11.8 使用函数名来调用函数

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

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

11.9 多重ELF

一般来说,在你的进程的地址空间中,一次有一个以上的ELF可用。让我们看一个使用

/bin/sh

以及其

libc

的例子。最初,我们看了

rop.rcx

,这个gadgets是不存在的,因为bash中没有

pop rcx; ret

这个gadgets。然后,现在我们也有来自libc的所有gadgets了。

context.binary = elf = ELF('/bin/sh')
libc = elf.libc

elf.address =0xAA000000
libc.address =0xBB000000

rop.rax
# Gadget(0xaa00eb87, ['pop rax', 'ret'], ['rax'], 0x10)
rop.rbx
# Gadget(0xaa005fd5, ['pop rbx', 'ret'], ['rbx'], 0x10)
rop.rcx
# Gadget(0xbb09f822, ['pop rcx', 'ret'], ['rcx'], 0x10)
rop.rdx
# Gadget(0xbb117960, ['pop rdx', 'add rsp, 0x38', 'ret'], ['rdx'], 0x48)

注意

rax

rbx

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

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

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

请注意,Pwntools能够使用

pop rdx; pop r12; ret

gadgets,并说明堆栈上需要的额外值。还要注意的是,每个项目的符号值都在

rop.dump()

中获取。例如,它显示我们正在设置

rdx=3435973836

11.10 获取一个shell

当我们了解了pwntools的ROP功能时,获得一个shell是很容易的!我们直接调用

execve

,并从内存中的某个地方找到一个

"/bin/sh/x00 "

的实例作为第一个参数传递进去。

context.binary = elf = ELF('/bin/sh')
libc = elf.libc

elf.address =0xAA000000
libc.address =0xBB000000

rop = ROP([elf, libc])

binsh =next(libc.search(b"/bin/sh\x00"))
rop.execve(binsh,0,0)

显示我们的ROP栈

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

提取ROP的原始字节

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

十二、日志

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

12.1 功能

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

  • error
  • warn
  • info
  • debug

例如:

>>> warn('Warning!')[!] Warning!
>>> info('Info!')[*] Info!
>>> debug('Debug!')

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

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

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

12.2 命令行

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

$ python exploit.py DEBUG

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

12.3 环境

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

>>> context.log_level ='debug'
log_console

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

>>> context.log_console = sys.stderr
log_file

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

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

12.4 管道

每个管子在创建时都可以单独控制其日志的粗略程度。只需将

level='...'

传递给对象的构造。

>>> io = process('sh', level='debug')[x] Starting local process '/usr/bin/sh' argv=[b'sh'][+] Starting local process '/usr/bin/sh' argv=[b'sh']: pid 34475>>> io.sendline('echo hello')[DEBUG] Sent 0xbbytes:b'echo hello\n'>>> io.recvline()[DEBUG] Received 0x6bytes:b'hello\n'b'hello\n'

这适用于所有的管子(

process

remote

等),也适用于类似管子的东西(如

gdb.attach

gdb.debug

)以及其他许多例程。

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

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

12.5 范围

有时你希望所有的日志都被启用,但只针对部分漏洞脚本。你可以手动切换

context.log_level

,或者你可以使用一个范围内的助手。

io = process(...)with context.local(log_level='debug'):# Things inside the 'with' block are logged verbosely
    io.recvall()
标签: 安全

本文转载自: https://blog.csdn.net/kelxLZ/article/details/123152529
版权归原作者 ZERO-A-ONE 所有, 如有侵权,请联系我们删除。

“Pwntools 2022简明手册”的评论:

还没有评论