[原创]vofa+ 注册认证逆向分析与破解实战
作者:小白 & 无锁不能 发表于:2026-5-27
前言
某天我在群里吹水,说最近用 vofa+ 挺好用的,就是每次启动都弹个"未授权"对话框很烦。群友说:"你找无锁不能啊,他专门搞这个的。"
于是我找到了无锁不能,想让他帮忙分析一下 vofa+ 的注册机制。
小白 :大佬,能帮我看看 vofa+ 的注册认证怎么破吗?每次启动都弹未授权对话框,太烦了。
无锁不能 :行,我先用 IDA + MCP 连上分析一下。
第一章 初探:发现 RSA 公钥
无锁不能打开了 IDA,连接上 vofa+.exe,开始分析。
无锁不能 :我先搜一下关键字符串。
很快就在二进制中找到了一个非常显眼的字符串——RSA 公钥:
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCfEsswjzV7nBqkf1TQl+WSyunw
vH6mGZBxoj3Dhr4c0DvmzgQmQ1m8r0DgZOD9qagWvule/00EVt42WPu/y43Jyxap
EsUWtv2rM0a+vLlG8tkAHb4QKrNBROgZnZK9hFiYfxXJKIO40HW8XrmzupVCisXL
B2MpAAKxj6czcSjEeQIDAQAB
-----END PUBLIC KEY-----
无锁不能 :这软件用 RSA-1024 做注册码验证,公钥就嵌在程序里,说明注册码是 RSA 加密的 JSON 数据。
小白 :那有公钥也没用啊,我们又没有私钥。
无锁不能 :没错,纯密码学角度没法伪造签名。但我们可以 patch 二进制跳过验证。
第二章 深入:逆向 VerificationClient 核心类
通过 IDA 的交叉引用功能,无锁不能定位到了公钥引用的位置——VerificationClient 类的构造函数和 CheckACode 方法。
构造函数(0x14001F120)
VerificationClient *__fastcall VerificationClient::VerificationClient (VerificationClient *this )
{
LocalInfo::LocalInfo (&this ->info);
FileIO::FileIO (&this ->fileio_, nullptr );
RandomString = MyTools::GetRandomString (&result, 32 );
QString::operator =(&this ->open_dyn1_, RandomString);
pub.d = QString::fromAscii_helper (
"-----BEGIN PUBLIC KEY-----"
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCfEsswjzV7nBqkf1TQl+WSyunw"
"vH6mGZBxoj3Dhr4c0DvmzgQmQ1m8r0DgZOD9qagWvule/00EVt42WPu/y43Jyxap"
"EsUWtv2rM0a+vLlG8tkAHb4QKrNBROgZnZK9hFiYfxXJKIO40HW8XrmzupVCisXL"
"B2MpAAKxj6czcSjEeQIDAQAB"
"-----END PUBLIC KEY-----" ,
271 );
}
CheckACode:注册码验证核心(0x14001FE70)
QJsonObject *__fastcall VerificationClient::CheckACode (
VerificationClient *this , QJsonObject *result, QString a_code)
{
QString::toUtf8 (a_code.d, &sBase64);
MyRSA::SetPubKey (&my_rsa, "-----BEGIN PUBLIC KEY-----..." );
v6. d = QByteArray::fromBase64 (...).d;
v7 = MyRSA::PublicDecryptAll (&my_rsa, &resulta, v6);
QJsonDocument::fromJson (...);
if (!contains ("MC" ) || !contains ("SD" ) || !contains ("ED" )
|| !contains ("R" ) || !contains ("KEY" ) || !contains ("IV" ))
return empty;
if (machine_code != local_machine_code)
return empty;
return json_obj;
}
小白 :所以注册码就是 Base64 编码的 RSA 加密 JSON?
无锁不能 :对。JSON 里包含这些字段:
字段
说明
MC
机器码(硬件指纹)
SD
有效期起始
ED
有效期截止
R
动态挑战应答
KEY
AES-256 密钥
IV
AES-256 初始化向量
第三章 挖坑:启动流程中的验证陷阱
小白 :那我直接 patch 掉 CheckActivated 函数,让它永远返回 true 不就行了?
无锁不能 :有道理,先试试看。
无锁不能反编译了 CheckActivated 函数:
CheckActivated(0x1400201F0)
_BOOL8 __fastcall VerificationClient::CheckActivated (VerificationClient *this )
{
secrect = "ewl61)r&a+33%7q*vae%qort+9*mouch6gc@@@aq4l3os#+#1h" ;
result = operator +(this ->open_dyn1_, this ->open_dyn2_);
v3 = MyTools::MD5Code (result, secrect);
this ->activated_ = operator ==(this ->open_md5_, v3);
return this ->activated_;
}
地址 0x140020271,修改:
原始:0F B6 43 7C movzx eax, byte ptr [rbx+7Ch] ; 返回 activated_
改: B0 01 mov al, 1 ; 直接返回 1
90 nop
小白 :我改了,但启动还是弹未授权对话框啊!咋回事?
无锁不能 :等等,我看看启动流程…… 发现问题了!
小白 :哪里不对?
无锁不能 :CheckActivated 是在运行时 才调用的,但 activated_ 这个标志是在启动时 由 Activate_from_file() 直接设置的。你的 patch 没覆盖到那个路径。
启动验证流程
程序启动 → Feedback::activate_from_file()
→ VerificationClient::Activate_from_file()
→ 读取 codepart1 文件(不存在)
→ 返回 false
→ activated_ = false (写了 Feedback 的成员变量)
→ QML 读取 activated 属性 → false → 弹窗!
小白 :原来 activated_ 是 Activate_from_file 写的,不是 CheckActivated 写的!怪不得打补丁没用。
无锁不能 :没错。CheckActivated 只是运行时校验完整性,真正的 activated_ 值在启动时就被 Activate_from_file() 设成了 false。
第四章 追根溯源:QML 属性读取路径
无锁不能 :让我追一下 QML 是怎么读到 activated 这个属性的。
通过分析 Feedback::qt_static_metacall 中的 ReadProperty case 3,找到了 QML 读取 activated 属性的实现:
Feedback::qt_static_metacall case 3(0x140048A87)
140048A87: 44 38 8E 30 01 00 00 cmp [rsi+130h], r9b ; froce_activated_ == 0 ?
140048A8E: B0 01 mov al, 1 ; 先假设已激活
140048A90: 75 07 jnz short +7 ; 如果 froce_activated_ !=0 → 跳过
140048A92: 0F B6 86 31 01 00 00 movzx eax, [rsi+131h] ; 否则读 activated_
140048A99: 88 07 mov [rdi], al ; 返回给 QML
无锁不能 :看到了吗?这里有两个因素决定返回给 QML 的值:
froce_activated_(offset 0x130)如果非零 → 直接返回 true
否则返回 activated_(offset 0x131)→ 启动时被设为 false
小白 :所以就算我 patch 了 CheckActivated,QML 也不走那条路?
无锁不能 :对!QML 的 activated 属性直接读的是 Feedback 对象的成员变量 activated_(offset 0x131),这个值是由 Feedback::activate_from_file() → VerificationClient::Activate_from_file() 设置的。你根本没堵对地方。
第五章 破局:正确的 Patch 方案
无锁不能 :要真正解决问题,需要在 QML 读取属性的路径上动手。
两个方案:
方案 A:改 activate_from_file 强制返回 true
Feedback::activate_from_file 在 0x140023384,把 call 改成直接 mov al, 1:
原始:e8 17 c9 ff ff call VerificationClient::Activate_from_file
改: B0 01 mov al, 1
90 90 90 nop nop nop
方案 B:改 QML 属性读取(推荐)
地址 0x140048A90,将 jnz 改为 jmp,永远跳过读取 activated_:
原始:75 07 jnz short +7 ; 只有在 froce_activated_ !=0 时才跳过
改: EB 07 jmp short +7 ; 永远跳过,直接返回 al=1
无锁不能 :方案 B 更干净,只动 2 个字节,不影响其他逻辑。改完以后,QML 拿到的 activated 永远是 true,未授权对话框就不会弹了。
小白 :明白了!原来我打补丁打错地方了,应该打在属性读取路径上,而不是打在 CheckActivated 上。
第六章 深度:完整注册机制总览
为了不留坑,无锁不能把整个注册认证体系完整逆向了一遍,以下是总结:
核心类 VerificationClient 方法总表
方法
地址
功能
VerificationClient()
0x14001F120
构造函数,加载 RSA 公钥
Activate(code)
0x14001F3C0
激活入口
Activate_from_file()
0x14001FCA0
启动时从文件恢复激活状态
Activate_from_file ()
0x140020B10
实际文件验证逻辑
CheckACode(code)
0x14001FE70
RSA 解密校验注册码 JSON
CheckActivated()
0x1400201F0
运行时验证
Disactivate()
0x140020280
清除激活状态
WriteStupidFiles()
0x140020620
加密写入冗余许可证文件
ReadStupidFiles()
0x1400203C0
读取并解密冗余许可证文件
next_open_dyn1()
0x140021A70
生成新动态挑战值
update_open_dyn1()
0x1400220E0
更新动态挑战值
Feedback::check_activated()
0x1400233F0
QML 层激活状态查询入口
激活流程
用户输入注册码
→ Activate(code)
→ CheckACode(code)
→ toUtf8 → Base64 解码
→ RSA 公钥解密 → 解析 JSON
→ 验证 MC/SD/ED/R/KEY/IV
→ 验证 MC == 本地机器码
→ 验证 R == MD5(open_dyn1_ + secret)
→ 写入 codepart1(或 codepart2)
→ AES-256 -CBC 加密许可证 JSON
→ WriteStupidFiles() 写入 3 个冗余路径
→ activated_ = true
运行时验证
CheckActivated ()
→ MD5 (open_dyn1_ + open_dyn2_ + secret) == open_md5_ ?
两个 secret 常量:
ewl61)r&a+33%7q*vae%qort+9*mouch6gc@@@aq4l3os#+#1h(50 字符)
ewl61)r&ax33%7q*vea%qort+9*mouch6gc@@@ap4l3os#+#1h(50 字符)
动态挑战更新
next_open_dyn1 ():
step1 = MD5 (open_dyn1_ + secret1)
step2 = MD5 (step1 + secret2)
return step2
防篡改检测
在 _Activate_from_file_ 中有三层时间篡改检测:
now < LDT(当前时间小于上次使用时间)
now < 系统上次启动时间
文件修改时间在"将来"
任意一个成立,标记 is_time_chaos_ = 1。
许可证机密文件
WriteStupidFiles 将 AES-256-CBC 加密后的 Base64 数据写入 3 个冗余路径(schema/、action_files/、textures/ 目录),读取时三者必须完全一致才认为有效。
第七章 三种破解方案
方案一:二进制补丁(推荐)
按优先顺序:
补丁 A :0x140048A90 75 07 → EB 07(QML 属性读取始终返回已激活)
补丁 B :0x140020271 0F B6 43 7C → B0 01 90(CheckActivated 返回 true)
补丁 C :0x140023384 将 call 改为 B0 01 90 90 90(activate_from_file 强制返回 true)
方案二:替换 RSA 公钥
将 0x14005F900 处 271 字节的 RSA-1024 公钥替换为自己的密钥对公钥,用私钥签发注册码。
方案三:API Hook
编写 DLL hook RSA_public_decrypt 或 PEM_read_bio_RSA_PUBKEY。
后记
小白 :原来如此!我一开始以为注册验证就是 CheckActivated 一个函数的事,没想到 activated_ 标志有这么复杂的写入路径。
无锁不能 :这就是逆向的乐趣所在。看一个函数 surface 很简单,但顺着数据流追下去,总能发现意想不到的依赖关系。这次的教训是:patch 之前一定要搞清楚数据流的源头在哪里,不要只看目标函数的表面逻辑。
小白 :学到了!数据流分析比单点 patch 重要得多。
参考资料
目标软件:vofa+(vofa+.exe)
分析工具:IDA Pro 8.x + iida-mcp
架构:x64,Qt 框架
版权声明 :本文仅供逆向技术学习交流使用,请勿用于商业用途。
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!