崙?zhàn):從OllyDbg調(diào)試到Patch爆破)
作為一個(gè)常年和OD、x64dbg打交道的二進(jìn)制愛好者CrackMe系列一直是我覺得性價(jià)比最高的練手材料。新160個(gè)CrackMe里前幾個(gè)題目都很適合拿來當(dāng)“熱身操”其中002號(hào)abexcm5更是經(jīng)典得不能再經(jīng)典。別看它體積小到只有幾個(gè)KB里面塞了完整的PE加載邏輯、導(dǎo)入表解析、API調(diào)用和條件跳轉(zhuǎn)能把它啃下來你基本就告別“只會(huì)拖進(jìn)OD看字符串”的新手階段了。這篇文章不打算泛泛而談原理而是直接帶你走一遍完整的逆向流程從載入到定位關(guān)鍵分支再一路到手工Patch和可執(zhí)行文件保存中間穿插我在實(shí)際操作中踩過的坑和總結(jié)的經(jīng)驗(yàn)。1. 項(xiàng)目概述與逆向目標(biāo)1.1 這個(gè)CrackMe是什么abexcm5是“新160個(gè)CrackMe”逆向練習(xí)序列中的第2個(gè)程序。它由一位ID為abex的作者編寫目標(biāo)極其純粹檢查程序運(yùn)行環(huán)境中的光驅(qū)是否存在。說是檢查光驅(qū)程序本身并不讀取光盤上的任何數(shù)據(jù)它只是調(diào)用Windows的API來獲取當(dāng)前磁盤類型如果結(jié)果正好等于“CD-ROM”類型程序就展示一段成功提示否則就彈出一個(gè)錯(cuò)誤對(duì)話框。你不需要輸入注冊(cè)碼也不需要找算法整個(gè)破解過程完全圍繞著“如何欺騙這個(gè)判斷”展開。我見過不少初學(xué)者剛拿到這個(gè)文件時(shí)有點(diǎn)懵因?yàn)樗炔粡桸AG窗口也不需要爆破跳轉(zhuǎn)去跳過某個(gè)煩人的計(jì)時(shí)器程序流程一眼就能看完。但正是因?yàn)楹?jiǎn)單它能讓你在沒有任何噪音的情況下看清楚一個(gè)Windows GUI程序從入口點(diǎn)開始經(jīng)過了哪些API調(diào)用以及一個(gè)條件跳轉(zhuǎn)是如何決定整個(gè)程序命運(yùn)的。說白了這就是一個(gè)“用匯編寫成的教學(xué)切片”。1.2 為什么要拿它開刀CrackMe這玩意兒的價(jià)值不在于“難度越高越牛”而在于難度階梯設(shè)置得是否合理。abexcm5正好處在一個(gè)黃金位置它比Hello World級(jí)別的程序多一點(diǎn)API交互又比那些帶反調(diào)試、帶虛擬機(jī)殼的商業(yè)程序簡(jiǎn)單到令人發(fā)指。對(duì)剛接觸逆向的人來說它有四個(gè)非常具體的訓(xùn)練價(jià)值。第一訓(xùn)練動(dòng)態(tài)調(diào)試的基本功載入程序、單步跟蹤、觀察寄存器變化這套動(dòng)作在這里可以完整走一遍。第二訓(xùn)練定位關(guān)鍵判斷的邏輯在一段不分函數(shù)的單薄代碼里找到“影響成敗的那個(gè)跳轉(zhuǎn)”。第三訓(xùn)練靜態(tài)修改字節(jié)碼的能力用工具直接改掉一小段機(jī)器碼讓程序行為發(fā)生變化。第四訓(xùn)練PE文件入口點(diǎn)的理解畢竟這小文件的代碼段和入口點(diǎn)距離極近你很快就能建立起“程序是從某個(gè)固定地址開始執(zhí)行”的空間感。只有把這四點(diǎn)練扎實(shí)了后面碰到的算法注冊(cè)機(jī)、反調(diào)試、加密殼之類的問題才不會(huì)顯得那么可怕。2. 環(huán)境準(zhǔn)備與工具選型2.1 運(yùn)行環(huán)境與系統(tǒng)兼容性做這類練習(xí)很多人第一反應(yīng)是“能不能直接在Win10/Win11上跑”。實(shí)話說abexcm5作為老程序在64位Windows上運(yùn)行一般沒大問題因?yàn)樗徽{(diào)用了MessageBoxA和GetDriveTypeA這類老牌API不涉及驅(qū)動(dòng)、不涉及特殊權(quán)限。不過不同機(jī)器上系統(tǒng)盤的返回值會(huì)有差異程序最終彈出的錯(cuò)誤對(duì)話框內(nèi)容也可能有細(xì)微區(qū)別這屬于正?,F(xiàn)象。為了調(diào)試體驗(yàn)更好我個(gè)人的建議是準(zhǔn)備一臺(tái)Windows 7虛擬機(jī)或者至少在虛擬機(jī)里再放一份XP鏡像。原因很簡(jiǎn)單老程序在老舊系統(tǒng)上行為更“規(guī)矩”而且OllyDbg在Windows 7虛擬機(jī)里掛載這種小程序幾乎零兼容性煩惱。如果你是純新手不想折騰虛擬機(jī)直接在Windows 10物理機(jī)的“兼容模式”下運(yùn)行通常也可以。萬一遇到程序起不來的情況Priority指向的往往不是代碼問題而是系統(tǒng)的DEP或UAC做了額外干涉關(guān)閉UAC或右鍵屬性里調(diào)整兼容性多半能解決。2.2 調(diào)試器選擇OllyDbg還是x64dbg32位的小體積可執(zhí)行文件我最推薦用原版OllyDbg 1.10或基于它的OllyDbg 2.01變體。理由很樸素它對(duì)這個(gè)體量的文件處理速度快、界面不花哨、單步快捷鍵順手F7進(jìn)入Call、F8步過、F2下斷點(diǎn)的肌肉記憶一旦建立后面好處多多。x64dbg當(dāng)然也可以而且它對(duì)現(xiàn)代Windows系統(tǒng)的適配更好如果你本來就習(xí)慣x64dbg完全可以用它來練這個(gè)案例。只是有一點(diǎn)要注意x64dbg在某些系統(tǒng)API的單步表現(xiàn)上會(huì)偏向反匯編分析對(duì)新手來說信息量稍大不如OllyDbg干凈。我這個(gè)案例里用的是OllyDbg下文所有按鈕和地址描述都基于它x64dbg用戶請(qǐng)自行對(duì)照。除了調(diào)試器還建議準(zhǔn)備一個(gè)十六進(jìn)制編輯工具比如HxD或者010 Editor。調(diào)試器里雖然能直接改字節(jié)碼但有時(shí)用十六進(jìn)制編輯器直接打開文件改幾個(gè)字節(jié)對(duì)理解“文件存儲(chǔ)形態(tài)”更有幫助。因?yàn)镃rackMe的整個(gè)代碼段十分短小你甚至可以在Hex編輯窗口里肉眼掃到關(guān)鍵跳轉(zhuǎn)指令對(duì)應(yīng)的機(jī)器碼這種直觀感是調(diào)試器給不了的。3. 靜態(tài)拆解入口點(diǎn)與關(guān)鍵結(jié)構(gòu)3.1 入口點(diǎn)分析用OllyDbg打開abexcm5.exe后默認(rèn)會(huì)停在系統(tǒng)斷點(diǎn)處按一下F9運(yùn)行到用戶入口點(diǎn)你會(huì)看到程序停在00401000附近。這個(gè)地址非??拷黀E文件頭意味著代碼段幾乎擠在文件最前面。如果一個(gè)程序入口點(diǎn)緊挨著節(jié)表區(qū)域通常說明它是用匯編直接寫的小程序沒有經(jīng)過編譯器額外布局。編譯型C程序一般不會(huì)這樣安置入口代碼它們往往會(huì)把啟動(dòng)函數(shù)放在代碼段的更深偏移處。入口點(diǎn)這里可以看到一個(gè)很典型的Win32匯編程序骨架先給MessageBoxA的各個(gè)參數(shù)依次壓棧然后調(diào)用一個(gè)系統(tǒng)API。參數(shù)順序從右往左壓入所以最后的push NULL是在給MessageBoxA傳hWnd前面的push字符串地址分別對(duì)應(yīng)Text和Caption??吹竭@樣的結(jié)構(gòu)基本就能判斷這個(gè)程序在它自己的WinMain邏輯里沒有做太多初始化直接就準(zhǔn)備彈框了。這里要特別注意一個(gè)細(xì)節(jié)程序調(diào)用MessageBoxA之后通常緊接著就是調(diào)用ExitProcess退出但在這個(gè)CrackMe里彈框之前還有一個(gè)GetDriveTypeA的調(diào)用。也就是說程序在執(zhí)行流程里先做了一次環(huán)境檢測(cè)再根據(jù)檢測(cè)結(jié)果決定彈出哪個(gè)對(duì)話框。檢測(cè)和彈框這兩件事被硬生生寫在了同一個(gè)代碼流中并沒有分成獨(dú)立函數(shù)。這種“沒有函數(shù)邊界”的代碼結(jié)構(gòu)在逆向小樣本時(shí)很常見你得適應(yīng)它。3.2 導(dǎo)入表與關(guān)鍵API一個(gè)小型Win32程序在磁盤上會(huì)額外攜帶一塊“導(dǎo)入表”數(shù)據(jù)里面記錄了程序運(yùn)行時(shí)需要從系統(tǒng)動(dòng)態(tài)庫里引入的函數(shù)名字和所在DLL。你可以用任何PE工具查看也可以在OllyDbg的“查看-導(dǎo)入表”菜單里直接看到。abexcm5的導(dǎo)入表非常精簡(jiǎn)基本就兩個(gè)核心函數(shù)MessageBoxA和GetDriveTypeA。這倆都是kernel32和user32里的地?cái)偧?jí)API沒有任何一層多余封裝。這告訴我們一個(gè)判斷程序行為的好方法當(dāng)你在調(diào)試器里看到一個(gè)程序?qū)牒瘮?shù)極少時(shí)它的邏輯就會(huì)相對(duì)直白。你完全可以沿著這幾個(gè)API調(diào)用點(diǎn)逐個(gè)下斷點(diǎn)用“斷到API再往回調(diào)”的思路反推程序的執(zhí)行路徑。說白了逆向的第一步不一定非要從匯編指令里摳字眼掃一眼導(dǎo)入表往往就能預(yù)判程序的意圖。另外GetDriveTypeA這個(gè)API從名字就能看出它和磁盤類型檢測(cè)有關(guān)。它的輸入?yún)?shù)是一個(gè)以反斜杠結(jié)尾的根目錄字符串比如“C:\”返回值是UINT類型代表當(dāng)前設(shè)備的類別。我們只需要記住其中0是未知2是可移動(dòng)磁盤3是固定磁盤5是光驅(qū)CD-ROM這幾個(gè)值就夠了。對(duì)這個(gè)CrackMe來說程序想要看到的“理想類型”就是5。4. 動(dòng)態(tài)調(diào)試全過程4.1 第一步單步到關(guān)鍵Call現(xiàn)在開始正式的單步調(diào)試。程序停在00401000入口點(diǎn)后我習(xí)慣按住F8一直步過同時(shí)眼睛盯著右側(cè)信息區(qū)的指令變化。別小看這一口氣的F8它能讓你快速體會(huì)到程序從入口點(diǎn)經(jīng)過一段“壓棧-調(diào)用-比較-跳轉(zhuǎn)”的流動(dòng)感。單步到調(diào)用GetDriveTypeA的那條指令時(shí)你會(huì)看到在它上方不遠(yuǎn)處的棧區(qū)或寄存器里提前準(zhǔn)備好了字符串參數(shù)。不同版本的文件里參數(shù)可能是“.\”或“C:\”這都不要緊你需要在意的只是這個(gè)參數(shù)最終作為root path傳入API然后API根據(jù)這個(gè)磁盤根路徑返回類型值。按一次F8跨過這個(gè)Call后反匯編窗口的下一條指令會(huì)顯示cmp eax, 5一類的判斷而寄存器窗口的EAX此時(shí)就是你獲得的第一手環(huán)境信息。以我經(jīng)常調(diào)試到的情況來說如果程序在Windows 10物理機(jī)上運(yùn)行系統(tǒng)盤基本都是固定磁盤類型EAX大概率等于3。如果放在某些U盤啟動(dòng)的“Windows To Go”環(huán)境里EAX還有可能等于2。無論它等于什么只要不是5程序就會(huì)走錯(cuò)誤分支。4.2 第二步觀察返回值與標(biāo)志位關(guān)鍵的一刻發(fā)生在cmp指令之后。cmp eax, 5的意思是把EAX里的值與5做減法比較比較結(jié)果不寫回EAX而是反映到標(biāo)志寄存器的ZF、CF這些位上。如果EAX等于5ZF會(huì)被置為1如果EAX不等于5ZF為0。緊跟在cmp后面的jne指令專門看ZFZF為0就跳轉(zhuǎn)到錯(cuò)誤提示分支ZF為1就不跳轉(zhuǎn)繼續(xù)沿成功分支執(zhí)行。很多人第一次調(diào)試時(shí)看不懂“jne不跳才是成功”是什么意思這里有個(gè)更直觀的生活類比JNE就是“如果不一樣才走”而程序希望得到“一樣”的結(jié)果所以只有EAX恰好是5才不觸發(fā)這個(gè)跳轉(zhuǎn)成功代碼才會(huì)執(zhí)行。說白了這個(gè)程序把“驗(yàn)證是否等于5”和“不等于5就彈錯(cuò)誤”寫在了一條跳轉(zhuǎn)里。理解了這個(gè)你手里就已經(jīng)攥著破解的全部鑰匙了。不過要注意一個(gè)容易忽略的點(diǎn)cmp執(zhí)行后ZF發(fā)生了變化但隨后如果執(zhí)行了任何其他影響標(biāo)志位指令ZF就會(huì)改變。在OllyDbg單步時(shí)你一旦跨過jne本身它要么跳了要么沒跳之后標(biāo)志位再變也無所謂因?yàn)榉种б呀?jīng)決定了。這也是為什么我們要在jne指令上先停下來看當(dāng)前ZF的值并想清楚接下來該不該讓它跳到錯(cuò)誤分支。4.3 第三步驗(yàn)證成功分支為了確認(rèn)成功分支里到底有什么我們不需要一開始就修改程序。最簡(jiǎn)單的方法是把jne的條件臨時(shí)“反過來”在OllyDbg中選中jne指令按空格彈出匯編修改窗口把jne改成jmp無條件跳轉(zhuǎn)顯然不行這會(huì)直接跳到錯(cuò)誤框你該改的是把jne改成一句“無操作”指令NOP或者把jne改成je等于就跳轉(zhuǎn)。把jne改成je后EAX等于3時(shí)永遠(yuǎn)不等于5所以ZF繼續(xù)保持為0je就不會(huì)跳成功分支照樣可以走到。這樣的“臨時(shí)改指令”是純內(nèi)存層面的修改不會(huì)寫回文件適合快速驗(yàn)證。改完后繼續(xù)F8單步你會(huì)穿過成功指向的指令最終看到MessageBoxA的參數(shù)被壓棧再按一下F8或F9屏幕上就彈出了成功提示框。到了這一秒破解目標(biāo)在邏輯上已經(jīng)被拿下了。剩下的問題只是“如何讓修改永久生效”。5. Patch實(shí)戰(zhàn)讓程序永遠(yuǎn)認(rèn)為光驅(qū)存在5.1 方案A修改條件跳轉(zhuǎn)把程序從“jne的錯(cuò)誤跳轉(zhuǎn)”改成“永遠(yuǎn)不跳”是最直觀的Patch方式。在OllyDbg的匯編窗口里右鍵選擇“復(fù)制到可執(zhí)行文件”前你首先要弄清目標(biāo)指令所在地址和原始字節(jié)。通常jne指令的機(jī)器碼是75開頭緊接著一個(gè)字節(jié)是相對(duì)偏移量。例如“75 1A”意思是當(dāng)前位置跳轉(zhuǎn)0x1A個(gè)字節(jié)。如果你把它改成90 90兩個(gè)NOP那么CPU在執(zhí)行完cmp后不會(huì)再被jne導(dǎo)向錯(cuò)誤分支而會(huì)自然落到成功路徑上行為等同于“無論光驅(qū)存在與否都認(rèn)為存在”。如果想更“優(yōu)雅”一點(diǎn)也可以把jne改成jmp讓程序強(qiáng)制執(zhí)行成功分支但這就失去了判定的意義顯得不夠自然。如果你用的是十六進(jìn)制編輯器直接改文件找到對(duì)應(yīng)偏移處把75改成EBjmp的短跳轉(zhuǎn)操作碼時(shí)要格外注意偏移計(jì)算問題。75后面的偏移量是相對(duì)jne下一條指令來算的改成jmp后仍然相對(duì)jmp下一條指令來算偏移值本身通常不用動(dòng)。也就是說假設(shè)原始代碼是75 0E把它改成EB 0E就能從jne變成jmp而且目標(biāo)地址還是同一個(gè)。這里最關(guān)鍵的提醒是修改前一定要確認(rèn)這個(gè)jne是不是你真正想要跳過的那個(gè)改錯(cuò)一個(gè)字節(jié)就可能把程序搞掛。5.2 方案B覆蓋返回值除了動(dòng)跳轉(zhuǎn)指令你還可以在GetDriveTypeA調(diào)用返回后立刻清空EAX并賦值為5。具體做法是找到call指令后的那一行在cmp eax, 5之前插入一條mov eax, 5或push 5/pop eax。這樣就算系統(tǒng)報(bào)告的真實(shí)類型是固定磁盤程序看到的也是CD-ROM后面的cmp和jne就跟真的一樣被執(zhí)行整個(gè)流程從結(jié)果上看仍然走向成功分支。這個(gè)方案的優(yōu)點(diǎn)是代碼改動(dòng)“更符合邏輯”程序沒有破壞原始跳轉(zhuǎn)結(jié)構(gòu)看起來只是把返回值替換成了預(yù)期的值。缺點(diǎn)是你要多插入一條指令處理不好可能擠掉接下來的指令。在OllyDbg里用空格鍵把cmp eax, 5改成mov eax, 5是不行的因?yàn)閏mp占用字節(jié)數(shù)和mov不同后面指令會(huì)錯(cuò)位。正確做法是在call后騰出空間插入幾條NOP或者干脆在內(nèi)存窗口中另找空地寫代碼再jmp回來。對(duì)于這個(gè)案例我建議新手還是優(yōu)先用方案A不要自找麻煩。5.3 保存修改后的文件動(dòng)態(tài)調(diào)試時(shí)的所有改動(dòng)只存在于內(nèi)存里想讓修改生效必須把改動(dòng)復(fù)制回可執(zhí)行文件。OllyDbg的流程是右鍵選中剛才修改過的指令選擇“復(fù)制到可執(zhí)行文件”在彈出的窗口里再點(diǎn)“全部修改”最后在右側(cè)可執(zhí)行文件窗口里右鍵選擇“保存文件到新路徑”輸入一個(gè)新文件名。這里有個(gè)我踩過無數(shù)次的坑千萬不要直接保存覆蓋原文件除非你另存了備份。因?yàn)橐坏┍4驽e(cuò)了或改壞了原文件就沒了。當(dāng)年我剛開始接觸逆向時(shí)把原程序覆蓋得連實(shí)驗(yàn)對(duì)象都沒了只能重新下載非常狼狽。正確的習(xí)慣是統(tǒng)一把Patch后的文件命名成“abexcm5_patched.exe”之類的名字保留原始樣本不動(dòng)后續(xù)調(diào)試還有原始版本能對(duì)照。保存完成后直接運(yùn)行這個(gè)新文件。如果一切正常它應(yīng)該不依賴光驅(qū)型號(hào)、不依賴系統(tǒng)環(huán)境二話不說彈出成功提示框。到這一步你的爆破就算正式完成。6. 常見問題與排查速查表6.1 調(diào)試中常見的坑這個(gè)CrackMe雖然簡(jiǎn)單但實(shí)際操作中遇到問題的朋友不少。最常見的現(xiàn)象是明明在jne處把指令改成了NOP運(yùn)行后還是彈錯(cuò)誤框。出現(xiàn)這種情況八成是改錯(cuò)了位置你改的是進(jìn)入錯(cuò)誤分支后某個(gè)位置的NOP真正的關(guān)鍵jne還在原處又或者你在靜態(tài)編輯器里改的偏移和OllyDbg顯示的地址沒有正確對(duì)應(yīng)把75改成了別的字節(jié)導(dǎo)致程序直接崩潰。另一個(gè)高頻坑是分不清DRIVE類型和盤符字母。有人看到一個(gè)盤符“D:”就以為它是光驅(qū)其實(shí)人家可能就是一塊普通硬盤分區(qū)。GetDriveTypeA識(shí)別的是磁盤物理類型跟盤符本身沒有絕對(duì)綁定關(guān)系。調(diào)試時(shí)盯著EAX的值就對(duì)了別靠腦補(bǔ)“D盤應(yīng)該是什么”。第三個(gè)坑是X64系統(tǒng)上運(yùn)行老文件彈錯(cuò)或閃退。XP時(shí)代寫的匯編程序有時(shí)會(huì)因?yàn)橄到y(tǒng)API重定向或棧對(duì)齊問題行為異常。如果你發(fā)現(xiàn)程序入口點(diǎn)還沒跑到就報(bào)錯(cuò)先試著把文件放進(jìn)虛擬機(jī)環(huán)境里跑或者用OllyDbg的“按F9運(yùn)行后斷在系統(tǒng)斷點(diǎn)”功能確認(rèn)文件有沒有被系統(tǒng)加載起來。6.2 問題速查表現(xiàn)象可能原因解決方向找不到GetDriveTypeA調(diào)用靜態(tài)分析時(shí)沒有跟蹤到API在OD命令框輸入bp GetDriveTypeA下斷點(diǎn)重跑EAX返回值與自己預(yù)期不符系統(tǒng)環(huán)境不同導(dǎo)致磁盤類型不同不必糾結(jié)只要不是5就會(huì)走錯(cuò)誤分支Patch后文件打開報(bào)錯(cuò)修改字節(jié)導(dǎo)致指令錯(cuò)位或PE校驗(yàn)失敗恢復(fù)原始文件僅修改jne字節(jié)不要?jiǎng)悠渌麉^(qū)域成功框彈不出來跳轉(zhuǎn)改成jmp但目標(biāo)地址算錯(cuò)對(duì)比改動(dòng)前后的字節(jié)長(zhǎng)度確認(rèn)跳轉(zhuǎn)偏移沒變程序運(yùn)行即退出入口點(diǎn)未被正確調(diào)試或系統(tǒng)API異常在入口點(diǎn)下斷使用F9重新載入這張表并不是大而全的排查手冊(cè)但覆蓋了初學(xué)者最常撞上的幾道墻。真遇到問題時(shí)放下鼠標(biāo)先問一句“我到底改了哪個(gè)字節(jié)、有沒有影響到周邊指令”答案往往就在眼前。7. 我的實(shí)操體會(huì)與建議就我個(gè)人而言abexcm5這個(gè)CrackMe帶給我的收益比很多復(fù)雜程序都大。它把逆向最初的幾個(gè)核心動(dòng)作壓縮在了一次完整的調(diào)試流程里讓我意識(shí)到一件事所謂“破解”很多時(shí)候根本不是玄學(xué)而是“找到那個(gè)影響到最終結(jié)果的判斷點(diǎn)”這種簡(jiǎn)單的工程問題。拿到任何程序先看入口API再盯比較指令和跳轉(zhuǎn)指令最后想辦法改變判斷結(jié)果這套方法論可以說是放之四海而皆準(zhǔn)。對(duì)于新手我的建議是做完這個(gè)案例后不要立刻去碰高難度的殼先把修改后的樣本與未修改樣本做一次二進(jìn)制對(duì)比用文件對(duì)比工具看看自己到底改了哪些字節(jié)再對(duì)著Windows API文檔把GetDriveTypeA的所有返回值都查一遍。這些看似瑣碎的動(dòng)作能幫你把“條件跳轉(zhuǎn)”“磁盤類型返回值”“PE指令存儲(chǔ)”這些概念徹底焊死在腦子里。后面無論你去解其他CrackMe還是去逆向真實(shí)軟件中的某段邏輯都會(huì)感謝現(xiàn)在愿意一步步走流程的自己。