本帖最后由 lvgcom 于 2026-6-5 16:53 编辑
之前碰到了一个被 Themida VM 保护的函数,脱壳工具跑不通、去虚拟化工具太复杂,想找一种更快的绕过方法。
这篇不教你脱壳,教你绕过去。
准备工作| 工具 | 用途 | 来源 | | x64dbg + ScyllaHide | 附加到 Themida 进程 | x64dbg.com(snapshot 版内置 ScyllaHide) | | Themidie 插件 | 专门过 Themida 反调试 | github.com/VenTaz/Themidie | | Python 3 (可选) | 写 loader 自动打补丁 | python.org | | ProcMon (可选) | 监视网络行为 | docs.microsoft.com/sysinternals |
从哪开始 —— 议题
核心问题:IsLicensed() 返回 False → 程序认为没有许可
根本原因:IsLicensed → CheckValidation → Themida VM(无法静态分析)
解决思路:在 CheckValidation 入口直接改成 return True,不进 VM | 下面按"定位函数 → 确认被 VM 保护 → 找到调用链 → 打补丁 → 验证"展开,每一步都有具体操作。
第 1 步:找到被 VM 保护的函数
1.1 用字符串定位
你要找的是许可相关的函数。在 x64dbg 里:
附加到目标进程(File → Attach → 选目标,ScyllaHide + Themidie 都得启用)
等附加完成后,右键反汇编窗口 → Search for → Current Module → String references
在搜索框输入可能的字符串:license、demo、expir、trial
你可能会看到:
[Asm] 纯文本查看 复制代码 Address String
-------------- ------------------------------
00xxxx500 "TLicManager"
00xxxx528 "LoadLicenseFile"
00xxxx550 "CheckValidation"
00xxxx580 "ActivateLicense"
00xxxx5B0 "你的试用期已到"
00xxxx5D0 "许可验证失败"
跟进 CheckValidation 那个字符串。右键 → Follow in Disassembler。x64dbg 会跳到引用这个字符串的代码处。
1.2 确认函数入口
字符串引用周围几行,你会看到类似这样的代码:
[Asm] 纯文本查看 复制代码 ; 往上翻找到函数入口
.text:????9A0 55 push rbp ; ← 入口在这里
.text:????9A1 48 89 E5 mov rbp, rsp
.text:????9A4 48 81 EC 88 00 00 00 sub rsp, 88h
.text:????9AB 48 8B 05 xx xx xx xx mov rax, [xxxxx] ; 加载 VM 入口
.text:????9B2 FF E0 jmp rax ; ← 进 VM!
记下这个地址(比如 target.exe+0x????9A0),这是你要 patch 的目标。
如果你看到的不是 jmp rax 而是正常的业务逻辑:
要么跟进错了函数
要么这个版本的 Themida 没对 CheckValidation 启用 VM
回字符串表,搜 LMX 或 Licensed,换一个函数试试
第 2 步:确认它真的是 VM 保护
VM 保护的函数有一个非常明显的特征:函数体很短(十几条指令),然后 jmp 到一个远地址。正常的许可验证函数会有几百条指令(日期解析、文件读取、签名比对),不可能只有十几条。
在你的函数入口附近,往下翻。如果你看到:
[Asm] 纯文本查看 复制代码 .text:????9A0 55 push rbp ; 标准序言
.text:????9A1 48 89 E5 mov rbp, rsp
.text:????9A4 48 81 EC 88 00 00 00 sub rsp, 88h ; 分配栈空间
.text:????9AB 48 8B 05 xx xx xx xx mov rax, [rip+...]; 加载 VM dispatcher
.text:????9B2 FF E0 jmp rax ; 跳进 VM 字节码
; ─────────────────────────────────────────────
; 从这往下,如果是 VM 保护:
; - 在 x64dbg 的 disasm 窗口里可能显示为 ???(无法反汇编)
; - 在 IDA 里显示为对齐填充或数据
; - 没有正常的 call/ret/mov 模式
确认了。这就是被 VM 保护的函数。
第 3 步:找到谁在调用它
要搞清楚这个函数的"业务含义"(它到底返回什么、返回值怎么用),需要看调用方。
在 CheckValidation 的入口处右键 → Search for → Current Module → References to this address。你会看到三个交叉引用:
[Asm] 纯文本查看 复制代码 ; 调用方 1
.text:????EFE mov rcx, [TLicManager] ; self
.text:????F02 xor edx, edx ; dl = 0 → 检查许可状态
.text:????F05 call CheckValidation ; ← 这里
; 调用方 2
.text:????F28 mov rcx, [TLicManager]
.text:????F2C mov edx, 1 ; dl = 1 → 检查过期
.text:????F31 call CheckValidation
; 调用方 3
.text:????F52 mov rcx, [TLicManager]
.text:????F56 mov edx, 2 ; dl = 2 → 检查 Demo 模式
.text:????F5B call CheckValidation
三个调用方分别传不同的 dl 值。需要看调用方函数的名字来确认。在调用方函数的入口(push rbp 位置)选中地址,x64dbg 的标签栏可能会显示函数名(如果 PDB 或 map 文件可用);如果没有,去看调用方函数的交叉引用——被哪些 UI 代码调用、被哪些菜单响应调用。
常见的调用场景:
IsLicensed → 主窗口初始化时调用,设为 False 会弹出许可对话框
IsExpired → 定时器调用,设为 True 会在状态栏显示过期提示
IsDemo → 功能入口调用,设为 True 会禁用高级功能
| 到此确认:三个独立的许可检查函数,全部调用同一个 CheckValidation,只是传参不同。
第 4 步:打补丁
4.1 最简单的一刀:patch CheckValidation
因为 CheckValidation 返回 bool(al),且函数入口没有 VM 之前需要保存的状态,所以可以直接在入口改:
[Asm] 纯文本查看 复制代码
55 push rbp ; 原始 (7 bytes):
48 81 EC 88 00 00 00 sub rsp, 88h ; Patch (3 bytes):
B0 01 mov al, 1 ; 硬编码返回 True
C3 ret ; 后面 4 字节不管,反正 ret 之后不会执行到
在 x64dbg 里操作:
选中 CheckValidation 入口的第一条指令(push rbp)
右键 → Binary → Edit(或直接按 Ctrl+E)
输入 B0 01 C3
确认。x64dbg 会自动把后面的字节标为 ??? 或者 NOP,没事。
按 Ctrl+F2 重启程序(或者 F9 继续运行看效果)
如果改完之后程序崩溃:检查你是不是在正确的地址上改的。取消修改(Ctrl+Z 或者不保存),用 Ctrl+G 跳到一个肯定对的地址(比如 kernelbase.IsDebuggerPresent),看反汇编是否正确,排除 x64dbg 反汇编引擎的问题。
4.2 改完一个还不够
改了 CheckValidation,IsLicensed 返回 True 了。但程序可能还在别的地方做检查:
问题 1:底部状态栏还在提示"许可即将过期"。
在 x64dbg 里搜这个字符串,跟进引用,你会找到一个设置 Visible 属性的地方:
[Asm] 纯文本查看 复制代码 mov byte [rbp+0x3F], 1 ; 原始: C6 45 3F 01
; 改成:
mov byte [rbp+0x3F], 0 ; C6 45 3F 00 → 始终隐藏
问题 2:顶部横幅显示红色警告。
跟进横幅控件的初始化代码,找 jne/je 条件跳转:
[Asm] 纯文本查看 复制代码 75 0C jne short @@skip ; 原始: 条件满足时跳过横幅
; 改成:
EB 0C jmp short @@skip ; 无条件跳过 → 横幅始终不显示
问题 3:IsExpired/IsDemo 也要 patch。
它们也调用 CheckValidation,但传参不同。为了安全,直接把它们也 patch 掉:
[Asm] 纯文本查看 复制代码 ; IsExpired → return False
31 C0 xor eax, eax
C3 ret
; IsDemo → return False
31 C0 xor eax, eax
C3 ret
问题 4:启动时有网络行为。
ProcMon 观察到目标启动后有往外连的尝试。不需要分析协议,直接 hosts 文件封掉:
以管理员身份打开记事本,编辑:
C:\Windows\System32\drivers\etc\hosts
在文件末尾加一行:
0.0.0.0 licenses.example.com
域名怎么知道的?ProcMon 里搜 TCP Connect 操作,看 Path 列。如果是域名字符串,直接抄进去。如果是 IP 地址,先 hosts 封 IP 再观察。
4.3 汇总:全部的 patch 点
搞定一个之后发现还有别的,这是正常的。我的建议是先收集——把所有可疑的地方的位置和原始字节记下来——再一口气改了验证。
# ← 是 "注释这一行"
1. IsLicensed: 55 48 81 EC... → B0 01 C3 # 始终 True
2. IsExpired: 55 48 81 EC... → 31 C0 C3 # 始终 False
3. IsDemo: 55 48 81 EC... → 31 C0 C3 # 始终 False
4. 启动联网检查: 09 → 08 # DFM AutoCheck 位
5. 顶部横幅: 75 0C → EB 0C # jne → jmp
6. CheckValidation: 55 48 81 EC... → B0 01 C3 # 根绕过
7. 底部过期提示: C6 45 3F 01 → C6 45 3F 00 # Visible=False
# 8. hosts 文件封锁域名(不是代码 patch,但同样重要)
第 5 步:验证 + 写 loader
5.1 在 x64dbg 里手动验证
全部改完之后,按 F9 让程序继续运行。检查:
。窗口标题是否从"试用模式"变成了"授权正式版"(或者不再有任何"试用"、"demo" 字样)
。底部有无红色警告横幅
。状态栏有无"即将过期"文字
。菜单里的高级功能是否可点击(不再是灰色) |
5.2 写 loader 自动化
因为补丁都在内存里、每次启动都要重新打,所以需要一个 loader。Python ctypes 版本最方便:
[Python] 纯文本查看 复制代码 import ctypes
from ctypes import wintypes
import time
# Win32 API 定义
k32 = ctypes.WinDLL("kernel32")
user32 = ctypes.WinDLL("user32")
# 启动目标(挂起)
si = wintypes.STARTUPINFO()
pi = wintypes.PROCESS_INFORMATION()
k32.CreateProcessW(
r"C:\Path\To\target.exe", None, None, None, False,
0x00000004, # CREATE_SUSPENDED
None, None, ctypes.byref(si), ctypes.byref(pi)
)
# 放行线程
k32.ResumeThread(pi.hThread)
# 等窗口出现 → Themida 解包完成
for _ in range(60): # 最多等 30 秒
if user32.FindWindowW("TMainForm", None):
break
time.sleep(0.5)
time.sleep(0.2) # 额外等 200ms 确保完全解包
# 打补丁
BASE = 0x140000000 # 目标用了哪个 ImageBase?
PATCHES = [
(BASE + 0x????EF0, b"\xB0\x01\xC3"),
(BASE + 0x????F20, b"\x31\xC0\xC3"),
# ... 其他补丁
]
PAGE_EXECUTE_READWRITE = 0x40
for addr, data in PATCHES:
old = wintypes.DWORD()
k32.VirtualProtectEx(pi.hProcess, addr, len(data),
PAGE_EXECUTE_READWRITE, ctypes.byref(old))
written = wintypes.SIZE_T()
k32.WriteProcessMemory(pi.hProcess, addr, data, len(data),
ctypes.byref(written))
print("Done")
注意:不能在 CREATE_SUSPENDED 时 patch。Themida 解包过程中会重写 .text 段,你 Suspended 时打的补丁会被覆盖。必须等解包完成——方法是等窗口出现。
常见踩坑
Q: WriteProcessMemory 返回 Error 487?
代码段没有写权限。先调 VirtualProtectEx 把保护改成 PAGE_EXECUTE_READWRITE(0x40),写完再改回去。
Q: 补丁打上了但标题还是"试用模式"?
可能还有一个你没发现的许可检查点。在标题字符串上设硬件读断点(bph "试用模式",r),运行程序,看标题在哪里被设置。
Q: 程序启动后闪退?
Themida 可能有完整性校验(CRC/hash check)。检查你是不是在解包完成之前 patch 的。如果确认时机正确,试试只 patch CheckValidation 一个点(最小改动),逐步加。
Q: 为什么要 mix 许可文件 + 补丁?
因为 LM-X SDK 的许可状态有两层检查:fLmxStatus(需要许可文件加载成功才能非零)和 CheckValidation(验证 KEY 签名)。只 patch 后者,如果是许可文件不存在/格式错误导致 fLmxStatus=0,IsLicensed 还是 False。所以需要:格式正确的许可文件(随便生成一个) + 所有验证点 patch。
总结
这篇的核心思路不是"打败 Themida VM",而是"绕过去不跟它打"。
| 方案 | 时间 | 难度 | 成果 | | 完整脱壳 | 数周 | 极高 | 独立 exe(可分发 | | VM 去虚拟化 | 数天 | 极高 | 还原的伪代码 | | 入口 patch | 几小时 | 低 | 每次启动需 loader | 入口 patch 的适用范围:返回值是 bool/enum 的函数、有独立入口点、没有副作用。对许可验证类函数几乎百试百灵。
参考文章:[参赛]Themida/Winlicense不用修复VM_OEP脱壳教程 作者:风吹屁屁凉
Themida 2.0.6.5 RISC VM 某一单handle分析 作者:starfall 星陨 |