pwntools 使い方
pwntoolsの使い方
tags: ctf
pwn
pwntools
howtouse
忘れないようにメモする。 公式のDocsとか、関数のdescriptionが優秀なのでそっちを読んだ方が正確だと思う。 でも日本語じゃないと読むのに時間がかかってしまうので日本語でメモする。
基本
基本的な機能の使い方。 プログラムへの入出力など。
from pwn import * # プログラムを実行するprocessを作る # cwdキーワードで現在のワーキングディレクトリが変更できる p = process('test_program') # p = remote('127.0.0.1', 12345) # 127.0.0.1の12345ポートに接続する、processとAPIが同じなのでそっくりそのまま同じように動作する ret = p.recv() # test_programの出力をEOFまでを受けとる ret = p.recvline() # 改行までを受けとる、改行が送られないとここで止まるので注意 ret = p.recvline(timeout=0.01) # recv系はtimeoutを設定できる、単位は秒。ハングするのが嫌なら設定しておくといい ret = p.recvuntil('some output') # 引数の文字列までを受けとる。 # ログとして出力する。context.log_levelに値を設定することで、debug, infoなど出力するログも操作できる log.info(ret) payload = '' payload += p32(64) # 数値をリトルエンディアンで32ビット長の文字列として変換する。 # 64なら'@\x00\x00\x00'になる。 # ほかにもp64という関数があり、こっちは64ビット長として変換する。 # 逆変換はu32()、符号無し整数としてデコードしてくれる。 payload += 'some shellcodes' # test_programにpayloadを送る。文字列ならなんでも。 p.send(payload) # 末尾になにもなし、多分EOFが付く p.sendline(payload) # これなら末尾に改行が付く # 実行すると入出力を直接表示、送信するようになる。 # 相手側でシェルを開いたときに起動すると対話的に操作できる p.interactive()
基本的にはrecv
とsend
系を使っていく。他にも便利機能がたくさんあるけど、これだけでもpwnを始められる。
実際のpwnの流れとしては、process
でプログラムを実行して解析しながらsend
やrecv
で送受信を行なって、シェルが取れたらinteractive
で直接操作する、というような感じになる
場合によってはローカル環境でプログラムを実行できるときもremote
を使うことがある、プログラムが直接ポートにバインドされてフォークするようないわゆるfork型のときとか。
ELFの解析
elfファイルの解析はIDAとかreadelfとかradare2でやったりするが、なんとpwntoolsからでもある程度解析ができる。 ちょっと関数のアドレスとかpltやgotのアドレスを使いたいときにはハードコーディングしなくて済むので、後から確認するとき"あれ、これなんだっけ?"とならずに済む。 ROP関連の機能にも使える。
from pwn import * elf = ELF('program') # 解析、ログにセキュリティ機構などの解析結果も表示される。 # 多分pwntoolsに付いてくるchecksecと同じ出力 # libcなどの共有ライブラリも解析できて、その場合はオフセットが分かる # ローカルとリモートでglibcが違うときとかに読み込むライブラリを変更すると便利 elf.plt['printf'] # plt領域にあるprintf関数のアドレス # ハードコーディングせずにすむのでこっちの方が良いと思う。 # ときどき間違ってるっぽい?理解が足りなくて勘違いしてるだけかもしれない # 心配ならobjdumpとかreadelfとかでも調べておこう elf.got['printf'] # 同じようにgot領域のアドレスも調べられる elf.symbols['local_variable'] # stripされてなかったりするとシンボルのアドレスも参照できる elf.bss() # セクションのアドレスなんかもある
他にも色々な情報が参照できるので最初に解析用のスクリプト作っておくのもいいかもしれない。
dir()
でメンバの名前見ればだいたい何の情報か想像も付くので、たとえ忘れても安心。
設定
context
は解析のための情報を渡すことができる、ようはconfig。
from pwn import * context.arch = 'amd64' # 解析するプログラムのアーキテクチャを設定できる、初期値は多分i386 context.log_level = 'debug' # ログのレベルを設定できる。普段は多分infoぐらいが表示される # pwntoolsのバグっぽいのに遭遇したらdebugにして見てみよう context(arch='amd64') # 関数から設定することもできる
ちょっと変わったバイナリを解析するときは、一度こっちの設定を確認して、適切かどうか見るといいかもしれない。 シェルコードの生成とか関数の挙動に影響するので、間違った設定になっていると思った通りにエクスプロイトが動かなかったりする。 お行儀良く最初に設定しておこう。
shellcode
shellstormでいいけど、pwntoolsにも簡単なものはあるので多少は楽にエクスプロイトが書けるかも。
スクリプト中でアセンブルもできる。書けるかどうかはともかく。
from pwn import * asm(shellcraft.sh()) # shellcraft.sh()はシェルを開くアセンブリコードを返す、asm()はそれをアセンブルする # shellcraftの返り値は単なるアセンブリコードなので、シェルコードの勉強に持ってこいだったりする # デフォルトが使えなかったときは攻撃対象に合わせて書き換えてみよう # アセンブリコードを文字列として渡すとアセンブルしてくれるので、必要に応じて書くこともできる asm(''' xor ebx, ebx lea eax, [ebx + 4] mov ecx, esp lea edx, [ebx + 0xff] int 0x80 ''')
asm
を使えば自作shellcodeを使うこともできるので、shellcodeを書く練習にももってこい。
shellcraft
のサブモジュールにはアーキテクチャやOSに対応したモジュールがあり、様々シェルコードが収録されているみたいなので一度探索してみると面白いかもしれない。
書式文字列攻撃
書式文字列攻撃のための機能もある。 攻撃を自動でやるようにする使い方もできるらしいが、関数のセッティングが面倒そうだったので、ここではペイロードを作成する機能の紹介だけにする。 基本的には書き込みたい値と書き込み先アドレスを指定すると、書式文字列攻撃で書き込みをするペイロードを返すといった感じ。
from pwn import * p = process('target_program') trg = ELF('target_program') trg_addr = trg.symbols['target_symbol'] # trg_addrに0x12345678と書き込む writes = {trg_addr : 0x12345678} offset = 11 # printfの引数があるオフセット、書式文字列攻撃ができるなら多分オフセットのリークもできる payload = fmtstr_payload(offset, writes, numbwritten=0) p.send(payload)
offset
には書式文字列バグのある箇所で、入力した文字列が出現する位置を指定する。
適当に%x
とか入力して調べる必要がある。
numbwritten
は既に書き込んだ文字数を指定する、デフォルトで0なのでこの例では本当はいらない。
書式文字列攻撃はprintf
のこれまで出力したバイト数を変数へ書き込むパラメータを利用して任意のアドレスへの書き込みをするので、これまでの出力数が正しくないとペイロードを組み立てることができない。
writes
に{書き込みたいアドレス : 書き込みたい値}
という形式で書き込んでいく値などを指定する。
fmtstr_payload
にそれらを渡すとペイロードを作成して返すようになっている。
ROP
ropチェインを組み立てることができる機能がある。 elfの解析結果を使えば自動的に呼び出しのフレームを作成したりできるので便利。
from pwn import * elf = ELF('test_program') rop = ROP(elf) # 直接値をスタックに積む rop.raw(0) # 関数などの呼び出しをする、シンボルがないと無理? # 引数も渡せる rop.call('read', [0, 4, 10]) # 横着なwrite call rop.write(1, 4, 10) # ropガジェットを勝手に探してきてスタックフレームの調節をしてくれるらしいので気にせず呼び出せる # 本当にちゃんと出来てるか知りたいときはダンプすることもできる # シンボルなどがあるときはその内容まで表示してくれるので分かりやすい print(rop.dump()) # ropガジェットの検索もできる # 返り値はGadgetクラスで直接rop.raw()に渡したりできる、むしろ直接渡した方が情報が表示されるので良い rop.find_gadget(['pop eax', 'ret']) # pop eax; ret # ガジェット一覧 rop.gadgets() # ropチェインの取得、文字列として rop.chain() str(rop)
call
がとても便利そう。ガジェットの検索はrp++とかの専用ツールほどフレキシブルな検索はやってくれないっぽいのでそういうのはやっぱり自分で探す必要がある(残念)。
デバッガ
自分の環境では動かなかったが、gdbでデバッグもできるらしい。
from pwn import * context.terminal = ['terminator', '-e'] p = process('program') gdb.attach(p) gdb.debug('program')
追記 どうも元のプログラムでプロセスIDを直接引数としてgdbに渡してるのが原因っぽい。gdbはプロセスにアタッチするとき、プロセスIDは-pオプションに渡さないといけないはず。そのことでIssueも立ってたけど、聞いてみたけど問題として認識してないっぽい? TODO: 直ってるかチェック
定数値
エクスプロイトを書くときや解析しているときは様々な定数値(システムコール番号とかマクロとか)がしょっちゅうでてくる。 でも全部憶えるのはまず無理なのでその都度調べるのだけど、pwntoolsにそのような定数値を調べられる機能がある。 一々ブラウザやman開いてスクリプトにメモするのも面倒なので積極的に使いたい機能の1つ。
from pwn import * # 名前で調べる、`execve`のシステムコール番号が返る constants.eval('SYS_execve') # 直接定数値をロード constants.SYS_execve # 他にもシグナル番号などもある constants.SIGKILL # どんな定数値があるかの一覧 help(constants)
定数値はしょっちゅう忘れるので地味に便利。
スクリプト中でマジックナンバーになって意味が分からなくなったりしないし。
context
の設定に従ってアーキテクチャに対して適切な値を返すので、設定してから使わないと変な値になったりする。
その他
pwntoolsはライブラリとしてだけではなく、コマンドラインから使うこともできる。
pwn --help
で使い方が表示される。
checksecとかちょっとしたアセンブラとか便利な機能も多いので一度覗いてみるといいかもしれない。
定数値検索のできるconstgrep
がとても便利。
おわり
使ったことのある機能はこれぐらい。 他にも色々便利そうな、書式文字列攻撃用のモジュールだったりflag管理用のモジュールだったりがあるので、公式を見ておくとワクワクできる。 使い方は分かりません。使われてない機能だとバグっぽいのもしばしばあったりするけれど、そういうときは他のコマンドで補おう。