2020 DEFCON Quals - Tiamat
題目:Tiamat
Tiamat
Challenge Info
Protovision just launched their new licensing server yesterday and the sysop over at Pirate’s Harbor BBS already has a copy. You should download it and try to be the first one to hack it.
Dockerfile
, service.conf
, wrapper
, liccheck.bin
, qemooo
, games
, banner_fail
Solution
Dockerfile
有些資訊:
/flag
:flag 存在這裡/lic
:/flag
的 md5,所以由a-f0-9
組成,共 0x20 字元/$x.mz
:$x = {1,2,3,4,5,6,7,8,9,a,b,c,d,e,f}
,內容都一樣
1.mz ~ f.mz
:
|
|
local 端執行服務的方法是:./qemooo liccheck.bin
,可以看到一隻龍,隨便輸入程式會直接結束
將 liccheck.bin
丟 IDA 分析,發現只有少數幾個指令有解出來:
總之先分析 qemooo
的部分
qemooo
= customized qemu
qemooo
有帶 symbol,丟 IDA 分析
在 tb_gen_code()
發現魔改 qemu
的痕跡:
QEMU 是個 JIT emulator,可以動態將各種架構的 binary 翻譯成 host machine 的指令執行,QEMU 不會一次翻譯一個指令,而是以 TB(Translataion Block)為單位,每次翻譯完一個 TB,QEMU 會將它暫存起來下次再遇到就可以直接執行,而沒有暫存的 TB 時,會呼叫 tb_gen_code()
進行轉換。
正常情況下 tb_gen_code()
會遍歷每條指令,並呼叫 gen_intermediate_code()
生成相對應的 IR(Intermediate Representation)
而 qemooo
則會根據一個固定陣列 tmap_arch
決定該指令,是哪種 architecture 跟 endian,再呼叫相對應的 gen_intemediate_code()
規則如下:
base_pc
=0x100d0
arch_index
=(pc - base_pc) >> 2
tmap_arch
:由0
~7
組成的陣列- 0:SPARC - little
- 1:SPARC - big
- 2:RISCV - little
- 3:RISCV - big
- 4:ARM - little
- 5:ARM - big
- 6:MIPS - little
- 7:MIPS - big
- 也就是
tmap_arch[arch_index]
決定第arch_index
個 instruction 要怎麼解
換句話說,liccheck.bin
是由 8 種架構指令組成的 binary
值得一提的是,這四個架構指令長度都是 4 bytes(32bits),之後寫 disassembler 會簡單一些
Dump tmap_arch
.text
section offset =0xd0
.text
section size =4112
->1028
instructions
|
|
Register Mapping
在不同架構下,同樣是 rsp
可能對應到不同個暫存器,所以在反編譯的結果中,增加 r0~r31
的表示會比較好 trace code,我的話是這樣表示:
|
|
轉換成:
|
|
但我的腳本有些暫存器沒對應好 QQ
Register in QEMU and qemooo
QEMU 使用 array 來儲存暫存器的值,根據 host machine 架構不同會有些許差異,而 qemooo
使用下方的 gpr
來儲存(gpr[0]
對應 r0
以此類推)
其中 SPARC 的 register 儲存方式特別不一樣,它的大小為 QWORD 而不是 DWORD,這樣 gpr
顯然不夠存,所以還會用到 regwptr
,對應方式如下:
這部分我主要是動態追出來的
Calling Convention:cpu_loop() -> do_syscall()
QEMU 用 cpu_loop()
模擬 cpu 執行,一般來說會進到 cpu_exec()
去執行 TB 翻譯跟執行不再出來,但在一些情況下例外,例如 interrupt,QEMU 使用軟體模擬 interrupt(helper_raise_exception()
),它被呼叫的時候會跳出 cpu_exec()
執行 do_syscall()
而 qemooo
會根據 cs->kvm_fd
(也就是 tmap_arch
)去呼叫對應架構的 do_syscall()
,這是它們的關係:
|
|
靜態看的話要修一下 switch case(因為 switch case 太大),或直接看組語
附上簡單的圖解(Edit/Other/Specify switch idom
)
下面是四個架構的 do_syscall()
:
SPARC(offset 1000)
這邊是用
regwptr
存
|
|
RISCV(offset 500)
|
|
ARM(offset 0)
|
|
MIPS(offset 4000)
這邊的 offset 要在呼叫時自己加上
|
|
除了 system call 參數之外回傳值也要注意,我這邊沒放就是了
Syscall Number: do_syscall() -> do_syscall1()
|
|
根據 num
決定要呼叫哪個 system call,有個超大 switch case 結構,跟前面一樣 case 超過五個所以編譯被使用 jump table 優化,IDA 判斷不出來,要自己修復,可以找到 system call number 跟 system call 的對應
下圖是 sys_write
的對應,我沒有全部 syscall 都確認過,但應該會照 QEMU 原始碼 linux-user/<arch>/
裡面的 system call table,然後加一個 offset(0, 500, 1000, 4000)變成新的 system call table
qemooo
Summary
看完可能會有點混亂?統整一下上面分析的內容:
gpr
,regwptr
都存在TCState
中,用來模擬暫存器- SPARC 每個暫存器佔 8 bytes(QWORD)
- ARM 的
r[0]
會存 syscall 回傳值,也就是一個能使用的暫存器 - 其他架構的
r0
就是常數 0
r[i] = gpr[i]
,ex[i] = regwptr[i]
qemooo
的行為大致理解了,接下來是分析 liccheck.bin
的行為
Customized Disassembler
顯然要靜態分析 liccheck.bin
現有工具並不管用,我是用 python + capstone 手寫 disassembler
安裝 capstone 時要注意一下,master branch 版本沒有支援 riscv
,要用 next
的版本
|
|
如果需要的話,我的腳本放這裡:gist link
為了要統一暫存器,我又寫了一個腳本直接做替換,比較好的做法應該是改 capstone,總之是垃圾 code 小心服用 QAQ
使用方法:
1 2
python3 dec.py > dec.ans python3 register_replace.py
並且安裝 VSCode ANSI Colors 插件
檢視
dec-re.ans
然後按右上角按鈕,就可以看到上色的結果
Dynamically Trace Host Machine Code
|
|
這樣就能靜態動態一起看(順便把我沒弄好的暫存器對應修好
liccheck.bin
Behavior
最後發現這是一個 menu 題
有幾個暫存器會被拿來做特殊使用:
r[15]
:存 4 bytes random numbermem[r[29]]
:輸入的 license(後面稱作input
)mem[r[29] + 4]
:xor 過後/lic
的內容
這邊簡單列幾個重要的 address:
0x105d4
:r[10]
:buffer to printr[11]
:print size- 相當於
write(1, r[10], r[11])
0x10198
:應該算是 menu 的 main function- 先要輸入 1 個字元存到
r[29] + 8
(也就是選一個選項) - 輸入是
g
的話,直接結束(exit()
)
- 先要輸入 1 個字元存到
總共有 7 個選項:
j
:- 要求再輸入 6 bytes(
oshua\n
)存在mem[r[29] + 0x14]
->0x32004
- 會看到訊息
GREETINGS PROFESSOR FALKEN.\n\nREADY\n
- 要求再輸入 6 bytes(
e
:- 要求輸入 32 bytes 的 license key,並且要在
0
~f
範圍內 - 將結果存在
mem[r[29]]
- 要求輸入 32 bytes 的 license key,並且要在
l
:- 檢查
mem[r[29] + 0x14]
前 4 bytes 為oshu
,否則結束(也就是要先通過j
才能用) - 接下來又是個菜單,每次輸入
1-9a-f
,會打開特定.mz
檔,然後輸出內容,內容從 Dockerfile 就知道了,都是垃圾
- 檢查
n
:- 從
/dev/urandom
讀 4 bytes 存到r[15]
- 最多執行 0x18 次(
ex[28] -= 1
)
- 從
p
:- 將 license key 印出來(從
mem[r[29]]
開始共 0x20 bytes)
- 將 license key 印出來(從
r
:- 直接跳回菜單開頭,沒啥用
v
:- 要求
mem[r[29]]
不可以為 0,否則結束(也就是要先e
輸入 license 才能用) - 最多執行 8 次(
ex[30] -= 1
) - 讀入
/lic
0x20 bytes,儲存在0x32024
,也就是input
後面 - 每次 4 bytes 用
r[15]
對[0x32024]
做 xor(r[15]
:random number) - 如果
input
跟xor /lic
一樣,就讀出/flag
並印出 - 否則輸出
Authorization failed!\n
然後跳回菜單
- 要求
知道流程,接下來就是找 bug 的時間了
Bug 1:Leak xor /lic
前情提要,我使用
arm[0]
代表 0 號暫存器,r[0]
就單純是常數 0
p
使用到 arm[0]
作為 print 的長度,並且 xor /lic
存在 input 後面,所以只要能控制 arm[0]
,就能 leak /lic
n
在 0x1065c
使用 arm 呼叫 open system call,會把回傳值 fd 存在 arm[0]
,並且直到跳回 menu 都不會被改掉
除此之外,有幾個地方 open 之後沒有 close,這會讓 fd 增加(每次 +1):
v
:0x10434
附近,單純沒有close
的 syscall0x10520
附近,單純沒有close
的 syscall(是在 print flag 的時候,所以這沒用)
n
:- 雖然
0x10670
看起來有syscall
,但mips
的 syscall number 要手動加 4000,也就是 4006 才會呼叫close
system call,而不是 6
- 雖然
v
可以用 0x8 次,n
可以用 0x18 次,而且 fd 會從 3 開始增加,所以完全可以把 fd 控制到 0x20,然後更改 r[0]
,最後 leak 出 xor /lic
這是 payload:
|
|
Find All Possible Licences
因為 /lic
的每個字元會在 0
~ f
範圍,可以寫腳本爆出所有可能的 license
可能性依照 /lic
(或是說 /flag
)而定,用 docker image archiveooo/pub:tiamat
版本的話只有 2 種可能的 license
但這樣還不能拿到 flag,因為 input
會拿去跟 xor /lic
做比較,但是輸入有範圍限制(0
~ f
),xor /lic
很有可能超出這個範圍
Bug 2:Set r[15]
(random number) to 0
讓 r[15] = 0
的話,選 v
的時候 xor /lic
值就跟 /lic
一樣了
而 j
選項裡面的這個指令可以達成這件事,因為 SPARC 使用 QWORD 進行運算,也就是運算不超過 DWORD 範圍時 r[15]
會被歸零
Solve!
集合上面的東西寫腳本,搞定:
|
|
Offical Hint After the Context
To complete the challenge, it is necessary to figure out how qemooo implements the various architectures and find the bugs that result from the interaction between them.
- SPARC, RISCV, MIPS all use a hardwired 0 in r0; however, for ARM the r0 register is where the syscall results are stored.
- SPARC registers skip every other one because it uses 64-bit register values, which means on a mov it will overwrite the adjacent register.
- The file close for one of the actions was the syscall value for a different architecure.
- Another action failed to close the file. Good luck!
Flag
OOO{Together well go all the way}