應用安全 --- PC安全 之 VMP2 IAT修復
加密工具選擇
vmp2.13.8
加密選項
-
內(nèi)存保護 (是):程序運行時,它的內(nèi)容會放在電腦的內(nèi)存里。這個選項就像是給內(nèi)存里的程序加了一個“障眼法”,防止破解者用特殊工具直接“偷看”內(nèi)存里的內(nèi)容,或者把程序從內(nèi)存里完整地復制出來。
-
導入表保護 (是):程序需要調(diào)用很多系統(tǒng)功能(比如顯示窗口、讀寫文件),“導入表”就像是程序的功能“通訊錄”。這個選項會把這份通訊錄加密或藏起來,讓破解者搞不清楚你的程序到底調(diào)用了哪些系統(tǒng)功能,增加了分析難度。
-
資源保護 (是):程序里包含的圖片、圖標、文字等“資源”也會被加密。就像把寶箱里的珠寶都用獨立的小盒子鎖起來,就算寶箱被打開了,里面的東西也拿不走。
-
壓縮輸出文件 (快速):這個選項會把整個程序文件壓縮變小,就像用真空袋打包衣服一樣。“快速”意味著壓縮和解壓的速度都比較快,能稍微平衡文件大小和運行速度。
代碼vm 表示啟用了一個“翻譯機”來保護代碼。
-
離開虛擬機時加密寄存器 (是):寄存器是CPU里用來臨時存放數(shù)據(jù)的小倉庫。當代碼從“火星文”模式切換回正常模式時,這個選項會立刻把小倉庫里的數(shù)據(jù)鎖起來,防止破解者偷看到關鍵的計算結(jié)果。
-
檢查虛擬機對象的完整性 (是):這個“翻譯機”本身也會被保護起來。程序會時刻檢查它自己是否被破解者動了手腳,如果被修改了,程序就不干了。
-
隱藏常量 (是):程序里的一些固定數(shù)值或字符串(比如一個密碼、一個網(wǎng)址)被稱為常量。這個選項會把它們都加密隱藏起來,讓破解者無法輕易在程序文件中找到這些敏感信息。
殼運行到OEP的大致流程

通過虛假eip進入vmp1段的入口
第一階段:尋找OEP(原始入口點)
核心思路解析
-
VMP的核心保護機制:VMP殼在程序啟動初期,會做大量的“準備工作”,比如解密自身的代碼、設置反調(diào)試陷阱等。其中非常關鍵的一步,就是使用 VirtualProtect 這個Windows API函數(shù)。
-
函數(shù)的作用:這個函數(shù)像一個“權限管家”,可以動態(tài)修改內(nèi)存區(qū)域的讀、寫、執(zhí)行權限。VMP利用它來抹除我們(分析者)在代碼區(qū)段和IAT(導入地址表)上設置的內(nèi)存訪問斷點。如果我們下的斷點被清除了,自然就無法中斷在關鍵位置。
-
斷點的不同類型:
-
內(nèi)存斷點:在某個內(nèi)存地址設置,一旦該地址被讀取、寫入或執(zhí)行,就會中斷。這種斷點容易被VirtualProtect清除。
-
硬件斷點:由CPU直接支持,數(shù)量有限(通常4個),但更底層,更難被檢測。其中,硬件“訪問”和“寫入”斷點不容易被VMP檢測到,但硬件“執(zhí)行”斷點則會被VMP發(fā)現(xiàn)。
-
利用以上原理,就可以設計出如下繞過VMP檢測、直達OEP的執(zhí)行流程。
詳細執(zhí)行流程步驟
以下是根據(jù)您提供的方法,整理出的完整、詳細的操作步驟:
前提:使用OD(OllyDbg)或x64dbg等調(diào)試工具載入加了VMP 2.13.8殼的程序。
第一步:首次運行與定位代碼段尾部
-
正常運行程序:直接在調(diào)試器中按 F9 鍵讓程序運行起來。程序會執(zhí)行VMP的外殼代碼,完成初始化,然后停在VMP自身的某個區(qū)段(例如截圖中提到的VMP1區(qū)段)。
-
找到代碼段末尾:切換到內(nèi)存窗口,找到程序的 .text(代碼段)。用鼠標滾輪或滾動條向下滾動,找到代碼段中最后一個非零字節(jié)數(shù)據(jù)的位置。
-
確定下斷點的位置:記下這個“最后一個有效值”的下一個地址。這個地址是代碼段的末尾,通常是代碼執(zhí)行流程中一個比較靠后的位置。
第二步:設置硬件斷點并重載
-
設置硬件訪問斷點:在剛才記下的那個地址上,右鍵點擊,選擇“斷點” -> “硬件,訪問”。因為這個位置在代碼段的末尾,當VMP外殼代碼即將執(zhí)行完畢,準備跳轉(zhuǎn)到真正的程序代碼(OEP)時,很可能會訪問到這附近區(qū)域。
-
重載程序:在調(diào)試器中,點擊“重新開始”或按 Ctrl+F2,讓程序回到最初的加載狀態(tài)。這一步是為了讓我們設置的斷點能在VMP的初始化流程中生效。
第三步:兩次中斷,繞過干擾
-
第一次中斷(F9):按下 F9 運行程序。程序會因為VMP外殼代碼在初始化過程中寫入數(shù)據(jù)到代碼段而觸發(fā)我們設置的硬件斷點,此時程序會中斷下來。
-
第二次中斷(F9):再次按下 F9 繼續(xù)運行。這里可能會遇到VMP為了迷惑分析者而故意設置的一些“干擾性”訪問,導致斷點再次被觸發(fā)。根據(jù)原文描述,這次中斷是“斷在干擾時”。我們只需再次忽略,繼續(xù)運行即可。
第四步:設置內(nèi)存斷點,直達OEP
-
設置內(nèi)存訪問斷點:經(jīng)過前兩輪的硬件斷點中斷,VMP的“權限修改”階段(即調(diào)用 VirtualProtect 清除內(nèi)存斷點)很可能已經(jīng)過去了。此時,我們可以在程序的整個代碼段(.text段)上設置一個內(nèi)存訪問斷點。
-
最后一次運行(F9):再次按下 F9 運行程序。由于VMP外殼已經(jīng)完成了它的使命,正準備跳轉(zhuǎn)到程序的原始入口點(OEP)去執(zhí)行真正的程序邏輯,這個跳轉(zhuǎn)動作必然會“訪問”到代碼段。
-
成功中斷在OEP:此時,我們設置的內(nèi)存訪問斷點會被觸發(fā),程序會精準地停在OEP處。這個時候,調(diào)試器里顯示的代碼就是程序原本的、未被加密的開始代碼了。
總結(jié):這個流程巧妙地利用了硬件斷點不易被檢測的特性,先讓程序跑過最危險的反調(diào)試和反內(nèi)存斷點階段,然后再利用內(nèi)存斷點覆蓋范圍廣的優(yōu)勢,精準地捕捉到從外殼代碼跳轉(zhuǎn)到原始代碼的那一瞬間,從而成功定位到OEP。
簡要說明
只要過了這個清除我們在代碼段 和 IAT 設置的內(nèi)存訪問斷點 VirtualProtect 函數(shù) 就可以在代碼段設置訪問斷點了。繞過方法就是設置的 硬件訪問 和 硬件寫入 斷點并不會被殼檢測到,但是硬件執(zhí)行斷點會被檢測到。
完整執(zhí)行一次程序,程序可以正常運行并自動終止,沒有加反調(diào)試。
點擊M按鈕在text右鍵在cpu中查看
將右鍵分析,刪除分析,就可以正常顯示匯編指令
拖到最后有數(shù)據(jù)的hex的內(nèi)存區(qū)域(最后一個不是00的值),右鍵設置硬件斷點
重新運行OD中的程序,按下兩次執(zhí)行按鈕(一次斷在寫入時,一次斷在干擾時),繞過vmp段內(nèi)兩次內(nèi)存斷點檢查代碼
點擊M按鈕在text右鍵設置內(nèi)存訪問斷點
執(zhí)行后自動斷在OEP位置,完成OEP查找
第二階段:API調(diào)用的類型分析
-
識別VMP的調(diào)用模式:發(fā)現(xiàn)VMP(VMProtect的縮寫)主要通過CALL、JMP、MOV等指令的變體來間接調(diào)用API,這些指令的機器碼長度通常是5到6字節(jié)。
有這三種
CALL [IAT] //以下簡稱十六進制的 FF15
JMP [IAT] //FF25
mov e?? , [iat] //MOV型
CALL [IAT]:
加殼前

后

從ff15變?yōu)?0e8,后面地址變?yōu)関mp的殼代碼。這里的50就是補碼
我們觀察到長度不變,因為so文件一處長度發(fā)生改變所有地址偏移都要變化引擎雪崩效應
JMP [IAT]:


mov e??,[IAT]


發(fā)現(xiàn)了一個VMP加殼的細節(jié):對于 mov eax, [地址] 這種5字節(jié)長的API調(diào)用,VMP會用一個同樣是5字節(jié)長的指令去替換它。因為尺寸剛好匹配,所以VMP不需要進行任何“填充”或“補碼”操作。
如果VMP要用一個5字節(jié)的安保零件去替換一個6字節(jié)的舊零件,那么就會多出1字節(jié)的空隙。為了不讓整個機器“錯位”,VMP就必須用一個沒有意義的“填充物”(專業(yè)術語叫補碼或Padding,比如圖中的 nop 指令)把這個空隙填上。
因為重定向的代碼為E8,是 5 字節(jié)的,因此殼會隨機填充一個字節(jié)。補碼上面或者下面都有可能。
關鍵的部分就是我們?nèi)绾沃兰用芮暗母袷?/p>
手動一步一步追蹤:
首先我們先要知道,被重定向的API,進入了 VMP0 區(qū)段,再經(jīng)過一系列的運算,得到真實的API的地址,放入堆棧,然后通過 RET 的方法進入真實的API

這是重定向CALL ,一直F7就可以到這一步。
但是手動跟著特工在復雜的“中轉(zhuǎn)站”里一步步走(用F7單步跟蹤)太累了,而且容易跟丟。
所以介紹了一個絕佳的自動化方法繞過vmp代碼:
使用OD(OllyDbg調(diào)試器)的“跟蹤步入”功能,并設置一個聰明的條件。
這個條件是:
EIP 位于范圍外 VMP0段首地址 ... VMP0段尾地址
這句話的通俗解釋就是告訴調(diào)試器:
“嘿,老兄!接下來我要追蹤一段代碼。我不關心它在‘VMP0中轉(zhuǎn)站’里面是怎么跑的,里面太復雜了。你就讓它在里面盡情地跑吧。但是,只要它的腳步一踏出‘VMP0中轉(zhuǎn)站’的范圍,你必須立刻停下來,告訴我它跑到了哪里!”
第三步:實際操作流程
-
設置好“崗哨”:把上面那個“EIP位于范圍外”的條件設置好。作者強調(diào)這個條件必須全程開啟,因為你的脫殼腳本也要靠它來自動分析。
-
找到一個假:隨便找一個被VMP處理過的CALL指令。
-
手動 F7 步入:手動按一下F7,你(EIP)就進入了“VMP0中轉(zhuǎn)站”的入口。
-
啟動自動追蹤:點擊OD的“跟蹤步入”功能。
-
自動完成!:調(diào)試器會飛速執(zhí)行“中轉(zhuǎn)站”里所有的復雜代碼,因為EIP一直在VMP0區(qū)段內(nèi),條件不滿足,所以不會停。直到最后執(zhí)行了那條RET指令,EIP一下子跳出了VMP0區(qū)段,到達了真實的API地址(比如winspool.ClosePrinter)。
-
“啪!” 條件滿足,調(diào)試器立刻停了下來。
-
最終效果:OD直接就停在了真實API的第一句代碼上,你根本不需要去關心VMP在中間到底做了什么復雜的運算。
總結(jié)一下:
這段話的核心就是教你如何利用調(diào)試器的一個條件跟蹤功能,來跳過VMP復雜、繁瑣的解密過程,實現(xiàn)“一鍵直達”被隱藏的真實API地址。這個方法是編寫自動化脫殼腳本的基礎和關鍵。
這個操作的核心是利用OD的運行跟蹤 (Run trace) 功能,并為其設置一個暫停條件。
第一步:準備工作 - 找到VMP0區(qū)段的地址范圍
在設置條件之前,你必須先知道“VMP0中轉(zhuǎn)站”的大門和后門在哪里。
-
打開內(nèi)存映射窗口:在OD的主界面,點擊頂部菜單欄的 "M" 圖標(Memory Map),或者按快捷鍵 Alt+M。
-
找到VMP0區(qū)段:在彈出的內(nèi)存窗口中,找到那個名為 .vmp0 或者 VMP0 的區(qū)段。
-
記錄地址:
-
記下這一行的 “基址 (Address)”,這就是 “VMP0段首地址”。
-
記下這一行的 “大小 (Size)”。
-
計算出 “VMP0段尾地址”。公式是:基址 + 大小 - 1。
舉例:
如果基址是 00B50000,大小是 00028000。
那么首地址就是 00B50000。
尾地址就是 00B50000 + 00028000 - 1 = 00B77FFF。現(xiàn)在你手里就有了設置條件所需的兩個關鍵地址了。
-
第二步:設置“跟蹤暫停”條件
-
打開調(diào)試選項:點擊OD頂部菜單欄的調(diào)試 ,設置條件
-
切換到“跟蹤”選項卡:在彈出的對話框中,點擊 “跟蹤 (Trace)” 選項卡。
-
進行設置:
-
① 勾選復選框:“EIP 位于范圍外”。
-
② 填入地址:將在第一步中得到的 “VMP0段首地址” 和 “VMP0段尾地址” 填入后面的兩個輸入框中。
-
③ 確認:點擊“確定 (OK)”保存設置。
現(xiàn)在,OD就已經(jīng)被你設置成一個聰明的“哨兵”了。
-
第三步:執(zhí)行跟蹤
現(xiàn)在萬事俱備,可以開始實際操作了。
-
找到一個重定向CALL:在OD的反匯編窗口(CPU窗口),找到一個被VMP處理過的CALL指令,比如 CALL 00B51234(這個地址在VMP0區(qū)段內(nèi))。
-
右鍵設置新的EIP,直接從這里開始執(zhí)行。
-
手動步入 (關鍵!):按下鍵盤上的 F7 鍵。這時,你會進入VMP0區(qū)段的內(nèi)部,EIP(指令指針)現(xiàn)在就在“中轉(zhuǎn)站”里面了。這一步是必須的,因為你得先進去,才能談“出來的時候暫停”。
-
啟動運行跟蹤:現(xiàn)在EIP已經(jīng)在VMP0區(qū)段內(nèi),你可以啟動自動跟蹤了。
-
點擊頂部菜單欄的 “調(diào)試 (Debug)” -> “運行跟蹤 (Run trace)” -> “步入 (Into)”。
-
或者直接使用快捷鍵 Ctrl + F11 (這是運行跟蹤的快捷鍵,它會應用你在選項中設置的暫停條件)。 注:原文提到的是“跟蹤步入”,在OD中,“運行跟蹤”是實現(xiàn)這一功能的正式名稱。
-
-
等待自動暫停:按下Ctrl+F11后,OD會開始飛速執(zhí)行代碼。你可能會看到屏幕閃爍,OD會記錄下成千上萬條指令。但你不用管它,因為它會在滿足你設定的條件時自動停下。
當VMP內(nèi)部的代碼執(zhí)行到最后那條 RET 指令時,EIP會一下子跳出VMP0區(qū)段的范圍,跳到真實的API函數(shù)地址上。
“啪!” 哨兵發(fā)現(xiàn)EIP跑到了“范圍外”,OD立刻自動暫停。
最終結(jié)果
此時,OD會停下來,而光標所在的位置,就是那個 ,例如 winspool.ClosePrinter。你就成功地跳過了所有復雜的中間過程,直達目的地。
記得,如原文所說,這個調(diào)試選項要**“全程開著”**,這樣你就可以對每一個需要分析的CALL重復“F7步入 -> Ctrl+F11運行跟蹤”這個流程,極大地提高脫殼效率。


腳本自動化追蹤的原理:
原始call類型判斷
判斷MOV型
執(zhí)行上述跟蹤步入步驟,開始執(zhí)行加密call
執(zhí)行前,將除了ESP,EBP以外的寄存器全部置0
RET出來的第一句是在代碼段。API放入寄存器的相應位置。
若已知不是MOV型:
執(zhí)行上述跟蹤步入步驟,開始執(zhí)行加密call
將 [ESP]置1,[ESP+4]置2,[ESP+8]置3,[ESP+C]置4
執(zhí)行完vmp后 RET到API,且[ESP]=1:FF25型,補碼在后
RET到API,且[ESP+4]=2:FF15型,補碼在前
RET到API,且[ESP+4]=1:FF15型,且補碼在后
RET到API,且[ESP+4]=3:FF25型,且補碼在前
若已知是MOV型:
RET回代碼段,且[ESP]=1:補碼在后
否則補碼在前
若補碼在前,則在重定向CALL的地址 -1 處開始修改,若補碼在后,則直接從重定向CALL的地址開始修改
運行自動化判斷腳本
在這個CALL右鍵點擊此處為新EIP,然后運行我寫的判斷類型腳本。就可以知道他的原型和RET出來之后的地址了。
//第一部分:初始化和變量設置 var text //聲明變量 text,用于存放代碼段的起始地址 var next //聲明變量 next,用于記錄當前分析到的指令地址 (EIP) next 變量是腳本的核心,它像一個游標,指向當前正要分析的指令。 var iat //聲明變量 iat,用于記錄當前IAT表填充到了哪個位置 var iatbf //聲明變量 iatbf,用于存放我們重建的IAT表的起始位置 var iatep //聲明變量 iatep,iat填充進度的備份 var api //聲明變量 api,用于臨時存放找到的API地址 var oep //聲明變量 oep,用于存放程序的原始入口點 (Original Entry Point) var vmp0ep //聲明變量 vmp0ep,用于存放VMP保護代碼段的入口地址 var dedao //聲明變量 dedao,用作一個標志,判斷當前指令是否是一個API調(diào)用 mov text,401000 //【需要手動填寫】設置代碼段的起始地址為 0x401000 mov oep,4612d1 //【需要手動填寫】設置OEP為 0x4612D1 mov vmp0ep,53c000 //【需要手動填寫】設置VMP代碼段的起始地址為 0x53C000 mov iatbf,481000 //【需要手動填寫】設置新IAT表的起始地址為 0x481000 mov next,text //讓分析指針 next 從代碼段的頭部開始 mov iat,iatbf //讓IAT填充指針 iat 指向新IAT表的頭部 mov iatep,iatbf //備份IAT填充指針 //第二部分:主循環(huán)和初步API檢測 pdlx: /// 是主循環(huán)的標簽。 mov iatep,iatbf //每次循環(huán)開始時,重置iat進度備份指針 mov eax,0 //清空通用寄存器,為模擬執(zhí)行做準備 mov edx,0 mov ecx,0 mov ebx,0 mov esi,0 mov edi,0 mov [esp],1 //在棧上設置一些標記值。這些值后面會用來區(qū)分不同的指令類型 mov [esp+4],2 mov [esp+8],3 STI //單步跟蹤指令 (Step Into) STI //再次單步,可能是為了越過某些VMP的Handler TI //跟蹤進入 (Trace Into) GN eip //【關鍵命令】獲取當前EIP指向地址的符號名(通常是已知的Windows API名) mov dedao,$RESULT //將GN命令的結(jié)果($RESULT)存入dedao變量 cmp dedao,0 //比較結(jié)果是否為0。如果不為0,說明EIP直接指向一個API je movexx ////如果dedao為0 (不是直接的API調(diào)用),則跳轉(zhuǎn)到movexx,檢查是否是mov類型的API調(diào)用 e??型 jmp ecpd //如果dedao不為0,說明是直接的API調(diào)用,跳轉(zhuǎn)到ecpd進一步判斷是哪種call/jmp //第三部分:mov 類型API調(diào)用檢測 movexx: //movexx 代碼塊就是用來檢測這種情況的。它挨個檢查每個通用寄存器,看里面存放的是不是API的地址。 GN eax //檢查eax寄存器里的值是不是一個API地址 cmp $RESULT,0 //如果結(jié)果不為0 jne eaxiat //就跳轉(zhuǎn)到eaxiat處理流程 GN ecx //檢查ecx... cmp $RESULT,0 jne ecxiat GN edx cmp $RESULT,0 jne edxiat GN ebx cmp $RESULT,0 jne ebxiat GN esi cmp $RESULT,0 jne esiiat GN edi cmp $RESULT,0 jne ediiat GN ebp cmp $RESULT,0 jne ebpiat jmp ecpd //如果所有寄存器里都不是API地址,說明不是mov型,繼續(xù)跳轉(zhuǎn)到ecpd判斷 //------------------------------------ move??qcf--------------------------------------------- //第四部分:mov 類型修復和日志記錄 //-----------------------------------------movxiufu---------------------------------------------- eaxiat: msg "mov eax,[0x00]" // //在調(diào)試器日志窗口顯示消息,表明識別到 "mov eax, [api_addr]" 模式 jmp pdapif //跳轉(zhuǎn)到處理流程的末尾,繼續(xù)下一次循環(huán) ecxiat: //打印補碼在前,mov ecx cmp [esp],1 //檢查之前在棧上設置的標記。用來區(qū)分不同的指令編碼或上下文//是1就屬于補碼在后的類型,不是1就是補碼在前的類型。 je ecxiath msg "qian:mov ecx,[0x00]" jmp pdapif ecxiath: //打印補碼在后,mov ecx msg "hou:mov ecx,[0x00]" jmp pdapif edxiat: cmp [esp],1 //是1就屬于補碼在后的類型,不是1就是補碼在前的類型。 je edxiath msg "qian:mov edx,[0x00]" jmp pdapif edxiath: msg "hou:mov edx,[0x00]" jmp pdapif ebxiat: cmp [esp],1 //是1就屬于補碼在后的類型,不是1就是補碼在前的類型。 je ebxiath msg "qian:mov ebx,[0x00]" jmp pdapif ebxiath: msg "hou:mov ebx,[0x00]" jmp pdapif esiiat: cmp [esp],1 //是1就屬于補碼在后的類型,不是1就是補碼在前的類型。 je esiiath msg "qian:mov esi,[0x00]" jmp pdapif esiiath: msg "hou:mov esi,[0x00]" jmp pdapif ediiat: cmp [esp],1 //是1就屬于補碼在后的類型,不是1就是補碼在前的類型。 je ediiath msg "qian:mov edi,[0x00]" jmp pdapif ediiath: msg "hou:mov edi,[0x00]" jmp pdapif ebpiat: cmp [esp],1 //是1就屬于補碼在后的類型,不是1就是補碼在前的類型。 je ebpiath msg "qian:mov ebp,[0x00]" jmp pdapif ebpiath: msg "hou:mov ebp,[0x00]" jmp pdapif //-------------------------------------------------------------------------------------- //第五部分:JMP/CALL 類型API調(diào)用判斷 (FF15/FF25) ecpd: cmp [esp],1 je ff25h cmp [esp+4],2 je ff15q cmp [esp+4],1 je ff15h cmp [esp+4],3 je ff25q //------------------------------------ ff 15 --------------------------------------------- ff15q: msg "qian: ff15" jmp pdapif ff15h: msg "hou: ff15" jmp pdapif //------------------------------------ ff 25 --------------------------------------------- ff25q: msg "qian: ff25" jmp pdapif ff25h: msg "hou: ff25" jmp pdapif //-------------------------判斷填充到哪,下一個有沒有重定向------------------------------------ // 是一個公共的跳出點,表示當前指令分析成功。 pdapif: ret //子程序返回,但在此腳本上下文中,可能意味著成功處理一條指令,準備回到主循環(huán)繼續(xù) //-----------------------------錯誤-------------------------------------------------------- //cw1, cw2, cw3 是錯誤處理模塊。當腳本遇到無法識別的指令模式時,它會調(diào)用 wrta 命令將當前指令的地址寫入到不同的文本文件中,方便后續(xù)手動分析這些“硬骨頭”。 cw1: wrta "C:\bscdx.txt",next //輸出未修復的 jmp pdlx cw2: wrta "C:\yichang.txt",next //輸出未修復的 jmp pdlx cw3: wrta "C:\bushimov.txt",next //輸出未修復的 jmp pdlx jieshu: msg "ok" ret //總結(jié) //這個腳本是一個高度定制化的IAT自動分析和重建工具。它的工作流程如下: //配置: 用戶手動填入目標程序的關鍵地址。 //循環(huán): 從代碼段開始,逐條指令執(zhí)行和分析。 //識別: 使用 GN 命令和模式匹配,判斷當前指令是否是一個API調(diào)用(無論是直接CALL、JMP,還是間接的MOV+CALL reg)。 //記錄: 使用 msg 命令在調(diào)試器中打印日志,記錄識別到的API調(diào)用模式。 //處理失敗: 如果遇到無法識別的指令,將其地址記錄到文本文件中。 //重建IAT(隱藏邏輯): 雖然腳本中沒有明確的寫入IAT的代碼(例如 mov [iat], api),但可以推斷,這個腳本的主要目的是識別和記錄。完整的工具鏈可能還包含另一部分腳本,該腳本會根據(jù)這個分析腳本的日志或中間結(jié)果,來實際地將解析出的API地址填充到 iatbf 指定的內(nèi)存區(qū)域中,從而完成IAT的重建。
第三階段:修復腳本的邏輯
核心邏輯
腳本的執(zhí)行流程是一個循環(huán):
-
搜索:根據(jù)用戶提供的“特征碼”(機器碼模式),在代碼段中尋找一個加密的CALL指令。
-
分析:使用前文提到的“堆棧指紋”方法,精確判斷出這個CALL的原始類型 (CALL/JMP/MOV) 和補碼位置。
-
修復:將原始的API調(diào)用指令(如 CALL [IAT地址])寫回代碼段。
-
重復:繼續(xù)搜索下一個符合條件的CALL,直到找不到為止。對于無法自動修復的調(diào)用,腳本會將其地址記錄在一個C盤文本文件中,供用戶后續(xù)手動處理。
用戶須知:關鍵的手動步驟
腳本無法直接運行,用戶必須手動完成以下關鍵配置:
-
計算“特征碼” (Feature Code):
-
目的:為了讓腳本能準確地只搜索那些跳轉(zhuǎn)到VMP代碼段的CALL指令。
-
方法:通過計算程序代碼段的開頭/結(jié)尾到VMP代碼段的結(jié)尾/開頭的CALL指令的相對偏移量,得出一個偏移范圍。這個范圍就是用于搜索的“特征碼”(如 E8????1?00)。
-
-
分批次、多次運行腳本:
-
由于特征碼通常不是一個,而是一組(如 E8????1?00, E8????2?00...),用戶必須手動修改腳本中的特征碼,并多次運行腳本。
-
每次運行前,都需要更新腳本里新IAT的起始地址,確保API地址能被依次、正確地填入。
-
總而言之,這個腳本將復雜的“堆棧指紋”分析過程自動化了,但仍需要用戶手動計算并提供搜索目標(特征碼),并通過多次運行來完成整個程序的修復工作。
核心思想
腳本通過搜索CALL指令的特定機器碼模式(特征碼)都落在一個特定范圍內(nèi),因此可以通過這個范圍來生成精確的搜索特征碼,避免全盤掃描,提高效率。
用戶操作步驟
這個腳本并非一鍵完成,而是一個需要用戶介入的半自動化、分批次的修復流程:
-
第一步:計算特征碼范圍
-
找到VMProtect代碼段(VMP0)的起始和結(jié)束地址。
-
通過計算從程序代碼段的頭/尾分別CALL到VMP0段的尾/頭的兩條指令,得到一個相對地址偏移量的最大值和最小值。
-
將這個數(shù)值范圍轉(zhuǎn)換成一組機器碼搜索模式(如 E8????1?00, E8????0F00 等),這就是需要用到的特征碼列表。作者特意避開了過于寬泛的模式(如E8????0?00)以防搜索到太多無關指令導致腳本變慢。
-
-
第二步:迭代運行腳本
-
運行第1次:將第一個特征碼和新IAT表的起始地址填入腳本并運行。腳本會修復一批API調(diào)用。
-
運行第2次:修改腳本,換上第二個特征碼,并且更新IAT地址,使其指向第一個批次修復完后下一個可用的空位。再次運行。
-
重復此過程:不斷地用列表里新的特征碼替換腳本中的舊特征碼,并同步更新IAT地址,直到所有特征碼都使用過一遍。
-
總而言之,用戶需要先親手計算出一組合適的“搜索密碼”(特征碼),然后像“換彈夾”一樣,手動地、分次地把這些密碼喂給腳本,并告知其新的數(shù)據(jù)該存放在哪里,才能最終完成全部修復工作。
我用白話文說明腳本執(zhí)行的示例
用硬件訪問斷點找到OEP的位置停下,取消所有設置過的斷點,并設置跟蹤步入,修改腳本的默認參數(shù)為你的程序的參數(shù),其中重點介紹的的是計算多個要修改call的hex特征碼,先填入第一個特征碼,右鍵加載od腳本,等待執(zhí)行完成,完成后修改你你現(xiàn)在的IAT地址在腳本中在執(zhí)行一遍直到?jīng)]有可以修復的iat,再次修改特征碼和下一個iat地址搜索修復下一個特征。
打開c盤查看修復錯誤文件,手動修復。比如476FB8地址就是一個錯誤。打開類型判斷腳本,我們知道了他是ff15,前補碼。RET出來后停在77FE0000。我們復制jmp前的三條指令,到jmp后地址的前面粘貼。我們直接手動在 476FB8-1 這個地址修改就行了。記得將API記錄下來,手動填到IAT中。
第四階段:手動修復
打開C盤找到修復失敗的api文件,手動修復。
-
定位疑難API:通過調(diào)試和觀察,找到那些被加密得最深的、或者有反調(diào)試機制的API調(diào)用。比如476FB8位置有一個被加密函數(shù)的api,
-
手動修改:
-
上面提供的判斷類型的腳本。可以先知道它的類型。比如,當腳本執(zhí)行完成后發(fā)現(xiàn)是FF15型,補碼在前,RET出來后停在77FE0000。
-
通過單步步入(F7)和跟蹤步入 進入函數(shù)內(nèi)部,找到真正的調(diào)用函數(shù)地址。比如,就是一個被偷掉開頭的函數(shù),從77FE0000,到77FE0003,就是VMP偷走的函數(shù)頭,下一個JMP就是跳回那個函數(shù)的對應行。我們將這個頭以二進制形式復制下來,然后跟入下面那個CALL,將頭補回去,你就可以知道這個函數(shù)是什么了。就是替換user32.77D2C90D前三個指令為調(diào)用者的前三個指令,并修改eip為 77D2C908
-
然后手動修改代碼,將原始的混淆指令替換為call user32.某個函數(shù)api。比如我們直接手動在 476FB8-1 這個地址修改就行了。記得將API記錄下來,手動填到IAT中。
-
-
Dump內(nèi)存和重建IAT:
-
Dump進程:在所有API調(diào)用都修復完成后,使用LordPE等工具完整地Dump出進程內(nèi)存,這是可以運行的但是跨平臺不能運行。
-
:使用ImportREC(導入表重建工具),也修復不了,因為我們修復出來的IAT是亂序的,并不是以一個一個DLL的順序排列好。用ODdump出來的那一份程序進行修改拖進 StudyPE+ x86 ,給它加一個區(qū)段,用于存放我們UIF修復的API。(在原有的IAT進行修復可能會出錯)
- 修復二次重定位的IAT:
- 再將添加了區(qū)段的脫殼版 拖進OD,在我們修復的IAT中找到二次重定位的API,將它更改,直接填入真實的API。(因為UIF識別不了重定向的api,會修復錯誤。) 很好找的, 看看數(shù)據(jù)和其他對比一下大小就知道了。(一般有兩個二次重定向)
![image]()
正常的、已修復的條目: -
-
00481394 77D3C702 user32.DrawTextA -
00481398 77D55B05 user32.GrayStringA -
0048139C 77F06F5A gdi32.Escape
請注意它們的第二個數(shù)據(jù)(地址):
77D3C702、77D55B05、77F06F5A。這些都是高地址(以77...開頭),它們就是系統(tǒng)“圖書館”里的**“真實頁碼”**(真實的API地址)。這些都是正確的。 -
-
需要你手動修復的“二次重定位”條目:
-
004813A4 0041B71D <jmp.&KERNEL32.GetVersion>
再看這一行,它的第二個數(shù)據(jù)(地址)是
0041B71D。-
“對比大小”:
0041B71D是一個低地址(以004...開頭),它和其他的77...地址一比,數(shù)值差別巨大。這就是作者說的“很好找”的原因。 -
“二次重定位”的含義: 這個
0041B71D地址不是GetVersion函數(shù)的“真實頁碼”。它指向的是程序自己內(nèi)部的某個地方(“書的第5頁”)。這個地方存放了一個“跳轉(zhuǎn)指令”(JMP),它才會第二次跳轉(zhuǎn)到“真實頁碼”。 -
“修復錯誤”的原因: 下一個工具(UIF)看到
0041B71D就會“懵掉”,它會以為0041B71D就是GetVersion的真實地址,從而導致排序和修復徹底失敗。
-
所以,這個步驟要求你:
在運行UIF工具之前,你必須手動去
0041B71D這個地址看一眼,找到它最終跳轉(zhuǎn)到的那個“真實頁碼”(比如是77E50000),然后回到這里,把0041B71D這個值手動改成77E50000。這樣,這個“新目錄”里的所有條目就都是“真實頁碼”了,
UIF才能正確地工作。-
-
修復IAT 打開UIF,填入PID,代碼段的頭,代碼段的尾,和我們添加的區(qū)段的頭部地址。勾選修復輸入表。然后點擊修復。修復后,排好了順序
-
![image]()
- 用UIF修復完后,我們就可以直接用REC進行跨平臺修復了。選擇我們加了區(qū)段的脫殼版,填好OEP偏移,和UIF修復后的地址偏移(也就是我們新加的區(qū)段頭地址減去400000)。點獲取。然后修復轉(zhuǎn)存到 你加了區(qū)段的脫殼版 上就可以完美修復了。XP系統(tǒng)下脫殼修復,WIN10上運行:
-
![image]()
-
總結(jié)
-
動靜結(jié)合:既有靜態(tài)分析(觀察代碼結(jié)構(gòu)),也有動態(tài)調(diào)試(設置斷點、跟蹤執(zhí)行)。
-
先自動后手動:首先嘗試用腳本批量解決大部分問題,再對少數(shù)疑難點進行精確的手動修復。
-
邏輯清晰:從尋找入口點,到分析API,再到編寫腳本和手動修復,最后到Dump和重建,整個流程一氣呵成,邏輯非常清晰。



浙公網(wǎng)安備 33010602011771號