Pwntools ハンズオン (環境構築・ローカル実行・リモート接続)
CTF の Pwn, Binary 系問題 (c 言語で書かれたプログラムが動くリモートサーバーにアクセスして, 脆弱性を突くことでコマンド実行・フラグ文字列の獲得を目指す問題) でよく使われるツールである pwntools について, 導入など問題を解く以前の基本的な部分についての覚え書きとサンプルコード
以前の記事 で自分用のチートシート的なものは作ったが, 整理も兼ねてサンプルコードを含めた解説をまとめたものがあった方が便利なので製作
検証環境
- Windows 11 Home
- Ubuntu 24.04 (WSL)
環境構築
- 公式ドキュメント では依存関係のインストールが初めにあるのでそちらをしておいた方がよさそうだが, Windows では pwntools のインストールのみでも動いた (基本的に Windows Support は experimental のためまじめに使う場合は wsl を使った方がよさそう)
依存関係のインストール
- 以下のコマンドを実行する
sudo apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
pwntools のインストール
- pip 経由
pip install pwntools
- uv 経由
uv add pwntools
インストールの確認
- pwntools のインストールされた環境 (uv などの場合は仮想環境を有効化した状態) でインタラクティブモードの python で以下を実行
from pwn import *
- 正しくインストールできていればエラーは吐かない
- また, コマンドラインツールの確認は以下を実行
pwn version
- バージョンが表示されれば完了
- uv 環境では以下で確認できる
uvx --from pwntools pwn version
make, Docker の準備 (省略可能)
- github で配布している本ページで用いるコードをコンパイルし手元の環境で試すには gcc のコンパイラなど c 言語のコンパイラが必要
- コンパイルのオプションについては c のファイル内にコメントで記述しており, また Makefile を用意しているため make の環境があればそちらも使える
- Docker はリモートサーバーを立てる時に使用する
- ctf の pwn 問題では基本的にプログラムがリモートサーバー上で動くので, 動作確認などでよく使うので docker コマンド (と docker compose) を使える状態にしておくと便利
- この記事で使うコードについては基本的に docker-compose.yml を作っているので以下のコマンドでリモートサーバーを立てれるようにしている
docker compose up
Hello World!
- Hello World を出力するだけのプログラムを用いてバイナリの実行・リモートの接続と文字の受け取りを試す
バイナリの準備
- まずは接続先となる問題バイナリを想定してサンプルコードを作成
// gcc -g hello.c -o ../build/hello
#include <stdio.h>
int main(void) {
puts("Hello World!");
}
- 上はシンプルな Hello World! の出力コード
pwntools を用いたローカルバイナリの実行
- pwntools ではローカルのバイナリを指定してコード内で実行できる
from pwn import *
# set binary path
context.binary = "../build/hello"
sh = process()
import は一般に
from pwn import *が用いられるcontext.binaryでバイナリファイルへのパスをセットすることでバイナリの情報が読み込まれるprocessでバイナリを実行することができる次に文字列を受け取る処理を記述する
# get one text line
text = sh.recvline()
print(text.decode())
sh.close()
recvlineは一行分の文字列を受け取る.recv系のメソッドはデータの受け取りを行うものがそろっている (recvであれば指定子バイト分受け取り,recvuntilでは特定のパターンが出てくるまで受け取ることができる)受け取るオブジェクトは
bytes型となる今回のバイナリは文字列を出力するだけなのであまり意味がないが,
sh.interactive()を用いるとインタラクティブシェルを起動できる (例えば ctf 問題で攻撃が成功して/bin/shを起動できたときによく使う)
pwntools を用いたリモートへの接続
- 一般の ctf の pwn 問題ではリモートサーバーに接続することが多い
- github のコード にも Docker でリモート環境を用意した
- pwntools ではリモートサーバーへの接続について,
remote関数を用いて接続することができる - 例としてローカルの 1337 ポートに接続する場合は以下のようなコードになる
host = "localhost"
port = "1337"
sh = remote(host, port)
remoteの返り値オブジェクトについてはprocessの返り値のオブジェクトと同様に使用できる
pwntools を用いた入力
- 次に入力を受け取るようなプログラムについて扱う
バイナリの準備
- 入力を受け取り出力する簡単なバイナリを用意する
// gcc -g in_out.c -o ../build/in_out
#include <stdio.h>
int main(void) {
printf("Input something: ");
char buf[0x10];
fgets(buf,sizeof(buf),stdin);
printf("Your input: %s", buf);
}
文字列の送信
- pwntools でまずはプロンプトの行までの文字列を受け取るコードを書く (前節の内容)
from pwn import *
# set binary path
context.binary = "../build/in_out"
_, host, port = "nc localhost 1337".split()
sh = remote(host, port)
prompt = sh.recvuntil(b"something:")
print("prompt: ", prompt.decode())
- 次に,
sendlineを用いて文字列を送る
sh.sendline(b"Hello Send!")
send系のメソッドは文字列の文字列の送信を行うものであり,sendであれば改行コードを付けずに送信し,sendlineafterでは特定の文字列が出現した後のタイミングで文字列を一行送信する