格式化字串筆記 Format String Bug (FSB)
概念
- ebp/rbp chain
檢測
透過大量的%s
使程式崩潰,用來確認漏洞的存在。
%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s
或者採用動態分析工具,比方說 gef 的 format-string-helper 會在不安全時暫停程式執行。
利用方式
- 洩漏堆疊 - fmtstr 可以做 information leaking 當中你可以用他把 stack dump 出來,所以在不知道 remote 的情況下會使用到這種技巧,也可以 leak rbp。
- 洩漏任意地址
- 覆蓋任意地址
- 劫持 GOT
- 劫持 return address
要觸發該漏洞可以透過靜態檢測是否存在 printf 系列函數如 sprintf/dprintf 等等, 若無,如果 RELRO : Partial
或更低的安全性時,蓋寫 GOT 也可以觸發 fmtstr,不過 FORTIFY
必須為 disabled
而 fmtstr 在利用上會根據可控字串是否在堆疊上,基本上如果堆疊保存可控字串,可以傳入特定地址,如果堆疊不保存可控字串就必須控制 rbp chain 進行利用。
利用可控堆疊格式化字串參見以下三個階段
階段一
格式化字串漏洞可以透過%x.%x.%x.%x.%x.%x
的方法印出內容,
也可採用%3$x
則可任意讀取堆疊(此例子為第三項,為4 * 3
),
透過數個%x
找到可控制堆疊位置。
%x
與%p
區別,%x
為4 bytes,%p
(指標)受到系統影響,常見的系統可能為4或8(不確定?)。
如何快速找到 %?p 在記憶體上面的位置?
-
如果 fmtstr buf 弄在 stack 上面,由於 calling convention,根據經驗,通常 x64 stack 可寫的部份大概會在
%6$p
左右,可以從該值開始尋找,原因是什麼?想一下應該是 x86_64 calling conventions 的原因,因為有 rdi, rsi, rdx, rcx, r8, r9 後面 rsi 開始數五個暫存器,後面第七個會被 push 到 stack,因為第一個被 rdi 使用走的,從 rsi 數。 -
通常想 leak 某總資料段的基底地址
-
透過腳本簡單迴圈進行
%?$p
的列印,同時透過 gdb 列印vmmap(proc.libs())
-
+-----------------+ | fmtstr | +------------+ %{}$p | primitive | | for range +---------->| +-----------+ | | | | | fmtstr | | +------------+ | | processes | | | +-----------+ | +------------+ | | | stack info |<----------+ +-----------+ | | save | 0x7f... | | fmtstr | | +------------+ | | remote | | | +-----------+ | | | +-----------------+
-
簡單分析地址前綴,輸出結果後可以用 grep 等等方式檢查前綴,基本上 ELF 裡面後三個 nibble 不會變化,這點可以用來檢測 return address,rbp 的值在 stack 中後三 nibble 可能會變化。
相關腳本範例參見
- BreakAllCTF (腳本尚未公開)
- pwn baby_fmt 腳本 baby_fmt_list.py
- pwn fmt-1 腳本 fmt-1_list.py 工具
- r888800009/gdb_toolbox
階段二
透過%n
覆蓋堆疊內容所指向的位置(也可以使用%hn
、%hhn
、%10$n
),
可以理解printf
令某參數為指標保存字數(參見參考printf)。
階段一找到的可控制堆疊開頭為輸入內容開頭,
因此可傳入格式如\x78\x56\x34\x12
指標(32bit),
並且透過%n
修改0x12345678
的內容。
階段三
編寫payload,採用%hhn
並且計算每 byte 的內容,%n
是printf
當下處理時輸出的總字數,
採用%hhn
比起%n
容易溢位,透過這個性質將傳入數值,使用%10c
的方式下去輸入,
上一個字數為240
並且現在輸入數值4
時,只須打印出(4 - 240) % 256
即可抵達對映的字數。
備註
若採用%x
可能沒辦法輸出小於 8 的值,改用%c
。
在 printf
時無法中途修改參數內容,只能透過一開始傳入的多個參數進行利用。
工具
任意寫入地址
import pwnlib
fmtstr_payload(6, {0x404050: 0xfaceb00c}, write_size='byte', numbwritten=0)
%?%p
的起始位置為 %1%p
-
6 是可控 buffer 位置在
%6$p
-
numbwritten 是 print 已經寫的字元寫才對
該工具使用時機如 BreakAllCTF 的 fmt-2
而如果並非要寫 int32 的話,透過 pwnlib.fmtstr.make_atoms
pwn.fmtstr.make_payload_dollar(2, [pwnlib.fmtstr.AtomWrite(0x0, 0x1, 0xff), pwnlib.fmtstr.AtomWrite(0x0, 0x1, 0x10)])
該用法在 0x13371111 寫入 0x1234 ,而該方法的 offset 是指向第一個寫入的 address 的意思
而該方法需要找到傳入的目標 address 所在的 offset ,可以計算前面 fmt 花費多少 byte 進而推導出 offset
pwnlib.fmtstr.make_payload_dollar(6, pwnlib.fmtstr.make_atoms({0x13371111:0x1234}, 1, 2, 0, 0, 'fast', []),countersize=1)
除錯
- 檢查 printf 的參數,攻擊的 payload 是否有前綴
- 若有必須考慮前綴所產生的字元,會導致 payload 所修改的值不是目標數值
習題
- hackme.inndy.tw
- echo
相似問題
- php 的
printf()
函數
參考
CTF Vulnerability pwn