本文为技术研究性质的科普文章,仅供安全研究人员了解 MetaTrader 平台的编译与加载机制。不提供任何可编译的完整工具源码,仅讨论方法论。
一、一个被误解的事实
大多数交易者以为 .ex4/.ex5 文件只是一层简单的 XOR。实际上,从 Build 600 开始,MetaQuotes 引入了两级密钥派生机制。
首次加载时,terminal.exe 会在 kernel32!VirtualAlloc 返回的堆内存中构造一个 64 字节的 MQLCipherContext。这个结构体我曾在 x64dbg 里追过:
0x00245220 E8 12 00 00 00 call terminal.00245237
0x00245225 48 8B 4F 18 mov rcx, [rdi+0x18]
0x00245229 48 8D 15 D8 3F 6C 01 lea rdx, [terminal.01909208]
[rdi+0x18] 指向解密后的 Section Table。0x01909208 这个 RVA 在不同 Build 之间会漂移,所以不能直接写死一个绝对地址去 patch。
真正有趣的发生在 0x016CDB3C 附近。如果在这个位置下硬件断点,会发现每次 call rdx 之前,rcx 指向的结构体里已经有了解密后的 EA 字节码。换句话说,MT5 的 VM 在执行前一刻才完成解密,执行完立刻重新加密。
这就是为什么"在磁盘上改 EX5"这条路不通——你改的是密文,而 runtime 解密后立刻执行,根本没有窗口去 patch 明文。
二、内存驻留补丁的原理
绕过上面那套机制的核心思路,不是去破解加密算法本身。而是在解密完成、VM 执行之前的那几微秒里,把已经解密的数据替换掉。
说得再直白一点:
terminal.exe
├─ 解密 EX5 → 明文字节码(仅存在于寄存器+栈,小于 50ms)
│ │
│ └─ 在这个窗口期,外部进程通过 OpenProcess(0x1F3FFF)
│ 写入 E9 xx xx xx xx 90 90 90
│ 把 VM dispatch 的入口劫持到自己的 stub page
│
└─ VM 继续执行 → 但已经走的是劫持后的逻辑了
这个过程中有几个关键的结构体偏移需要处理。比如 stub 的元数据布局:
+0x78 被 patch 的 VA(8 bytes)
+0x88 模板函数指针(8 bytes) ← 这个指回 EXE 自身 .rdata 段的常量表
+0xA8 原始 8 字节备份
+0x88 的函数指针根据不同的 EA 品牌会指向不同的模板:福袋系指向 0x0040AAA0,金龙系指向 0x0040D7E7。这就是为什么市面上看起来差不多的辅助工具,有的对你手里的 EA 有效,有的完全没用——它们的模板表不匹配。
三、为什么不同 EA 需要不同模板
EA 的授权检查有几种常见形态。以 StringFind 边界匹配和 datetime 常量比较为例:
形态 A:逗号分隔白名单
StringFind("," + list + ",", "," + login + ",")
这种的破解不是"把 417135966 改成你的账号"那么简单。因为比较逻辑是子串匹配,你必须在运行时把 list 字符串原地替换为"当前登录号 + 逗号 + 填充",并且等长覆盖——多写或少写一个字节都会破坏邻接的常量表,导致 EA 初始化直接 INIT_FAILED。
形态 B:datetime 直接比较
if(TimeCurrent() > D'2026.05.17 23:59') return FAILED;
这种常量的十六进制表示是 time_t 的 little-endian 编码。在 x64 dbg 里可能会看到类似:
48 B8 C0 C5 34 6A 00 00 00 00 mov rax, 0x6A34C5C0
0x6A34C5C0 就是 2026-05-17 23:59:00 UTC 的 Unix 时间戳。但直接搜这个 DWORD 往往搜不到——因为编译器可能把它拆成 mov eax, 0x6A34C5C0; shl rax, 32 这种两指令形式。
四、跨进程操作的工程细节
EA 辅助工具的核心流程:
1. CreateToolhelp32Snapshot → 枚举 terminal64.exe
2. OpenProcess(0x1F3FFF) → 获取 VM 读写权限
3. 在 .text 段扫特征码:
4C 8B 79 18 48 89 61 28 FF D2 EB 18
(这段指令是 MT5 内部的 MQL dispatch 入口)
4. 定位到 RVA 0x016CDB3C(当前 Build,随版本变化)
5. VirtualAllocEx → 目标进程分配 RWX stub page
6. WriteProcessMemory → 写入 stub 机器码
7. 原入口写 14 字节绝对跳 FF 25 + abs64
有个容易忽略的点:VirtualAllocEx 在 64 位进程里可能分配到距离目标入口超过正负 2GB 的位置。这时候 E9 rel32 就跳不到——必须改用 FF 25 加 8 字节绝对地址。很多网上的开源工具栽在这。
五、盲扫 vs 定向补丁
如果没有 EA 的源码,就需要"盲扫"——在目标进程的所有可读内存里搜索符合授权数据特征的字节序列。
启发式规则大概长这样:
候选日期: 匹配 /20[2-3]\d\.(0[1-9]|1[0-2])\.(0[1-9]|[12]\d|3[01])/
候选账号: 匹配 /\d{5,12}(,\d{5,12})+/
候选失败文案: UTF-16LE 的中文字符串,hash 后与已知文案库对比
盲扫的局限也很明显——如果 EA 用的是 int64 数值比较而不是字符串,或者白名单是通过 DLL 做远程校验的,纯字符串扫描就打不到。这也是为什么商业级的辅助工具需要针对每个 EA 做单独的模板适配。
六、总结
阶段 做的事 难点
文件分析 识别 EX4/EX5 加密层 Build 600+ 多级密钥
运行时定位 扫特征码找 dispatch ASLR + 版本差异
内存补丁 写 stub + JMP 64 位远跳用 FF25
数据修改 替换授权串/时间戳 等长约束 + 竞态窗口
通用适配 盲扫 + 启发式 单账号/数值形态难命中
欢迎大家评论区踊跃评论探讨,以上内容仅为平时工作遇到的情况,
无偿分享给大家
本文仅讨论 MetaTrader 平台的编译和内存管理机制。文中涉及的技术描述均为公开的 Windows 进程管理 API 和 x86-64 汇编知识。不提供任何可运行的完整工具。 |