GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

应用安全 --- PC安全 之 VMP2 IAT修复

加密工具选择

vmp2.13.8

加密选项

  • 内存保护 (是):程序运行时,它的内容会放在电脑的内存里。这个选项就像是给内存里的程序加了一个“障眼法”,防止破解者用特殊工具直接“偷看”内存里的内容,或者把程序从内存里完整地复制出来。

  • 导入表保护 (是):程序需要调用很多系统功能(比如显示窗口、读写文件),“导入表”就像是程序的功能“通讯录”。这个选项会把这份通讯录加密或藏起来,让破解者搞不清楚你的程序到底调用了哪些系统功能,增加了分析难度。

  • 资源保护 (是):程序里包含的图片、图标、文字等“资源”也会被加密。就像把宝箱里的珠宝都用独立的小盒子锁起来,就算宝箱被打开了,里面的东西也拿不走。

  • 压缩输出文件 (快速):这个选项会把整个程序文件压缩变小,就像用真空袋打包衣服一样。“快速”意味着压缩和解压的速度都比较快,能稍微平衡文件大小和运行速度。

代码vm  表示启用了一个“翻译机”来保护代码。  

  • 离开虚拟机时加密寄存器 (是):寄存器是CPU里用来临时存放数据的小仓库。当代码从“火星文”模式切换回正常模式时,这个选项会立刻把小仓库里的数据锁起来,防止破解者偷看到关键的计算结果。

  • 检查虚拟机对象的完整性 (是):这个“翻译机”本身也会被保护起来。程序会时刻检查它自己是否被破解者动了手脚,如果被修改了,程序就不干了。

  • 隐藏常量 (是):程序里的一些固定数值或字符串(比如一个密码、一个网址)被称为常量。这个选项会把它们都加密隐藏起来,让破解者无法轻易在程序文件中找到这些敏感信息。

 

壳运行到OEP的大致流程

image

 通过虚假eip进入vmp1段的入口

 

 

第一阶段:寻找OEP(原始入口点)

核心思路解析

  1. VMP的核心保护机制:VMP壳在程序启动初期,会做大量的“准备工作”,比如解密自身的代码、设置反调试陷阱等。其中非常关键的一步,就是使用 VirtualProtect 这个Windows API函数。

  2.  函数的作用:这个函数像一个“权限管家”,可以动态修改内存区域的读、写、执行权限。VMP利用它来抹除我们(分析者)在代码区段和IAT(导入地址表)上设置的内存访问断点。如果我们下的断点被清除了,自然就无法中断在关键位置。

  3. 断点的不同类型

    • 内存断点:在某个内存地址设置,一旦该地址被读取、写入或执行,就会中断。这种断点容易被VirtualProtect清除。

    • 硬件断点:由CPU直接支持,数量有限(通常4个),但更底层,更难被检测。其中,硬件“访问”和“写入”断点不容易被VMP检测到,但硬件“执行”断点则会被VMP发现。

利用以上原理,就可以设计出如下绕过VMP检测、直达OEP的执行流程。

详细执行流程步骤

以下是根据您提供的方法,整理出的完整、详细的操作步骤:

前提:使用OD(OllyDbg)或x64dbg等调试工具载入加了VMP 2.13.8壳的程序。

第一步:首次运行与定位代码段尾部

  1. 正常运行程序:直接在调试器中按 F9 键让程序运行起来。程序会执行VMP的外壳代码,完成初始化,然后停在VMP自身的某个区段(例如截图中提到的VMP1区段)。

  2. 找到代码段末尾:切换到内存窗口,找到程序的 .text(代码段)。用鼠标滚轮或滚动条向下滚动,找到代码段中最后一个非零字节数据的位置。

  3. 确定下断点的位置:记下这个“最后一个有效值”的下一个地址。这个地址是代码段的末尾,通常是代码执行流程中一个比较靠后的位置。

第二步:设置硬件断点并重载

  1. 设置硬件访问断点:在刚才记下的那个地址上,右键点击,选择“断点” -> “硬件,访问”。因为这个位置在代码段的末尾,当VMP外壳代码即将执行完毕,准备跳转到真正的程序代码(OEP)时,很可能会访问到这附近区域。

  2. 重载程序:在调试器中,点击“重新开始”或按 Ctrl+F2,让程序回到最初的加载状态。这一步是为了让我们设置的断点能在VMP的初始化流程中生效。

第三步:两次中断,绕过干扰

  1. 第一次中断(F9):按下 F9 运行程序。程序会因为VMP外壳代码在初始化过程中写入数据到代码段而触发我们设置的硬件断点,此时程序会中断下来。

  2. 第二次中断(F9):再次按下 F9 继续运行。这里可能会遇到VMP为了迷惑分析者而故意设置的一些“干扰性”访问,导致断点再次被触发。根据原文描述,这次中断是“断在干扰时”。我们只需再次忽略,继续运行即可。

第四步:设置内存断点,直达OEP

  1. 设置内存访问断点:经过前两轮的硬件断点中断,VMP的“权限修改”阶段(即调用 VirtualProtect 清除内存断点)很可能已经过去了。此时,我们可以在程序的整个代码段(.text段)上设置一个内存访问断点。

  2. 最后一次运行(F9):再次按下 F9 运行程序。由于VMP外壳已经完成了它的使命,正准备跳转到程序的原始入口点(OEP)去执行真正的程序逻辑,这个跳转动作必然会“访问”到代码段。

  3. 成功中断在OEP:此时,我们设置的内存访问断点会被触发,程序会精准地停在OEP处。这个时候,调试器里显示的代码就是程序原本的、未被加密的开始代码了。

总结:这个流程巧妙地利用了硬件断点不易被检测的特性,先让程序跑过最危险的反调试和反内存断点阶段,然后再利用内存断点覆盖范围广的优势,精准地捕捉到从外壳代码跳转到原始代码的那一瞬间,从而成功定位到OEP。

 

简要说明

只要过了这个清除我们在代码段 和 IAT 设置的内存访问断点 VirtualProtect 函数  就可以在代码段设置访问断点了。绕过方法就是设置的 硬件访问 和 硬件写入 断点并不会被壳检测到,但是硬件执行断点会被检测到。

完整执行一次程序,程序可以正常运行并自动终止,没有加反调试。

点击M按钮在text右键在cpu中查看

将右键分析,删除分析,就可以正常显示汇编指令

拖到最后有数据的hex的内存区域(最后一个不是00的值),右键设置硬件断点

重新运行OD中的程序,按下两次执行按钮(一次断在写入时,一次断在干扰时),绕过vmp段内两次内存断点检查代码

点击M按钮在text右键设置内存访问断点

执行后自动断在OEP位置,完成OEP查找

 

第二阶段:API调用的类型分析

  1. 识别VMP的调用模式:发现VMP(VMProtect的缩写)主要通过CALLJMPMOV等指令的变体来间接调用API,这些指令的机器码长度通常是5到6字节。

    有这三种

     CALL [IAT]      //以下简称十六进制的 FF15

    JMP [IAT]      //FF25

     mov e?? , [iat]   //MOV型

 

CALL [IAT]:

加壳前

image

 后

image

 从ff15变为50e8,后面地址变为vmp的壳代码。这里的50就是补码

我们观察到长度不变,因为so文件一处长度发生改变所有地址偏移都要变化引擎雪崩效应

 

JMP [IAT]:

image

image

 

 mov e??,[IAT]

image

image

 

发现了一个VMP加壳的细节:对于 mov eax, [地址] 这种5字节长的API调用,VMP会用一个同样是5字节长的指令去替换它。因为尺寸刚好匹配,所以VMP不需要进行任何“填充”或“补码”操作。

如果VMP要用一个5字节的安保零件去替换一个6字节的旧零件,那么就会多出1字节的空隙。为了不让整个机器“错位”,VMP就必须用一个没有意义的“填充物”(专业术语叫补码Padding,比如图中的 nop 指令)把这个空隙填上。

 因为重定向的代码为E8,是 5 字节的,因此壳会随机填充一个字节。补码上面或者下面都有可能。

关键的部分就是我们如何知道加密前的格式

 

手动一步一步追踪:

首先我们先要知道,被重定向的API,进入了 VMP0 区段,再经过一系列的运算,得到真实的API的地址,放入堆栈,然后通过 RET 的方法进入真实的API

image

 这是重定向CALL ,一直F7就可以到这一步。

但是手动跟着特工在复杂的“中转站”里一步步走(用F7单步跟踪)太累了,而且容易跟丢。

 

所以介绍了一个绝佳的自动化方法绕过vmp代码:

使用OD(OllyDbg调试器)的“跟踪步入”功能,并设置一个聪明的条件。

这个条件是:
EIP 位于范围外 VMP0段首地址 ... VMP0段尾地址

这句话的通俗解释就是告诉调试器:

“嘿,老兄!接下来我要追踪一段代码。我不关心它在‘VMP0中转站’里面是怎么跑的,里面太复杂了。你就让它在里面尽情地跑吧。但是,只要它的脚步一踏出‘VMP0中转站’的范围,你必须立刻停下来,告诉我它跑到了哪里!

第三步:实际操作流程

  1. 设置好“岗哨”:把上面那个“EIP位于范围外”的条件设置好。作者强调这个条件必须全程开启,因为你的脱壳脚本也要靠它来自动分析。

  2. 找到一个假:随便找一个被VMP处理过的CALL指令。

  3. 手动 F7 步入:手动按一下F7,你(EIP)就进入了“VMP0中转站”的入口。

  4. 启动自动追踪:点击OD的“跟踪步入”功能。

  5. 自动完成!:调试器会飞速执行“中转站”里所有的复杂代码,因为EIP一直在VMP0区段内,条件不满足,所以不会停。直到最后执行了那条RET指令,EIP一下子跳出了VMP0区段,到达了真实的API地址(比如winspool.ClosePrinter)。

    • “啪!” 条件满足,调试器立刻停了下来。

最终效果:OD直接就停在了真实API的第一句代码上,你根本不需要去关心VMP在中间到底做了什么复杂的运算。

总结一下:

这段话的核心就是教你如何利用调试器的一个条件跟踪功能,来跳过VMP复杂、繁琐的解密过程,实现“一键直达”被隐藏的真实API地址。这个方法是编写自动化脱壳脚本的基础和关键。

 

这个操作的核心是利用OD的运行跟踪 (Run trace) 功能,并为其设置一个暂停条件

第一步:准备工作 - 找到VMP0区段的地址范围

在设置条件之前,你必须先知道“VMP0中转站”的大门和后门在哪里。

  1. 打开内存映射窗口:在OD的主界面,点击顶部菜单栏的 "M" 图标(Memory Map),或者按快捷键 Alt+M

  2. 找到VMP0区段:在弹出的内存窗口中,找到那个名为 .vmp0 或者 VMP0 的区段。

  3. 记录地址

    • 记下这一行的 “基址 (Address)”,这就是 “VMP0段首地址”

    • 记下这一行的 “大小 (Size)”

    • 计算出 “VMP0段尾地址”。公式是:基址 + 大小 - 1

    举例:
    如果基址是 00B50000,大小是 00028000
    那么首地址就是 00B50000
    尾地址就是 00B50000 + 00028000 - 1 = 00B77FFF

    现在你手里就有了设置条件所需的两个关键地址了。

第二步:设置“跟踪暂停”条件

  1. 打开调试选项:点击OD顶部菜单栏的调试 ,设置条件

  2. 切换到“跟踪”选项卡:在弹出的对话框中,点击 “跟踪 (Trace)” 选项卡。

  3. 进行设置

    • ① 勾选复选框“EIP 位于范围外”

    • ② 填入地址:将在第一步中得到的 “VMP0段首地址” 和 “VMP0段尾地址” 填入后面的两个输入框中。

    • ③ 确认:点击“确定 (OK)”保存设置。

    现在,OD就已经被你设置成一个聪明的“哨兵”了。

第三步:执行跟踪

现在万事俱备,可以开始实际操作了。

  1. 找到一个重定向CALL:在OD的反汇编窗口(CPU窗口),找到一个被VMP处理过的CALL指令,比如 CALL 00B51234(这个地址在VMP0区段内)。

  2. 右键设置新的EIP,直接从这里开始执行。

  3. 手动步入 (关键!):按下键盘上的 F7 键。这时,你会进入VMP0区段的内部,EIP(指令指针)现在就在“中转站”里面了。这一步是必须的,因为你得先进去,才能谈“出来的时候暂停”。

  4. 启动运行跟踪:现在EIP已经在VMP0区段内,你可以启动自动跟踪了。

    • 点击顶部菜单栏的 “调试 (Debug)” -> “运行跟踪 (Run trace)” -> “步入 (Into)”

    • 或者直接使用快捷键 Ctrl + F11 (这是运行跟踪的快捷键,它会应用你在选项中设置的暂停条件)。 注:原文提到的是“跟踪步入”,在OD中,“运行跟踪”是实现这一功能的正式名称。

  5. 等待自动暂停:按下Ctrl+F11后,OD会开始飞速执行代码。你可能会看到屏幕闪烁,OD会记录下成千上万条指令。但你不用管它,因为它会在满足你设定的条件时自动停下。

    当VMP内部的代码执行到最后那条 RET 指令时,EIP会一下子跳出VMP0区段的范围,跳到真实的API函数地址上。

    “啪!” 哨兵发现EIP跑到了“范围外”,OD立刻自动暂停

最终结果

此时,OD会停下来,而光标所在的位置,就是那个 ,例如 winspool.ClosePrinter。你就成功地跳过了所有复杂的中间过程,直达目的地。

记得,如原文所说,这个调试选项要**“全程开着”**,这样你就可以对每一个需要分析的CALL重复“F7步入 -> Ctrl+F11运行跟踪”这个流程,极大地提高脱壳效率。

image

 

image

 

 

脚本自动化追踪的原理:

原始call类型判断

判断MOV型

执行上述跟踪步入步骤,开始执行加密call

执行前,将除了ESP,EBP以外的寄存器全部置0

RET出来的第一句是在代码段。API放入寄存器的相应位置。

 

若已知不是MOV型:

执行上述跟踪步入步骤,开始执行加密call

将 [ESP]置1,[ESP+4]置2,[ESP+8]置3,[ESP+C]置4
执行完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调用

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填充指针


//第二部分:主循环和初步API检测
pdlx:                    /// 是主循环的标签。
mov iatep,iatbf      //每次循环开始时,重置iat进度备份指针
mov eax,0              //清空通用寄存器,为模拟执行做准备
mov edx,0
mov ecx,0
mov ebx,0
mov esi,0
mov edi,0
mov [esp],1            //在栈上设置一些标记值。这些值后面会用来区分不同的指令类型
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命令的结果($RESULT)存入dedao变量    
cmp dedao,0             //比较结果是否为0。如果不为0,说明EIP直接指向一个API    
je movexx                         ////如果dedao为0 (不是直接的API调用),则跳转到movexx,检查是否是mov类型的API调用  e??型
jmp ecpd //如果dedao不为0,说明是直接的API调用,跳转到ecpd进一步判断是哪种call/jmp


//第三部分:mov 类型API调用检测   
movexx:                    //movexx 代码块就是用来检测这种情况的。它挨个检查每个通用寄存器,看里面存放的是不是API的地址。
GN eax                 //检查eax寄存器里的值是不是一个API地址
cmp $RESULT,0        //如果结果不为0
jne eaxiat            //就跳转到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型,继续跳转到ecpd判断

//------------------------------------ move??qcf---------------------------------------------
//第四部分:mov 类型修复和日志记录
//-----------------------------------------movxiufu----------------------------------------------
eaxiat:    
msg "mov eax,[0x00]"    // //在调试器日志窗口显示消息,表明识别到 "mov eax, [api_addr]" 模式
jmp pdapif                   //跳转到处理流程的末尾,继续下一次循环


ecxiat:        //打印补码在前,mov ecx
cmp [esp],1   //检查之前在栈上设置的标记。用来区分不同的指令编码或上下文//是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调用判断 (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            //子程序返回,但在此脚本上下文中,可能意味着成功处理一条指令,准备回到主循环继续

//-----------------------------错误--------------------------------------------------------
//cw1, cw2, cw3 是错误处理模块。当脚本遇到无法识别的指令模式时,它会调用 wrta 命令将当前指令的地址写入到不同的文本文件中,方便后续手动分析这些“硬骨头”。
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




//总结
//这个脚本是一个高度定制化的IAT自动分析和重建工具。它的工作流程如下:
//配置: 用户手动填入目标程序的关键地址。
//循环: 从代码段开始,逐条指令执行和分析。
//识别: 使用 GN 命令和模式匹配,判断当前指令是否是一个API调用(无论是直接CALL、JMP,还是间接的MOV+CALL reg)。
//记录: 使用 msg 命令在调试器中打印日志,记录识别到的API调用模式。
//处理失败: 如果遇到无法识别的指令,将其地址记录到文本文件中。
//重建IAT(隐藏逻辑): 虽然脚本中没有明确的写入IAT的代码(例如 mov [iat], api),但可以推断,这个脚本的主要目的是识别和记录。完整的工具链可能还包含另一部分脚本,该脚本会根据这个分析脚本的日志或中间结果,来实际地将解析出的API地址填充到 iatbf 指定的内存区域中,从而完成IAT的重建。

 

 

 

 

 

第三阶段:修复脚本的逻辑

核心逻辑

脚本的执行流程是一个循环:

  1. 搜索:根据用户提供的“特征码”(机器码模式),在代码段中寻找一个加密的CALL指令。

  2. 分析:使用前文提到的“堆栈指纹”方法,精确判断出这个CALL的原始类型 (CALL/JMP/MOV) 和补码位置。

  3. 修复将原始的API调用指令(如 CALL [IAT地址])写回代码段。

  4. 重复:继续搜索下一个符合条件的CALL,直到找不到为止。对于无法自动修复的调用,脚本会将其地址记录在一个C盘文本文件中,供用户后续手动处理。

 

用户须知:关键的手动步骤

脚本无法直接运行,用户必须手动完成以下关键配置:

  1. 计算“特征码” (Feature Code)

    • 目的:为了让脚本能准确地只搜索那些跳转到VMP代码段的CALL指令。

    • 方法:通过计算程序代码段的开头/结尾到VMP代码段的结尾/开头的CALL指令的相对偏移量,得出一个偏移范围。这个范围就是用于搜索的“特征码”(如 E8????1?00)。

  2. 分批次、多次运行脚本

    • 由于特征码通常不是一个,而是一组(如 E8????1?00E8????2?00...),用户必须手动修改脚本中的特征码,并多次运行脚本

    • 每次运行前,都需要更新脚本里新IAT的起始地址,确保API地址能被依次、正确地填入。

总而言之,这个脚本将复杂的“堆栈指纹”分析过程自动化了,但仍需要用户手动计算并提供搜索目标(特征码),并通过多次运行来完成整个程序的修复工作。

 

根据您提供的特征码生成方法,我来总结这个VMP特征码生成和使用的完整流程:

特征码生成原理

1. 确定VMP0区段范围

  • VMP0区段: 53C000 ~ 615000

  • 计算调用偏移量范围

2. 计算极限偏移值

调用VMP0段尾(615000):

text
地址: 00401000
指令: E8 FB3F2100
计算: 00615000 - 00401000 - 5 = 0x213FFB
小端序: FB 3F 21 00

调用VMP0段首(53C000):

text
地址: 004801E3  
指令: E8 18BE0B00
计算: 0053C000 - 004801E3 - 5 = 0x0BBE18
小端序: 18 BE 0B 00

3. 确定偏移量范围

  • 最小偏移: 0x0BBE18 → 18 BE 0B 00

  • 最大偏移: 0x213FFB → FB 3F 21 00

关键发现: 所有调用VMP0的CALL指令,其E8后面的4字节值在 0x0BBE18 到 0x213FFB 之间

特征码分类

按第三字节分组:

text
E8????0B00  // 第三字节 = 0B
E8????0C00  // 第三字节 = 0C  
E8????0D00  // 第三字节 = 0D
E8????0E00  // 第三字节 = 0E
E8????0F00  // 第三字节 = 0F
E8????1?00  // 第三字节 = 10-1F
E8????2?00  // 第三字节 = 20-2F

为什么不合并?

  • E8????0?00 会匹配到太多非VMP调用

  • 主要匹配TEXT段内的正常调用,干扰严重

  • 分开搜索提高效率和准确性

脚本执行流程

1. 初始设置

asm
mov iat, 481000          // IAT起始地址
mov 特征码, "E8????0B00"  // 第一个特征码

2. 循环执行过程

第一次运行:

  • 特征码: E8????0B00

  • 搜索范围: 整个代码段

  • 修复结果: 修复匹配的VMP调用

  • IAT更新: 自动推进到下一个空闲位置

第二次运行:

asm
mov iat, [新地址]        // 更新IAT起始
mov 特征码, "E8????0C00" // 更换特征码

后续运行:

重复上述过程,依次使用所有特征码

3. 特征码执行顺序

text
1. E8????0B00
2. E8????0C00  
3. E8????0D00
4. E8????0E00
5. E8????0F00
6. E8????1000  // 注意:实际要展开1?00
7. E8????1100
...
N. E8????2F00  // 最后使用2?00系列

实际操作步骤

脚本要依赖 OD 的跟踪步入,记得设置好条件

第一步:准备脚本变量

asm
var iat_start = 481000    // IAT起始地址
var current_iat = iat_start
var patterns = ["E8????0B00", "E8????0C00", ...] // 所有特征码

第二步:循环执行

对于每个特征码:

  1. 设置当前特征码

  2. 设置当前IAT位置

  3. 运行搜索修复脚本

  4. 记录修复结果

第三步:处理特殊情况

OD插件BUG处理:

  • 某些调用可能第一次搜索不到

  • 可以重复运行同一特征码

  • 或手动分析遗漏的调用

特征码展开:

实际上 E8????1?00 和 E8????2?00 需要展开为:

text
E8????1000, E8????1100, ..., E8????1F00
E8????2000, E8????2100, ..., E8????2F00

技术要点总结

1. 理论基础

  • 利用VMP调用偏移量的确定范围

  • 通过小端序分析字节特征

  • 基于统计学的方法分类特征码

2. 实践优势

  • 精准匹配: 避免大量误报

  • 渐进修复: 逐步修复所有调用

  • 容错处理: 处理OD插件的不稳定性

3. 效率考虑

  • 分批次处理: 避免一次性处理过多特征

  • 内存管理: 有序的IAT空间分配

  • 重复利用: 避免重复修复同一API

4. 注意事项

  • 地址更新: 每次运行前更新IAT指针

  • 特征码顺序: 按字节值从小到大执行

  • 错误处理: 记录无法修复的地址供手动处理

这个方法通过系统化的特征码分类和渐进式修复策略,有效解决了VMP IAT修复的复杂性问题,虽然过程较为繁琐,但是修复效果相对可靠。

 
 
 
 

 

 

核心思想

脚本通过搜索CALL指令的特定机器码模式(特征码)都落在一个特定范围内,因此可以通过这个范围来生成精确的搜索特征码,避免全盘扫描,提高效率。

用户操作步骤

这个脚本并非一键完成,而是一个需要用户介入的半自动化、分批次的修复流程:

  1. 第一步:计算特征码范围

    • 找到VMProtect代码段(VMP0)的起始和结束地址。

    • 通过计算从程序代码段的头/尾分别CALL到VMP0段的尾/头的两条指令,得到一个相对地址偏移量的最大值和最小值

    • 将这个数值范围转换成一组机器码搜索模式(如 E8????1?00E8????0F00 等),这就是需要用到的特征码列表。作者特意避开了过于宽泛的模式(如E8????0?00)以防搜索到太多无关指令导致脚本变慢。

  2. 第二步:迭代运行脚本

    • 运行第1次:将第一个特征码新IAT表的起始地址填入脚本并运行。脚本会修复一批API调用。

    • 运行第2次:修改脚本,换上第二个特征码,并且更新IAT地址,使其指向第一个批次修复完后下一个可用的空位。再次运行。

    • 重复此过程:不断地用列表里新的特征码替换脚本中的旧特征码,并同步更新IAT地址,直到所有特征码都使用过一遍。

总而言之,用户需要先亲手计算出一组合适的“搜索密码”(特征码),然后像“换弹夹”一样,手动地、分次地把这些密码喂给脚本,并告知其新的数据该存放在哪里,才能最终完成全部修复工作。

 

我用白话文说明脚本执行的示例

用硬件访问断点找到OEP的位置停下,取消所有设置过的断点,并设置跟踪步入,修改脚本的默认参数为你的程序的参数,其中重点介绍的的是计算多个要修改call的hex特征码,先填入第一个特征码,右键加载od脚本,等待执行完成,完成后修改你你现在的IAT地址在脚本中在执行一遍直到没有可以修复的iat,再次修改特征码和下一个iat地址搜索修复下一个特征。

打开c盘查看修复错误文件,手动修复。比如476FB8地址就是一个错误。打开类型判断脚本,我们知道了他是ff15,前补码。RET出来后停在77FE0000。我们复制jmp前的三条指令,到jmp后地址的前面粘贴。我们直接手动在 476FB8-1 这个地址修改就行了。记得将API记录下来,手动填到IAT中。

 

 

第四阶段:手动修复

打开C盘找到修复失败的api文件,手动修复。

  1. 定位疑难API:通过调试和观察,找到那些被加密得最深的、或者有反调试机制的API调用。比如476FB8位置有一个被加密函数的api,

  2. 手动修改

    • 上面提供的判断类型的脚本。可以先知道它的类型。比如,当脚本执行完成后发现是FF15型,补码在前,RET出来后停在77FE0000。

    • 通过单步步入(F7)和跟踪步入 进入函数内部,找到真正的调用函数地址。比如,就是一个被偷掉开头的函数,从77FE0000,到77FE0003,就是VMP偷走的函数头,下一个JMP就是跳回那个函数的对应行。我们将这个头以二进制形式复制下来,然后跟入下面那个CALL,将头补回去,你就可以知道这个函数是什么了。就是替换user32.77D2C90D前三个指令为调用者的前三个指令,并修改eip为 77D2C908

    • 然后手动修改代码,将原始的混淆指令替换为call user32.某个函数api。比如我们直接手动在 476FB8-1 这个地址修改就行了。记得将API记录下来,手动填到IAT中。

  3. Dump内存和重建IAT

    • Dump进程:在所有API调用都修复完成后,使用LordPE等工具完整地Dump出进程内存,这是可以运行的但是跨平台不能运行。

    • :使用ImportREC(导入表重建工具),也修复不了,因为我们修复出来的IAT是乱序的,并不是以一个一个DLL的顺序排列好。用ODdump出来的那一份程序进行修改拖进 StudyPE+ x86 ,给它加一个区段,用于存放我们UIF修复的API。(在原有的IAT进行修复可能会出错)

    • 修复二次重定位的IAT:
    • 再将添加了区段的脱壳版 拖进OD,在我们修复的IAT中找到二次重定位的API,将它更改,直接填入真实的API。(因为UIF识别不了重定向的api,会修复错误。) 很好找的, 看看数据和其他对比一下大小就知道了。(一般有两个二次重定向)

      image

       

      正常的、已修复的条目:
        • 00481394 77D3C702 user32.DrawTextA

        • 00481398 77D55B05 user32.GrayStringA

        • 0048139C 77F06F5A gdi32.Escape

        请注意它们的第二个数据(地址):77D3C70277D55B0577F06F5A。这些都是高地址(以77...开头),它们就是系统“图书馆”里的**“真实页码”**(真实的API地址)。这些都是正确的。

      1. 需要你手动修复的“二次重定位”条目:

        • 004813A4 0041B71D <jmp.&KERNEL32.GetVersion>

        再看这一行,它的第二个数据(地址)是 0041B71D

        • “对比大小”: 0041B71D 是一个低地址(以004...开头),它和其他的 77... 地址一比,数值差别巨大。这就是作者说的“很好找”的原因。

        • “二次重定位”的含义: 这个 0041B71D 地址不是 GetVersion 函数的“真实页码”。它指向的是程序自己内部的某个地方(“书的第5页”)。这个地方存放了一个“跳转指令”(JMP),它才会第二次跳转到“真实页码”。

        • “修复错误”的原因: 下一个工具(UIF)看到 0041B71D 就会“懵掉”,它会以为 0041B71D 就是 GetVersion 的真实地址,从而导致排序和修复彻底失败。

      所以,这个步骤要求你:

      在运行UIF工具之前,你必须手动去 0041B71D 这个地址看一眼,找到它最终跳转到的那个“真实页码”(比如是 77E50000),然后回到这里,把 0041B71D 这个值手动改成 77E50000

      这样,这个“新目录”里的所有条目就都是“真实页码”了,UIF 才能正确地工作。

    • 修复IAT 打开UIF,填入PID,代码段的头,代码段的尾,和我们添加的区段的头部地址。勾选修复输入表。然后点击修复。修复后,排好了顺序

    • image

       

    • 用UIF修复完后,我们就可以直接用REC进行跨平台修复了。选择我们加了区段的脱壳版,填好OEP偏移,和UIF修复后的地址偏移(也就是我们新加的区段头地址减去400000)。点获取。然后修复转存到 你加了区段的脱壳版 上就可以完美修复了。XP系统下脱壳修复,WIN10上运行:
    • image

       

总结

  • 动静结合:既有静态分析(观察代码结构),也有动态调试(设置断点、跟踪执行)。

  • 先自动后手动:首先尝试用脚本批量解决大部分问题,再对少数疑难点进行精确的手动修复。

  • 逻辑清晰:从寻找入口点,到分析API,再到编写脚本和手动修复,最后到Dump和重建,整个流程一气呵成,逻辑非常清晰。

 

 

 

 

 

 

 

 

 

 

浅谈VMProtect 2.13.8 IAT修复 - 吾爱破解 - 52pojie.cn

posted on 2025-10-31 07:57  GKLBB  阅读(79)  评论(0)    收藏  举报