首页
社区
课程
招聘
[原创]基于 VMProtect授权锁架构的RSA算法另类Bypass思路
发表于: 2026-10-3 15:45 4477

[原创]基于 VMProtect授权锁架构的RSA算法另类Bypass思路

2026-10-3 15:45
4477

由于笔者非科班出身,在本领域的全部知识都为自行学习,故本篇提到的一些"术语"可能并不标准,见谅


VMProtect (下面简称VMP) 提供的授权锁机制,本质上是一种基于 RSA 算法的代码级授权绑定保护手段。

具体而言:在未加壳的原始程序中,被打上授权锁标记(如 VMProtectBeginUltraLock)的函数逻辑是完整的。但经过 VMP 引擎编译加壳后,该函数的核心代码会被抽离、加密并转化为虚拟化指令。此时,该函数处于“锁定”状态,不再具备完整的独立执行能力。

若该函数要被正常执行,必须在运行时依赖一个关键的动态解密因子(通常表现为一个 8 字节的核心校验/解密密钥,即 ProductCode)。而这个至关重要的解密因子,正是被封存在客户端输入的序列号(SerialNumber)中。当程序运行时,VMP 必须通过其内置的 RSA 公钥指数以及模数 对序列号进行解密,提取出ProductCode,才能完成受保护函数的动态解密与上下文拼接。


简而言之:VMP 的序列号并不仅仅是一个非黑即白的通行证,它本身就是受保护代码赖以运行的"解密密钥"。这也意味着,整个授权体系的绝对命门,就落在序列号的 RSA 解密环节上。



先说一下RSA算法的解密公式:


其中,m 代表解密出的明文结果(包含至关重要的 ProductCode 等),c 代表密文体(即客户端输入的序列号),e 代表公钥指数,而 n 则是 RSA 的模数。


明确了公式后,我们来看看业界在应对 VMP 授权锁时的标准手段。

常规思路的本质都是替换n(也就是模数),只要换上我们自己生成的n,就能用对应的自己的私钥自行签发合法的序列号(当然,前提是在正版授权通过的环境下先提取出那个关键的 8 字节 ProductCode)。

要实现替换 n,目前主流无非是一动、一静两种手段,但这两种手段在现代 VMP 面前都显得越发吃力:

1、通过动态调试,定位到程序将 n 载入大数结构(Bignum)的那一刻,直接在内存中将其替换。

痛点: 虽然省力,但这种方法往往只对老版本的 VMP 奏效。在较新的 VMP 版本中,完整的 n 几乎永远不会以连续、明文的形态暴露在内存中。


2、还原 VM 指令,逆向出具体的解密算法,找到程序中静态存储且被加密的 n。用还原出的算法将我们自己的 n 加密后,覆盖回原程序的文件中。

痛点: 这种方法近乎无痕,非常优雅。但代价是极其高昂的时间成本。面对 VMP 犹如天书般的虚拟指令流,需要极大的毅力和分析能力,且不同版本之间的算法往往不一致,复用性极差。



那么为什么在黑盒状态下,追溯 n 简直是一场噩梦?

这正是本篇另辟蹊径,完全放弃替换 n 的根本原因:

VMP 在进行 RSA 解密时,会在堆上专门开辟并规划出一块内存区域(我称其为“VM运算区”)。在这个VM运算区内,VMP 确实会把明文 n 的片段一个个地、位置并不明确地、写入内存。但对于一个完全黑盒的程序而言,当我们找到了一些疑似写入明文 n 片段地位置并试图记录这些片段时会发现:即使我们通过硬件断点或日志追踪,捕获了目标地址所有 DWORD/QWORD (32/64位) 的写入操作,保存下来的日志依然犹如天书。因为在这个 VM 运算区里面,伴随着真实 n 片段的写入,VMP 还会插入大量的垃圾指令和虚假写入。在我们完全不知道原始明文 n 的任何一个片段的情况下,面对日志里成千上万次真假难辨的内存操作,想要从中精准挑出真正的 n 的片段并拼凑完整,无异于大海捞针。



既然在黑盒环境下死磕模数 n 是一条看不到尽头的死胡同,那我们不妨跳出底层虚拟机指令的泥潭,重新审视 RSA 算法本身的数学根基。

回顾 RSA 的核心解密方程,一直以来,几乎所有的攻防焦点都集中在 n 身上,逆向者们绞尽脑汁想要替换它,却往往忽略了公式中另一个至关重要的参数——公钥指数 e。


在业界标准中,e 通常是一个固定的小常数(如 65537,即 0x10001)。本篇所探讨的另类思路,正是将矛头对准了这个 e。


本思路基于一个最基础的数学常识:任何数的 1 次方,都等于它本身。

如果我们能通过某种手段,在程序进行模幂运算之前,将这个公钥指数 e 强行修改为 1。那么,这个看似坚不可摧的非对称加解密公式,就会在瞬间发生“坍缩”,公式将直接退化为:


随之而来的是第二步:在 RSA 算法的规范中,输入的密文体 c 在数值上必定严格小于模数 n。那么,一个较小的数值 c 尝试对一个更大的数值 n 进行取模运算,结果会发生什么?

答案是:结果依然是它本身。

于是,整个 RSA 解密体系的最终数学表达就退化成了:


这意味着它达到了绝对的 输入 = 输出,而整个过程中我们丝毫不关心黑盒的n,而是转而针对明确的e。


那么理论部分介绍完毕,接下来开始实操验证。

以VMProtect 3.10.6 Demo版本为例,先编写一个Demo程序,逻辑很简单,输入序列号,按回车,授权通过则会弹出MessageBox:




编译完成以后,添加授权锁,选择RSA-2048算法并生成一个正版授权序列号然后加壳。


前置条件依然是拿到那个8字节的ProductCode顺便找到Base64解码函数(这一步没什么技术含量教程满地都是,故不过多赘述)

提取到当前程序的ProductCode为:33 8E B3 AC 08 C0 26 0C



随后将重新程序拖入x32dbg中运行,在Base64解码函数头部设置断点,随后输入正版序列号,程序在Base64解码函数头部断下。(序列号的存放位置就位于VM运算区的下方)


让该函数执行完毕,得到内存中经过Base64解码后的密文体,并对它设置硬件访问断点


接下来断下来的位置,就是在读取密文体并转换为大数的位置


在这个位置可以观察堆栈中的地址,找到它转大数的Buffer,并对着它已经转换完毕的字节下硬件访问断点,F9跑起来以后,再次断下来的位置就位于大数运算函数中(也就是真正的解密函数),且尚未开始真正运算的位置。


此时我们需要稍作思考:

既然e和n都是以加密的形式静态存放在程序的只读段中,那么这个大数运算函数就必然会以相对位置的形式传参并读取这个静态且加密的e以及n

但问题在于:此时的代码 几乎 处于全段VM的状态,它几乎不可能以push或mov的形式压栈。



然而,计算机的底层是纯粹的数学和物理,这里要引用翁恺老师的一句话:“计算机的世界里没有黑魔法”。

VMP 无论有多厉害,它的虚拟化和混淆无论有多抽象,它都不可能颠覆冯诺依曼架构,更不可能打破物理 CPU 运算的客观规律。



既然明确了在几乎全段 VM 状态下,它不可能直接使用原生真实的 push 和 mov 暴露意图,那么它的虚拟化抽象指令就必然要在内部模拟数据流转的过程。而无论这个过程被伪装得多复杂,当它真正要将数据送入底层运算单元时,那些关键参数(指针、长度)就必然要在物理内存中“实体化”,其数据结构的内存布局也必然会暴露出现实世界中标准密码学组件的痕迹。那么此时,我们就可以在程序的 VM 内存工作区中,直接利用这些客观存在的物理特征,跨过指令的迷宫,直接搜索出它喂给解密函数的参数,其中就必然包含那个静态的 e 的相对位置。


冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 2026-10-5 07:25 被Gmcixy2531编辑 ,原因:
收藏
点赞 ・17
打赏
分享
最新回复 (18)
雪    币: 248
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2 楼
大佬 牛皮    通俗易懂的爽贴    有用的知识+1
2026-10-4 05:20
0
雪    币: 8315
活跃值: (7682)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
3 楼
太牛了
2026-10-4 11:50
0
雪    币: 40
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4 楼
大佬,怎么私信你
2026-10-4 14:15
0
雪    币: 28
活跃值: (1530)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5 楼
666666666666666666666
2026-10-4 17:04
0
雪    币: 7348
活跃值: (7859)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6 楼
厉害,不错。
2026-10-4 21:57
0
雪    币: 622
活跃值: (2787)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
7 楼
很酷
2026-10-5 10:10
0
雪    币: 823
活跃值: (5835)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
8 楼
妙
2026-10-8 16:09
0
雪    币: 1
活跃值: (4494)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9 楼
做个动画演示就更好了。
2026-10-9 10:31
0
雪    币: 1068
活跃值: (1726)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10 楼
所以最关键的还是要定位E
2026-10-9 10:55
0
雪    币: 6359
活跃值: (6567)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11 楼
所以作者如果看到了会不会下次RSA都进行了 E 验证哈哈......  甚至还加入RSA算法内判断E的值
2026-10-11 19:46
0
雪    币: 360
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12 楼
Mxixihaha 所以作者如果看到了会不会下次RSA都进行了 E 验证哈哈...... 甚至还加入RSA算法内判断E的值
好问题,但判断本身就依赖读取,攻防没有止境。
2026-10-11 22:25
0
雪    币: 6359
活跃值: (6567)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13 楼
Gmcixy2531 好问题,但判断本身就依赖读取,攻防没有止境。
是的,只是攻防的步骤就多很多了, 作者还判断返回内容是否为输入内容等等, 因为E是一个DOWRD  所以放入寄存器之后比较和各种骚操作对于分析者就麻烦多了.      攻防又拉长更多的技术操作了......
2026-10-12 13:04
0
雪    币: 1261
活跃值: (1560)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14 楼
可否把样本开源学习下
2026-10-15 16:51
0
雪    币: 360
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15 楼
xqq524148626 可否把样本开源学习下
代码文章里有写,自己加壳就行了
2026-10-15 16:59
0
雪    币: 1261
活跃值: (1560)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16 楼
Gmcixy2531 代码文章里有写,自己加壳就行了
看了好几遍才理解为啥没有exe样本,原来是因为跟机器码绑定了
2026-10-16 16:27
0
雪    币: 1261
活跃值: (1560)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17 楼
Gmcixy2531 代码文章里有写,自己加壳就行了
如果现在搞到一个客户端程序与配套机器的序列号,这种情况还能再生成序列号吗?
2026-10-16 16:34
0
雪    币: 360
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18 楼
xqq524148626 如果现在搞到一个客户端程序与配套机器的序列号,这种情况还能再生成序列号吗?
你并没有看懂这篇帖子在讲什么
2026-10-16 22:01
0
雪    币: 484
活跃值: (1398)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19 楼
天生逆向的大佬   为什么大家都没有想到 膜拜
2026-10-21 16:26
0
游客
登录 | 注册 方可回帖
返回