Edited at

Pwntoolsの機能と使い方まとめ【日本語】#CTF #Pwn

Pwntoolsにある色々な機能を使いこなせていない気がしたので、調べてまとめた。


Pwntoolsとは

GallopsledというCTF チームがPwnableを解く際に使っているPythonライブラリ


pwntools is a CTF framework and exploit development library.

Written in Python, it is designed for rapid prototyping and development, and intended to make exploit writing as simple as possible.

引用: http://docs.pwntools.com/en/stable/index.html



Tube

プログラムの入出力に使われる機能


標準入出力


データの受信



  • recv(n): nバイト分のデータを受け取る


  • recvline(): 一行分(改行が来るまで)のデータを受け取る


  • recvuntil(hoge): hogeが来るまでデータを受け取る


  • recvregex(pattern): patternで指定された正規表現にマッチするまでデータを受け取る


  • recvrepeat(timeout): タイムアウトになるかEOFに達するまでデータを受け取る


  • clean(): バッファリングされたデータを破棄する


データの送信



  • send(data): dataを送信する


  • sendline(line): 引数に与えたデータに「\n」を付けて送信する


プロセスと基本機能

processを使うことでプロセスとやりとりできる。

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'


インタラクティブな通信

interactive()を使うことで任意のプロセスとインタラクティブに通信ができる。

from pwn import *

io = process('sh')

io.interactive()


ネットワーク通信

remoteを使うことでリモートホストと通信できる。(IPv4/IPv6のどちらにも対応)

from pwn import *

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


Utility

汎用的な機能


整数の操作

pack()unpack()を使って「int型 <=> bytes型」の変換ができる。



  • pack(int): 数値をbytes型に変換する


  • unpack(data): bytes型を数値に変換する

from pwn import *

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

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

hex(unpack('\xef\xbe\xad\xde'))
# 0xdeadbeef

エンディアンやワードサイズなどを指定するパラメータがある。



  • word_size: 変換された整数のワードサイズを指定(ビット単位)



    • 'all'を指定すると自動でいい感じになる

    • すでに指定してある関数がある。



      • p8(), u8()


      • p16(), u16()


      • p32(), u32()


      • p64(), u64()






  • endianness: 変換された整数のエンディアンを指定(デフォルトはリトルエンディアン)


    • リトルエンディアン: 'little'

    • ビッグエンディアン: 'big'




  • sign: 符号属性を指定(符号付きか否か)


    • 符号付き: 'signed'

    • 符号なし: 'unsigned'



from pwn import *

pack(0xdeadbeef, word_size='64')
# '\xef\xbe\xad\xde\x00\x00\x00\x00'

pack(0xdeadbeef, word_size='64', endian='little')
# '\xef\xbe\xad\xde\x00\x00\x00\x00'

pack(0xdeadbeef, word_size='64', endian='big')
# '\x00\x00\x00\x00\xde\xad\xbe\xef'

pack(0xdeadbeef, word_size='all', endian='little')
# '\xef\xbe\xad\xde'

pack(0xffffffff, word_size='all', endian='little', sign='unsigned')
# '\xff\xff\xff\xff\x00'

pack(0xffffffff, word_size='all', endian='little', sign='signed')
# '\xff\xff\xff\xff'

from pwn import *

p32(0xcafebabe) # '\xef\xbe\xad\xde'
hex(u32('\xef\xbe\xad\xde')) # 0xcafebabe

context.archを指定していれば、それを基に自動でいい感じにしてくれる。

from pwn import *

context.arch='i386'
pack(0xfacefeed) # '\xed\xfe\xce\xfa'

context.arch='amd64'
pack(0xfacefeed) # '\xed\xfe\xce\xfa\x00\x00\x00\x00'


ファイル入出力

ファイル入出力もすごく簡単にできる。

from pwn import *

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

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

read('filename', 3)
# 'dat'


ハッシュ化やエンコーディング

Pwntoolsにはデータを様々な形式に変換することができる関数が用意されている。

以下のような変換を簡単に行える。


  • base64

  • ハッシュ(md5, sha1等)

  • URLエンコーディング

  • 16進エンコード

  • ビット操作

  • hexダンプ


base64



  • b64e(s): 引数に与えら文字列をbase64エンコード


  • b64d(s): 引数に与えられた文字列をbase64デコード

from pwn import *

b64e('hello')
# 'aGVsbG8='

p64d('aGVsbG8=')
# 'hello'


ハッシュ(md5, sha1等)



  • md5sumhex(s): 引数に与えられた文字列のMD5チェックサムを計算し、16進エンコードした値を返す


  • md5filehex(file): 引数に与えられたfileの中の文字列のMD5チェックサムを計算し、16進エンコードした値を返す

from pwn import *

md5sumhex('hello')
# '5d41402abc4b2a76b9719d911017c592'

write('file', 'hello')
md5filehex('file')
# '5d41402abc4b2a76b9719d911017c592'

sha1sumhex('hello')
# 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d'


URLエンコーディング



  • urlencode(s): 引数に与えら文字列をURLエンコード


  • urldecode(s): 引数に与えら文字列をURLデコード

from pwn import *

urlencode('ABC, Hello!')
# '%41%42%43%2c%20%48%65%6c%6c%6f%21'

urldecode('%41%42%43%2c%20%48%65%6c%6c%6f%21')
# 'ABC, Hello!'


ビット操作



  • bits(s): 引数(文字列か数値)をビットのリストに変換する


  • unbits(s): イテラブルなビット列を文字列に変換する

from pwn import *

bits(0b01000001)
# [0, 1, 0, 0, 0, 0, 0, 1]

bits('A')
# [0, 1, 0, 0, 0, 0, 0, 1]

unbits([0, 1, 0, 0, 0, 0, 0, 1])
# 'A'

0と1を別の表現に置き換えることもできる。

from pwn import *

bits('A', zero='-', one='+')
# ['-', '+', '-', '-', '-', '-', '-', '+']

bits('A', zero='(=_=)', one='(@_@)')
# ['(=_=)', '(@_@)', '(=_=)', '(=_=)', '(=_=)', '(=_=)', '(=_=)', '(@_@)']


hexダンプ

from pwn import *

print hexdump(read('/dev/urandom', 32))
# 00000000 0e da 19 d2 ff 26 5e 78 52 a2 66 8e 20 32 57 ff │····│·&^x│R·f·│ 2W·│
# 00000010 cf 31 ef 76 90 ec 33 bd e0 83 a3 42 77 3d 3b fa │·1·v│··3·│···B│w=;·│
# 00000020


パターン文字列を生成する

BOFのオフセットを調べるときなどに使える関数がある。

普段は、gdbでpattcとかpattoとかを使っている。



  • cyclic(n): nバイトの長さのパターン文字列を生成する


  • cyclic_find(s): 部分文字列sの位置を計算する

from pwn import *

cyclic(64)
# 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaa'

cyclic_find('ahaa')
# 27

cyclic(64, alphabet=string.ascii_uppercase)
# 'AAAABAAACAAADAAAEAAAFAAAGAAAHAAAIAAAJAAAKAAALAAAMAAANAAAOAAAPAAA'

cyclic_find('FAAA', alphabet=string.ascii_uppercase)
# 20


Context

Pwntools内では対象のOSやアーキテクチャ、ビット幅などの設定はcontextというグローバル変数によって管理される。

以下のようなexploitの冒頭はよく見かけると思う。

from pwn import *

context.arch = 'amd64'

こうすることで、exploit内の諸設定をamd64向けのバイナリ用に自動で設定してくれる。

また、context.binaryを指定すれば、exploit内の様々な値を自動的にそのバイナリに適したものにしてくれる。

from pwn import *

context.binary = './challenge-binary'


Contextの設定


arch

対象のバイナリのアーキテクチャを設定する

設定できる項目は、'aarch64', 'arm', 'i386', 'amd64'などがある。(他にも結構ある。)

デフォルトは、'i386'


bits

対象のバイナリにおける1ワードのサイズ(32bitか64bitか)を設定する


binary

対象のアーキテクチャや、ビット幅、エンディアンをバイナリファイルから推測して設定する。

from pwn import *

context.clear()
context.arch, context.bits
# ('i386', 32)

context.binary = '/bin/bash'
context.arch, context.bits
# ('amd64', 64)

context.binary
# ELF('/bin/bash')


endian

対象のマシンのエンディアンを設定する。

必要なときに、'big'littleのどちらかを設定する。

デフォルトは、littleだがcontext.archによって変わる。


log_file

ログを出力するファイルを設定する。


log_level

ログの冗長性を設定する。

以下の文字列で指定することができる。(他にも整数で指定する方法もある。)


  • 'CRITICAL'

  • 'DEBUG'

  • 'ERROR'


  • 'INFO'(デフォルト)

  • 'NOTSET'

  • 'WARN'

  • 'WARNING'


sign

整数のパッキングとアンパッキングの際に、符号付き整数として処理するか、符号なし整数として処理するかを設定する。

必要なときに、'signed''unsigned'のどちらかを設定する。

デフォルトは、'unsigned'


terminal

新しいウィンドウを開く時に優先して使うターミナルソフトを指定する。

デバッグする時などに使う機能。

デフォルトでは、x-terminal-emulatortmuxを使うようになっている。


timeout

通信の際のデフォルトのタイムアウトまでのの時間を設定する。


update

一度に複数の値を設定できる。

context.os   = 'linux'

context.arch = 'arm'

としていたのが、

context.update(os='linux', arch='arm')

でよくなる。


ELFs

Pwntoolsでは、ELFファイルの操作が簡単にできるようになっている。


ELFファイルの読み込み

パスを指定することで、ELFファイルを読み込める。

ELFファイルが読み込まれると、ファイルのセキュリティ機構の情報がprintされる。

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 (0x400000)
# FORTIFY: Enabled


シンボルの利用

PwntoolsはELFファイル内の利用できるシンボルの情報を辞書型で保持している。(形式は、{name: data})



  • ELF.symbols: 以下のものを含む既知のシンボルの情報


  • ELF.got: GOTエントリの情報


  • ELF.plt: PLTエントリの情報


  • ELF.functions: 関数の情報(DWARFシンボルが必要)

これらを利用することで、アドレスのハードコーディングを避けられるため、exploitの堅牢性を高めることができる。


use_sym.py

from pwn import *

e = ELF('/bin/bash')

print '%#x -> write' % e.symbols['write']
print '%#x -> got.write' % e.got['write']
print '%#x -> plt.write' % e.plt['write']
print '%#x -> list_all_jobs' % e.functions['list_all_jobs'].address



use_sym.py(実行結果)

0x41d870 -> write

0x6f4148 -> got.write
0x41d870 -> plt.write
0x4479a0 -> list_all_jobs


ベースアドレスの変更

ASLR回避の際などにベースアドレスを調整するのも簡単にできる。


change_base_addr.py

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 = 0x12340000

print '%#x -> base address' % e.address
print '%#x -> entry point' % e.entry
print '%#x -> execve' % e.symbols['execve']



change_base_addr.py(実行結果)

0x400000 -> base address

0x420560 -> entry point
0x41dc20 -> execve
---
0x12340000 -> base address
0x12360560 -> entry point
0x1235dc20 -> execve


ELFファイルの調査

'/bin/sh'のアドレスなどを探すときに便利なsearch()というメソッドがある。

writable=Trueというパラメータを渡すと、書き込み可能なセクションからのみ検索してくれる。



  • search(s): 指定した文字列sを探して、イテレータを返す。


search_elf.py

from pwn import *

e = ELF('/bin/bash')

for address in e.search('/bin/sh\x00'):
print hex(address)



search_elf.py(実行結果)

[*] '/bin/bash'

Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
0x420c62
0x420d3e

実際のexploitでは以下のように使っている。

e = ELF('challenge-binary')

binsh = next(e.search('/bin/sh\x00'))


Assembly

Pwntoolsでは、ほとんどのアーキテクチャで簡単にアセンブラを実行できる。

また、カスタマイズできるシェルコードがあらかじめ用意されており、すぐに利用できるようになっている。


基本的な使い方

asm()を使うことで、アセンブラを機械語に変換することができる。

from pwn import *

print repr(asm('xor edi, edi'))
# '1\xff'

print enhex(asm('xor edi, edi'))
# 31ff

disasm()を使うことで、機械語をアセンブラに変換することができる。

from pwn import *

print repr(disasm('\x90'))
# ' 0: 90 nop'

print repr(disasm(asm('xor edi, edi')))
# ' 0: 31 ff xor edi,edi'


準備されているアセンブラ(shellcraft)

shellcraftを使うと、あらかじめ用意されているシェルコードを簡単に利用できる。

また、利用できるシェルコードは、カスタマイズ可能なものになっている。

どのようなシェルコードが用意されているかは、公式ドキュメントを参照。


shellcraft.py

from pwn import *

help(shellcraft.sh)
print '---'
print shellcraft.sh()
print '---'
print enhex(asm(shellcraft.sh()))



shellcraft.py(実行結果)

Help on function sh in module pwnlib.shellcraft.i386.linux:

sh()
Execute a different process.

>>> p = run_assembly(shellcraft.i386.linux.sh())
>>> p.sendline('echo Hello')
>>> p.recv()
'Hello\n'

---
/* 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

---
6a68682f2f2f73682f62696e89e368010101018134247269010131c9516a045901e15189e131d26a0b58cd80


他にもこんな感じで使える。

from pwn import *

print shellcraft.read()
# /* read(fd=0, buf=0, nbytes=0) */
# xor ebx, ebx
# xor ecx, ecx
# xor edx, edx
# /* call read() */
# push SYS_read /* 3 */
# pop eax
# int 0x80

print disasm(asm(shellcraft.write()))
# 0: 31 db xor ebx,ebx
# 2: 31 c9 xor ecx,ecx
# 4: 31 d2 xor edx,edx
# 6: 6a 04 push 0x4
# 8: 58 pop eax
# 9: cd 80 int 0x80


コマンドラインツール

Pwntoolsには、アセンブラを扱うコマンドが3つ用意されており、これらはコマンドラインで使用できる。

用意されているコマンドは以下の3つ。



  • asm: アセンブラ -> 機械語


  • disasm: 機械語 -> アセンブラ


  • shellcraft: 前述のshellcraftのコマンド版


asm

$ asm nop

90

$ asm 'xor edi, edi'
31ff

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

出力の形式を--formatオプションで指定できる。(短縮系は-f)

指定できる形式は、以下の4つ。


  • raw

  • hex

  • string

  • elf

$ asm nop --format=raw

\x90

$ asm nop --format=hex
90

$ asm nop --format=string
'\x90'

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


disasm

asmの逆をしてくれる。

$ disasm cd80

0: cd 80 int 0x80
$ asm nop | disasm
0: 90 nop


shellcraft

コマンド版shellcraftでは、contextをarch.os.templateという形式で全て指定する必要がある。

$ shellcraft i386.linux.sh

6a68682f2f2f73682f62696e6a0b5889e331c999cd80

$ shellcraft -fasm
/* 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


Fmtstr(FSAを自動化する機能群)

FSA(書式文字列攻撃)を自動化する機能がある。


関数の利用

fmtstr_payload()を利用することで、FSAのペイロードを自動で生成することができる。

引数で以下の4つのパラメータを指定することができる。



  • offset(int): フォーマット文字列までのオフセット


  • writes(dict): 書き込み先のアドレスと値のディクショナリ


    • ディクショナリの形式: {書き込み先アドレス:書き込む値}




  • numbwritten(int): printfが既に出力したバイト数


  • write_size(str): 何バイト毎に書き込むか(hhnかhnかnのやつ)



    • byte: 1バイトずつ


    • short: 2バイトずつ


    • int: 4バイトずつ



from pwn import *

fmtstr_payload(1, {0x0:0xcafebabe}, numbwritten=0, write_size='short')
# '\x00\x00\x00\x00\x02\x00\x00\x00%47798c%1$hn%4160c%2$hn'

fmtstr_payload(41, {0x0:0xcafebabe}, numbwritten=10, write_size='byte')
# '\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00%164c%41$hhn%252c%42$hhn%68c%43$hhn%204c%44$hhn'


FmtStrの利用

また、FmtStrというクラスを利用すると、FSA(書式文字列攻撃)を自動化できる。

FmtStrに与えるパラメータは以下の通り。



  • execute_fmt(function)(必須): 対象のプロセスとの通信を呼び出す関数


  • offset(int): フォーマット文字列までのオフセット


  • padlen(int): ペイロードの前に追加したいパディングのサイズ


  • numbwritten(int): printfが既に出力したバイト数

以下のメソッドを利用して、ペイロード生成に必要な情報をオブジェクトに伝えたり、実際に生成したペイロードを対象のプロセスに送信したりする。



  • write(addr, data): どこのaddrにどんなdataを書き込みたいかを(オブジェクトに)伝えるためのメソッド


  • execute_write(): ペイロードを生成し、オブジェクトを作る際に渡した関数を利用して対象のプロセスに送信する

from pwn import *

# ペイロードを送信するために呼び出される関数
def send_payload(payload):
log.info("payload = %s" % repr(payload))
p.sendline(payload)
return p.recv()

# FmtStrオブジェクトを作り、関数を渡す。
fmtstr = FmtStr(execute_fmt=send_payload) # ペイロードを送信する際に使う関数を渡す
fmtstr.write(0x12340000, 0xdeadbeef) # 0x12340000 に 0xdeadbeef を書き込む
fmtstr.write(0x56780000, 0xcafebabe) # 0x56780000 に 0xcafebabe を書き込む
fmtstr.execute_writes() # 対象のプロセスにペイロードを送信


GDB

GDBでデバッグもできる。

うまく使えないときはcontext.terminalの指定を確認してみると良いと思う。


よく使いそうな機能

主に、以下の2つの関数を使いそう。



  • attach(): 既存のプロセスにアタッチする


  • debug(): デバッガ上で新しいプロセスを起動し、最初の命令で止める

公式ドキュメントにも、以下のような記述があった。


The attach() and debug() functions will likely be your bread and butter for debugging.



attach()

新しいターミナルを開き、そこでGDBを起動して対象のプロセスにアタッチする関数。

引数に対象のプロセスを与えると、そのプロセスにアタッチできる。

プロセスは、pidによって指定したり、プロセス名などで渡すことができる。

以下のサンプルでは、プロセスIDが1234のプロセスにアタッチしている。

from pwn import *

gdb.attach(1234)

アタッチした後に実行するGDBスクリプトは、文字列かファイルの形式で与える。

from pwn import *

# プロセスを開始する
bash = process('bash')

# デバッガーにアタッチする
gdb.attach(bash, '''
set follow-fork-mode child
break execve
continue
'''
)

# 対話モードに切り替える
bash.sendline('whoami')


debug()

指定したコマンドラインでGDBサーバを起動して、GDBを起動してプロセスにアタッチする関数。

以下のようにして、使う。

from pwn import *

# 新しいプロセスを作りmainで止める
io = gdb.debug('bash', '''
break main
continue
'''
)

# bashにコマンドを送信
io.sendline('echo Hello!')

# 対話モードに切り替える
io.interactive()

debug()ではattach()と違い、GDBスクリプトは文字列で渡す必要がある。

また、ssh=を使いsshインスタンスを渡すことで、debug()はリモートマシン上に新しいプロセスを作ることができる。

from pwn import *

# SSHサーバーに接続
shell = ssh('passcode', 'pwnable.kr', 2222, password='guest')

gdbscript = '''
break main
continue
'''

# プロセスを起動
io = gdb.debug(['bash'], ssh=shell, gdbscript=gdbscript)

# bashにコマンドを送信
io.sendline('echo hello')

# 対話モードに切り替える
io.interactive()


デバッグ機能のポイント

attach()debug()も起動時にgdbにスクリプトを渡せる。

ブレークポイントを自動でセットしたりすることができる。


プロセスへのアタッチ

既存プロセスにアタッチするときはattach()を使う。

remoteオブジェクトを与えると、自動的にアタッチするべきプロセスを見つけてくれる。


プロセスのスポーン

プロセスの状態は様々なため、attach()だけでは対応しきれない。

例えば、mainの開始地点などの非常に早い段階でプロセスにアタッチして、デバッグしたいときは、debug()を使う。

なお、返り値はdebug()tubeオブジェクト。(remoteprocessと同様)

そのため、通常と同じようにプロセスとのやりとりができる。


その他の便利な機能

他にも使えそうな関数が2つあった。



  • debug_assembly(): ELFファイルを作ってデバッガ上で起動する


    • 定義されたシンボルがGDBで使える


    • asm()の明示的な呼び出しを保持する




  • debug_shellcode(): ELFファイルを作ってデバッガ上で起動する


debug_assembly()

引数に与えたアセンブラからELFファイルを作ってデバッガ上で動かせる。

後述のdebug_shellcode()も同じ機能を持つが、debug_assembly()は以下の2点の特徴があり、その点でdebug_shellcode()とは異なる。


  • 定義されたシンボルがGDBで使える


  • asm()の明示的な呼び出しを保持する

from pwn import *

assembly = shellcraft.echo('Hello, world!')

io = gdb.debug_assembly(assembly)

io.recvline()
# 'Hello world!'


debug_shellcode()

引数に与えたアセンブラからELFファイルを作ってデバッガ上で動かせる。

from pwn import *

assembly = shellcraft.echo('Hello, world!')
shellcode = asm(assembly)

io = gdb.debug_shellcode(shellcode)

io.recvline()
# 'Hello, world!'


ROP

ROP(Return Oriented Programming)用の機能

バイナリの中のシンボルを検索するROPオブジェクトを作り、それを利用してROPチェーンを構築していく。

from pwn import *

elf = ELF('challenge-binary')
rop = ROP(elf)


機能

先に、一通り使えそうな機能を紹介する。



  • raw(): valueをROPチェーンに加える。


  • call(): 関数などの呼び出しなどをROPチェーンに加える。


  • find_gadget(instructions): 引数で指定された命令のROP Gadgetを探す。


  • dump(): ROPチェーンを読みやすい形式でダンプする。


  • chain(): ROPチェーンをビルドする。


raw()

引数に与えた値をROPチェーンに加えてくれる。

引数に与える値はstr型かint型で渡す。

from pwn import *

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

rop.raw(0)
rop.raw(0xdeadbeef)
rop.raw('AAAAAAAA')

print rop.dump()
# 0x0000: 0x0
# 0x0004: 0xdeadbeef
# 0x0008: 'AAAA' 'AAAAAAAA'
# 0x000c: 'AAAA'


call()

関数などの呼び出しなどをROPチェーンに加える。

以下のような形式で使う。



  • call(resolvable, arguments=())



    • resolvable(str, int): 呼び出したい関数のシンボル名やそのアドレスなど


    • argument(list): 呼び出す対象に与える引数



from pwn import *

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

rop.call('read', [0, 0xdeadbeef, 4])

print rop.dump()
# 0x0000: 0x41dbb0 read(0, 3735928559, 4)
# 0x0004: 'baaa' <return address>
# 0x0008: 0x0 arg0
# 0x000c: 0xdeadbeef arg1
# 0x0010: 0x4 arg2

# このように短縮して書くこともできる
# 次のフレームのための調節も自動的にやってくれる
rop.write(1, 0xcafebabe, 4)

print rop.dump()
# 0x0000: 0x41dbb0 read(0, 3735928559, 4)
# 0x0004: 'baaa' <return address>
# 0x0008: 0x0 arg0
# 0x000c: 0xdeadbeef arg1
# 0x0010: 0x4 arg2
# 0x0014: 0x41d870 write(1, 3405691582, 4)
# 0x0018: 'gaaa' <return address>
# 0x001c: 0x1 arg0
# 0x0020: 0xcafebabe arg1
# 0x0024: 0x4 arg2


find_gadget()

引数に与えた命令のROP Gadgetを探してくれる。

返り値のGadgetクラスはraw()に直接渡すことができる。

(便利な機能だが、rp++の方が色々と強い。)

from pwn import *

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

rop.raw(rop.find_gadget(['pop rdi', 'ret'])) # pop rdi; ret

print rop.dump()
# 0x0000: 0x41e844 pop rdi; ret


dump()

ROPチェーンをとても読みやすい形式でダンプしてくれる。


chain()

ROPチェーンを使える状態(str型)にして返してくれる。

from pwn import *

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

rop.raw('A'*8)
rop.write(1, 2, 3)

print rop.dump()
# 0x0000: 'AAAA' 'AAAAAAAA'
# 0x0004: 'AAAA'
# 0x0008: 0x41d870 write(1, 2, 3)
# 0x000c: 'daaa' <return address>
# 0x0010: 0x1 arg0
# 0x0014: 0x2 arg1
# 0x0018: 0x3 arg2

rop.chain()
# AAAAAAAAp�A\x00daaa\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00


constants

異なるアーキテクチャやOSの定数に素早くアクセス可能になるような機能がある。

システムコール番号や標準入出力のファイルディスクリプタの番号を使うときなどに使える。

context.archに応じて正しい値を返してくれる。

from pwn import *

elf = ELF('/bin/bash')

print 'SYS_read: %d % constants.eval('SYS_read')
# SYS_read: 3

print 'SYS_read: %d % constants.SYS_read
# SYS_read: 3

read()システムコールのシステムコール番号である「3」が返されている。


Pwntoolsのコマンド

Pwntoolsはコマンドラインで使えるコマンドが幾つかある。(前述のasm, disasm, shellcraftもそのうちの一つ)

使い方は、pwn --helpまたはpwn -hで参照できる。

$ pwn --help


usage: pwn [-h]
{asm,checksec,constgrep,cyclic,debug,disasm,disablenx,elfdiff,elfpatch,errno,hex,phd,pwnstrip,scramble,shellcraft,template,unhex,update}
...

Pwntools Command-line Interface

positional arguments:
{asm,checksec,constgrep,cyclic,debug,disasm,disablenx,elfdiff,elfpatch,errno,hex,phd,pwnstrip,scramble,shellcraft,template,unhex,update}
asm Assemble shellcode into bytes
checksec Check binary security settings
constgrep Looking up constants from header files. Example:
constgrep -c freebsd -m ^PROT_ '3 + 4'
cyclic Cyclic pattern creator/finder
debug Debug a binary in GDB
disasm Disassemble bytes into text format
disablenx Disable NX for an ELF binary
elfdiff Compare two ELF files
elfpatch Patch an ELF file
errno Prints out error messages
hex Hex-encodes data provided on the command line or stdin
phd Pwnlib HexDump
pwnstrip Strip binaries for CTF usage
scramble Shellcode encoder
shellcraft Microwave shellcode -- Easy, fast and delicious
template Generate an exploit template
unhex Decodes hex-encoded data provided on the command line
or via stdin.
update Check for pwntools updates

optional arguments:
-h, --help show this help message and exit


総評

この記事をまとめるにあたり、公式ドキュメントの偉大さを肌で感じる事ができた。

まとめるのは大変だったが、知らない機能がたくさんあったのでやってよかった。

この記事でPwnがもっと手軽なものになればいいなと思う。


参考