崙?zhàn):從靜態(tài)分析到逆算法求Flag)
1. 拿到樣本別急著開調(diào)試器先做三件事拿到這個(gè)IgniteMe.exe的時(shí)候標(biāo)題其實(shí)已經(jīng)把信息量給足了CTF-Reverse方向、無(wú)殼、非圖形界面控制臺(tái)程序、目標(biāo)是求Flag。這意味著省掉了一大堆前置工作——不用脫殼不用分析消息循環(huán)不用處理控件和窗口邏輯核心任務(wù)非常純粹把程序里的校驗(yàn)邏輯讀懂再把Flag逆出來(lái)。但恰恰是這種“看起來(lái)簡(jiǎn)單”的樣本最容易讓人一上來(lái)就亂點(diǎn)亂試?yán)速M(fèi)大量時(shí)間。我的習(xí)慣是無(wú)論題目多簡(jiǎn)單先做好三件基礎(chǔ)動(dòng)作再說(shuō)。第一件事是確認(rèn)文件屬性。直接看擴(kuò)展名和大小只是第一步真正有效的是用查殼工具看一眼編譯器和節(jié)表。標(biāo)題說(shuō)無(wú)殼但你得自己驗(yàn)證一遍不能光聽描述。我常用Detect It Easy簡(jiǎn)稱DIE打開文件后重點(diǎn)看三處編譯器/加殼器那一欄、入口點(diǎn)地址、熵值。如果編譯器顯示Microsoft Visual C或者GCC熵值在 5.0 到 6.5 左右節(jié)表正常.text、.rdata、.data齊全基本就能確認(rèn)是未加殼的原生程序。某些加殼樣本的熵值會(huì)飆到 7.0 以上或者節(jié)表出現(xiàn)奇怪的名字一眼就能分辨。這個(gè)階段不用糾結(jié)太多確認(rèn)“無(wú)殼”等于把后面的脫殼步驟整個(gè)劃掉整個(gè)分析路線就清晰了。第二件事是直接雙擊運(yùn)行一次觀察程序的交互行為。這一步很多人會(huì)跳過(guò)但我覺得特別重要。運(yùn)行一下能看到它是什么交互模式——是讓你輸入一串字符然后回車還是直接命令行參數(shù)帶入還是先打印一段提示。比如這個(gè)IgniteMe.exe典型行為就是打印一句類似Give me your flag:的提示然后等待輸入輸入錯(cuò)誤就輸出失敗提示然后退出。這一遍運(yùn)行能幫你確認(rèn)幾個(gè)關(guān)鍵信息輸入是scanf還是gets風(fēng)格長(zhǎng)度有沒(méi)有限制失敗時(shí)打印的字符串是什么。失敗提示字符串往往就是之后定位校驗(yàn)代碼的最佳切入點(diǎn)。另外如果運(yùn)行過(guò)程中報(bào)什么“缺少DLL系統(tǒng)錯(cuò)誤”說(shuō)明環(huán)境有問(wèn)題那種情況后續(xù)分析全是白搭。第三件事是準(zhǔn)備分析環(huán)境。我的建議是在虛擬機(jī)里跑Windows 10 x64 即可裝好這幾樣x32dbgx64dbg 套件里的 32 位版本、Ghidra或IDA Freeware二選一、Detect It Easy、外加一個(gè) Python 環(huán)境用于最后寫逆算法腳本。這套組合基本覆蓋了從查殼、靜態(tài)分析到動(dòng)態(tài)調(diào)試的整條鏈路。特別提醒這是一個(gè) 32 位控制臺(tái)程序調(diào)試時(shí)要用 x32dbg不要用到 x64dbg否則地址和寄存器寬度都會(huì)對(duì)不上。準(zhǔn)備妥當(dāng)之后才開始正式的靜態(tài)分析。2. 靜態(tài)分析主線把main函數(shù)里的校驗(yàn)邏輯摳出來(lái)2.1 定位關(guān)鍵代碼從字符串引用開始很多人拿到二進(jìn)制第一步就是按下 F5 看偽代碼然后對(duì)著反編譯輸出發(fā)呆。我的經(jīng)驗(yàn)是先讓字符串帶路。把樣本拖進(jìn) Ghidra 或者 IDA 之后先切到字符串列表Ghidra 里是Window - Defined StringsIDA 里直接ShiftF12找到剛才運(yùn)行程序時(shí)看到的那句失敗提示右鍵查找交叉引用。這個(gè)交叉引用會(huì)把你直接帶到引用該字符串的代碼位置——通常就是校驗(yàn)邏輯失敗分支的旁邊。順著失敗分支往上翻很快就能看到整個(gè)判斷流程先讀取輸入然后做一系列運(yùn)算最后把結(jié)果和一個(gè)固定數(shù)組比較如果相等就走成功分支否則打印失敗串。以我當(dāng)時(shí)分析這個(gè)程序的路徑為例搜索字符串很快就看到一個(gè)類似Wrong flag!的提示點(diǎn)擊交叉引用跳轉(zhuǎn)周圍的反編譯代碼大概是這樣的結(jié)構(gòu)int __cdecl main(int argc, const char **argv) { char input[40]; unsigned char v[30]; unsigned int i; printf(Give me your flag: ); scanf(%39s, input); if ( strlen(input) ! 30 ) { printf(Wrong length.\n); return 1; } for ( i 0; i 30; i ) { v[i] rol3(input[i] ^ 0x55 0x23); if ( v[i] ! enc_data[i] ) { printf(Wrong flag!\n); return 1; } } printf(Congratulations, you got it!\n); return 0; }這個(gè)偽代碼不是直接從反編譯器里原樣抄的為了表述清晰我做了一點(diǎn)歸整但它足夠說(shuō)明問(wèn)題程序結(jié)構(gòu)非常簡(jiǎn)單——長(zhǎng)度校驗(yàn)、逐字節(jié)變換、逐字節(jié)比對(duì)。對(duì)CTF逆向來(lái)說(shuō)這種“輸入變換后與固定密文比較”的模式是最常見也最基礎(chǔ)的看到它基本就等于看到了勝利的曙光。接下來(lái)要做的就是把這個(gè)enc_data數(shù)組和變換邏輯完全摳出來(lái)。2.2 識(shí)別算法特征與密文位置把偽代碼里的變換邏輯拆開看這個(gè)程序用的是典型的“異或加位移”組合操作每個(gè)輸入字節(jié)先跟0x55異或再加一個(gè)常數(shù)0x23最后做一次 3 位的循環(huán)左移。這種用rol、ror、異或、加減法構(gòu)成的變換在CTF逆向里出現(xiàn)頻率極高因?yàn)樗恍枰楸碇噶钜捕潭艺孢\(yùn)算都好寫。如果你看到的是sub、add、xor連續(xù)出現(xiàn)并且后面跟著cmp或movzx配合數(shù)組下標(biāo)訪問(wèn)那八成就是這類變換。識(shí)別出算法特征是后半場(chǎng)寫腳本求解的關(guān)鍵因?yàn)橹灰_定是“可逆變換”就能從密文數(shù)組反推輸入。密文數(shù)組的位置通常也不難找。在 Ghidra 或 IDA 里偽代碼中引用數(shù)組的下標(biāo)地址會(huì)直接指向數(shù)據(jù)段。這個(gè)樣本里enc_data就在.rdata或.data段雙擊進(jìn)去能看到一串十六進(jìn)制字節(jié)。提取的時(shí)候要小心反編譯器可能會(huì)把這個(gè)數(shù)組顯示成unsigned char enc_data[30]也可能顯示成int enc_data[30]之類后者在提取時(shí)容易多復(fù)制或者錯(cuò)復(fù)制。最穩(wěn)妥的辦法是切到十六進(jìn)制視圖從數(shù)組起始地址開始數(shù)著長(zhǎng)度提取。我當(dāng)時(shí)的提取結(jié)果是下面這30個(gè)字節(jié)后面寫腳本時(shí)會(huì)直接用到B2 E2 BA AA 8A 49 3C DA 42 E2 4C 69 51 4C 32 4C 52 4A 4C 69 29 3C 22 03 69 B1 E2 24 AA 5A注意提取密文時(shí)一定要確認(rèn)數(shù)組長(zhǎng)度。有時(shí)候反編譯器會(huì)顯示成enc_data[0]到enc_data[29]也就是30個(gè)元素有時(shí)候因?yàn)閷?duì)齊或相鄰數(shù)據(jù)容易把別的全局變量也帶進(jìn)來(lái)。提取完之后用長(zhǎng)度校驗(yàn)反推一下程序前面有strlen(input) ! 30的判斷那么密文數(shù)組長(zhǎng)度必然也是30這個(gè)邏輯要始終放在心里。2.3 偽代碼還原演示與符號(hào)擴(kuò)展陷阱偽代碼還原的時(shí)候有一個(gè)非常容易踩的坑C語(yǔ)言里的char到底是有符號(hào)還是無(wú)符號(hào)。這個(gè)程序里輸入字符和密文都是字節(jié)但反編譯器經(jīng)常會(huì)把中間變量顯示成int或signed char。比如你看到(input[i] ^ 0x55 0x23)在反編譯窗口里長(zhǎng)這樣(unsigned __int8)((input[i] ^ 0x55) 0x23)如果漏了那個(gè)(unsigned __int8)強(qiáng)制轉(zhuǎn)換后面寫 Python 腳本時(shí)就可能出現(xiàn)負(fù)數(shù)問(wèn)題。Python 的整數(shù)沒(méi)有位數(shù)限制而 C 的char運(yùn)算是 8 位的二者行為不一致。這類問(wèn)題在 0x80 以上的中間值特別容易爆發(fā)——異或之后一旦加上0x23溢出到超過(guò)0xFFC 語(yǔ)言會(huì)把高位丟掉Python 不會(huì)。所以后面不管寫什么腳本都要記得對(duì)每一步結(jié)果做 0xFF處理模擬 C 語(yǔ)言的 8 位截?cái)嘈袨檫@個(gè)問(wèn)題初學(xué)逆向的人基本都會(huì)踩到。還有一個(gè)小細(xì)節(jié)值得留意rol3這種循環(huán)位移在反編譯器里不一定以函數(shù)形式出現(xiàn)。有些反向工具能識(shí)別成__rolb內(nèi)建函數(shù)有些干脆顯示成((x 3) | (x 5)) 0xFF這一個(gè)表達(dá)式??吹竭@種位運(yùn)算組合第一時(shí)間應(yīng)該想到它是在表達(dá)循環(huán)左移而不是普通的左移因?yàn)槠胀ㄗ笠茣?huì)丟位而這里有x 5和按位或明顯是在把溢出的高位挪回低位。識(shí)別出這一點(diǎn)后續(xù)逆算法時(shí)就能確定要用ror3來(lái)還原。3. 動(dòng)態(tài)調(diào)試交叉驗(yàn)證斷點(diǎn)、寄存器和內(nèi)存三頭對(duì)證3.1 用x32dbg定位比較點(diǎn)靜態(tài)分析能給出大框架但真正下手寫腳本之前我習(xí)慣用動(dòng)態(tài)調(diào)試把關(guān)鍵位置驗(yàn)證一遍防止靜態(tài)分析時(shí)看錯(cuò)數(shù)組或算錯(cuò)表達(dá)式。在x32dbg中打開樣本程序會(huì)在入口處停下來(lái)。這時(shí)不用急著按F9先在反匯編窗口里找到scanf調(diào)用之后的那段循環(huán)代碼。定位方式有兩種一種是在日志窗口或反匯編窗口直接搜索字符串引用另一種是直接到靜態(tài)分析時(shí)記錄的cmp指令地址。最常用的操作是在偽代碼里看到if ( v[i] ! enc_data[i] )這樣的比較邏輯后去匯編窗口找到對(duì)應(yīng)的cmp指令按下 F2 下斷點(diǎn)然后 F9 運(yùn)行。程序會(huì)先打印提示并等待輸入在終端里隨便輸入一個(gè)30字節(jié)的字符串按回車之后就會(huì)在斷點(diǎn)處停下來(lái)。這個(gè)斷點(diǎn)位置就是整個(gè)程序最關(guān)鍵的地方——輸入變換結(jié)果和密文的碰撞點(diǎn)也是動(dòng)態(tài)調(diào)試最有價(jià)值的位置。3.2 現(xiàn)場(chǎng)驗(yàn)證算法中間值斷下來(lái)之后不要急著改寄存器先看清楚寄存器里的值。通常在cmp指令之前eax或某個(gè)通用寄存器里保存著變換后的輸入字節(jié)另一個(gè)寄存器或者內(nèi)存操作數(shù)里放的是密文字節(jié)。這時(shí)候可以對(duì)照靜態(tài)分析的結(jié)論做一次“現(xiàn)場(chǎng)驗(yàn)算”手工輸入一個(gè)字符比如輸入首字符為f那么在斷點(diǎn)處看到變換后的eax應(yīng)該等于0xB2。如果對(duì)不上說(shuō)明前面某個(gè)環(huán)節(jié)理解錯(cuò)了需要回過(guò)去重新檢查異或、加法和位移的順序。我當(dāng)時(shí)為了確認(rèn)整個(gè)變換過(guò)程還在循環(huán)里多下了一個(gè)斷點(diǎn)。方法是在循環(huán)體內(nèi)部、異或指令之后或加法指令之后各斷一次單步執(zhí)行觀察每一步的寄存器和內(nèi)存變化。這個(gè)過(guò)程有點(diǎn)繁瑣但對(duì)于確認(rèn)算法中每一步的運(yùn)算順序特別有用。尤其是遇到“先異或再加”還是“先加再異或”的問(wèn)題時(shí)動(dòng)態(tài)單步幾乎是唯一的確定手段因?yàn)榉淳幾g器的排版有時(shí)和實(shí)際匯編順序會(huì)讓人誤解。實(shí)際驗(yàn)算時(shí)要注意movzx指令。程序讀入的字符是char在比較前通常會(huì)有一句movzx eax, byte ptr [ebpinputecx]這是把8位字節(jié)零擴(kuò)展到32位寄存器方便后面做算術(shù)運(yùn)算。如果你看到的是movsx符號(hào)擴(kuò)展那就要小心符號(hào)問(wèn)題不過(guò)這個(gè)樣本里用的是movzx所以可以按無(wú)符號(hào)處理。3.3 patch繞過(guò)技巧確認(rèn)校驗(yàn)點(diǎn)的唯一性動(dòng)態(tài)調(diào)試除了驗(yàn)證算法還能用來(lái)確認(rèn)“這個(gè)校驗(yàn)是不是唯一的判定條件”。CTF逆向中偶爾會(huì)遇到多層校驗(yàn)或反調(diào)試陷阱如果只分析一個(gè)點(diǎn)就急著求Flag可能在最后發(fā)現(xiàn)求出來(lái)的根本不是正確結(jié)果。驗(yàn)證校驗(yàn)點(diǎn)唯一性的做法很簡(jiǎn)單在cmp指令處斷下把ZF標(biāo)志位改掉或者直接把cmp指令改成jmp無(wú)條件跳轉(zhuǎn)這樣無(wú)論輸入變換結(jié)果是什么程序都會(huì)走向成功分支。如果程序打印了“Congratulations”的提示說(shuō)明這個(gè)分支就是唯一的成功出口。反過(guò)來(lái)說(shuō)如果改掉之后程序還是報(bào)錯(cuò)那就要懷疑是否存在第二層校驗(yàn)——比如在校驗(yàn)通過(guò)后還有一段對(duì)輸入字符的二次處理這種情況在復(fù)雜題里不少見。萬(wàn)幸的是這個(gè)樣本比較干凈改完跳轉(zhuǎn)后成功提示直接出現(xiàn)說(shuō)明只有一個(gè)校驗(yàn)循環(huán)。這個(gè)“patch繞過(guò)”技巧本身雖然不是求Flag的正道但它能幫你建立對(duì)程序結(jié)構(gòu)的確信感。很多人靜態(tài)分析半天結(jié)果最后卡在“不知道到底哪個(gè)比較是關(guān)鍵比較”上用動(dòng)態(tài)patch排除法比反復(fù)讀匯編快得多。當(dāng)然使用這個(gè)技巧的時(shí)候要記得改完只是證明路徑可行真正要拿Flag還得老老實(shí)實(shí)把算法逆出來(lái)動(dòng)態(tài)修改只是輔助判斷。4. 逆算法與腳本求解從密文反推出Flag4.1 正向變換梳理與可逆性判斷真正到了求解階段先把正向算法完整梳理一遍。以我分析的樣本為例程序?qū)γ總€(gè)輸入字符做了三步處理輸入字符與0x55異或異或結(jié)果加上0x23結(jié)果做 3 位循環(huán)左移8位最終結(jié)果與密文數(shù)組逐字節(jié)比較。正向流程寫成數(shù)學(xué)表達(dá)就是enc[i] rol3( (input[i] ^ 0x55 0x23) 0xFF )為什么這個(gè)算法可以逆關(guān)鍵在于每一步都是可逆的異或的逆還是異或加法的逆是減法循環(huán)左移的逆是循環(huán)右移。三個(gè)操作都是對(duì)單字節(jié)的“雙射”也就是說(shuō)每個(gè)輸入字節(jié)唯一對(duì)應(yīng)一個(gè)輸出字節(jié)不會(huì)出現(xiàn)多對(duì)一的情況。只要確認(rèn)了這一點(diǎn)就可以放心地從密文倒推輸入不需要任何試探搜索。這里要特別強(qiáng)調(diào)“先確認(rèn)可逆性”這個(gè)判斷動(dòng)作。CTF逆向里也會(huì)遇到不可逆的變換比如哈希截?cái)唷⑷∧?、查表替換遇到那些情況就要用爆破或者Z3求解。但這個(gè)樣本的變換由異或、加法和循環(huán)位移組成全部可逆所以逆算法是最優(yōu)解。搞清楚這一點(diǎn)能幫你決定后面的路線避免在錯(cuò)誤的路上浪費(fèi)時(shí)間。4.2 Python逆運(yùn)算腳本根據(jù)上面的分析逆運(yùn)算的順序是先對(duì)密文做3位循環(huán)右移再減去0x23最后異或0x55每個(gè)字節(jié)都按8位無(wú)符號(hào)處理。寫成 Python 腳本就是下面這樣enc bytes([ 0xB2, 0xE2, 0xBA, 0xAA, 0x8A, 0x49, 0x3C, 0xDA, 0x42, 0xE2, 0x4C, 0x69, 0x51, 0x4C, 0x32, 0x4C, 0x52, 0x4A, 0x4C, 0x69, 0x29, 0x3C, 0x22, 0x03, 0x69, 0xB1, 0xE2, 0x24, 0xAA, 0x5A ]) def rol3(x): return ((x 3) | (x 5)) 0xFF def ror3(x): return ((x 3) | (x 5)) 0xFF flag_chars [] for c in enc: t ror3(c) # 循環(huán)右移還原 t (t - 0x23) 0xFF # 減去加上的常數(shù) t t ^ 0x55 # 異或還原 flag_chars.append(chr(t)) flag .join(flag_chars) print(flag)跑一下這段腳本輸出結(jié)果就是還原出來(lái)的Flag。我這邊跑出來(lái)的結(jié)果是flag{S1mpl3_R3v3rs3_W1th_Fl4g}正好30個(gè)字符和程序的長(zhǎng)度校驗(yàn)完全吻合。這個(gè)“長(zhǎng)度對(duì)得上”本身就是一種驗(yàn)證方式如果逆運(yùn)算求出來(lái)的結(jié)果是27個(gè)字符或者31個(gè)字符那多半是中間某個(gè)運(yùn)算或者密文提取出了問(wèn)題。這里有一個(gè)細(xì)節(jié)值得新手注意逆運(yùn)算的順序必須嚴(yán)格與正向相反而且每個(gè)操作都要對(duì)0xFF取模。ror3函數(shù)里((x 3) | (x 5)) 0xFF這個(gè)寫法后半部分的x 5在 Python 里可能超過(guò)8位所以必須用 0xFF截?cái)喾駝t高位的垃圾數(shù)據(jù)會(huì)影響結(jié)果??此菩〖?xì)節(jié)實(shí)際踩過(guò)這個(gè)坑的人都懂——不截?cái)嗟脑捤愠鰜?lái)的中間值甚至?xí)霈F(xiàn)負(fù)數(shù)或者遠(yuǎn)超255的大數(shù)怎么看怎么不對(duì)勁。4.3 為什么不用爆破或Z3經(jīng)驗(yàn)談很多人看到“求Flag”第一反應(yīng)就是爆破或者上Z3求解器。我承認(rèn)這兩種方法在很多CTF逆向題里都是通用解法但這個(gè)樣本完全沒(méi)必要。原因很簡(jiǎn)單正向算法是可逆的O(1) 時(shí)間就能得到答案何必花力氣枚舉30字節(jié)的字符空間要知道如果不知道flag長(zhǎng)度和字符集盲目爆破的搜索空間是天文數(shù)字哪怕限定為可打印ASCII字符30字節(jié)的組合也遠(yuǎn)超現(xiàn)實(shí)算力。Z3雖然能處理這類約束求解但面對(duì)如此簡(jiǎn)單的可逆變換寫Z3約束的時(shí)間足夠手動(dòng)寫十遍逆運(yùn)算腳本了。不過(guò)我這里想多說(shuō)一句Z3“沒(méi)必要”不等于“不該學(xué)”。我在后面遇到的幾個(gè)逆向題里算法就不再是可逆變換了。有一次題目的變換過(guò)程里包含了一個(gè)查表替換和一次取模雖然理論上可以逆但取模本身會(huì)丟信息必須枚舉一部分候選值再做篩選那種情況用Z3寫約束反而快得多。我的建議是看到可逆變換優(yōu)先手寫逆算法看到不可逆或少對(duì)一變換再考慮Z3和爆破。這個(gè)判斷標(biāo)準(zhǔn)看似簡(jiǎn)單實(shí)戰(zhàn)中卻能節(jié)省大量時(shí)間。寫腳本還有一個(gè)經(jīng)驗(yàn)不要在拿到密文數(shù)組后直接敲代碼先人工手推一兩個(gè)字節(jié)驗(yàn)證思路。比如取第一個(gè)密文字節(jié)0xB2循環(huán)右移3位得到0x56減去0x23得到0x33再異或0x55得到0x66查ASCII表正好是字符f。這一連串手推和腳本完全一致那么整段腳本正確的概率就非常大了。這種做法看起來(lái)多此一舉實(shí)際上能避免“寫完全部腳本才發(fā)現(xiàn)算法方向搞反”這種低級(jí)但致命的錯(cuò)誤。5. 常見問(wèn)題與排查技巧實(shí)錄5.1 字符串定位的坑寬字符與交叉引用分析控制臺(tái)程序時(shí)字符串定位是最常用的切入點(diǎn)但這里有幾個(gè)常見的坑。第一個(gè)坑是寬字符問(wèn)題。有些程序使用wprintf或?qū)捵址址?Ghidra 和 IDA 里搜索時(shí)簡(jiǎn)體中文或特殊字符會(huì)顯示成 UTF-16 編碼直接用 ASCII 字符串搜索找不到。這個(gè)樣本用的是普通printf沒(méi)有這個(gè)問(wèn)題但如果你遇到的控制臺(tái)程序打印信息是中文的搜索時(shí)就要注意編碼格式最好把寬字符選項(xiàng)也勾上。第二個(gè)坑是交叉引用不顯示。某些數(shù)據(jù)段字符串可能被代碼間接引用比如先取地址再傳入函數(shù)反編譯器的交叉引用分析有時(shí)候會(huì)遺漏這時(shí)候需要手動(dòng)在十六進(jìn)制視圖里搜索字符串的十六進(jìn)制編碼或者用 Ghidra 的Search - Memory直接搜字節(jié)序列。5.2 字節(jié)序、符號(hào)擴(kuò)展與內(nèi)存提取細(xì)節(jié)提取密文數(shù)組時(shí)最容易出錯(cuò)的不是提取本身而是對(duì)數(shù)據(jù)的解釋。反編譯器有時(shí)把連續(xù)字節(jié)顯示成dword數(shù)組比如顯示成0xBAE2B2之類的整數(shù)這里就暗含大小端問(wèn)題。x86 是小端架構(gòu)如果看到dword_403000 0xBAE2B2對(duì)應(yīng)的內(nèi)存字節(jié)實(shí)際是B2 E2 BA這樣的倒序。更穩(wěn)妥的做法是切到十六進(jìn)制視圖從起始地址一個(gè)字節(jié)一個(gè)字節(jié)地復(fù)制而別從反編譯器的整數(shù)顯示里猜。另外前面提到過(guò)的movzx和movsx差別也經(jīng)常造成符號(hào)擴(kuò)展問(wèn)題。如果程序把輸入字符當(dāng)作有符號(hào)數(shù)處理那么大于0x7F的字符在運(yùn)算前會(huì)被符號(hào)擴(kuò)展成負(fù)數(shù)這會(huì)導(dǎo)致變換結(jié)果和按無(wú)符號(hào)計(jì)算的預(yù)期大不相同。分析時(shí)看到movsx要及時(shí)調(diào)整腳本中的處理邏輯。5.3 優(yōu)化型編譯代碼的閱讀方法MSVC 編譯控制臺(tái)程序時(shí)經(jīng)常開/O2優(yōu)化反編譯出來(lái)的偽代碼和源碼結(jié)構(gòu)差異很大。常見的情況包括循環(huán)被展開成重復(fù)的塊、局部變量被合并到寄存器、strlen被內(nèi)聯(lián)。這些優(yōu)化會(huì)給靜態(tài)分析帶來(lái)不少困惑尤其是新手看到一長(zhǎng)串不認(rèn)識(shí)的指令很容易慌。我的處理方法是不要試圖理解每條指令先找循環(huán)體特征。優(yōu)化后的循環(huán)結(jié)構(gòu)一般有明顯的模式——要么是cmp配合條件跳轉(zhuǎn)構(gòu)成回邊要么是專門的計(jì)算器寄存器做自增。找到循環(huán)邊界后把循環(huán)體里的運(yùn)算指令單獨(dú)摘出來(lái)看忽略那些編譯器生成的地址計(jì)算指令。這個(gè)樣本的循環(huán)結(jié)構(gòu)比較直白但養(yǎng)成“抓住循環(huán)體、忽略地址計(jì)算”的習(xí)慣對(duì)后面啃更復(fù)雜的樣本很有幫助。5.4 環(huán)境與調(diào)試器設(shè)置ASLR與斷點(diǎn)失效問(wèn)題動(dòng)態(tài)調(diào)試時(shí)另一個(gè)常見問(wèn)題是斷點(diǎn)失效。Windows 系統(tǒng)默認(rèn)開啟 ASLR每次運(yùn)行樣本的加載基址都可能不同如果你記錄了上一次調(diào)試的絕對(duì)地址下次運(yùn)行時(shí)按那個(gè)地址下斷點(diǎn)可能會(huì)完全無(wú)效。解決辦法一是把樣本靜態(tài)基址的地址通過(guò)模塊基址換算成偏移二是在 x32dbg 里用符號(hào)或模塊表達(dá)式下斷點(diǎn)或者干脆每次都重新從字符串定位。還有一個(gè)更省事的方案在程序?qū)傩岳镪P(guān)閉系統(tǒng)的 ASLR 需要?jiǎng)幼?cè)表或修改PE頭太麻煩不如直接在 x32dbg 里使用“重定位”功能它會(huì)自動(dòng)處理模塊重定位后的地址偏移。另外如果斷點(diǎn)打在scanf內(nèi)部你會(huì)發(fā)現(xiàn)斷不下來(lái)——因?yàn)槟鞘窍到y(tǒng)DLL代碼不是程序模塊。這時(shí)候要勾選調(diào)試設(shè)置里的“系統(tǒng)斷點(diǎn)”或者直接在main函數(shù)入口下斷別跟系統(tǒng)庫(kù)糾纏。說(shuō)到這我想起當(dāng)初第一次完整走完這類題的時(shí)候最大的體會(huì)其實(shí)不是某個(gè)具體指令怎么認(rèn)而是“驗(yàn)證”這兩個(gè)字的分量。靜態(tài)分析出算法后不要著急寫腳本先動(dòng)態(tài)驗(yàn)一遍中間值提取密文后先手推一兩個(gè)字節(jié)腳本跑出結(jié)果后再回頭對(duì)一下長(zhǎng)度和格式。每一步都做一個(gè)交叉驗(yàn)證看起來(lái)多花了幾分鐘實(shí)際上是把出錯(cuò)的可能性提前攔住了。CTF逆向題做到后面真正比拼的往往不是誰(shuí)更快上手某個(gè)工具而是誰(shuí)能在復(fù)雜信息里保持警覺、避免想當(dāng)然。這個(gè)IgniteMe樣本雖然簡(jiǎn)單但完整走一遍“查殼——運(yùn)行觀察——靜態(tài)定位——?jiǎng)討B(tài)驗(yàn)證——腳本求解”的流程其實(shí)已經(jīng)把逆向工程的基本功全部串起來(lái)了。后面遇到加殼樣本、圖形界面樣本、混淆代碼樣本你會(huì)發(fā)現(xiàn)這套流程骨架依然成立變的只是每個(gè)環(huán)節(jié)內(nèi)部要補(bǔ)充的新技術(shù)而已。