还记得很早以前看VMP一撮JMP搞晕了自己,那个时候太浮躁,现在静下心来看指令,比较容易有头绪,这个就算是个笔记,一步步的记录下自己分析的过程,本来想找个博客写,但懒得注册了,做事情莫着急,慢慢来~~~~~~~~~~~~~~
待虚拟的程序:CrackMe_ JE.exe
VMP版本:VMProtect_Ultimate2.08
虚拟后的程序:CrackMe_ JE.vmp.exe
虚拟选择:仅虚拟 其他保护都不选
分析测试代码: OD插件辅助分析测试
虚拟前的指令代码:
.text:0040159E cmp eax, 0D2360h
.text:004015A3 pop edi
.text:004015A4 jz short loc_4015BA
.text:004015A6 push 0 ; uType
.text:004015A8 push offset Caption ; "成功"
.text:004015AD push offset Text ; "注册成功"
.text:004015B2 push 0 ; hWnd
.text:004015B4 call ds:MessageBoxA
.text:004015BA
.text:004015BA loc_4015BA: ; CODE XREF: sub_401560+44j
.text:004015BA add esp, 100h
.text:004015C0 retn
虚拟后变成类似这样
.vmp0:00406F08 push 61D36029h
.vmp0:00406F0D pushf ; Push Flags Register onto the Stack
.vmp0:00406F0E mov dword ptr [esp+4], 6DA8003Fh
.vmp0:00406F16 jmp loc_406EEC ; Jump
这里的乱序是代码物理存储位置的顺序从新拼接
本来连续的几个指令被打散,然后用JMP CALL RETN连接
目前我只看了JMP和CALL
JMP 毫无疑问 就是无条件跳转去拼接代码
CALL 其实也是无条件跳转 只是多了ESP-4这个步骤 等价于PUSH JMP
所以我的思路是这样
分析过程中 如果遇到CALL 指令
首先记录CALL的目标地址,然后转换为对应的PUSH JMP 指令 再到CALL的目标地址继续解析
最终任务为替换这中间的所有CALL 为对应的PUSH JMP 指令 (分析发现PUSH内容是没有意义的,关键是要完成ESP -4)
这个为我想到解决代码乱序问题的一个思路
如果遇到JMP 指令 就直接分析目的地址的指令就可以了
如果遇到JMC指令(条件跳转)就需要用以上思路递归分析目的地址
程序中我自己加了个区段,用来存放乱序规整后的代码,我用OD插件完成这个工作,这样可以测试整理后的代码是不是对的,也可以利用OD的反汇编引擎确定指令长度等信息。
这只是第一步的第一步 后面还有好多要做 慢慢来
补充修正1:
需要采用递归下降反汇编条件跳转指令
如果碰到条件跳转,记录目标地址,继续往下反汇编直到遇到RETN指令
然后再继续反汇编条件跳转的目标地址,如此递归
目前发现VM开始代码部分JCC指令走的分枝虽然有不同,但是代码是等价的,
也就是无论走哪个分枝作用都是相同的,最后的RETN也起到拼接的作用 修正补充2:
retn 指令代表每个HANDLE的结束 不是拼接作用
前面说了代码乱序 现在说说代码变形
根据自己手工识别发现下面的规律
变形规律一.
1.有多个指令对特定寄存器操作
2.在这些指令中该寄存器是目的操作数
3.该寄存器在特定范围内没有为其他指令作为源操作数
如果同时满足 1 2 3条件
则该范围内只保留对特定寄存器操作的最后一个指令
,其他对该寄存器的操作指令会被最后一个指令覆盖掉,
所以其他指令都无效,都NOP掉
(注:一般最后一条指令是R32寄存器,
其他指令中包含该寄存器的32 16 8位版本都无效,都需要NOP) 变形规律二.
栈平衡
一般会有一条类似的指令平衡栈
lea esp, [esp+XXh]
如果栈平衡,且栈的操作不影响后续指令,
那么平衡的栈指令都可以NOP掉
变形规律三.
操作标志寄存器的指令 EFL 一般都无效
在分析的HANDLE里面 发现跳转和不跳转指令都是等价的
所以标志寄存器的操作指令就没有意义了
下面结合一个具体的HANDLE说明下上述规律
(注:HANDL是解释VMPCODE的函数)
说明下 这个HANDLE是经过乱序整理后的,保存在我新加的节DEVMP上面
(根据IDA的栈指针功能 很快能定位出平衡栈的那段代码) 栈指针
DeVmp.tx:00449F08 038 sal dl, cl ; EDX 操作 NOP
DeVmp.tx:00449F0A 038 cmc ; NOP (符合变形规律三)
DeVmp.tx:00449F0B 038 mov eax, [ebp+0]
DeVmp.tx:00449F0E 038 add dh, bl ; EDX 操作 NOP
DeVmp.tx:00449F10 038 bsf dx, di ; EDX 操作 NOP
DeVmp.tx:00449F14 038 cmc ; NOP (符合变形规律三)
DeVmp.tx:00449F15 038 mov edx, [ebp+4] ; 以上的EDX操作全部取消 (符合变形规律一)
DeVmp.tx:00449F18 038 cmc ; NOP (符合变形规律三)
DeVmp.tx:00449F19 038 clc ; NOP (符合变形规律三)
DeVmp.tx:00449F1A 038 cmp ch, 65h ; NOP (符合变形规律三)
DeVmp.tx:00449F1D 038 not eax
DeVmp.tx:00449F1F 038 nop
DeVmp.tx:00449F20 038 nop
DeVmp.tx:00449F21 038 nop
DeVmp.tx:00449F22 038 nop
DeVmp.tx:00449F23 038 nop
DeVmp.tx:00449F24 038 push ecx ; 栈操作 NOP
DeVmp.tx:00449F25 03C cmc ; NOP (符合变形规律三)
DeVmp.tx:00449F26 03C cmp dh, 0D3h ; NOP (符合变形规律三)
DeVmp.tx:00449F29 03C not edx
DeVmp.tx:00449F2B 03C push 0 ; 栈操作 NOP
DeVmp.tx:00449F30 040 and eax, edx
DeVmp.tx:00449F32 040 nop
DeVmp.tx:00449F33 040 nop
DeVmp.tx:00449F34 040 nop
DeVmp.tx:00449F35 040 nop
DeVmp.tx:00449F36 040 nop
DeVmp.tx:00449F37 040 pusha ; 栈操作 NOP
DeVmp.tx:00449F38 060 mov word ptr [esp+0Ch], 5A02h ; 栈操作 NOP
DeVmp.tx:00449F3F 060 mov [ebp+4], eax
DeVmp.tx:00449F42 060 mov [esp+4], ah ; 栈操作 NOP
DeVmp.tx:00449F46 060 pushf ; 栈操作 NOP
DeVmp.tx:00449F47 064 push 0 ; 栈操作 NOP
DeVmp.tx:00449F4C 068 mov byte ptr [esp], 77h ; 栈操作 NOP
DeVmp.tx:00449F50 068 pushf ; 栈操作 NOP
DeVmp.tx:00449F51 06C pop dword ptr [esp+2Ch] ; 栈操作 NOP
DeVmp.tx:00449F55 068 push 0
DeVmp.tx:00449F5A 06C push dword ptr [esp+30h] ; 以下两条指令等价于
DeVmp.tx:00449F5A ; pushf
DeVmp.tx:00449F5A ; pop [ebp]
DeVmp.tx:00449F5E 070 pop dword ptr [ebp+0]
DeVmp.tx:00449F61 06C pushf ; 栈操作 NOP
DeVmp.tx:00449F62 070 push esp ; 栈操作 NOP
DeVmp.tx:00449F63 074 lea esp, [esp+3Ch] ; 以上指令栈平衡 对栈的内存赋值都无效 (符合变形规律二)
;(刚开始栈大小为 038 执行完这个指令后 栈也是 038 所以栈平衡了)
DeVmp.tx:00449F67 038 nop
DeVmp.tx:00449F68 038 nop
DeVmp.tx:00449F69 038 nop
DeVmp.tx:00449F6A 038 nop
DeVmp.tx:00449F6B 038 nop
DeVmp.tx:00449F6C 038 setns al ; NOP 00449F77覆盖AL
DeVmp.tx:00449F6F 038 nop
DeVmp.tx:00449F70 038 nop
DeVmp.tx:00449F71 038 nop
DeVmp.tx:00449F72 038 nop
DeVmp.tx:00449F73 038 nop
DeVmp.tx:00449F74 038 xadd al, dl ; NOP 00449F77覆盖AL
DeVmp.tx:00449F77 038 mov al, [esi-1]
DeVmp.tx:00449F7A 038 cmc ; NOP
DeVmp.tx:00449F7B 038 sub dx, 4E62h ; EDX操作无效 NOP
DeVmp.tx:00449F80 038 shrd edx, eax, 0Eh ; EDX操作无效 NOP
DeVmp.tx:00449F84 038 cmp di, bp ; NOP
DeVmp.tx:00449F87 038 sub al, bl
DeVmp.tx:00449F89 038 clc ; NOP
DeVmp.tx:00449F8A 038 btr dx, dx ; NOP
DeVmp.tx:00449F8E 038 add al, 9Dh
DeVmp.tx:00449F90 038 mov dx, 0F191h ; EDX操作无效 NOP
DeVmp.tx:00449F94 038 not al
DeVmp.tx:00449F96 038 cmc ; NOP
DeVmp.tx:00449F97 038 rol al, 3
DeVmp.tx:00449F9A 038 sar dl, cl ; EDX操作无效 NOP
DeVmp.tx:00449F9C 038 sub bl, al
DeVmp.tx:00449F9E 038 setl dh ; EDX操作无效 NOP
DeVmp.tx:00449FA1 038 mov dl, bl ; EDX操作无效 NOP
DeVmp.tx:00449FA3 038 dec dx ; EDX操作无效 NOP
DeVmp.tx:00449FA6 038 dec esi
DeVmp.tx:00449FA7 038 btc dx, ax ; EDX操作无效 NOP
DeVmp.tx:00449FAB 038 sub dl, dh ; EDX操作无效 NOP
DeVmp.tx:00449FAD 038 sub dl, 6Eh ; EDX操作无效 NOP
DeVmp.tx:00449FB0 038 ror dl, 1 ; EDX操作无效 NOP
DeVmp.tx:00449FB2 038 movzx eax, al
DeVmp.tx:00449FB5 038 lea edx, ds:158B988Ch ; EDX操作无效 NOP
DeVmp.tx:00449FBC 038 bts dx, si ; EDX操作无效 NOP
DeVmp.tx:00449FC0 038 cmp si, 0BD0Bh ; NOP
DeVmp.tx:00449FC5 038 push 0 ; NOP
DeVmp.tx:00449FCA 03C mov edx, ds:dword_4053EF[eax*4] ; 以上对EDX的操作无效 (符合变形规律一)
DeVmp.tx:00449FD1 03C add esp, 4 ; NOP
DeVmp.tx:00449FD4 038 jb loc_4483A4 ;目标地址和楼下地址 代码功能相同
DeVmp.tx:00449FDA 038 pusha
DeVmp.tx:00449FDB 058 push 0F83ECAE3h
DeVmp.tx:00449FE0 05C clc
DeVmp.tx:00449FE1 05C neg edx
DeVmp.tx:00449FE3 05C pusha
DeVmp.tx:00449FE4 07C add edx, 0
DeVmp.tx:00449FEA 07C push 0
DeVmp.tx:00449FEF 080 mov [esp+80h+var_80], ecx
DeVmp.tx:00449FF2 080 mov [esp+80h+var_3C], edx
DeVmp.tx:00449FF6 080 push esi
DeVmp.tx:00449FF7 084 push [esp+84h+var_84]
DeVmp.tx:00449FFA 088 push [esp+88h+var_3C]
DeVmp.tx:00449FFE 08C retn 50h ;retn 50 相当于 jmp 和 ESP + 54
;所以到00449FD4 之间的栈也是平衡的 (符合变形规律二)
;这段代码等价于
;neg edx
;jmp edx
这里注意 EBP 是非常重要的 所以要根据上文推到处[esp + 30] 存放着 EFL
06C push dword ptr [esp+30h] ; 以下两条指令等价于
; pushf
;pop [ebp]
070 pop dword ptr [ebp+0]
这段代码根据以上规律整理后变成如下这样
该段指令的等价指令 (可以在原HANDLE上做跳转到这里来验证优化代码是否有问题,验证通过)
mov eax, [ebp+0]
mov edx, [ebp+4]
not eax ;这里就是传说中的或非门
not edx ; 直接看指令是先非再与 但是 NOT(A) AND NOT(B)= NOT(A AND B)
and eax, edx ; 也就是先非再与 等价于 先或再非 也就是或非门
mov [ebp+4], eax
pushf
pop [ebp]
mov al, [esi-1] ;以下是通用指令 计算并跳转到下一个HANDLE (每个HANDLE 都有同样的指令)
sub al, bl
add al, 9Dh
not al
rol al, 3
sub bl, al
dec esi
movzx eax, al
mov edx, ds:dword_4053EF[eax*4]
neg edx
jmp edx
X86OPCODE
8B 45 00 8B 55 04 F7 D0 F7 D2 21 D0 89 45 04 9C
8F 45 00 8A 46 FF 2A C3 04 9D F6 D0 C0 C0 03 2A
D8 4E 0F B6 C0 8B 14 85 EF 53 40 00 F7 DA FF E2
这里补充下或非门知识
或非门可以表达一切逻辑运算
维基百科
http://zh.wikipedia.org/zh/%E6%88%96%E9%9D%9E%E9%97%A8
逻辑运算公式
http://course.cug.edu.cn/21cn/%CA%FD%D7%D6%B5%E7%D7%D3%BC%BC%CA%F5%BB%F9%B4%A1/800x600/web/text_web/03/03030000.htm
按照上面的思路 代码变形就可以人眼识别了
修正补充3:
我记得最初喜欢上破解,还是喜欢爆破的方式,让输入正确的注册码和错误的注册码,都走正确的流程
这个在注册码不参与功能函数解密的情况下是非常通用的方法,简单而暴力,就像年轻的激情,这种甜蜜的
岁月直到遇到vmp。年轻有精力但是浮躁,现在好了些,(是不是代表自己老了 ~~)所以静下来研究,当时的困惑迎刃而解。
大家都知道条件跳转是爆破的关键,每个条件跳转都有两个分支,走A分支或者走B分支,这里假定A分支是注册
成功的流程,这里就出现个问题,如何知道是A分支是呢,一般的方法是取反条件跳转,看程序的现象,弹出注册成功之类的
,其实还有个方法,你有正确的注册码,输入上后发现走到了A分支,那么A分支必然是成功的分支了,如果取反这个条件跳转
那么即使输入错误的,也会走A分支,这就完成了爆破!
具体到当前文章讨论的VMP,VMP里面有个HANDLE 代表跳转,具体跳到哪里取决于前面的运算
HANDLE指令如下
DeVmp.tx:004B9F08 ; 函数等价于 (验证通过)
DeVmp.tx:004B9F08 ; mov esi, [ebp+0]
DeVmp.tx:004B9F08 ; add ebp, 4
DeVmp.tx:004B9F08 ; mov ebx, esi
DeVmp.tx:004B9F08 ; add esi, [ebp+0]
ESI 是VMPOPCODE的指针 作用相当于X86指令的EIP
这里ESI = [ebp+0] + [ebp+4]
EBP和EBP+4里面保存的内容来自前面HANDLE的运算
聪明的你想到怎么回事了没
假定你有正确的注册码,你就可以确定正确流程的ESI
然后你就可以patch 这个HANDLE ,即使输入错误的注册码,ESI 也是对的
然后就爆破成功了
具体到这个例子就是
004B9F08 执行到这里 且 ESI == 004B9F08 的时候
执行完这个HANDLE的时候 ESI == 00406FDE 注册成功 ESI == 00406F8A 注册失败
因为两个分支之间的OPCODE 大小未知(除非代码已经还原),也就是已知错误的分支无法得到正确的分支
所以没有正确的注册码就没有正确的分支地址,也就是此爆破的前提是有一个有效的注册码
~~
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
上传的附件: