本章主要针对逆向中最复杂的vmp虚拟机/保护技术进行解析.
核心思想:
vmp只不过是积累了大量的混淆/膨胀 罢了
关于还原:
体力活罢了
vmp学习笔记
- 本课程主要负责 讲解vmp相关的知识点
- 通过本课程的学习可以掌握 vmp逆向相关的技术要点
- 可以做到还原简单的 vmp程序
0x1 文件结构
- vmp 会对程序的
pe结构
进行大幅度更改 - 基于vmp 3.4
0x1.1 PE 头变更
- pe头是可执行程序中的一个特殊的文件结构
- pe头中保存着 可执行程序的一些附加信息:编译器版本,编译版本,文件标识,代码体积,代码入口点等信息
- vmp会直接修改程序的 pe头信息,对其中一些信息进行修改
这里是windows对pe头的数据结构定义
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
- pe标志肯定是
4550
,没有改变
- 文件头部只修改了解表数量
- 可选头部中进行了大量的更改,核心是修改了程序入口点:
AddressOfEntryPoint
- 数据目录也进行了大量的修改
- 抹除了debug信息
- 修改了 导入目录,导出目录,资源目录
0x1.2 区段变更
- 通常情况下加了vmp壳后的程序,多了vmp0,vmp1两个区段,区段名是可以修改的
- text段实际还是原程序的text段,rdata也是原程序的rdata段,data段也是原程序的data段, rsrc也是原程序的rsrc段
- 但是如果资源加密了,那么rsrc段只会保留原图标,原版权资源
vmp0段是vmp的虚拟机段,vmp1段是外壳段,也是保护后入口处
- vmp0 可以理解为功能代码实现的地方
- vmp1 是存放vmp寄存器等数据存储的地方
这是程序原始的区段
这是加密后的区段
仔细观察可以发现:
- vmp将 一些节表的大小(
text
,rdata
,data
)映射为0 - 唯一有用的数据是节表
vmp1
vmp1
才是程序的入口点,负责解析释放资源,运行虚拟机
0x2 反调试
- 由于vmp是一个虚拟机程序,本身就有很强的反调试性能,因此vmp本身并没有花太多精力在反调试上
- 使用一些常见的反调试插件就可以实现反调试了,这里推荐使用插件
SharpOD
- 接下来主要介绍一下 vmp 反调试的原理,作为了解即可
kernel32.IsDebuggerPresent
检测返回值
kernel32.CheckRemoteDebuggerPresent
检测返回值
ntdll.ZwQueryInformationProcess
传入不存在的ProcessInformationClass参数,0x1e,检测返回值
ntdll.ZwSetInformationThread
参数ThreadInformationClass传入0x11,对调试器隐藏线程
ntdll.ZwQuerySystemInformation 2次
内核调试器,枚举内核模块
kernel32.CloseHandle
传入无效handle,有调试器的情况下会抛出异常
0x3 虚拟机检测
- vmp直接使用 汇编指令
cpuid
来检测虚拟机 cpuid
可以返回当前是否是虚拟机,甚至是直接解析出虚拟机的字符串名称- 这里只给出一个简单试验demo以及对策,有需要的话后面会有专门的章节讲这一个知识点
实现代码
bool check_by_cpuid ()
{
bool IsUnderVM = false;
__asm {
xor eax, eax;
inc eax;
cpuid;
bt ecx, 0x1f;
jc UnderVM;
nop;
jmp NotUnderVM;
UnderVM:;
mov IsUnderVM, 0x1;
NotUnderVM:
nop;
}
return IsUnderVM;
}
解决方案:直接修改vmware
的虚拟机指令屏蔽掉 cpuid指令即可
cpuid.1.ecx="0---:----:----:----:----:----:----:----"
0x4 逻辑门
- vmp 中大量使用 逻辑门电路来实现 膨胀混淆
- 这里主要讲解一些基础的逻辑门知识点
0x4.1 基本法则
一下3个是逻辑运算的基本算法:
- 与 ->同真 则真
- 或 ->有真 则真
- 非 ->真假 互换
下面两个是常用的比较复杂的组合门电路
名称 | 符号 | 作用 |
---|---|---|
或非门 | nor | 先进行或 运算,然后对结果进行非 运算 |
与非门 | nand | 先进行与 运算,然后对结果进行非 运算 |
异或 | xor | 相异为1 |
同或 | xnor | 相同为1 |
0x4.2 代换公式
或非代换:
$ nor(a,b)=!(a|b)= !a\ \&\ !b $
$ not(a)= (!a)\ \&\ (!a)=nor(a,a)$
$ and(a,b) = a \& b = !(!a) \& !(!b) = nor(!a, !b) = nor(nor(a,a), nor(b,b)) $
$ or(a,b) = !(!(a|b)) = !(nor(a,b)) = nor(nor(a,b),nor(a,b)) $
与非代换:
nand(a,b)=!(a\&b)=!a+!b= (!a) | (!b)$
$not(a) = !a = (!a) | (!a) = nand(a,a)$
$and(a,b) = a \& b = !(!(a\&b)) = !(nand(a,b)) = nand(nand(a,b),nand(a,b))$
$or(a,b) = a | b = !(!a) | !(!b) = nand(!a, !b) = nand(nand(a,a), nand(b,b))$
0x5 关于资源
- vmp重新定义了PE资源的结构
- vm化了系统的资源函数,相关的资源操作都会进入vmp自 己的定义里
- 下面是资源相关的API
BeginUpdateResource
CopyImage
EndUpdateResource
EnumResLangProc
EnumResNameProc
EnumResourceLanguages
EnumResourceLanguagesEx
EnumResourceNames
EnumResourceNamesEx
EnumResourceTypes
EnumResourceTypesEx
EnumResTypeProc
FindResource
FindResourceEx
FreeResource
LoadImage
LoadResource
LockResource
SizeofResource
UpdateResource
FormatMessage
LoadAccelerators
LoadBitmap
LoadCursor
LoadIcon
LoadImage
LoadMenu
LoadString
0x6 eflag/rflag 标志位
- eflag/rflag 是处理器用于保存计算状态的寄存器
- eflag/rflag 在常规的汇编编程中属于底层操作,一般直接由专门的运算指令自动处理
- vmp处理后的程序会大量使用 eflag寄存器来达到混淆的目的
- 完整系统的学习eflag寄存器是逆向vmp的必要条件
0x6.1 标志位定义
这是intel关于 eflag的说明
这里给一张对应的中文说明(这就是传说中的那张图)
这是与运算相关的6个标志位
名称 | 位置 | 标志 | 意义 |
---|---|---|---|
CF | bit 0 | Carry flag | 进位标志,运算时产生进位或借位,CF=1 |
PF | bit 2 | Parity flag | 奇/偶校验标志,奇校验PF=1 |
AF | bit 4 | Auxiliary Carry flag | 辅助进位标志,如低4位往高4位有进位或借位,AF=1 |
ZF | bit 6 | Zero flag | 零标志,运算结果为0,ZF=1 |
SF | bit 7 | Sign flag | 符号标志,运算结果为负,SF=1 |
OF | bit 11 | Overflow flag | 溢出标志 |
DF: (bit 10)方向标志位
- 用来控制字符操作指令
- 置标志位,使字符操作指令 由起始地址自动递减
- 清除标志位,使字符操作指令由起始地址自动递增
- 注意此处DF的值是由程序员进行设定的 cld命令是将DF设置为0,std命令是将DF设置为1
TF:调试标志位
- 当TF=1时,处理器每次只执行一条指令,即单步执行
IF:中断允许标志位
- 它用来控制8086是否允许接收外部中断请求
- 若IF=1,8086能响应外部中断,反之则屏蔽外部中断;
系统标志位和IOPL:TF,IF,IOPL,NT,RF,VM,AC,VIF,VIP,ID
- 这些都是系统级标志位
- 应用层不常用
- 不需要关心这些
RFLAG为64位的标志寄存器
- 在64位中,eflag扩展到64为并且叫做
rflag
- rflag的高32位保留未使用,低32位 和eflag是一样的.
0x6.2 实例
- 这里列举一些 实例来展示 标志位是如何变化的
ZF:
mov eax,10
sub eax,10
;结果eax=0,ZF=1
SF:
mov eax,10
sub eax,11
;结果eax= -1,SF=1
CF:
mov eax,ffffffff
add eax,1
;结果eax=0,CF=1
系统判断CF标志时,把eax的初始值当作无符号数
OF:
mov eax,7fffffff
add eax,1
;结果eax=80000000,OF=1
系统判断CF标志时,把eax的初始值当作有符号数
0x6.3 标志位 控制指令
- 在这些状态标志中,只有
CF
标志可以直接修改,使用STC
、CLC
和CMC
指令 - 除此之外位指令(
BT
、BTS
、BTR
和BTC
)可以将指定的位复制到CF标志中 状态标志允许单个算术操作生成三种不同数据类型的结果:无符号整数、有符号 整数和BCD整数
- 如果算术运算的结果被视为无符号整数,则
CF
标志表示超出范围的条件(进位或借用) - 如果被视为有符号整数(两个补数),则
OF
标志 表示进位或借用 - 如果被视为
BCD
数字,AF
标志表示进位或借位。SF
标志表示 有符号整数的符号。ZF
标志表示有符号或无符号整数零 - 当对整数执行多精度算术时,CF标志与带进位(ADC)的加法和带借位(SBB)指令 的减法一起使用,以将进位或借用从一个计算传播到下一个计算
- 如果算术运算的结果被视为无符号整数,则
这里是详细的标志位控制指令
指令 | 影响标志位 | 作用 |
---|---|---|
STC | CF | 设置进位标志 |
CLC | CF | 清除进位标志 |
CMC | CF | 取反进位标志 |
CLD | DF | 清除方向标志 |
STD | DF | 设置方向标志 |
LAHF | EFLAG | 将eflag的低16位加载进AH寄存器中 |
SAHF | EFLAG | 将AH寄存器的值存储到eflag的低16位中 |
PUSHF/PUSHFD | EFLAG | 将标志寄存器压入栈中 |
POPF/POPFD | EFLAG | 将栈中的数据推出到标志寄存器中 |
STI | IF | 设置中断标志 |
CLI | IF | 清除中断标志 |
BT | CF=Bit(BitBase, BitOffset) | 设置cf为指定位的值。 ZF标志不受影响。 未定义OF、SF、AF和PF标 志。 |
BTS | CF=Bit(BitBase, BitOffset); Bit(BitBase, BitOffset)=1; | 设置cf为指定位的值,并且设置指定位的值为1。 ZF标志不受影响。 未定义OF、SF、AF 和PF标志。 |
BTR | CF=Bit(BitBase, BitOffset);<br/>Bit(BitBase, BitOffset)=0; | 设置cf为指定位的值,并且设置指定位的值为0。 ZF标志不受影响。 未定义OF、SF、AF 和PF标志。 |
BTC | CF=Bit(BitBase,BitOffset);<br/>Bit(BitBase,BitOffset)=NOT Bit(BitBase, BitOffset) | 设置cf为指定位的值,指定位的值取反。 ZF标志不受影响。 未定义OF、SF、AF 和PF标志。 |
BSF | BSF(Bit Scan Forward) | 位扫描找1, 低 -> 高,找到是 1 的位后, 把位置数给参数一并置 ZF=0,找不到1时, 置 ZF=1 |
BSR | BSR(Bit Scan Reverse) | 位扫描找1, 高 -> 低,找到是 1 的位后, 把位置数给参数一并置 ZF=0,找不到1时, 置 ZF=1 |
这里给一段 bsf/bsr
的案例
__asm
{
pushad
mov ebx, 000011100b
bsf eax, ebx; eax=2
bsr eax, ebx; eax=4
popad
}
2表示从低到高找,0,1,2位找到1
4表示从高到低找,最高位1在第4位(0,1,2,3,4)
0x6.4 标志位 判断指令
setxx
是一系列专用指令,用于获取标志位的状态- 工作原理: 根据EFLAGS寄存器中状态标志(CF、SF、OF、ZF和PF)的设置,将目标操作数设置为0或1
相关术语:
- 术语"above"和"below"与CF标志相关联,并引用两个无符号整数值之间的 关系
- 术语"greater"和"less"与SF和OF标志相关联,比较有符号操作数
- "小于","大于",缩写"l","g"
- "below","above"比较无符号操作数,"低","高",缩写"b","a"
交替指令:
- 许多SETCC指令操作码都有交替的助记符
- 例如,SETG(如果更大则设置字节)和SETNLE(如果不小于或相等则设置字节)具有相同的操作码,并对相同的条件进行测试:ZF等于0,SF等于OF
- 提供这些交替助记符以使代码更易理解
- 一些语言将逻辑语言表示为具有所有位集的整数。 这种表示可以通过为 SETCC指令选择逻辑相反的条件,然后递减来获得。
指令 | 作用 | 备注 |
---|---|---|
SETE/SETZ | Set byte if equal (ZF=1) | |
SETNE/SETNZ | Set byte if not equal (ZF=0). | |
SETA/SETNBE | Set byte if above (CF=0 and ZF=0). | |
SETAE/SETNB/SETNC | Set byte if above or equal (CF=0). | |
SETB/SETNAE/SETC | Set byte if below (CF=1). | |
SETBE/SETNA | Set byte if below or equal (CF=1 or ZF=1). | |
SETG/SETNLE | Set byte if greater (ZF=0 and SF=OF). | |
SETGE/SETNL | Set byte if greater or equal (SF=OF). | |
SETL/SETNGE | Set byte if less (SF≠ OF) | |
SETLE/SETNG | Set byte if less or equal (ZF=1 or SF≠ OF). | |
SETS | Set byte if sign (SF=1). | |
SETNS | Set byte if not sign (SF=0). | |
SETO | Set byte if overflow (OF=1) | |
SETNO | Set byte if not overflow (OF=0). | |
SETPE/SETP | Set byte if parity (PF=1). | |
SETPO/SETNP | Set byte if not parity (PF=0). | |
TEST | 逻辑比较 |
0x6.5 标志位 条件赋值
CMOVxx
是条件复制指令,本质上仍然是赋值指令- 区别在于:条件复制指令根据 标志位自动判断是否执行 赋值
- 这些指令可以将16位、32位或64位值从内存移动到通用寄存器或从一个通用 寄存器移动到另一个通用寄存器。 不支持8位寄存器操作数的条件移动。
- 条件复制指令一般成对出现。例如,
CMOVA
(如果高),CMOVNBE
(如果不低于等于)指令是操作码0F47H的备用助记符 - 有些处理器并不支持 这些指令,在程序中可以使用
cpuid
指令来判断
指令 | 作用 | 备注 |
---|---|---|
CMOVE/CMOVZ | Move if equal (ZF=1). | |
CMOVNE/CMOVNZ | Move if not equal (ZF=0). | |
CMOVA/CMOVNBE | Move if above (CF=0 and ZF=0). | |
CMOVAE/CMOVNB | Move if above or equal (CF=0). | |
CMOVB/CMOVNAE | Move if below (CF=1). | |
CMOVBE/CMOVNA | Move if below or equal (CF=1 or ZF=1) | |
CMOVG/CMOVNLE | Move if greater (ZF=0 and SF=OF) | |
CMOVGE/CMOVNL | Move if greater or equal (SF=OF). | |
CMOVL/CMOVNGE | Move if less (SF≠ OF). | |
CMOVLE/CMOVNG | Move if less or equal (ZF=1 or SF≠ OF). | |
CMOVC | Move if carry (CF=1). | |
CMOVNC | Move if not carry (CF=0). | |
CMOVO | Move if overflow (OF=1). | |
CMOVNO | Move if not overflow (OF=0). | |
CMOVS | Move if sign (SF=1). | |
CMOVNS | Move if not sign (SF=0). | |
CMOVP/CMOVPE | Move if parity even (PF=1). | |
CMOVNP/CMOVPO | Move if not parity (PF=0). |
0x6.6 标志位 条件跳转
jcc
条件跳转指令根据 标志位 判断是否进行跳转- 目标指令用相对偏移(相对于EIP寄存器中指令指针当前值的有符号偏移)指定跳转地址
由于状态标志的特定状态有时可以用两种方式解释,因此为一些操作码定义 了两个助记符
例如,JA(跳转如果高)指令和JNBE( 如果不低于或相等)指 令是操作码77H的备用助记符。
- CC指令不支持远跳(跳转到其他代码段)
指令 | 作用 | 备注 |
---|---|---|
JE/JZ | Jump short if equal (ZF=1). | |
JNE/JNZ | Jump short if not equal (ZF=0). | |
JA/JNBE | Jump short if above (CF=0 and ZF=0). | |
JAE/JNB | Jump short if above or equal (CF=0). | |
JB/JNAE | Jump short if below (CF=1). | |
JBE/JNA | Jump short if below or equal (CF=1 or ZF=1). | |
JG/JNLE | Jump short if greater (ZF=0 and SF=OF). | |
JGE/JNL | Jump short if greater or equal (SF=OF). | |
JL/JNGE | Jump short if less (SF≠ OF). | |
JLE/JNG | Jump short if less or equal (ZF=1 or SF≠ OF). | |
JC | Jump short if carry (CF=1). | |
JNC | Jump short if not carry (CF=0). | |
JO | Jump short if overflow (OF=1). | |
JNO | Jump short if not overflow (OF=0). | |
JS | Jump short if sign (SF=1). | |
JNS | Jump short if not sign (SF=0). | |
JPO/JNP | Jump short if parity odd (PF=0). | |
JPE/JP | Jump short if parity (PF=1). | |
LOOPZ/LOOPE Decrement count; | jump short if count ≠ 0 and ZF = 1. | |
LOOPNZ/LOOPNE Decrement count; | jump short if count ≠ 0 and ZF = 0. |
0x7 VMP 核心原理
- 取
vmp 1.09
作为研究对象
目前主流的软件虚拟机技术分为以下三类:
基于堆栈的虚拟化技术
实际情况中 栈的使用频率比较高
- 基于寄存器的虚拟化技术
3地址代码
代码例子: x = y op z
一种中间语言,编译器使用它来改进代码转换效率
每个三地址码指令,都可以被分解为一个四元组(4-tuple):(运算符,操作数1,操作数2,结果)
因为每个陈述都包含了三个变量,所以它被称为三地址码
关于加密/混淆:
- vmp利用大量 位运算,逻辑 运算指令 来解密数据,每个版本都有自己的特色,核心就是利用偏门的方法来模拟基本运算
关于指令序列:
- 在二进制层面观察所有指令集的定义,可以发现所有指令都可以分解为:
指令码 操作数
的格式 - vmp的虚拟指令 也遵守这个规则
0x7.1 实现逻辑
本程序源码如下
#include "stdafx.h"
#include <Windows.h>
void AddVMTest()
{
__asm mov eax,1
}
int AddCalc(int a,int b)
{
return a + b;
}
void ApiTest()
{
MessageBoxA(0,"Hello","VMPTest",0);
}
int main(int argc, char* argv[])
{
AddVMTest();
AddCalc(2,3);
ApiTest();
getchar();
return 0;
}
0x7.1.1 加密代码块
使用 VMP_1.09 的最强加密,尝试对如下汇编代码进行加密
mov eax,[ebp+0x8]
add eax,[ebp+0xc]
如下图选择加密块
选择最强加密
最强加密:所有 指令/handle/操作码/操作数/... 都需要解密才能使用,还有大量的混淆代码
最弱加密: 去除了栈混淆,解密算法
0x7.1.2 vm 入口代码
函数原始的完整汇编代码
00401070 | 55 | push ebp |
00401071 | 8BEC | mov ebp,esp |
00401073 | 83EC 40 | sub esp,40 |
00401076 | 53 | push ebx |
00401077 | 56 | push esi | esi:"minkernel\\ntdll\\ldrinit.c"
00401078 | 57 | push edi | edi:"LdrpInitializeProcess"
00401079 | 8D7D C0 | lea edi,dword ptr ss:[ebp-40] |
0040107C | B9 10000000 | mov ecx,10 |
00401081 | B8 CCCCCCCC | mov eax,CCCCCCCC |
00401086 | F3:AB | rep stosd |
00401088 | 8B45 08 | mov eax,dword ptr ss:[ebp+8] |;目标1
0040108B | 0345 0C | add eax,dword ptr ss:[ebp+C] |;目标2
0040108E | 5F | pop edi | edi:"LdrpInitializeProcess"
0040108F | 5E | pop esi | esi:"minkernel\\ntdll\\ldrinit.c"
00401090 | 5B | pop ebx |
00401091 | 8BE5 | mov esp,ebp |
00401093 | 5D | pop ebp |
00401094 | C3 | ret |
处理后的汇编代码
00401070 | 55 | push ebp |
00401071 | 8BEC | mov ebp,esp |
00401073 | 83EC 40 | sub esp,40 |
00401076 | 53 | push ebx |
00401077 | 56 | push esi | esi:EntryPoint
00401078 | 57 | push edi | edi:EntryPoint
00401079 | 8D7D C0 | lea edi,dword ptr ss:[ebp-40] | edi:EntryPoint
0040107C | B9 10000000 | mov ecx,10 | ecx:EntryPoint
00401081 | B8 CCCCCCCC | mov eax,CCCCCCCC |
00401086 | F3:AB | rep stosd |
00401088 | E9 E9B10200 | jmp vmtest.vmp.42C276 |;修改1
0040108D | 4A | dec edx |;修改2
0040108E | 5F | pop edi | edi:EntryPoint
0040108F | 5E | pop esi | esi:EntryPoint
00401090 | 5B | pop ebx |
00401091 | 8BE5 | mov esp,ebp |
00401093 | 5D | pop ebp |
00401094 | C3 | ret |
观察后发现:
原始汇编代码
00401088 | 8B45 08 | mov eax,dword ptr ss:[ebp+8] |;目标1
0040108B | 0345 0C | add eax,dword ptr ss:[ebp+C] |;目标2
被修改为
00401088 | E9 E9B10200 | jmp vmtest.vmp.42C276 |;修改1,这里跳转到 vmp1
0040108D | 4A | dec edx |;修改2
汇编代码 jmp vmtest.vmp.42C276
就是跳转到vmp1
流程中的关键跳转
接下来流程转移到如下代码
0042C276 | 68 69C14200 | push vmtest.vmp.42C169 |;保存 第一个解密密钥,key1
0042C27B | E9 67F1FFFF | jmp vmtest.vmp.42B3E7 |;这跳转到vmp0
0042C280 | 47 | inc edi | edi:EntryPoint
指令解析:
0042C276
处的push
是第一个解密密钥,用于解密下一个指令流0042C27B
最终跳转到vmp0
中执行相关代码/功能,这里就是虚拟机的和核心实现
0x7.1.3 vm 解析器/handler
虚拟机实现的关键代码如下
0042B3E7 | 9C | pushfd |;保存eflag寄存器
0042B3E8 | 60 | pushad |;保存通用寄存器
0042B3E9 | 68 00000000 | push 0 |;第二个解密密钥,key2
0042B3EE | 8B7424 28 | mov esi,dword ptr ss:[esp+28] |;这里获取到的就是之前 push的第一个密钥,key1
0042B3F2 | BF 00B04200 | mov edi,vmtest.vmp.42B000 |;指向vmp 上下文,俗称 vm_context
0042B3F7 | FC | cld |
0042B3F8 | 89F3 | mov ebx,esi |; ebx=key1
0042B3FA | 033424 | add esi,dword ptr ss:[esp] |; esi+=[esp] -> esi+=key2 ->esi=key1+key2
0042B3FD | AC | lodsb |
; 将[esi]指向的那一个字节放在 al中,al=byte [esi]=byte [key1+key2]=byte [42C169]=27
; 同时 执行 esi += 1
; 此时eax=cccc cc27
; 这里取出来的 27 其实就是 vmp虚拟指令的 操作码1
; 这开始 解析/解密 操作码
0042B3FE | 00D8 | add al,bl |
0042B400 | 04 E1 | add al,E1 |
0042B402 | 34 FE | xor al,FE |
0042B404 | C0C0 03 | rol al,3 |
0042B407 | 04 2B | add al,2B |
; 这里操作码2
; 此时eax=cccc cca7
0042B409 | 00C3 | add bl,al |;更新 bl所代表的key ebx=key1+al
; 这条指令运行前 eax=cccc cca7, ebx=0042 C169
; 注意是 bl+al,这种加法溢出后不会影响高位
; 最终 ebx=0042 C110
0042B40B | 0FB6C0 | movzx eax,al |
; 将al扩展成4字节,高位补0,eax=0000 00a7
; 这里获得的eax 才是真正的操作码
0042B40E | 8D0C85 AFB44200 | lea ecx,dword ptr ds:[eax*4+42B4AF]|;handler表
; 内存地址 42B4AF 处保存的是 地址表
; 这个表中的地址就是 所谓 的 handler
; handler: vmp指令实现的具体过程
; [eax*4+42B4AF] 的目的就是获取到 操作码(eax)对应的 指令实现地址
; 此时 ecx=0042B19B
0042B415 | FF21 | jmp dword ptr ds:[ecx] |;跳转到指令地址
0042B417 | D9EB | fldpi |
....
- 后面一大段膨胀混淆的代码就不贴出来了
- 核心是函数头部的
pushfd,pushad
,用于保存原始的 处理器状态 0042B3E9
处的push 0
是第二个解密密钥0042B3E9
开始的代码用于初始化vm 以及寄存器
handle
表的具体内容如下
;内存地址 数值
0042B4AF 0042C0A4 ¤ÀB.
0042B4B3 0042B8CB ˸B.
0042B4B7 0042B46C l´B.
......
0042B74B 0042B19B .±B. ;目标 handler
handler
中的每一项都是 vmp虚拟机的一个虚拟指令的实现
总结一下关于寄存器的使用
esi
指向的是 指令流,相当于VM 的 EIP
指令中一些操作数也会保存在这里
最强加密中 这里取出的数据都是加密的,需要配合 bl 解密
al
暂时保存加密中的 指令,通过[esi]
获取bl
用于解密操作码解密当前 指令 和准备下次解密用的 bl 都是在一起进行的
eax
保存的是解密后的指令码
handler
是一个地址数组,保存的vm虚拟出的指令实现(取决于eax
.有时候是一个函数的实现)edi
指向VM_context
,实际上就是vm虚拟的寄存器
0x7.1.4 handler 虚拟机
承接上面的代码,这里直接分析 handler
函数 0042B19B
的实现
代码如下
0042B19B | 0FB606 | movzx eax,byte ptr ds:[esi] |;取出下一个 操作指令/操作数
0042B19E | 00D8 | add al,bl |
0042B1A0 | 83C6 01 | add esi,1 |;操作指令下标+1
;这里一段代码负责解密al
0042B1A3 | FEC8 | dec al |
0042B1A5 | F6D0 | not al |
0042B1A7 | C0C8 04 | ror al,4 |
0042B1AA | FEC8 | dec al |
0042B1AC | 00C3 | add bl,al |
;更新下一次 的解密key
;此时eax=0000 0002
0042B1AE | 8F0487 | pop dword ptr ds:[edi+eax*4] |
;这里弹出的就是之前 push的 key2,代码 0042B3E9 处
;将数据 pop 到 edi[eax]
; edi 里 保存的是vm的虚拟寄存器,vm context
;==至此第一个 handle 执行结束 ==
0042B1B1 | E9 47020000 | jmp vmtest.vmp.42B3FD |
; 重新跳转到 虚拟机入口
; 位于 0x7.1.3 虚拟机入口
; 到这里 可以发现 地址 42B3FD 开始的代码就是 vmp 循环解析指令的过程
; 这里也可以 作为一个 虚拟指令的运算周期结束
...
0x7.1.5 vm_context
在代码0042B1AE
处将edi
指向的 vm context
dump出来
; 内存地址 数值 文本
0042B000 48E43409 .4äH
0042B004 41E10EF8 ø.áA
0042B008 345D7A8F .z]4 ;-->最终将数据保存到这里,为 0000 0000
0042B00C 1867F86D møg.
0042B010 B9E6D0A3 £Ðæ¹
0042B014 1F2B6620 f+.
0042B018 297F5C4A J\.)
0042B01C 9B703C43 C<p.
0042B020 4BB0A9C0 À©°K
0042B024 4ECD5359 YSÍN
0042B028 2F1828C6 Æ(./
0042B02C 9B6C8370 p.l.
0042B030 1AD14840 @HÑ.
0042B034 AF0D41A4 ¤A.¯
0042B038 7AA21721 !.¢z
0042B03C C3E2C8F6 öÈâÃ
0042B040 66C98C66 f.Éf
同时将此时栈中的数据dump出来
; 地址 数值
0019FE58 00000000
0019FE5C 0019FED0
0019FE60 00401520 vmtest.vmp.EntryPoint
0019FE64 0019FED0
0019FE68 0019FE7C
0019FE6C 00233000
0019FE70 02470DB0 &"ALLUSERSPROFILE=C:\\ProgramData"
0019FE74 00000000
0019FE78 CCCCCCCC
0019FE7C 00000206
0019FE80 0042C169 vmtest.vmp.0042C169 ;这里就是 一开始 push的 key1
;从这里开始就不用看了
0019FE84 0019FF30
0019FE88 00401520 vmtest.vmp.EntryPoint
0019FE8C 00233000
0019FE90 CCCCCCCC
0019FE94 CCCCCCCC
0019FE98 CCCCCCCC
0x7.1.5 指令流/循环解析
- 目前为止,已经对 vmp的原理和模式有一个认知了
- 根据上文所讲,
esi
中保存的是指令流标号 决定了使用哪个handler
- 现在对
esi
指向的指令流 进行一下总结
esi, 0042C169
完整的数据表
0042C169 27 C0 7E F7 D3 0C 24 AD 7A 73 C7 B0 13 5C 65 BE 'À~÷Ó.$.zsÇ°.\e¾
0042C179 B8 21 0C B5 2B 25 80 8E 4F 80 CF 4F 31 D9 4D BD ¸!.µ+%..O.ÏO1ÙM½
0042C189 E3 FC 99 84 EC 19 25 4E B7 64 9D 73 10 B0 1C 62 ãü..ì.%N·d.s.°.b
0042C199 27 3D 2C 89 AE 7C 65 8E 70 54 0A E0 8B 41 0E C5 '=,.®|e.pT.à.A.Å
0042C1A9 CA 24 8B 46 D8 CA D1 DA 6F 28 16 AC 1B 11 7E AF Ê$.FØÊÑÚo(.¬..~¯
0042C1B9 F3 E5 FB E0 87 39 EF 3C F4 03 EF FE 2A 99 02 69 óåûà.9ï<ô.ïþ*..i
0042C1C9 4D 91 A8 F7 BA E0 8A C4 76 4A 57 AC 9F 67 F2 EF M.¨÷ºà.ÄvJW¬.gòï
0042C1D9 A5 62 C0 FD 21 08 C5 2B ED 28 4E 24 46 E2 3B 7B ¥bÀý!.Å+í(N$Fâ;{
0042C1E9 5D 70 57 C1 1A 82 76 DF D7 D6 12 9F CE D5 66 86 ]pWÁ..vß×Ö..ÎÕf.
0042C1F9 B5 60 CC 64 CD E5 42 19 A9 3A 8B 2D 60 88 52 B3 µ`ÌdÍåB.©:.-`.R³
0042C209 F9 4C 3C F3 F8 1A BE 1D D5 1F B5 C2 6E ED 18 7C ùL<óø.¾.Õ.µÂní.|
0042C219 71 8F EF 17 3C 46 F1 59 1A 9D 1E 8A 03 A5 52 8B q.ï.<FñY.....¥R.
0042C229 61 FE 13 A6 B9 9F C0 8F A0 AE E1 22 4C 42 7B 7B aþ.¦¹.À. ®á"LB{{
0042C239 D1 19 13 39 8B 97 90 BC 01 87 C7 0D 6B D3 A9 9B Ñ..9...¼..Ç.kÓ©.
0042C249 C1 97 34 04 3E 68 6E 85 77 7E 8C B3 BF 29 91 75 Á.4.>hn.w~.³¿).u
0042C259 2E 68 66 96 FF 57 0B 4C 17 4D 80 89 F8 F1 B4 8A .hf.ÿW.L.M..øñ´.
0042C269 5C D2 FE 74 A0 B6 07 E0 7C 95 F5 2E 20 68 69 C1 \Òþt ¶.à|.õ. hiÁ
0042C279 42 00 E9 67 F1 FF FF 47 00 00 00 00 00 00 00 00 B.égñÿÿG........
- 首先执行
esi[0]=27
,解密后eax=a7
对应handler 0042B19B
,是一个pop指令 handler 0042B19B
中取esi[1]=c0
,解密后eax=2
,对应pop需要的操作数
handler 0042B19B
的pop指令运行完成后,重新jmp到0042B3FD
代码 0042B3FD
就是vmp内部的一个指令解析器- 接下去取出指令
esi[2]=7e
,解密后eax=a7
,因此这里会取得和第一步一样的handler 0042B19B
handler 0042B19B
中取esi[3]=f7
,解密后eax=???
,对应pop需要的操作数
handler 0042B19B
的pop指令运行完成后,重新jmp到0042B3FD
- 这里就是一个完整的循环了
0042B3FD
就是所谓的解析器vm_translation
0x7.1.6 context对照表
- 上文已经提到:vm会用虚拟出的
vm_context
来充当原始指令中的寄存器 - 要注意的是,这里的顺序不是固定的
- 下面这张表格是给后面分析程序时打草稿用的
内存地址 | 下标 | 作用 |
---|---|---|
0042B000 | 0 | |
0042B004 | 1 | |
0042B008 | 2 | |
0042B00C | 3 | |
0042B010 | 4 | |
0042B014 | 5 | |
0042B018 | 6 | |
0042B01C | 7 | |
0042B020 | 8 | |
0042B024 | 9 | |
0042B028 | 10 | |
0042B02C | 11 | |
0042B030 | 12 | |
0042B034 | 13 | |
0042B038 | 14 | |
0042B03C | 15 | |
0042B040 | 16 |
0X7.2 加法还原
- 使用ida/dbg 对 vmp 最强加密后的 exe文件进行 分析
- 需要还原出核心的逻辑结构
- 在ida中写出详细的注释
- 核心是对
vm_translation=0042B415
下断点,捕捉handler
0x7.2.1 push 3
当esi=0042C1BE
时,对应的handle
为0042B93A
,此handler
负责将第1个参数3
保存到栈中
0x7.2.2 push 2
当esi=0042C1BF
时,对应的handle
为0042B91A
,此handler
负责将第2个参数2
保存到栈中
0x7.2.3 执行 add
当eax=00000076
时可以发现原来的两个参数已经进入到当前栈中了,此时esi=0042C1C1
继续跟进到当前的handler
地址是0042C099
可以发现当前handle
就是负责处理 原始的加法运算
运行完成之后可以发现 运算的结果已经保存到栈中了
接下去几个handle
肯定是将栈中的5
保存到vm_context
0x7.3 msgbox还原
- 我们这里直接使用
x64
的日志功能来确认 具体实现的handler
通过分析已经得知地址0042B2D8
就是当前程序的 handler
分发器
直接在这个地址下如下的条件断点
其中 日志语法为eax={eax};esi={esi};handler={[eax*4+0x42B63B]}
,可以同时捕获对应的handler
地址
设置完成后直接运行程序,当出现 弹窗时 从日志窗口中可以取得如下日志
eax=56;esi=42C081;handler=42BAFB
eax=56;esi=42C083;handler=42BAFB
eax=56;esi=42C085;handler=42BAFB
eax=56;esi=42C087;handler=42BAFB
eax=56;esi=42C089;handler=42BAFB
eax=56;esi=42C08B;handler=42BAFB
eax=56;esi=42C08D;handler=42BAFB
eax=56;esi=42C08F;handler=42BAFB
eax=56;esi=42C091;handler=42BAFB
eax=56;esi=42C093;handler=42BAFB
eax=6A;esi=42C095;handler=42B404
eax=8F;esi=42C097;handler=42B4D3
eax=7F;esi=42C09C;handler=42B4E7
eax=89;esi=42C09E;handler=42C042
eax=7F;esi=42C09F;handler=42B4E7
eax=8F;esi=42C0A1;handler=42B4D3
eax=89;esi=42C0A6;handler=42C042
eax=6A;esi=42C0A7;handler=42B404
eax=7F;esi=42C0A9;handler=42B4E7
eax=8F;esi=42C0AB;handler=42B4D3
eax=89;esi=42C0B0;handler=42C042
eax=8F;esi=42C0B1;handler=42B4D3
eax=7F;esi=42C0B6;handler=42B4E7
eax=70;esi=42C0B8;handler=42C042
eax=84;esi=42C0B9;handler=42B525
eax=7F;esi=42C0BA;handler=42B4E7
eax=7F;esi=42C0BC;handler=42B4E7
eax=7F;esi=42C0BE;handler=42B4E7
eax=7F;esi=42C0C0;handler=42B4E7
eax=7F;esi=42C0C2;handler=42B4E7
eax=7F;esi=42C0C4;handler=42B4E7
eax=7F;esi=42C0C6;handler=42B4E7
eax=7F;esi=42C0C8;handler=42B4E7
eax=7F;esi=42C0CA;handler=42B4E7
eax=7F;esi=42C0CC;handler=42B4E7
eax=6E;esi=42C0CE;handler=42B37B
因此esi=42C0CE
时对应的handler=42B37B
就是messagebox的实现
观察右下角的堆栈,保存着原始数据
0x7.4 api调用总结
这里以 messagebox作为例子:
- 在执行你的API调用之前,跟普通PE一样,输入表是Ready的,也就是MessageBox地址是准备好的
- 从字节码中拿出MessageBox在输入表中位置,然后经过一次重定位(VMProtect自己的重定位方式),获取到MessageBox的真实地址
- 真实堆栈压入一个进入VMP的函数地址
- 真实堆栈压入MessageBox地址
- popad popfd还原前面进入虚拟机的现场
- 退出虚拟机,ret方式 跳入MessageBox
- MessageBox执行完毕之后,MessageBox内部的Ret 直接就返回到了之前准备好的进入虚拟机函数的入口地址
- 进入虚拟机,继续执行虚拟代码
0x8 vmp2
- 本章主要讲解一些插件的使用,在分析vmp1.x/2.x的时候有很大的帮助
- 这里以
vmp 2.3.18
作为典型 - 仍然是使用
esi
作为指令流的ip寄存器
以下几点是高版本vmp改动的地方:
- 增加大量膨胀,混淆指令
- 栈存放的方式改为 x80386
- 基本原理不变,整体结构发生改变
- 在重定位上做手脚
0x8.1 逻辑总结
这里先对vmp 2.3.18
相对于v1
的改变进行总结:
- 首先是
esi
指向的指令流,前后需要解密 3次 - 其次是
eax(al)
代表的指令/操作码,前后需要解密4次 ebx(bl)
代表的decode key
参与 操作码eax(al)
,以及操作数esi[?]
的解密过程- 解密后的操作码
eax(al)
同时也参与ebx(bl) decode key
的下一轮解密过程 handler
中的地址数据是被加密的,加密的handler
临时保存在ecx
中,解密后通过push ;return
指令来完成跳转esi[?]
中保存的 操作数也是被加密的edi
指向vmp虚拟的寄存器ebp
被vmp当作虚拟机的esp
来使用- 即使原始exe没有重定位信息(随机基址),vmp也会分配
vm_relloc
不过值为0
关键流程如下:
- 保存key1,key2
- 利用key1 解密出 deCodeKey
- 用deCodeKey解密操作码
- 用解密完成的操作码更新deCode
- 获取handler
- 解密handler
- push ret跳转到handler
- 取我们的操作数(加密的)
- 用deCodeKey解密操作数
- 用解密完成的操作数更新deCode
- 真正执行我们的handler代码
- 重新跳转到 vm 解析器
这张图介绍了 vmp的栈结构
0x8.2 新增混淆
0x8.2.1 临近混淆
先看下面一段代码
0042DAC8 | E9 00000000 | jmp $+5 |
0042DACD | E9 00000000 | jmp $+5 |
0042DAD2 | 68 B76A7692 | push 92766AB7 |
0042DAD7 | E8 00000000 | call $+5 | call $0
0042DADC | C70424 E5852550 | mov dword ptr ss:[esp],502585E5 |
- 汇编代码中会有
$+num
这类的地址代码 指的是根据当前指令的地址,动态偏移
num
得到新地址可以理解为 address=eip+num
- 但是
jmp/call
这类指令的长度本身就只有5字节 - 因此
jmp $+5
指的是跳转到下一条指令 - 同样
call $+5
指的是call 下一条指令的地址
0x8.2.2 栈混淆
.vmp0:0042DAE3 pushf ; 这里开始就是混淆代码
.vmp0:0042DAE4 mov byte ptr [esp+58h+var_58], dh
.vmp0:0042DAE7 pushf
.vmp0:0042DAE8 mov [esp+5Ch+var_58], cx
.vmp0:0042DAED lea esp, [esp+8] ; 修复之前混淆的 栈
新版vmp特别喜欢用这类代码来删除栈
lea esp, [esp+number]
0x8.2.3 esp混淆
.vmp0:0042B30B mov [esp+28], ecx
.vmp0:0042B30F pusha ; 这里实际上执行的是
.vmp0:0042B30F ; sub esp,4*8
.vmp0:0042B30F ; 本意并不一定是保存寄存器
.vmp0:0042B310 lea esp, [esp+60] ; 这条指令和上一个 pusha 是一堆混淆指令
.vmp0:0042B310 ; 用于混淆esp
.vmp0:0042B310 ; 相当于 lea esp,[esp+28]
0x8.3 fkvmp
- 这是一个基于 od的插件
- 针对目标:VMProtect 1.x 2.x (大概到 VMProtect 2.09)
- 使用方法简单,在虚拟机入口处(push/call 或 jmp)右键 FKVMP -> start 即可
- 可以所有Handler已经为指令都已经识别出来,初始化压栈的寄存器顺序也会打印出来。
- 本工具(可能是第1次完成)字节码的还原的实现(其实可以认为是虚拟机指令的反汇编)使对 VMProtect 的人工分析成为可能
- 缺点是只支持单一的基本块,每次分析到 SetEIP 指令(实际就是跳转指令)就会停止,需要人工操作,去分析下一基本块。
0x8.4 zeus
- 这是一个全自动脱壳插件
- 可以理解为 fkvmp的一个国内分支
- 这是一个基于 od的插件
- 适用于 vmp2
- 脱壳后,需用UIF跟REC修复,才能实现跨平台
- 推荐使用
首先分析汇编代码,找到 vm的入口点,比如下面这段代码中地址004010BA
就是vm的入口点
.text:004010A0 push ebp
.text:004010A1 mov ebp, esp
.text:004010A3 sub esp, 40h
.text:004010A6 push ebx
.text:004010A7 push esi
.text:004010A8 push edi
.text:004010A9 lea edi, [ebp+var_40]
.text:004010AC mov ecx, 10h
.text:004010B1 mov eax, 0CCCCCCCCh
.text:004010B6 rep stosd
.text:004010B8 mov esi, esp
.text:004010BA jmp vm_entry ; 进入vm虚拟机
接下来在od中选中这行代码,然后右击选择分析
接下来打开od的日志窗口就可以看到相关信息了
0x8.5 VMP分析插件v1.4
- 针对目标:VMProtect 1.x 2.x
- 类型:Ollydbg / Immunity Debugger 插件
- 功能:Handler 识别、虚拟字节码调试、表达式化简
可以认为是 FKVMP 的超级加强版,在同类工具之中功能最为强度的工具。
首先该工具支持了跨基本块的分析,可以一次性将全部虚拟机字节码提取出来。除此外,还有许多亮点功能:
- 虚拟指令级别的调试。可以像调试汇编一样调试虚拟机指令,可以单步运行一条虚拟指令,并查看虚拟寄存器、虚拟栈的信息。
- 表达式转化及化简。本功能会在虚拟指令级别进行数据流和控制流的分析,进行字节码的收缩。 VMProtect 是栈机结构,同时有 NOR 逻辑的混淆膨胀,字节码的收缩还原一直是研究的重点,本工具可以完成字节码收缩过程,输出收缩后的表达式。唯一不同是没有转化成原始的 x86 代码。但在分析上,已经可以提供极为有力的参考。
- 支持字节码的 Patch。由于加密的存在,字节码 Patch 一直是十分痛苦的过程,本工具可以像 Patch 普通指令一样 Patch 虚拟指令。
- 支持自定义模板。模板包括 Handler 识别模板和表达式化简模板。该工具提供了模板文件及模板修改工具。理论上,可以通过对模板文件的修改使及兼容所有版本的 VMProtect 1.x 2.x。( 3.x 因为虚拟机结构变化不能支持)
- 下图是工具使用截图。该工具以插件的形式,实现了一个与原生OD非常相似的VMP调试界面,寄存器、栈的内容可以实时查看
- 使用方法简单,在虚拟机入口处(push/call 或 jmp)右键 VMP分析插件 -> 分析虚拟程序 完成分析(f9)。然后可以打开插件的虚拟指令窗口、调试窗口查看字节码并进行调试分析。
这个插件的核心是将vmp相关的膨胀混淆指令按照语义还原成简单的类x86指令,可能会出现很多不认识的指令,这时候打开编辑虚拟指令信息
就可以查看详细的注释
0x8.6 编译器还原
核心原理:
将
VMP分析插件v1.4
或者xxdisasm
生成的虚拟汇编还原成c语言
虚拟寄存器之类的 可以还原成局部变量
- 利用编译器 编译/优化
- 最后利用ida打开处理后的二进制文件即可
0x9 vmp sdk 使用
- vmp 安装完成后会带有
sdk
开发包 - 使用这个sdk可以完成一些比较高级的加密功能
- 这里主要介绍 如何在程序开发中引入vmp的sdk进行保护
VMProtectSDK.h
- 注意
ddk/sys
是驱动文件 - 这里以加密字符串的功能作为例子
- 参考链接
0x9.1 sdk 结构
安装目录如下
+---Include
| +---ASM
| | VMProtectSDK.inc
| |
| +---C
| | VMProtectDDK.h
| | VMProtectSDK.h //c语言程序主要使用这个 头文件开发
| |
| \---Pascal
| VMProtectSDK.pas
|
\---Lib //运行时需要使用的库文件
| VMProtectDDK32.sys
| VMProtectDDK64.sys
| VMProtectSDK32.dll
| VMProtectSDK64.dll
|
+---COFF //编译时需要连接的lib文件
| VMProtectDDK32.lib
| VMProtectDDK64.lib
| VMProtectSDK32.lib
| VMProtectSDK64.lib
|
\---OMF
VMProtectSDK32.lib
下面这个是一份VMProtectSDK.h
的demo
#pragma once
#ifdef _WIN64
#pragma comment(lib, "VMProtectSDK64.lib")
#else
#pragma comment(lib, "VMProtectSDK32.lib")
#endif
#ifdef __cplusplus
extern "C" {
#endif
// protection
__declspec(dllimport) void __stdcall VMProtectBegin(const char *);
__declspec(dllimport) void __stdcall VMProtectBeginVirtualization(const char *);
__declspec(dllimport) void __stdcall VMProtectBeginMutation(const char *);
__declspec(dllimport) void __stdcall VMProtectBeginUltra(const char *);
__declspec(dllimport) void __stdcall VMProtectBeginVirtualizationLockByKey(const char *);
__declspec(dllimport) void __stdcall VMProtectBeginUltraLockByKey(const char *);
__declspec(dllimport) void __stdcall VMProtectEnd(void);
// utils
__declspec(dllimport) BOOL __stdcall VMProtectIsDebuggerPresent(BOOL);
__declspec(dllimport) BOOL __stdcall VMProtectIsVirtualMachinePresent(void);
__declspec(dllimport) BOOL __stdcall VMProtectIsValidImageCRC(void);
__declspec(dllimport) char * __stdcall VMProtectDecryptStringA(const char *value);
__declspec(dllimport) wchar_t * __stdcall VMProtectDecryptStringW(const wchar_t *value);
__declspec(dllimport) BOOL __stdcall VMProtectFreeString(void *value);
// licensing
enum VMProtectSerialStateFlags
{
SERIAL_STATE_FLAG_CORRUPTED = 0x00000001,
SERIAL_STATE_FLAG_INVALID = 0x00000002,
SERIAL_STATE_FLAG_BLACKLISTED = 0x00000004,
SERIAL_STATE_FLAG_DATE_EXPIRED = 0x00000008,
SERIAL_STATE_FLAG_RUNNING_TIME_OVER = 0x00000010,
SERIAL_STATE_FLAG_BAD_HWID = 0x00000020,
SERIAL_STATE_FLAG_MAX_BUILD_EXPIRED = 0x00000040,
};
#pragma pack(push, 1)
typedef struct
{
WORD wYear;
BYTE bMonth;
BYTE bDay;
} VMProtectDate;
typedef struct
{
INT nState; // VMProtectSerialStateFlags
wchar_t wUserName[256]; // user name
wchar_t wEMail[256]; // email
VMProtectDate dtExpire; // date of serial number expiration
VMProtectDate dtMaxBuild; // max date of build, that will accept this key
INT bRunningTime; // running time in minutes
BYTE nUserDataLength; // length of user data in bUserData
BYTE bUserData[255]; // up to 255 bytes of user data
} VMProtectSerialNumberData;
#pragma pack(pop)
__declspec(dllimport) INT __stdcall VMProtectSetSerialNumber(const char * SerialNumber);
__declspec(dllimport) INT __stdcall VMProtectGetSerialNumberState();
__declspec(dllimport) BOOL __stdcall VMProtectGetSerialNumberData(VMProtectSerialNumberData *pData, UINT nSize);
__declspec(dllimport) INT __stdcall VMProtectGetCurrentHWID(char * HWID, UINT nSize);
// activation
enum VMProtectActivationFlags
{
ACTIVATION_OK = 0,
ACTIVATION_SMALL_BUFFER,
ACTIVATION_NO_CONNECTION,
ACTIVATION_BAD_REPLY,
ACTIVATION_BANNED,
ACTIVATION_CORRUPTED,
ACTIVATION_BAD_CODE,
ACTIVATION_ALREADY_USED,
ACTIVATION_SERIAL_UNKNOWN,
ACTIVATION_EXPIRED
};
__declspec(dllimport) INT __stdcall VMProtectActivateLicense(const char *code, char *serial, int size);
__declspec(dllimport) INT __stdcall VMProtectDeactivateLicense(const char *serial);
__declspec(dllimport) INT __stdcall VMProtectGetOfflineActivationString(const char *code, char *buf, int size);
__declspec(dllimport) INT __stdcall VMProtectGetOfflineDeactivationString(const char *serial, char *buf, int size);
#ifdef __cplusplus
}
#endif
0x9.2 字符串加密解析
- 下面这个 代码demo 用于 测试 vmp的字符串加密功能的实现
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include "VMProtectSDK.h"
int main(int argn,char ** args,char ** env)
{
//获取解密后的字符串
char * str_decode=VMProtectDecryptStringA("被加密字符");
MessageBox(0,str_decode,"标题",MB_OK);
//释放内存
VMProtectFreeString(str_decode);
return 0;
}
这是原始代码
.text:00401077 push ebp
.text:00401078 mov ebp, esp
.text:0040107A sub esp, 10h
.text:00401080 mov [ebp+lpMem], 0
.text:00401087 mov [ebp+var_8], 0
.text:0040108E mov [ebp+var_C], esp
.text:00401091 push offset unk_47B2F8 ;对应原始的参数
.text:00401096 call VMProtectDecryptStringA ;vm加密函数
.text:0040109B cmp [ebp+var_C], esp
.text:0040109E jz short loc_4010B7
这是被vmp处理后的代码
.text:00401077 push ebp
.text:00401078 mov ebp, esp
.text:0040107A sub esp, 10h
.text:00401080 mov [ebp+var_4], 0
.text:00401087 mov [ebp+var_8], 0
.text:0040108E mov [ebp+var_C], esp
.text:00401091 jmp loc_4B792C ;修改1,虚拟机入口点,可以用 vmp分析插件直接对这里进行分析
.text:00401096 call sub_45A4E0 ;修改2
.text:0040109B cmp [ebp-0Ch], esp
.text:0040109E jz short loc_4010B7
- 这里虚拟机的核心是利用 大量 逻辑运算进行解密
- 没必要深究 逻辑运算是如何实现的,每个版本都不一样
- 要注意的是返回值肯定是解密后的字符串
0x? vmp3.4 iat调用
- vmp 会对所有 iat 调用进行混淆
- 这里要注意的是无论怎么混淆 最终都会得出 目标函数的 函数地址
首先来看一下原始程序中 关于iat调用的代码:
- 直接获取函数
LoadStringA
的函数地址放在edi
中 - 随后通过
call edi
的方式来调用
接下来我们看一下vmp处理后的代码:
- 原来的iat调用变成了一个
ppo;call
调用 - 这个call会调转到
vmp0
中 - 直接将
496b3e
处的实现代码提取出来
00496B3E 90 nop
00496B3F E9 EE571000 jmp 0059C332
0059C332 873C24 xchg dword ptr ss:[esp],edi
0059C335 E9 1FBFF4FF jmp 004E8259
004E8259 57 push edi
004E825A 66:0F40F9 cmovo di,cx ;无关指令
004E825E 50 push eax
004E825F 9F lahf ;无关指令
004E8260 B8 D2144000 mov eax,0x4014D2
004E8265 E9 A3751B00 jmp 0069F80D
0069F80D 8B80 6A9E1800 mov eax,dword ptr ds:[eax+0x189E6A]
0069F813 0F40FB cmovo edi,ebx;无关指令
0069F816 66:8BFF mov di,di;无关指令
0069F819 8D80 543D9053 lea eax,dword ptr ds:[eax+0x53903D54]
0069F81F 8BF8 mov edi,eax
0069F821 66:0FB6C7 movzx ax,bh;无关指令
0069F825 86C4 xchg ah,al;无关指令
0069F827 58 pop eax
0069F828 E9 4422D6FF jmp 00401A71
00401A71 C3 retn
对以上代码进行简化,去除无关指令获得以下代码
0059C332 873C24 xchg dword ptr ss:[esp],edi ;将虚拟机的 返回地址应用到真机中
004E8259 57 push edi
004E825E 50 push eax
004E8260 B8 D2144000 mov eax,0x4014D2
0069F80D 8B80 6A9E1800 mov eax,dword ptr ds:[eax+0x189E6A]
0069F819 8D80 543D9053 lea eax,dword ptr ds:[eax+0x53903D54]
0069F81F 8BF8 mov edi,eax ;计算出 目标api的地址,保存到寄存器 edi中
0069F827 58 pop eax
00401A71 C3 retn ;跳转到 虚拟机指定的 返回地址与 0x0059C332 对应
- 首先 api的调用会被vmp转换成
pop a;call b
的形式 a
就是vmp虚拟机中的返回地址,利用代码0x0059C332
将返回地址应用到真机中- 代码
0x00401A71
负责跳转到a
代表返回地址
3 comments
好耶
esp混淆这节里面注释中的“相当于 mov esp,[esp+28]”,这里mov是否应该是lea?感觉好像不太对
多谢,已改正