吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3867|回复: 32
收起左侧

[原创] 学习 VMP 虚拟机吧(四):VMP2 还是那台机器

  [复制链接]
wolfSpicy 发表于 2026-10-14 00:12

VMProtect  ·  架构笔记

VMP2 还是 VMP1 那台机器

骨架没换。难的是 max:壳和函数拆成两台派发器,滚动解密也不再是看一眼字节就懂。

VMP1 做完再看 VMP2,第一感觉是难了很多。派发器不直给,handler 藏在弯路里,max 一启动还先跑一台壳。拆开之后会发现,里面还是那五样东西:VIP、派发器、handler、虚拟栈、虚拟寄存器。

能从 VMP1 带过来的是分析格式:在派发器上逐条记账,看虚拟栈多了什么、少了什么,以此来认指令。带不过来的是具体哪条指令、哪个寄存器、哪张表。那些每个样本都不一样。

一样的部分

不管一代还是二代,不管 speed 还是 max,虚拟机都在重复同一件事。

从 VIP 指向的地方取出一条字节码
用手里的密钥把这条字节码解开
派发器按解开的结果,跳进对应的 handler
handler 去改虚拟栈,或者改虚拟寄存器
然后回到派发器,取下一条

VIP 就是「下一条字节码在哪」。虚拟栈用来传参数、放计算结果。虚拟寄存器是一块跟着这次调用走的小内存,handler 按编号去读它、写它。派发器本身不干活,它只负责把每一条字节码送进正确的 handler。

所以看到一份新的 VMP,不要先问「这是第几代、操作码表在哪」。先问:谁在决定下一次取指,取出来的字节交给谁,handler 改的是栈还是那块小内存。这三个问题答上了,这台机器就认出来了。

一眼看出的差别:派发器长什么样

同一件「取字节 → 进 handler」,两边写出来的样子差很多。下面都是真实样本里的派发核心,中间的垃圾指令已经拿掉了。

VMP1.09 SPEED:三句就结束。

lodsb                                    ; AL = [ESI],ESI += 1
movzx eax, al                            ; 这个字节就是 opcode
jmp   dword ptr [eax*4+0x423B8D]         ; 直接进 handler

日志里看到的那个字节,几乎可以直接当操作码用。handler 做完,jmp 回 lodsb,下一轮开始。

VMP1.09 MAX:多了 BL,但还是一眼能看到跳表。

lodsb
add   al, bl
not   al
dec   al
xor   al, 0xBD
rol   al, 0x07
add   bl, al                             ; 钥匙跟着滚
movzx eax, al
jmp   dword ptr [eax*4+0x42384D]         ; 仍然是直接 jmp

字节先和 BL 搅一轮,才是 opcode。跳转方式没变:还是 jmp [表],handler 做完还是 jmp 回 lodsb。

VMP2 SPEED:解 opcode 还看得见,进 handler 绕了一圈。

xor   bl, al
movzx eax, al
mov   edx, dword ptr [eax*4+0x4229f5]    ; 表里拿出来的还不是地址
add   edx, 0xbb352db0                    ; 再加一个常数,才是 handler
...                                      ; 中间夹着 pushfd / pushad
push  dword ptr [esp+0x30]               ; 把算好的地址压成返回地址
ret   0x34                               ; 用 ret 跳进去,不是 jmp

表项是编码过的,handler 入口也不再是 jmp 直达。但从「xor bl → movzx → 查表」这一串,仍然能认出自己站在派发器上。

VMP2 MAX:派发器本身也拆散了。取指、解密、查表不一定挨在一起;进 handler 仍然是 push + ret;同一个功能的 handler,换一个样本地址就全换。更关键的是:启动时跑的那台,和进了函数之后跑的那台,不是同一张表。

同一份 add_two 的 max 样本里,全程能撞上五处查表,它们只归属两张表:

; 壳(启动阶段,表 0x0056f55e)
00571d3d  mov  edx, dword ptr [eax*4+0x0056f55e]
00570ff2  mov  edx, dword ptr [eax*4+0x0056f55e]   ; 另一条分支,同一张表

; 函数(原入口之后,表 0x00423f10)
004207b2  mov  edx, dword ptr [eax*4+0x00423f10]
00420fc9  mov  edx, dword ptr [eax*4+0x00423f10]   ; 另一条分支,同一张表

日志里同一条 op=35,站在不同表上,跳进去的地方完全不一样:

壳:   op=35  handler 原始值 f0fc5600  → 字节交换后是 0056fcf0
函数: op=35  handler 原始值 95244200  → 对应 00422495

数字碰巧都是 35,不是同一条指令。一台表上的两个查表点可以合并跟踪;两张表必须分开学。这就是 max 相对 speed、相对一代,多出来的那一步:先分清自己站在哪一台里。

把四段并排放:

取到的字节是不是 opcode 怎么进 handler 做完怎么回来
VMP1 SPEED 基本就是 jmp [表] jmp 回 lodsb
VMP1 MAX 和 BL 异或一轮后才是 jmp [表] jmp 回 lodsb
VMP2 SPEED 和 BL 搅过一轮 表项再解码,ret 进去 清栈后回到派发
VMP2 MAX 更深,且壳 / 函数各一套 同上,还要先认清是哪一台 同上

一代难在「认指令」。二代难在「先把派发器认出来」。骨架没换,外壳厚了。

VMP2 难在哪

难不在状态机换了,难在二代把解释器本身也打乱了。同一版 VMP2 保护出来的两个程序,派发器长得不一样,同一个功能的 handler 也不在同一个地方。一代里「认出版本,套已经逆过的指令表」这件事,二代做不到。

另一个变化是字节码不再裸着放。一代里,日志里看到的那个字节,常常就是操作数本身,或者只跟一个很短的密钥异或一下。二代把解开这一步藏进了各条 handler,而且还越藏越深。这就是下面说的滚动解密。

speed:还是一台派发器

VMP2 的 speed 和 VMP1 最像。整个程序里被保护的函数,跳进的是同一台派发器。找到它,后面就和一代一样。

做法也很熟:在派发器上每执行一条就记一行。这一行里要有取指位置、原始字节、跳进了哪个 handler、虚拟栈顶现在是什么。一条指令认没认对,只看一件事:用这一行栈顶算出来的结果,是不是出现在下一行。对上了,这条指令就可以起名。

举一个最小的例子。连续两行的栈顶是这样:

这一条:栈上是 3 和 5,handler 是某一段代码
下一条:栈上只剩 8
那一段代码就是加法。

真实样本里也能直接撞上。一份 1 + 2 的小函数,VMP2 speed 压缩成 152 条虚拟指令之后,栈顶会先后变成:

... s0=00000002
... s0=00000001  s1=00000002
... s0=00000003                  ; 1+2 出现了,这一条就是 add

不必先读懂 handler 里的 x86。栈对上了,指令就有名字了。

把加减、读内存、写内存、离开虚拟机都这样认完,指令集就齐了。之后还原某一个函数,只是在用这套指令集读它的字节码,不再是在猜。speed 可以走到这一步,最后写成普通代码。一代能做完的事,speed 也能做完,因为始终只有一台派发器,不用先猜自己现在站在哪一台里。

函数和函数之间也好分。一次原生的调用进去、返回出来,就是一台函数的边界。内存基址换成一块新的,通常就是下一台。不要靠数有多少条字节码来猜哪里结束。

max:壳一台,函数又一台

max 才是相对一代变化最大的地方。程序刚启动时先跑的那段虚拟机,不是你要看的函数。那是壳。壳的工作是把真正的程序展开到内存里,展开完才进入原入口。被虚拟化的函数用的是另一台派发器,查表的地方和 handler 都跟壳不是同一套。

启动
  -> 壳的派发器,把后面的程序展开出来
  -> 原入口
  -> 函数自己的派发器
  -> 用户名、注册码这一类逻辑在这里面算

同一份 add_two(原来就 3 条 x86:mov / add / ret),保护级别一升,数字会差一个数量级。下面这些都是你自己样本上数过的:

文件体积 虚拟指令(进 handler 次数) 底层真实 x86(函数段追踪) 备注
原文件(未保护) 115 KB — 3 vmp_add.exe / vmp_add_map.exe,同一份 add_two
VMP1.09 SPEED 136.5 KB 48 约 600 多行 15 类 handler;映像约 152 KB
VMP1.09 MAX 137 KB 229 约 4000 行 33 类 handler;多出来的主要是 BL 和解密;映像约 152 KB
VMP2 SPEED 126 KB 152 约 11653 条(vmp0) 全程约 14 万条 x86,几乎没有壳;映像约 136 KB
VMP2 MAX 730 KB 函数 1991 / 全程约 34 万 函数约 16.7 万 / 全程约 1.34 亿 含启动壳;映像约 1484 KB;vmp1 约 1.13 亿,vmp0 约 0.21 亿

文件体积从原文件就能对上。未保护 115 KB;一代 speed / max 涨到 136.5 / 137 KB,几乎一样大,差的是跑起来的指令条数(48 → 229,x86 六百多 → 四千)。二代 speed 126 KB,max 直接到 730 KB,相对原文件大约 6 倍——壳和那台上亿条 handler 一起写进文件里了。二代 speed → max,函数段虚拟指令从 152 涨到 1991;再加上壳,全程到 34 万条虚拟指令、上亿条 x86。膨胀不只是「每条 handler 多写了几句」,整条指令流程更长,落盘的 exe 在二代 max 上才明显一起胖。

34 万听起来吓人,真正要认的不是 34 万条,是两张表上那两百多个操作码。同一张表、同一个 opcode,做的是同一件事;壳那张表和函数那张表,数字碰巧一样也不算同一条指令。

这里说的「吐出源代码」,不是把虚拟机还原成 C。壳写回去的是展开后的程序。展开完的那份东西也不能当成「壳已经脱完、可以当普通程序跑」。壳在展开时留下的一些指针,离开当时的进程就无效了,单独拿展开结果去跑,会在进虚拟机时崩掉。

这里有个容易混的说法:有人把「虚拟机脱壳」理解成「把 VM 去掉」。实际不是。

壳启动完之后做的事,通常就三件:把加密区解密展开、找到 OEP、把 IAT 修到能正常导入。做完这些,程序能从原入口跑起来。你这份 max 里也能对上——前面上亿条是壳在跑,最后 ret 到 00401370,.text 里原先是零的位置也被填上了。

但当初勾成 Virtualization 的函数,脱完壳还在。00401010 仍然是一条 jmp 进派发器,字节码还在,handler 还在。壳解决的是「能不能跑起来」;该被 VM 的函数,脱完也还是 VM。要把那些函数还原成普通 x86,还是得站回函数那台派发器上,用栈变化去认指令——那是另一条线,不是 dump + 修 IAT 能做完的。

怎么知道自己碰到了两台,而不是一台里的两段代码?看同一类字节,在启动阶段和在进了函数之后,派发器跳进去的地方是不是同一处。不是同一处,就是两台。两台要分开跟踪,指令集也分开学。设计是一样的,都是取指、查表、进 handler,只是表不同。

分析格式仍然是 VMP1 那套。差别只是要先站对派发器。站在壳上,看到的是展开过程。站在函数那台上,才能看到用户名和注册码怎么比。

滚动解密,也就是 BL 解密

字节码不会以明文躺在那里。取出一个字节之后,要先用一个一直在变的密钥把它解开,这个密钥的低位通常就是 BL。解完,BL 自己再滚动一下,给下一条用。所以叫滚动解密。

一代的 max 里,这一步比较浅,而且集中在派发器门口。日志里看到一个字节,和当前的 BL 搅一下,就是这条指令真正用的数:

日志里的字节是 20
当前 BL 是 05
真正的操作数 ≈ 20 和 05 做过一轮运算之后的结果

对照真实指令,就是前面那串 add / not / dec / xor / rol,解完马上 jmp [表]。handler 里面如果还要再取一个寄存器编号,会再用 BL 解一次,但公式短,回跳目标固定。

二代的 max 把这一步做深了。那个字节还不是操作数。它要先和 BL 做一次运算,运算结果才是「去虚拟寄存器的第几号」。立即数也一样,字节码里连续的几个字节是密文,要和整个 EBX 搅过一轮,才是压进虚拟栈的那个数。

日志里的字节仍然是 20
它先和 BL 做运算,得到一个编号
handler 再用这个编号,去读虚拟寄存器里的值
所以 20 本身不是数据,只是一把钥匙

同一个 handler 里,一代和二代的「取寄存器编号」也能并排放:

; VMP1.09 MAX 的 V_POP:编号解完就 pop
lodsb
add   al, bl
inc   al
ror   al, 0x01
not   al
dec   al
add   bl, al
pop   dword ptr [edi+eax*4]
jmp   派发器的 lodsb                 ; 回来的路很短

; VMP2 MAX:同类事情埋在更长的 handler 里
; 取字节、和 BL 运算、改虚拟栈,中间还夹着
; 不影响结果的 cmp / test / push / ret
; 做完不是 jmp 回固定地址,而是清掉一截栈再回去

跟踪的时候就要多记一个 BL。只记原始字节,会把密文当成操作数,后面的指令集全部对不上。speed 轻一些,原始字节离真正的操作数近,看栈的变化就能起名。max 必须先把「字节经过 BL 之后是什么」写成一步,再去认加减和读内存。

另一件一起变强的事是查找表。speed 里常见的做法是:程序先放一张 0 到 255 的表,再按一个种子把它打乱,打乱之后就不再动。你可以把这张表抽出来,离线使用。max 里这张表放在虚拟寄存器旁边,一开始也是 0 到 255,但程序跑起来之后还在改它。同一个下标,真正来读的那一刻是一个值,前后可能是另一个值。所以不能在中途把整张表倒出来,当成一份永远不变的常数。

学完指令集之后

speed 学完就可以往下写。字节码翻译成加减、查表、读内存,一个函数能收成普通代码,拿原来的输入跑,中间结果和虚拟机对得上,这台就算做完了。

max 学完指令集,往往还看不到「输入怎么变成那个被比较的数」。中间会隔着很长一串循环移位、乘一个固定数、再加一个固定数、再做一次异或。这些常数看起来眼熟,像某种公开哈希的收尾,拿去对常见哈希通常对不上。不要在这里猜算法名字。

如果目的是拿到注册码,不必把这条链走完。比较发生的时候,程序算出来的数和用户填的数会同时出现在虚拟栈上。自己填一个特别好认的注册码,比如每一组都是同一个数字,再去日志里找「一边是我填的、另一边是别的」那几条比较。另一边就是这台程序想要的值。

这个办法要核对三次才算数。同一组输入再跑两遍,取出来的数必须一样。换一个同样好认、但内容不同的注册码再跑,取出来的数还必须不变,这样才能肯定它只跟用户名有关。改用户名里的一个字符,这个数必须跟着变,旧注册码也必须失败。

放在一起看

VMP1 SPEED VMP1 MAX VMP2 SPEED VMP2 MAX
派发器 一台,三句能认出来 一台,门口多 BL 一台,ret 进去 壳一台 + 函数一台
你要先做的事 找到 lodsb 同上,再记下 BL 找到查表那一拍 先分清现在是壳还是函数
记账方式 派发器上逐条记,用栈的变化认指令 同左,多记 BL 同左 方式相同,两台分开记
滚动解密 几乎没有 字节和 BL 搅一轮就接近原值 有,但离栈顶近 字节还要再运算,结果才是下标或立即数
查找表 固定 固定 打乱后通常不再变 跑的过程中还在改
同一份 add_two 膨胀(原文件 115 KB) 136.5 KB;48 条虚拟 / 约 600 行 x86 137 KB;229 条 / 约 4000 行 126 KB;152 条 / 约 1.2 万 x86 730 KB;函数 1991 条;全程约 34 万条 / 1.34 亿 x86
做到哪算完 指令集齐了,函数能写成普通代码 同左 同左 指令集也能学;要注册码的话,从比较的那一刻取

下次可以照着做的三步

  • 先分阶段。刚启动就进虚拟机的,先当成壳。等原入口之后,再找函数自己的派发器。两边跳进去的代码不一样,就确定是两台。
  • 在函数那台上按 VMP1 的格式记账。每条记下原始字节和当时的 BL。认指令只认栈:这一行算出来的,必须出现在下一行。
  • 如果只要最终被比较的那个数,就填一个好认的输入,到比较发生的地方把另一边读出来。先别从第一个字符顺着混淆往下推。

VMP2 没有换掉 VMP1。speed 证明一台派发器时,一代的做法还能走完。max 多出来的是分清两台,以及把 BL 解密当成指令的一部分,而不是取指之前的一个小步骤。

免费评分

参与人数 15吾爱币 +14 热心值 +14 收起 理由
Fourseasons + 1 我很赞同!
jiyu0418 + 1 谢谢@Thanks!
yyaipg + 1 + 1 热心回复!
InfiniteBoy + 1 + 1 用心讨论,共获提升!
AG6 + 1 我很赞同!
wapj20260721 + 1 用心讨论,共获提升!
guotianyun + 1 + 1 用心讨论,共获提升!
fengbolee + 1 + 1 用心讨论,共获提升!
c271687388 + 1 + 1 谢谢@Thanks!
soyiC + 1 + 1 用心讨论,共获提升!
user23456 + 1 + 1 我很赞同!
笨笨家的唯一 + 1 + 1 我很赞同!
Sound + 3 + 1 已经处理,感谢您对吾爱破解论坛的支持!
xinr9527 + 1 我很赞同!
156608225 + 2 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| wolfSpicy 发表于 2026-10-14 00:18
现在ai已经非常强大了 但我觉得正是这样更不能放弃原理研究 我们使用ai 我们的能力取决于ai的上限 而不是把一切交给ai
 楼主| wolfSpicy 发表于 2026-10-14 00:15
我是对vmp2max版本程序进行破解成功 才有所心得想跟大家分享
sc8633 发表于 2026-10-14 01:16
yaochangfu 发表于 2026-10-14 08:16
感谢分享
guanzz 发表于 2026-10-14 08:35
学 习 下 !
faith9527 发表于 2026-10-14 11:07
wolfSpicy 发表于 2026-10-14 00:18
现在ai已经非常强大了 但我觉得正是这样更不能放弃原理研究 我们使用ai 我们的能力取决于ai的上限 而不是把 ...

大佬太强了
msn882 发表于 2026-10-14 21:34
wolfSpicy 发表于 2026-10-14 00:18
现在ai已经非常强大了 但我觉得正是这样更不能放弃原理研究 我们使用ai 我们的能力取决于ai的上限 而不是把 ...

ai分析不了VMP吧?
 楼主| wolfSpicy 发表于 2026-10-14 21:47
msn882 发表于 2026-10-14 21:34
ai分析不了VMP吧?

我用过gpt6.1 它是可以解析出vmp2的
fengshi073 发表于 2026-10-15 08:30
wolfSpicy 发表于 2026-10-14 21:47
我用过gpt6.1 它是可以解析出vmp2的

主要是ai有时候也会犯错之类的
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-11-5 03:18

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表