吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1724|回复: 8
收起左侧

[原创] 绕过 Themida VM 保护的函数 (不需要脱壳、不需要修 VM)

[复制链接]
lvgcom 发表于 2026-6-5 16:42
本帖最后由 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 星陨

免费评分

参与人数 2吾爱币 +2 热心值 +2 收起 理由
junjia215 + 1 + 1 用心讨论,共获提升!
helian147 + 1 + 1 谢谢@Thanks!

查看全部评分

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

Jx29 发表于 2026-6-5 22:34
ai写的吧
Hou 发表于 2026-6-5 21:07
Themida的授权系统设计真这么傻的吗?几个ret就能搞定...?
跟小白写的 bool 正版=登录(卡号)有啥区别
heartingrass 发表于 2026-6-6 09:40
oranges 发表于 2026-6-6 14:37
出现这种问题,不是壳的问题,而是会不会加壳的问题。
如果加壳的人把主要逻辑代码放入VM中处理和判断,还是很难去分析的。
 楼主| lvgcom 发表于 2026-6-6 19:56
oranges 发表于 2026-6-6 14:37
出现这种问题,不是壳的问题,而是会不会加壳的问题。
如果加壳的人把主要逻辑代码放入VM中处理和判断,还 ...

是的,VM这个我还没有得研究
 楼主| lvgcom 发表于 2026-6-6 20:01
Hou 发表于 2026-6-5 21:07
Themida的授权系统设计真这么傻的吗?几个ret就能搞定...?
跟小白写的 bool 正版=登录(卡号)有啥区别{:1_92 ...

你也可以验证一下
oranges 发表于 2026-6-14 11:50
lvgcom 发表于 2026-6-6 19:56
是的,VM这个我还没有得研究

不过你的思路很好,可以在针对加壳虚拟化不是那么有效时,能够达到自己的逆向目标
c271687388 发表于 2026-6-15 08:19
学习下,感谢分享!!!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-7-10 19:04

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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