操:API斷點(diǎn)定位與補(bǔ)丁保存全解析)
x64dbg彈窗破解這套流程我前前后后折騰了大半年踩過(guò)的坑比吃過(guò)的鹽都多。今天把這套完整的過(guò)程梳理一遍把那些文檔里壓根不會(huì)寫的細(xì)節(jié)和報(bào)錯(cuò)一并說(shuō)出來(lái)給后面接手同類需求的朋友省點(diǎn)時(shí)間。需要先聲明一點(diǎn)這套流程只適合用在你自己寫的演示程序、CTF比賽靶場(chǎng)、或者你擁有明確授權(quán)的軟件調(diào)試上。逆向分析本身是安全研究的基礎(chǔ)技能但用在未經(jīng)授權(quán)的商業(yè)軟件上就是另一回事了分寸自己要把握好。1. 項(xiàng)目目標(biāo)與整體思路拆解1.1 目標(biāo)分析彈窗的“底座”是什么拿到一個(gè)“想去掉彈窗”的任務(wù)第一步不是急著開(kāi)調(diào)試器而是搞清楚這個(gè)彈窗到底是什么類型。彈窗只是表象它的底層大體分三類MessageBox系列最樸素的模態(tài)彈窗調(diào)用MessageBoxA/MessageBoxW/MessageBoxEx顯示一段文字加一個(gè)按鈕一般在網(wǎng)絡(luò)驗(yàn)證、試用期提醒里見(jiàn)得最多。DialogBox系列基于Win32資源對(duì)話框的彈窗用DialogBoxParamA/CreateDialogParam這類API創(chuàng)建常見(jiàn)于自定義的消息窗體外觀更復(fù)雜可以帶輸入框、列表、圖片。自繪窗口程序自己創(chuàng)建窗口類自己處理WM_PAINT、WM_CREATE等消息不走系統(tǒng)的標(biāo)準(zhǔn)彈窗API。這類最麻煩字符串搜索往往搜不到關(guān)鍵內(nèi)容需要從窗口創(chuàng)建流程逐層往下查。我在這次項(xiàng)目里用的演示程序是一個(gè)叫demo_check.exe的試運(yùn)行工具每次啟動(dòng)時(shí)都會(huì)彈出一個(gè)“試用期已到期請(qǐng)續(xù)費(fèi)”的提示框點(diǎn)“確定”后程序還會(huì)繼續(xù)運(yùn)行只是每次打開(kāi)都很煩。這就屬于典型的MessageBoxA 型NAG彈窗也就是常說(shuō)的“煩人窗口”。這種彈窗的破解思路是所有彈窗類型里最標(biāo)準(zhǔn)的——定位調(diào)用點(diǎn)、分析邏輯、拆掉調(diào)用或逆轉(zhuǎn)跳轉(zhuǎn)。1.2 為什么首選x64dbg市面上調(diào)試器不少OllyDbg在32位時(shí)代幾乎是標(biāo)配但面對(duì)現(xiàn)在越來(lái)越多的64位程序它的老版本顯得力不從心。x64dbg雖然是OllyDbg的后繼者但它是獨(dú)立架構(gòu)的既能調(diào)試32位也能調(diào)試64位進(jìn)程界面、快捷鍵、腳本系統(tǒng)、插件體系都更適合現(xiàn)代逆向。我對(duì)x64dbg的偏愛(ài)主要是三個(gè)理由一是它的可視化能力做得好內(nèi)存布局、?;厮?、寄存器狀態(tài)、調(diào)用棧都是實(shí)時(shí)刷新的對(duì)著一堆匯編代碼翻來(lái)翻去的時(shí)候非常有用。二是它的插件生態(tài)相當(dāng)成熟像ScyllaHide處理反調(diào)試、腳本插件跑自動(dòng)化流程這些在彈窗破解里都可能用到。三是它自帶數(shù)據(jù)庫(kù)和標(biāo)簽管理你在分析過(guò)程中做的改動(dòng)、命名、注釋、斷點(diǎn)狀態(tài)都能保存下來(lái)下一次打開(kāi)還能接著上一次的工作繼續(xù)干。當(dāng)然任何調(diào)試器本身只是工具核心還是你對(duì)匯編指令、調(diào)用約定、PE結(jié)構(gòu)、Windows消息機(jī)制這些底層知識(shí)的掌握程度。調(diào)試器好上手真正拉開(kāi)差距的是底層知識(shí)的熟練度。1.3 破解策略的選擇針對(duì)彈窗功能破壞點(diǎn)一般有四個(gè)可選方向攔截API調(diào)用在彈窗API上下斷點(diǎn)程序一調(diào)就斷然后讓它直接跳過(guò)彈窗邏輯返回繞過(guò)展示。修改調(diào)用點(diǎn)把彈窗API所在的那幾條指令直接NOP掉或改成跳轉(zhuǎn)程序找不到彈窗入口自然就不會(huì)彈。改寫條件跳轉(zhuǎn)有些彈窗外圍會(huì)有判斷邏輯比如“如果過(guò)期了就彈窗”把這種判斷跳轉(zhuǎn)直接逆轉(zhuǎn)或NOP讓程序認(rèn)為還沒(méi)到期也就不會(huì)彈。修改資源/字符串把提示文本改掉或者刪掉對(duì)話框資源但這只能治標(biāo)彈窗邏輯還在破解痕跡太明顯。我這次走的方案是先定位調(diào)用點(diǎn)觀察它的調(diào)用邏輯把決定彈不彈窗的關(guān)鍵跳轉(zhuǎn)改成無(wú)條件跳過(guò)。這樣做的好處是改動(dòng)最小——不破壞程序的正常流程不會(huì)引發(fā)運(yùn)行時(shí)崩潰也方便驗(yàn)證效果。2. 調(diào)試前的準(zhǔn)備工作和靜態(tài)偵察2.1 先把程序“體檢”一遍拿到程序直接上調(diào)試器是一種常見(jiàn)的錯(cuò)誤做法。調(diào)試器加載目標(biāo)程序后如果程序自身有反調(diào)試機(jī)制一檢測(cè)到調(diào)試環(huán)境就會(huì)主動(dòng)退出那時(shí)你連程序日志都看不到排查起來(lái)特別被動(dòng)。所以事先做靜態(tài)檢查是必須的。我習(xí)慣先用查殼工具比如Die或PEiD快速看一下程序是否加殼。如果檢測(cè)結(jié)果顯示“Borland C”、“Microsoft Visual C”這類正常編譯特征說(shuō)明沒(méi)加殼可以直接上x64dbg。如果看到UPX、ASPack或者VMProtect、Themida這類特征那就得先考慮脫殼或應(yīng)對(duì)虛擬化保護(hù)否則動(dòng)態(tài)調(diào)試會(huì)非常痛苦。這步花不了多少時(shí)間但能避免后面大量無(wú)效操作。查殼工具提示“保護(hù)器: 無(wú)”或者“編譯器: Microsoft Visual C/C”基本就能放心往下走了。2.2 用“字符串搜索”找到彈窗的入口線索解決了殼的疑慮下一步就是把程序跑起來(lái)看一下有沒(méi)有導(dǎo)入函數(shù)能直接指向彈窗API。打開(kāi)x64dbg的“符號(hào)”窗口過(guò)濾MessageBox如果能在導(dǎo)入表里看到MessageBoxA或者M(jìn)essageBoxW事情就已經(jīng)成功了一半。不過(guò)更直接的辦法永遠(yuǎn)是搜索字符串。我這次在x64dbg里按下CtrlB打開(kāi)搜索面板搜索字符串“試用期”結(jié)果在兩個(gè)位置有發(fā)現(xiàn)一個(gè)是.rdata段的靜態(tài)文本另一個(gè)是調(diào)用函數(shù)附近的局部字符串。雙擊跳轉(zhuǎn)到數(shù)據(jù)窗口按CtrlR查找所有引用該字符串的指令地址程序里處在彈窗調(diào)用點(diǎn)附近的位置就浮出水面了。這里有個(gè)細(xì)節(jié)要記住如果搜不到檢查一下編碼環(huán)境。現(xiàn)在的程序大部分用寬字符Unicode在x64dbg的搜索框里默認(rèn)按ASCII搜中文搜不到很正常。這時(shí)候要切換成UTF-16或Unicode編碼再搜一次。說(shuō)白了就是把搜索條件設(shè)置成跟程序編碼匹配這是新手第一道坎。2.3 從導(dǎo)入表反推彈窗API字符串搜索不一定每次都靈。有些程序會(huì)把字符串加密或者動(dòng)態(tài)拼接比如“試”和“用期”拼在一起之后再做展示這樣你搜“試用期”就沒(méi)有任何結(jié)果。這時(shí)候就得換思路直接在導(dǎo)入表里查MessageBoxA/MessageBoxW/DialogBoxParam對(duì)函數(shù)下斷點(diǎn)讓它運(yùn)行時(shí)自己撞到斷點(diǎn)上。具體操作是在x64dbg底部命令框輸入bp MessageBoxA然后按下F9運(yùn)行。程序啟動(dòng)后彈窗出現(xiàn)x64dbg會(huì)立刻斷在MessageBoxA的入口。你往調(diào)用棧里看就能找到是誰(shuí)調(diào)用了這個(gè)API。不管字符串怎么藏API調(diào)用的行為是暴露在外的這是動(dòng)態(tài)分析最底層的邏輯。3. 動(dòng)態(tài)調(diào)試全過(guò)程逐段拆解3.1 系統(tǒng)斷點(diǎn)與模塊加載時(shí)機(jī)x64dbg加載demo_check.exe后會(huì)先停在系統(tǒng)斷點(diǎn)System Breakpoint就是程序還沒(méi)開(kāi)始執(zhí)行入口點(diǎn)之前的位置。這時(shí)候程序主模塊其實(shí)已經(jīng)映射進(jìn)內(nèi)存了但還沒(méi)有進(jìn)入入口函數(shù)。很多新手在這一步就開(kāi)始敲指令卻發(fā)現(xiàn)斷點(diǎn)下不上原因往往是模塊還沒(méi)完成初始化。正確做法是先讓程序跑起來(lái)停在入口點(diǎn)或第一次彈窗的位置再下針對(duì)API的斷點(diǎn)。拿到系統(tǒng)斷點(diǎn)后按F9直接運(yùn)行程序會(huì)一直跑直到撞上你下的API斷點(diǎn)或主模塊入口。注意在x64dbg里設(shè)置斷點(diǎn)以后程序不會(huì)自動(dòng)停在入口得像上面說(shuō)的用“運(yùn)行到入口點(diǎn)”功能。雖然現(xiàn)在版本里常常自帶一個(gè)入口斷點(diǎn)選項(xiàng)但你最好養(yǎng)成手動(dòng)管理斷點(diǎn)的習(xí)慣后續(xù)破解會(huì)更順手。3.2 命中彈窗API后的?;厮菀坏┏绦驈棿俺霈F(xiàn)MessageBoxA斷點(diǎn)命中整個(gè)調(diào)試器畫面會(huì)跳到系統(tǒng)DLL通常是user32.dll的代碼區(qū)域。這時(shí)候先別細(xì)看這坨代碼重點(diǎn)在調(diào)用棧Call Stack窗口。調(diào)用棧會(huì)列出從當(dāng)前函數(shù)往上追溯的知識(shí)鏈像這樣000000000014F920 00007FF612345678 demo_check0xABCD 000000000014F928 00007FF612345900 demo_check0x1234 000000000014F930 00007FF612346010 demo_check0xA567第一行是當(dāng)前代碼放置的位置第二行是調(diào)用者以此類推。雙擊第二行或者按Ctrl↑直接跳轉(zhuǎn)到調(diào)用MessageBoxA的那條指令也就是彈窗邏輯的核心位置。在x64下函數(shù)調(diào)用參數(shù)的前四個(gè)放在rcx、rdx、r8、r9里MessageBoxA的函數(shù)簽名是int MessageBoxA( HWND hWnd, // rcx LPCSTR lpText, // rdx 文本內(nèi)容 LPCSTR lpCaption, // r8 窗口標(biāo)題 UINT uType // r9 按鈕/圖標(biāo)類型 );所以你看rdx寄存器里的值指向的字符串就是彈窗顯示的正文r8指向的是標(biāo)題。這種對(duì)應(yīng)關(guān)系在識(shí)別彈窗邏輯時(shí)非常好用一眼就能看出程序在哪里組織這些參數(shù)、從哪里取得文本內(nèi)容、哪里是分支判斷。3.3 彈窗邏輯的定位與NOP處理跳到調(diào)用點(diǎn)之后看到的典型匯編序列大概是這樣00401234 mov r9d, 0 ; uType 0040123B lea rdx, [rip0x7FFF] ; lpText 00401242 lea r8, [rip0x7FF5] ; lpCaption 00401249 xor ecx, ecx ; hWnd 0040124B call MessageBoxA ; 彈窗! 00401250 test eax, eax這就是一個(gè)標(biāo)準(zhǔn)彈窗調(diào)用點(diǎn)。要把它“摘掉”我一般會(huì)最優(yōu)先把call MessageBoxA這一條指令NOP掉。在x64dbg里光標(biāo)定位到這條指令選中后按空格或者右鍵“二進(jìn)制”-“用NOP填充”指令就變成了6個(gè)字節(jié)的0x90。但NOP掉調(diào)用以后程序邏輯會(huì)有個(gè)問(wèn)題之前如果彈窗的返回值eax會(huì)被后續(xù)代碼用到現(xiàn)在eax里的值變得不確定有概率引起程序行為異常。所以更穩(wěn)妥的做法是看彈窗前后有沒(méi)有條件跳轉(zhuǎn)找到?jīng)Q定“要不要彈”的那條分支直接逆轉(zhuǎn)或讓它直接跳過(guò)。比如常見(jiàn)的結(jié)構(gòu)是00401210 cmp byte ptr [rip0x10A0], 0 00401217 je short 00401240 ; 如果某個(gè)標(biāo)志位為0跳過(guò)彈窗 ... 00401240 ...如果你把je改成jmp就是無(wú)條件跳轉(zhuǎn)到“跳過(guò)彈窗”的那條路徑條件是死是活都不影響了。但我這次的實(shí)際場(chǎng)景有點(diǎn)特殊程序里的彈窗代碼雖然入口有判斷但判斷標(biāo)志位在程序運(yùn)行過(guò)程中寫死了直接改跳轉(zhuǎn)反而可能導(dǎo)致后續(xù)代碼找不到本應(yīng)存在的彈窗流程。經(jīng)過(guò)反復(fù)測(cè)試我最后選擇了“NOP調(diào)用手動(dòng)修復(fù)eax”的組合方式效果最穩(wěn)定。3.4 保存補(bǔ)丁與文件轉(zhuǎn)儲(chǔ)調(diào)試器里的修改是停在內(nèi)存層面的程序退出以后一切都還原了。要讓補(bǔ)丁長(zhǎng)期生效得把它寫回文件。具體操作在x64dbg里改好指令后右鍵選擇“復(fù)制到可執(zhí)行文件”彈出新窗口再右鍵“保存文件”就能導(dǎo)出一個(gè)打上補(bǔ)丁的新exe。注意有些程序有自我校驗(yàn)或者數(shù)字簽名直接改動(dòng)文件會(huì)讓程序檢測(cè)到異常啟動(dòng)時(shí)會(huì)報(bào)錯(cuò)。這種情況就得加一個(gè)繞過(guò)校驗(yàn)的邏輯比如找到校驗(yàn)函數(shù)入口直接讓它永遠(yuǎn)返回“未修改”或者用內(nèi)存補(bǔ)丁的方式在運(yùn)行時(shí)動(dòng)態(tài)patch不直接改動(dòng)磁盤文件。我這次做的demo_check.exe沒(méi)有校驗(yàn)所以直接保存到新文件就行。如果有校驗(yàn)我的經(jīng)驗(yàn)是做一個(gè)外部加載補(bǔ)丁啟動(dòng)時(shí)隨調(diào)試器或DLL注入強(qiáng)制修改內(nèi)存這樣磁盤文件保持原樣校驗(yàn)自然也不會(huì)被觸發(fā)。4. 文件補(bǔ)丁的兩種保存方式與驗(yàn)證4.1 直接把補(bǔ)丁寫回PE文件上一節(jié)提到的“復(fù)制到可執(zhí)行文件”就是最直接的寫回方式。x64dbg會(huì)把當(dāng)前被修改過(guò)的這一段指令以及我們編輯后的字節(jié)連同重定位信息一起寫進(jìn)新的PE文件。這里有個(gè)坑修改后的文件大小不能變。你把指令改成0x90字節(jié)數(shù)沒(méi)變寫回沒(méi)問(wèn)題。但如果你要插入一條原來(lái)沒(méi)有的跳轉(zhuǎn)指令占用了新的空間可能就需要擴(kuò)大節(jié)區(qū)或者借用文件頭的空隙而不是簡(jiǎn)簡(jiǎn)單單覆蓋幾個(gè)字節(jié)。所以NOP填充是最省事的這也是為什么很多人喜歡用NOP而不是寫一個(gè)跳轉(zhuǎn)到別處的指令。另外別忘了保存文件前確認(rèn)當(dāng)前修改的地址是在主模塊里不是DLL里。如果你是在user32.dll的地址上改東西那就麻煩了寫回文件的時(shí)候會(huì)把整個(gè)DLL的字節(jié)也帶上完全沒(méi)有意義。4.2 用Scylla或工具做轉(zhuǎn)儲(chǔ)與修復(fù)如果程序加了殼或者有加殼保護(hù)直接修改原始PE文件通常無(wú)效因?yàn)檎嬲拇a在運(yùn)行時(shí)才解密磁盤上的字節(jié)可能是密文。這種情況下就需要“轉(zhuǎn)儲(chǔ)”內(nèi)存中已解密的代碼然后修復(fù)導(dǎo)入表。x64dbg里一般搭配Scylla插件來(lái)做這件事。在當(dāng)前進(jìn)程狀態(tài)正確的地方比如OEP也就是原始入口點(diǎn)用Scylla抓取當(dāng)前進(jìn)程的映像它會(huì)自動(dòng)修復(fù)IAT導(dǎo)入地址表并重新生成一個(gè)瘦身版的exe。這個(gè)文件就是把運(yùn)行時(shí)已經(jīng)展開(kāi)的代碼保存下來(lái)后續(xù)可以直接往上面做靜態(tài)補(bǔ)丁。需要注意轉(zhuǎn)儲(chǔ)出來(lái)的文件和你加殼前的原始程序在結(jié)構(gòu)上不完全一致有的程序跑起來(lái)會(huì)缺這個(gè)缺那個(gè)。所以如果是加殼程序建議先把殼脫掉再做彈窗破解整個(gè)鏈路會(huì)順暢得多。4.3 運(yùn)行驗(yàn)證與回歸測(cè)試補(bǔ)丁做完不能彈窗消失就收工我習(xí)慣做三輪驗(yàn)證第一輪雙擊補(bǔ)丁后的exe確認(rèn)不彈窗、程序主界面正常出現(xiàn)、主要按鈕功能可用。第二輪把補(bǔ)丁后的文件丟到x64dbg里再跑一遍看程序啟動(dòng)有沒(méi)有異常、有沒(méi)有觸發(fā)別的自我保護(hù)。第三輪多開(kāi)幾次程序確認(rèn)補(bǔ)丁效果穩(wěn)定不會(huì)出現(xiàn)偶發(fā)崩潰。有的程序在不同啟動(dòng)路徑下行為不同比如命令行參數(shù)不同、資源加載順序不同反復(fù)開(kāi)關(guān)幾次才能暴露問(wèn)題。我在驗(yàn)證過(guò)程中遇到過(guò)一種比較隱蔽的狀況彈窗補(bǔ)丁生效了但程序后續(xù)的試用期邏輯也在走比如定時(shí)檢查到“試用已到期”于是主窗口標(biāo)題或者功能按鈕被禁用。這時(shí)候光改彈窗沒(méi)解決根本問(wèn)題得回到代碼里找那個(gè)決定“是否過(guò)期”的標(biāo)志位或者比較運(yùn)算一并處理掉。5. 常見(jiàn)錯(cuò)誤與排查技巧實(shí)錄5.1 字符串搜索定位失敗的排查清單這是彈窗破解里出現(xiàn)頻率最高的一個(gè)坑我在自己的實(shí)操里總結(jié)了一套排查順序現(xiàn)象可能原因處理辦法搜“試用期”沒(méi)有任何結(jié)果編碼不對(duì)ASCII搜中文切換UTF-16/Unicode編碼再搜字符串搜索有結(jié)果但“引用”列表為空字符串是動(dòng)態(tài)拼接的沒(méi)有被直接引用對(duì)附近代碼下斷點(diǎn)觀察引用點(diǎn)很多不清楚哪一處是彈窗同一段字符串被多處復(fù)用用bp MessageBoxA走動(dòng)態(tài)斷點(diǎn)定位字符串在資源文件里不在內(nèi)存中程序調(diào)用LoadString加載在LoadStringW上下斷點(diǎn)看參數(shù)定位字符串搜索只是一個(gè)切入點(diǎn)它更像是地圖上的一個(gè)標(biāo)記。圖上的標(biāo)記可能指到好幾個(gè)岔路口真正決定走哪條路還是要靠動(dòng)態(tài)調(diào)試。5.2 斷點(diǎn)命不中的場(chǎng)景有時(shí)候你明明下了bp MessageBoxA程序也彈了窗但調(diào)試器就是不斷。遇到這種情況我馬上查兩件事第一程序彈的是不是系統(tǒng)自繪窗口根本沒(méi)走M(jìn)essageBoxA。比如有些程序用TaskDialog接口或者用WPF那套渲染那API就完全不一樣。這種情況需要在創(chuàng)建窗口、顯示窗口相關(guān)的API上斷點(diǎn)比如CreateWindowExW。第二程序是不是在彈窗之前先做了反調(diào)試檢測(cè)檢測(cè)到x64dbg就拒絕執(zhí)行彈窗流程改走了靜默路徑。這時(shí)畫面表現(xiàn)為程序消失或卡住斷點(diǎn)永遠(yuǎn)等不來(lái)命中。處理辦法是先加載ScyllaHide插件開(kāi)啟常用反調(diào)試?yán)@過(guò)選項(xiàng)比如隱藏調(diào)試器進(jìn)程名、處理IsDebuggerPresent、NtQueryInformationProcess這些API的返回值。5.3 反調(diào)試帶來(lái)的“假死”和“自殺”反調(diào)試是彈窗破解里最容易讓人心態(tài)炸裂的話題。程序檢測(cè)調(diào)試器的方式五花八門常見(jiàn)的有調(diào)用IsDebuggerPresent或CheckRemoteDebuggerPresent返回真就直接退出。通過(guò)NtQueryInformationProcess查詢調(diào)試端口。檢測(cè)特定窗口、特定注冊(cè)表鍵或者運(yùn)行時(shí)間隨機(jī)數(shù)校驗(yàn)。利用WH_DEBUG鉤子偵測(cè)是否有外部消息注入。對(duì)付這些手段ScyllaHide可以在R3層面把大部分調(diào)用結(jié)果和諧掉。啟動(dòng)x64dbg前先保證ScyllaHide插件已啟用然后勾選“隱藏PEB”、“繞過(guò)NtQueryInformationProcess”、“抑制斷點(diǎn)檢測(cè)”等選項(xiàng)。如果你用的程序比較硬核還有驅(qū)動(dòng)級(jí)反調(diào)試普通方式就無(wú)能為力了只能考慮內(nèi)核調(diào)試或者手動(dòng)patch反調(diào)試函數(shù)本身。不過(guò)那種場(chǎng)景已經(jīng)超出“破解彈窗”的范疇屬于更專業(yè)的對(duì)抗范疇這里不多展開(kāi)。5.4 運(yùn)行時(shí)崩潰與指令邊界問(wèn)題補(bǔ)丁改完程序要么正常要么秒退或崩潰。秒退先自定義查看事件日志和程序本身的行為模式但九成九是因?yàn)槟愀腻e(cuò)了指令邊界。比如你原本想覆蓋一條6字節(jié)的指令但你的NOP操作不小心多覆蓋了一個(gè)字節(jié)把后面那條指令的頭部也碰掉了整個(gè)解碼流程錯(cuò)位程序一跑就炸。還有一種常見(jiàn)錯(cuò)誤是你以為改的是調(diào)用點(diǎn)但實(shí)際上改的是跳轉(zhuǎn)表或者函數(shù)開(kāi)頭破壞了函數(shù)棧幀對(duì)齊。x64下棧幀不對(duì)齊的直接后果就是后續(xù)調(diào)用系統(tǒng)API時(shí)崩潰。所以每次改動(dòng)之前先觀察這條指令的長(zhǎng)度看反匯編窗口右側(cè)的字節(jié)長(zhǎng)度顯示再?zèng)Q定NOP多少字節(jié)。改完以后最好從頭再“單步走”一遍這條分支看到確實(shí)按照預(yù)期跳過(guò)了再做保存。5.5 文件校驗(yàn)與簽名還原的處理前面提過(guò)很多程序有自校驗(yàn)。這類程序在修改文件后啟動(dòng)時(shí)會(huì)計(jì)算自身文件的哈希和內(nèi)置值比對(duì)發(fā)現(xiàn)不一致就直接拒絕運(yùn)行。你的補(bǔ)丁做得再完美也白搭。實(shí)際處理的辦法就兩種一種是讓校驗(yàn)永遠(yuǎn)“成立”找到校驗(yàn)函數(shù)返回的地方把最終比較的結(jié)果改寫成“一致”。比如程序在算文件哈希后和一個(gè)常量做比較你可以把cmp后的跳轉(zhuǎn)改成永遠(yuǎn)不會(huì)跳走的路徑或者直接讓jne變成jmp。另一種就是在內(nèi)存里打補(bǔ)丁。啟動(dòng)時(shí)程序先運(yùn)行原版文件你用一個(gè)外部工具比如在系統(tǒng)啟動(dòng)加載一個(gè)DLL注入器在程序運(yùn)行后將補(bǔ)丁寫進(jìn)內(nèi)存。這種方案麻煩點(diǎn)但繞過(guò)一切磁盤校驗(yàn)因?yàn)槲募緛?lái)就是原版的。我自己在做某公司安全開(kāi)發(fā)中心的授權(quán)驗(yàn)證程序時(shí)遇到的就是自校驗(yàn)彈窗的組合。最后用x64dbg跑起來(lái)找到校驗(yàn)函數(shù)逆轉(zhuǎn)其中對(duì)“文件被修改”的判斷之后才順利把彈窗邏輯摘掉。6. 事后優(yōu)化與腳本化補(bǔ)丁方案6.1 用腳本自動(dòng)打補(bǔ)丁如果你經(jīng)常要處理類似的彈窗破解任務(wù)每次都手動(dòng)打開(kāi)x64dbg、定位、改指令、保存文件效率太低。x64dbg支持腳本命令可以把步驟沉淀下來(lái)。比如先記錄要修改的地址、原始字節(jié)、NOP填充的范圍寫成一個(gè)腳本StartRun SetBreakpoint 401234 Run ...實(shí)際上x64dbg官方腳本語(yǔ)法主要是基于命令行的你可以在命令行窗口直接執(zhí)行匯編指令和斷點(diǎn)操作。寫過(guò)一段時(shí)間腳本之后你會(huì)發(fā)現(xiàn)自己對(duì)程序結(jié)構(gòu)理解得更深從“用鼠標(biāo)點(diǎn)來(lái)點(diǎn)去”進(jìn)階到“用命令精準(zhǔn)控制”。我這里分享一個(gè)我常用的二分法先寫腳本暴力遍歷MessageBoxW斷點(diǎn)只要一命中就自動(dòng)記錄調(diào)用棧、寄存器快照然后繼續(xù)運(yùn)行。多次運(yùn)行以后把這些記錄做對(duì)比彈窗路徑和正常路徑之間的差異就一目了然。6.2 動(dòng)態(tài)補(bǔ)丁加載器的思路除了靜態(tài)修改文件也可以做一個(gè)補(bǔ)丁加載器在程序啟動(dòng)的時(shí)候通過(guò)導(dǎo)入表劫持或者DLL注入在DllMain里直接改寫目標(biāo)函數(shù)的指令。這樣你甚至不用改原始exe所有破解邏輯都留在外部DLL里。這個(gè)方案的好處是維護(hù)起來(lái)靈活不污染原始文件壞處是啟動(dòng)依賴外部文件容易被殺毒軟件攔截。如果你只是做安全研究驗(yàn)證這種方式更優(yōu)雅也更接近真實(shí)攻防場(chǎng)景里“內(nèi)存馬”的思路。不過(guò)要提醒一句做這種加載器需要你對(duì)PE加載流程、DLL注入原理有扎實(shí)的掌握否則很容易出現(xiàn)“DLL加載但補(bǔ)丁沒(méi)生效”的尷尬局面。我見(jiàn)過(guò)很多半吊子的動(dòng)態(tài)補(bǔ)丁器就是因?yàn)镈LL加載時(shí)機(jī)晚于彈窗邏輯執(zhí)行彈窗已經(jīng)彈完了補(bǔ)丁才寫進(jìn)去等于白忙活。6.3 補(bǔ)丁的穩(wěn)定性驗(yàn)證思路補(bǔ)丁做完不是結(jié)束穩(wěn)定性驗(yàn)證是重頭戲。我會(huì)準(zhǔn)備一個(gè)清單程序連續(xù)啟動(dòng)20次不出現(xiàn)偶發(fā)崩潰或彈窗殘留。在程序內(nèi)部跳轉(zhuǎn)、切換頁(yè)面、保存數(shù)據(jù)等操作后確認(rèn)功能不與原版有差異。如果有網(wǎng)絡(luò)功能確認(rèn)補(bǔ)丁后網(wǎng)絡(luò)請(qǐng)求正常不影響登錄、同步等模塊。當(dāng)前系統(tǒng)環(huán)境之外再扔到虛擬機(jī)里測(cè)一遍排除環(huán)境因素導(dǎo)致的假陽(yáng)性結(jié)果。我踩過(guò)最深的一個(gè)坑是補(bǔ)丁后程序能正常啟動(dòng)但一進(jìn)主界面的“關(guān)于”窗口就崩潰。后來(lái)排查發(fā)現(xiàn)是因?yàn)槲野岩惶幈緫?yīng)保留NOP的call指令直接改沒(méi)了而程序在后續(xù)流程里還會(huì)讀取那次調(diào)用的返回值eax返回值為空指針導(dǎo)致后續(xù)解引用失敗。這給我留下的教訓(xùn)是能不破壞調(diào)用棧、寄存器約束的補(bǔ)丁方式就絕不隨意NOP調(diào)用。優(yōu)先改跳轉(zhuǎn)條件其次NOP調(diào)用最后才考慮改寫指令序列。7. 寫在最后一些更深的體會(huì)這套流程走下來(lái)我最大的感受是破解一個(gè)彈窗本身不難真正難的是在不破壞程序原有邏輯的前提下干凈地移除或繞過(guò)彈窗。這要求你把匯編層面的數(shù)據(jù)流、控制流都看清了再動(dòng)手去動(dòng)它。如果前面哪個(gè)環(huán)節(jié)還不太熟練我給一個(gè)循序漸進(jìn)的練習(xí)建議不需要一上來(lái)就死磕復(fù)雜的加殼程序先拿自己寫的演示程序練手反復(fù)做“定位彈窗API、讀取調(diào)用參數(shù)、修改跳轉(zhuǎn)條件、保存補(bǔ)丁、驗(yàn)證回歸”這幾件事直到不再依賴搜索字符串能直接從調(diào)用棧推理出彈窗入口這一套技術(shù)就算真正掌握了。后面有時(shí)間的話我會(huì)再寫一篇關(guān)于如何用腳本批量掃描程序內(nèi)的彈窗模式、以及如何處理多彈窗和嵌套彈窗的文章那時(shí)候需要的工具和思路會(huì)更豐富一些。