化:堆內(nèi)存路徑探索實(shí)戰(zhàn))
angr 學(xué)到現(xiàn)在基礎(chǔ)流程已經(jīng)跑熟了。但有一次在分析某嵌入式固件里的堆分配邏輯時(shí)發(fā)現(xiàn)一個(gè)很怪的現(xiàn)象程序明明從 stdin 讀完數(shù)據(jù)寫(xiě)進(jìn)了 malloc 返回的緩沖區(qū)后面拿緩沖區(qū)首地址做偏移參與比較angr 卻總在幾個(gè)分支上來(lái)回剪枝找不到能觸發(fā)漏洞的路徑。排查到最后根因就是malloc 返回的那個(gè)地址以及它對(duì)應(yīng)的內(nèi)存單元在符號(hào)執(zhí)行里沒(méi)有按我預(yù)期的方式被符號(hào)化。這篇筆記就專(zhuān)門(mén)記錄這個(gè)事。它解決的核心問(wèn)題是當(dāng)目標(biāo)程序?qū)?malloc 返回的地址做算術(shù)運(yùn)算、作為索引基址、或者直接把地址值本身參與條件分支時(shí)angr 的默認(rèn)堆模擬和具體地址模型會(huì)限制路徑探索此時(shí)需要主動(dòng)對(duì)“malloc 地址單元”做符號(hào)化處理。文章會(huì)從 angr 默認(rèn)的 malloc 行為講起用三個(gè)遞進(jìn)的小實(shí)驗(yàn)把那層窗戶紙捅破最后給出我在實(shí)際分析中踩過(guò)的一堆坑。適合已經(jīng)能跑通 angr 基礎(chǔ)腳本、但對(duì)符號(hào)化地址和符號(hào)化內(nèi)存還一知半解的人。1. 為什么要做 malloc 地址單元符號(hào)化1.1 先看 angr 默認(rèn)怎么處理 malloc大多數(shù)人的第一個(gè) angr 腳本大概就是加載一個(gè)樣例程序跑entry_state然后explore到某個(gè)地址。這時(shí)候 angr 會(huì)通過(guò)庫(kù)函數(shù)模擬SimProcedures接管malloc。默認(rèn)的SimHeapBrk會(huì)維護(hù)一個(gè)模擬的 brk 堆區(qū)每次調(diào)用 malloc 都返回一個(gè)具體的地址比如第一次分配返回0x603000第二次返回0x603100。也就是說(shuō)在默認(rèn)配置下malloc 返回值的BitVec是一個(gè)常數(shù)不是符號(hào)變量。這個(gè)常數(shù)本身沒(méi)有任何約束能力它只代表“程序認(rèn)為這塊堆區(qū)域在哪”。你不需要為此做什么但你要意識(shí)到這一點(diǎn)malloc 給你的是一個(gè)具體地址而具體地址意味著它的值不可能“變化”。如果你只是單純地讓程序讀完數(shù)據(jù)、做比較那具體地址完全夠用。但一旦程序里出現(xiàn)下面這類(lèi)邏輯char *buf malloc(0x100); char *p buf user_offset; if (p buf 0x80) { ... }或者更直接一點(diǎn)if ((unsigned long)buf % 0x10 0) { ... }那具體地址就帶來(lái)限制了。buf user_offset這條式子里面符號(hào)部分只有user_offset所以 angr 能探索的路徑只能沿著這個(gè)偏移量展開(kāi)。萬(wàn)一真實(shí)程序里buf的地址校驗(yàn)和某個(gè)外部條件耦合angr 就完全模擬不出來(lái)了。1.2 “地址是具體值、地址單元是符號(hào)值”的心智模型這里我建議先把一個(gè)概念掰清楚地址和地址單元是兩回事。地址一個(gè)用于映射內(nèi)存位置的數(shù)值比如0x603000。它可以是常數(shù)也可以是符號(hào)表達(dá)式。地址單元內(nèi)存建模時(shí)“地址 → 字節(jié)”的映射關(guān)系。0x603000處那 8 個(gè)字節(jié)就是 0x603000 單元的內(nèi)容。內(nèi)容值可以是符號(hào)。angr 默認(rèn)狀態(tài)就是地址是具體值地址單元的內(nèi)容可以是符號(hào)。你用read(0, buf, len)讀入一堆符號(hào)字節(jié)angr 內(nèi)部就是在0x603000 offset這些具體地址上掛了一個(gè)個(gè)符號(hào)表達(dá)式。路徑分支的時(shí)候求解器只需要處理這些符號(hào)內(nèi)容。所以“malloc 地址單元符號(hào)化”實(shí)際上有兩種含義缺一不可對(duì) malloc 返回地址指向的單元做符號(hào)讀寫(xiě)讓分配區(qū)內(nèi)各個(gè)字節(jié)的內(nèi)容符號(hào)化。對(duì) malloc 返回地址本身做符號(hào)化讓 “buf 指向哪里” 這件事也成為一個(gè)可被約束的變量。第二種比第一種更“重”大部分場(chǎng)景用第一種就夠了但坑恰恰出在第一次想嘗試第二種的人身上。1.3 必須符號(hào)化地址單元的兩種典型場(chǎng)景我復(fù)盤(pán)了一下真正需要主動(dòng)做 malloc 地址單元符號(hào)化的情況基本就兩類(lèi)第一類(lèi)是輸入長(zhǎng)度不可控導(dǎo)致的“符號(hào)寬度寫(xiě)內(nèi)存”。比如程序分配malloc(len)其中l(wèi)en來(lái)自輸入。angr 默認(rèn)遇到符號(hào)長(zhǎng)度分配時(shí)返回的地址依然是具體的只是分配大小是符號(hào)。寫(xiě)數(shù)據(jù)進(jìn)去時(shí)就會(huì)出現(xiàn)“符號(hào)大小往具體地址寫(xiě)”的問(wèn)題如果不做約束求解器要同時(shí)考慮長(zhǎng)度和內(nèi)容很容易路徑爆炸。第二類(lèi)是地址參與敏感運(yùn)算。比如程序把malloc返回地址強(qiáng)轉(zhuǎn)成整數(shù)然后和用戶輸入做異或、取模、比較或者利用malloc返回地址作為數(shù)組基址而索引是符號(hào)值。這種情況下具體地址和符號(hào)地址的行為差異會(huì)直接影響可達(dá)路徑。你只有把 malloc 地址本身也符號(hào)化讓求解器在合法堆區(qū)間內(nèi)把頭幾個(gè)字節(jié)的“地址值”約束出來(lái)后面的路徑才可能打開(kāi)。搞清楚這兩類(lèi)場(chǎng)景你就可以按圖索驥了。2. 第一個(gè)實(shí)驗(yàn)對(duì) malloc 返回地址指向的單元寫(xiě)入符號(hào)值2.1 構(gòu)造一個(gè)最小可復(fù)現(xiàn)的樣例程序?yàn)榱税褑?wèn)題講透我手動(dòng)構(gòu)造了一個(gè)最小樣例沒(méi)有任何真實(shí)業(yè)務(wù)邏輯純粹演示“malloc 分配 → 讀入 → 比較→ 分支”的完整鏈路。C 代碼如下#include stdio.h #include stdlib.h void win(void) { puts(win); } int main(void) { char *buf malloc(0x100); if (!buf) return 1; read(0, buf, 0x40); unsigned long v *(unsigned long *)(buf 0x18); if (v 0xdeadbeefcafebabeULL) { win(); } return 0; }用 gcc 編譯成靜態(tài)鏈接的二進(jìn)制命名為sample_x。這個(gè)程序的核心邏輯是這樣的讀取輸入到一個(gè)具體地址buf指向的內(nèi)存單元取出buf0x18處的 8 字節(jié)與一個(gè)常量比較。對(duì) angr 來(lái)說(shuō)read(0, buf, 0x40)應(yīng)該產(chǎn)生 0x40 個(gè)符號(hào)字節(jié)它們分布在 malloc 返回的地址單元中后續(xù)load可以取到這些符號(hào)字節(jié)。2.2 從狀態(tài)對(duì)象看符號(hào)地址單元在 angr 里往 malloc 返回的地址單元里放符號(hào)內(nèi)容最簡(jiǎn)單的方式就是讓程序自己跑read。但如果read不是標(biāo)準(zhǔn)調(diào)用或者程序是從文件映射讀數(shù)據(jù)你就要手動(dòng)處理。手動(dòng)往地址單元寫(xiě)符號(hào)值的 API 是state.memory.store和state.memory.load。下面這段代碼演示在模擬狀態(tài)下如何把 8 個(gè)字節(jié)的符號(hào)值放到一個(gè)具體地址0x603018上import angr proj angr.Project(./sample_x, auto_load_libsFalse) state proj.factory.entry_state() # 取 malloc 返回地址默認(rèn)具體地址通常為 0x603000 附近 malloc_addr 0x603018 # 把 8 個(gè)字節(jié)的符號(hào)值依次寫(xiě)入該地址單元 val8 state.solver.BVS(magic_value, 64) state.memory.store(malloc_addr, val8, endnessIend_BE) # 讀出來(lái)驗(yàn)證 loaded state.memory.load(malloc_addr, 8, endnessIend_BE) print(state.solver.simplify(loaded))這里有個(gè)容易忽略的坑如果程序是小端store和load的endness必須一致。大多數(shù) x86 程序是小端所以默認(rèn)你應(yīng)該用Iend_LE而不是大端。之前我圖省事不傳endness結(jié)果讀出來(lái)的值與寫(xiě)入的符號(hào)表達(dá)式拼接順序完全反了求解結(jié)果自然對(duì)不上。2.3 驗(yàn)證一符號(hào)值確實(shí)進(jìn)入了目標(biāo)地址單元寫(xiě)完store之后不能只看打印結(jié)果你要真正驗(yàn)證“這個(gè)符號(hào)值是在那個(gè)地址單元里”。最直接的辦法是給符號(hào)值加一個(gè)約束然后求解state.solver.add_constraints(loaded 0xdeadbeefcafebabeULL) if state.solver.satisfiable(): print(SAT, solution:, hex(state.solver.eval(loaded))) else: print(UNSAT)如果求解器返回 SAT說(shuō)明loaded這個(gè)符號(hào)表達(dá)式確實(shí)可以等于目標(biāo)常量也就是說(shuō)這個(gè)地址單元的內(nèi)容完全可控。這一步驗(yàn)證的是具體地址單元符號(hào)化成功。后面程序里的if分支自然能走上真實(shí)路徑。2.4 驗(yàn)證二讀出的符號(hào)值能參與條件分支求解驗(yàn)證完單元可寫(xiě)再把這一步接到程序分支上。直接跑exploreproj angr.Project(./sample_x, auto_load_libsFalse) state proj.factory.entry_state() simgr proj.factory.simulation_manager(state) simgr.explore(find0x400000 win_offset) # win 的真實(shí)偏移按反匯編結(jié)果填默認(rèn)情況下angr 能自己處理read的符號(hào)化也可以到達(dá)win函數(shù)地址。這說(shuō)明對(duì)于“把符號(hào)內(nèi)容寫(xiě)入 malloc 地址單元”這件事內(nèi)置機(jī)制已經(jīng)幫你完成了你不需要手動(dòng)store符號(hào)值。但是如果你的目標(biāo)程序不走 stdin而是從內(nèi)存映射文件讀數(shù)據(jù)、或者直接從一個(gè)固定地址讀內(nèi)存那么手動(dòng)store就是必需品。很多模擬設(shè)備固件分析里指針和地址單元全部要靠手動(dòng)符號(hào)化因?yàn)?angr 不知道設(shè)備寄存器什么時(shí)候?qū)懭搿?. 進(jìn)階一把 malloc 返回的地址本身變成符號(hào)3.1 通過(guò) hook malloc 返回符號(hào)值現(xiàn)在進(jìn)入更硬核的部分讓 malloc 返回的地址本身是一個(gè)符號(hào)值。先看一個(gè)需要這么干的樣例#include stdio.h #include stdlib.h int main(void) { unsigned long addr; char *buf malloc(0x100); scanf(%lx, addr); if (addr (unsigned long)buf) { puts(equal); } else if (addr (unsigned long)buf 0x80) { puts(half); } return 0; }這個(gè)程序把用戶輸入的地址和buf地址做比較。如果 angr 使用默認(rèn) malloc 模擬buf是一個(gè)固定值那么addr buf這條分支自然可以被探索因?yàn)閍ddr是符號(hào)輸入可以等于具體地址。但如果buf本身不確定情況就反過(guò)來(lái)了。假設(shè)程序來(lái)自一個(gè)高度混淆的固件真實(shí)的 malloc 地址受環(huán)境隨機(jī)化影響你需要把buf建模成符號(hào)值。hook malloc 的寫(xiě)法并不復(fù)雜import angr class SymbolicMalloc(angr.SimProcedure): def run(self, size): # size 是符號(hào)值但這里最好具體化 concrete_size self.state.solver.eval(size) # 返回一個(gè)符號(hào)地址并約束該地址指向合法的堆區(qū)間 sym_addr self.state.solver.BVS(malloc_sym_addr, self.state.arch.bits) self.state.solver.add_constraints(sym_addr 0x600000) self.state.solver.add_constraints(sym_addr concrete_size 0x700000) return sym_addr proj angr.Project(./sample_y, auto_load_libsFalse) proj.hook_symbol(malloc, SymbolicMalloc()) state proj.factory.entry_state()這段代碼有幾個(gè)要點(diǎn)size在run里可能是一個(gè)符號(hào)表達(dá)式因?yàn)閙alloc(len)中的len可能來(lái)自外部輸入。先eval求出當(dāng)前狀態(tài)下的一個(gè)可行值再作為約束區(qū)間右端的參考。符號(hào)地址需要加范圍約束否則求解器可能給你一個(gè)0x414141414141這種完全無(wú)效的地址后面讀寫(xiě)內(nèi)存直接報(bào)錯(cuò)。約束區(qū)間要留足空間。如果concrete_size是 0x100右端不能剛好是0x700000否則越界讀寫(xiě)依然可能落在未映射區(qū)域。建議把區(qū)間放寬到0x700000 - concrete_size。3.2 讓符號(hào)地址參與指針運(yùn)算地址符號(hào)化之后最直接的收益是程序里buf offset這種指針運(yùn)算不再是一個(gè)“具體地址加一個(gè)符號(hào)”而是“符號(hào)地址加一個(gè)符號(hào)”。這種情況下angr 的內(nèi)存模型會(huì)面臨“符號(hào)地址讀寫(xiě)”的挑戰(zhàn)。比如下面的分支if (addr (unsigned long)buf 0x80) { puts(half); }buf 0x80是符號(hào)表達(dá)式addr也是符號(hào)表達(dá)式。angr 會(huì)把它們都轉(zhuǎn)成 BitVec 表達(dá)式放到求解器里兩邊都可以約束看起來(lái)沒(méi)什么問(wèn)題。但如果你在這個(gè)符號(hào)地址上讀寫(xiě)內(nèi)存事情就麻煩了char *buf malloc(0x100); char c buf[user_index];user_index是符號(hào)值時(shí)buf[user_index]實(shí)際上等價(jià)于load(symbolic_base user_index)。angr 默認(rèn)支持符號(hào)地址的 load但它會(huì)把這個(gè)符號(hào)地址“具體化”成若干候選地址再分別嘗試讀取。候選地址的集合如果太大性能會(huì)驟降。這就是為什么很多場(chǎng)合下符號(hào)地址不一定是好事。你要結(jié)合具體場(chǎng)景決定是符號(hào)化 malloc 返回值還是只符號(hào)化地址單元內(nèi)容。3.3 約束符號(hào)地址指向合法堆區(qū)剛才的 hook 代碼里加了地址范圍約束但那只算“粗約束”。在實(shí)際分析中你還需要考慮對(duì)齊問(wèn)題。一個(gè)典型坑很多指令集要求 16 字節(jié)對(duì)齊的棧/堆訪問(wèn)。如果 malloc 返回符號(hào)地址沒(méi)有對(duì)齊約束后續(xù)涉及到對(duì)齊假設(shè)的指令就會(huì)產(chǎn)生不可滿足路徑。加約束的方式如下self.state.solver.add_constraints((sym_addr 0xF) 0)還有一點(diǎn)符號(hào)地址最好不要每次都新建一個(gè)新的BVS否則兩個(gè) malloc 調(diào)用之間沒(méi)有任何關(guān)聯(lián)無(wú)法模擬“第二次分配位于第一次分配之后”的堆布局。可以引入一種“連續(xù)分配”約束self.last_malloc_addr 0x600000 self.solver.add_constraints(sym_addr last_malloc_addr 0x10)這類(lèi)約束會(huì)在你的路徑分析里形成一定的順序語(yǔ)義讓地址模型更接近真實(shí)堆。3.4 地址具體化策略的控制參數(shù)地址符號(hào)化之后你不一定希望所有內(nèi)存讀寫(xiě)都走符號(hào)地址那樣求解器壓力太大。angr 提供了一組具體化策略concretization strategies可以精確控制哪些符號(hào)地址在什么時(shí)候被解析成具體值。相關(guān)參數(shù)主要是state.memory.address_concretization這個(gè)列表。比如from angr.state_plugins.sim_memory import concretization_strategies state.memory.address_concretization ( concretization_strategies.SimpleConcretizationStrategy(limit4), concretization_strategies.AnyConcretizationStrategy(), )如果希望某個(gè)符號(hào)地址直接具體化為前幾個(gè)可行解可以用帶limit的策略。如果希望 angr 把符號(hào)地址當(dāng)作符號(hào)值保留就用SymbolicConcretizationStrategy。記住一個(gè)原則符號(hào)地址參與讀寫(xiě)的目的是擴(kuò)展路徑不是為了給求解器增加負(fù)擔(dān)。凡是你能通過(guò)少量約束把地址鎖定的地方盡量鎖定只在關(guān)鍵位置保留符號(hào)地址。4. 進(jìn)階二符號(hào)長(zhǎng)度下的批量單元符號(hào)化4.1 大小也是符號(hào)值時(shí)內(nèi)存單元批量處理有的目標(biāo)程序是這么寫(xiě)的size_t len; char *buf; scanf(%zu, len); buf malloc(len); read(0, buf, len);輸入控制len再控制 read 的長(zhǎng)度。angr 默認(rèn)處理這種模式時(shí)會(huì)試圖把符號(hào)長(zhǎng)度的 read 轉(zhuǎn)換成對(duì)地址單元的一串符號(hào)字節(jié)賦值。問(wèn)題在于len是符號(hào)值你不知道具體寫(xiě)多少個(gè)符號(hào)字節(jié)。這時(shí)常見(jiàn)的思路是把len約束到一個(gè)有限區(qū)間例如len 0x80然后按最大可能長(zhǎng)度符號(hào)化所有地址單元state.solver.add_constraints(len 0x80) for i in range(0x80): b state.solver.BVS(cell_%d % i, 8) state.memory.store(buf_addr i, b)但這樣會(huì)導(dǎo)致讀出的數(shù)據(jù)量比實(shí)際 len 大。所以程序里如果后續(xù)用len做邊界條件這套方法仍然能跑只是冗余符號(hào)字節(jié)會(huì)帶來(lái)額外的分支。4.2 用 state.solver.If 構(gòu)造邊界判斷另一個(gè)更精細(xì)的做法不手動(dòng)鋪滿所有單元而是用state.solver.If構(gòu)造“條件內(nèi)容”。比如你只想符號(hào)化len個(gè)字節(jié)for i in range(0x80): cond i len b state.solver.If(cond, state.solver.BVS(cell_%d % i, 8), state.solver.BVV(0, 8)) state.memory.store(buf_addr i, b, endnessIend_LE)這樣每個(gè)單元的內(nèi)容都是If(len i, 符號(hào)值, 0)。求解器在判定某條路徑是否可行時(shí)會(huì)自動(dòng)把len和cell_i聯(lián)合約束。這個(gè)方案的缺點(diǎn)是表達(dá)式變大求解變慢優(yōu)點(diǎn)是很精確不會(huì)引入多余的真實(shí)分支。4.3 探索路徑爆炸時(shí)如何限流符號(hào)長(zhǎng)度加上符號(hào)內(nèi)容很快會(huì)撞上路徑爆炸。如果發(fā)現(xiàn) explore 卡死第一件事就是壓縮符號(hào)維度用state.solver.concretize(len)直接把長(zhǎng)度燒成一個(gè)固定值再往下跑比如依次固定為0x10、0x20、0x80分別探索。對(duì)符號(hào)數(shù)據(jù)字節(jié)只對(duì)參與比較的相對(duì)偏移位置做符號(hào)化其余字節(jié)用具體值0。設(shè)置find和avoid列表縮小模擬路徑范圍。我有一個(gè)習(xí)慣第一遍跑先加一堆具體約束驗(yàn)證邏輯通順第二遍再逐步放寬觀察新增可達(dá)路徑。直接一上來(lái)就全部符號(hào)化的結(jié)果往往是“看起來(lái)能跑但你等不到結(jié)果”。5. 常見(jiàn)問(wèn)題與避坑記錄5.1 UnsafeSymbolicAssignmentError 的三種解決套路這是做符號(hào)地址寫(xiě)入時(shí)最常踩的異常。當(dāng) angr 試圖在符號(hào)地址上執(zhí)行store而當(dāng)前地址具體化策略無(wú)法找到一個(gè)可行解時(shí)就會(huì)直接拋UnsafeSymbolicAssignmentError。我實(shí)踐下來(lái)有三次有效解法第一給符號(hào)地址增加明確的數(shù)值范圍約束。很多人只寫(xiě)了 0x600000忘記加 0x700000導(dǎo)致求解器可以選一個(gè)高地址區(qū)間但該區(qū)間沒(méi)有映射store 無(wú)法完成。第二使用state.options.add(angr.options.CONCRETIZE)。這會(huì)把所有符號(hào)地址強(qiáng)制具體化簡(jiǎn)單粗暴適合快速驗(yàn)證目標(biāo)分支是否存在。代價(jià)是丟失了地址相關(guān)的路徑覆蓋。第三改用符號(hào)地址的“臨時(shí)具體化”。在 store 前手動(dòng)求解一個(gè)候選地址然后把這個(gè)候選地址具體化store 完再保留原有符號(hào)約束candidate state.solver.eval(sym_addr) state.memory.store(candidate, value, ...這條路適合你已經(jīng)知道一個(gè)安全地址區(qū)間但不想全局開(kāi) CONCRETIZE 的場(chǎng)景。5.2 為什么求出的模型數(shù)值看起來(lái)不對(duì)符號(hào)求解完成后你eval某個(gè)地址或數(shù)據(jù)經(jīng)常發(fā)現(xiàn)結(jié)果稀奇古怪。比如你期望buf0x18的值為0xdeadbeefcafebabe但拿到的卻是0xbabe...之類(lèi)。多數(shù)時(shí)候是小端序沒(méi)有對(duì)齊多數(shù)字節(jié)訪問(wèn)時(shí)符號(hào)位都留了洞。排查步驟很簡(jiǎn)單確認(rèn)load的endness和你的目標(biāo)架構(gòu)字節(jié)序一致。確認(rèn)size到底是多少字節(jié)。如果程序讀取 4 字節(jié)你卻 load 8 字節(jié)會(huì)帶進(jìn)來(lái)未約束的高 32 位結(jié)果自然不穩(wěn)定。檢查是否漏了state.solver.add_constraints。內(nèi)存單元里不只包含你主動(dòng)放的符號(hào)值還可能包含 read 產(chǎn)生的一大堆未約束符號(hào)字節(jié)它們也會(huì)參與求解干擾模型結(jié)果。5.3 hook malloc 與 angr 自帶堆模擬的沖突有些人自定義 hook malloc 之后發(fā)現(xiàn)后續(xù)的free、calloc、realloc全亂了。原因很簡(jiǎn)單你只 hook 了malloc其他堆函數(shù)仍然是 angr 默認(rèn)的堆模擬兩套堆狀態(tài)不互通。解決思路有兩種如果你只是想把 malloc 返回值變成符號(hào)那么不要拆掉默認(rèn)的 SimHeap而是在默認(rèn) SimProcedure 的基礎(chǔ)上包裝先調(diào)用原實(shí)現(xiàn)再在返回前把地址改成符號(hào)值。如果你要完全接管堆邏輯就必須同時(shí) hookmalloc、free、calloc、realloc等一套函數(shù)并且自己維護(hù)一個(gè)模擬堆的符號(hào)地址分配表。我一般推薦第二種只用在高度定制化的固件分析里普通用戶態(tài)程序保持默認(rèn)就好。5.4 性能優(yōu)化符號(hào)地址與具體地址混用最后一個(gè)建議也是我實(shí)際分析中最受益的一點(diǎn)不要把所有 malloc 地址都符號(hào)化。你只要把關(guān)鍵路徑上參與比較、運(yùn)算的 malloc 返回地址符號(hào)化即可其他函數(shù)內(nèi)部的 malloc 完全可以交給默認(rèn)堆模擬。用一張表總結(jié)幾種策略的適用情形策略地址性質(zhì)單元內(nèi)容適用場(chǎng)景主要限制默認(rèn) malloc 模擬具體可符號(hào)常規(guī) stdin/file 輸入地址值固定無(wú)法參與復(fù)雜分支手動(dòng)向具體地址單元寫(xiě)符號(hào)具體符號(hào)模擬非標(biāo)準(zhǔn)輸入源需要知道地址hook malloc 返回符號(hào)地址符號(hào)可符號(hào)地址參與運(yùn)算/比較求解慢需約束合法區(qū)間CONCRETIZE 全開(kāi)符號(hào)→具體符號(hào)快速驗(yàn)證可達(dá)性丟失地址相關(guān)路徑這張表基本覆蓋了我后面大半年的 angr 使用場(chǎng)景。遇到問(wèn)題先對(duì)照一下往往能少走很多彎路。最后再說(shuō)一點(diǎn)個(gè)人的體會(huì)angr 本身的 API 并不難背真正卡住人的是“什么時(shí)候需要符號(hào)化、符號(hào)化到什么程度”。malloc 地址單元符號(hào)化這件事核心不是寫(xiě)幾行 hook而是你要想清楚這個(gè)符號(hào)地址到底參與哪些運(yùn)算、會(huì)影響哪些分支。想明白了代碼只是順手的事。后面我再遇到類(lèi)似堆管理邏輯分析基本都是一上來(lái)先畫(huà)清楚地址流向再?zèng)Q定哪些 malloc 該符號(hào)化、哪些該保持具體整個(gè)分析效率一下子提升了很多。