内存修改与代码注入¶
一次数值搜索¶
金币显示 1200。玩家搜索 1200,花掉 200 后继续筛选 1000,通常就能缩小到几个地址。改成 999999,如果购买逻辑直接读这个值,修改立即生效。
第一次搜索失败不代表数值受保护。界面显示的是 1200,内存里可能是 1200.0f、定点数、经过缩放的整数,或者只是某个结构体里的一个字段。攻击者会改用未知值扫描,观察“增加、减少、不变”,再结合指针和结构特征定位。
这类修改的关键不在工具,而在客户端是否把本地值当成最终结果。只要客户端能决定余额、伤害或掉落,内存保护只能延长时间,不能改变结论。
地址为什么会变¶
修改器每次启动都要重新找地址,常见原因有:
- 模块加载基址受 ASLR 影响。
- 场景加载、对象池和堆分配顺序改变。
- 对象创建和销毁导致地址复用。
- 字符串、资源和配置的路径随版本变化。
- 游戏更新后结构体字段和函数指令发生变化。
因此,稳定定位通常不是记一个绝对地址,而是记“模块基址 + 偏移”、多级指针链,或者一段不容易重复的字节特征。
数值扫描¶
常见扫描方式解决的问题不同:
| 方法 | 用法 | 容易失败的地方 |
|---|---|---|
| 精确值 | 已知血量为 87,直接搜索 | 浮点、定点、压缩存储,显示值和内存值不一致 |
| 变化筛选 | 搜索未知值,受伤后筛选“减少” | 数值同时被多个 UI 和缓存副本读取 |
| 不变筛选 | 排除场景切换时不断变化的数据 | 结果集仍然很大,需要结合对象结构 |
| 指针扫描 | 从当前地址反推模块基址和偏移链 | 重启、版本更新或对象池变化后失效 |
| AOB 扫描 | 按函数开头或结构体附近的字节特征查找 | 编译器版本、优化和热更会改变字节 |
浮点数扫描还要设置容差。显示 1200,内存可能是 1199.9998,尤其经过移动、伤害或资源转换后。攻击者不会只扫一次,而是让数值发生变化,再用两次结果求交集。
结构体和对齐也会影响定位。一个字段旁边可能是指针、对象 ID、冷却计时和网络同步字段;改中显示值不一定生效,实际逻辑可能读另一个副本。理解这些以后,防护目标就不该是“让地址搜不到”,而是让最终结果不依赖单个可改字段。
内存加密改变不了什么¶
把金币异或一个 key,或者放进自定义结构,精确值搜索会失效。但还有几条路:
- 搜索一个未知值,消费金币后筛选变化。
- Hook 解密函数,在返回值处读明文。
- 找到 UI 或购买逻辑,直接改显示和传入参数。
- 保存密文和解密逻辑,下一次启动直接复用。
key 最终会出现在进程里。即使拆成多段、按字段派生子 key,解密结果仍然要在某个位置变成可用的整数或浮点值。内存加密适合提高直接扫描成本,适合字符串和配置,不适合作为经济系统的最后保障。
Hook 改的是控制流¶
Hook 不只是“改一个值”,而是让游戏执行攻击者的代码。常见位置有三类:
- IAT/GOT Hook:修改导入表,让某个被调用的函数指向自己的实现。只影响通过该导入项发起的调用。
- Inline Hook:改写函数开头几条指令,跳到自己的代码,再执行被覆盖的指令并返回。要处理指令长度、线程暂停和缓存刷新。
- 运行时 Hook:修改 IL2CPP 方法指针、Mono 方法表或脚本虚拟机的分发表。它绕过了前两种对具体指令的依赖,但强依赖运行时版本。
这也是为什么“校验一段代码哈希”不能覆盖所有情况。IAT Hook 没改目标函数,运行时 Hook 可能只改数据表,热更新还会让代码地址整体变化。
注入路径¶
Windows¶
外部进程修改游戏,通常先获得与游戏相同或更高的权限,然后依次做:
OpenProcess
→ VirtualAllocEx
→ WriteProcessMemory
→ CreateRemoteThread 或修改线程上下文
→ LoadLibrary / 手动映射 / 执行 shellcode
OpenProcess 需要相应访问权限,受进程完整性级别、权限和缓解策略影响。DLL 注入只是其中一种方式,手动映射可以不留下正常模块记录,线程也可以从私有可执行内存开始。1
进程缓解策略能提高部分做法的成本。例如 ACG 限制动态代码生成,CIG 限制未签名二进制,CFG 限制间接调用目标。它们不是反作弊的完整方案,开启前要确认 Unity 版本、原生插件、热更和崩溃收集是否兼容。2
Android¶
普通应用不能直接读别的应用内存。Root、调试版本、同 UID 或系统权限会改变限制。常见入口有:
- Frida 通过调试或注入方式附加进程。
- LSPosed 在 Java 和部分 native 调用路径上挂钩。
- 重打包后直接加入
.so或修改启动代码。 - 虚拟多开、容器和模拟器改变进程与文件环境。
- 悬浮窗、无障碍和录屏被用来读界面或模拟输入,这条路不一定需要读内存。
Frida 和 LSPosed 都是合法开发和测试工具。看到相关字符串、端口或模块名只能说明环境可能被修改,不能单独证明作弊。
移动端篡改、Hook 和环境检测的验证方法,可以对照 OWASP MASTG 的分类。4
iOS¶
非越狱设备的代码签名和沙箱限制更强。越狱环境可以注入 dylib、附加调试器、修改进程内存;重签名安装则更多用于改包和替换资源。
iOS 上不要依赖一个固定路径或进程名判断越狱。越狱工具会隐藏文件、挂载和 URL scheme,设备也可能处于开发或企业分发环境。能作为服务端判断依据的,是 App Attest 这类平台证明和账号行为,而不是客户端自己报一个 jailbroken=true。3
检测的误报和成本¶
未知模块¶
枚举进程模块很便宜,但“未知模块”不等于作弊。输入法、录屏、直播、显卡覆盖层、杀毒软件和崩溃收集都可能加载到游戏进程。Windows 上还可以手动映射模块,使其不出现在正常模块链表里。
更可靠的判断需要组合:
- 模块是否有有效签名,签名者是谁。
- 模块路径是否在临时目录、用户下载目录或可写目录。
- 内存区域是否可执行但没有对应文件。
- 线程起始地址是否落在未知或私有内存。
- 同一构建、同一账号是否重复出现同一模块。
即便如此,也只能作为风险信号。把模块列表当白名单会频繁误伤,尤其是带覆盖层的直播和辅助功能软件。
调试器¶
IsDebuggerPresent 和 CheckRemoteDebuggerPresent 只能发现一部分附加方式。内核调试、隐藏调试端口和先附加再 Hook 检测函数都可能绕过。发布构建误带 get-task-allow、崩溃分析工具或开发插件也会产生误报。
调试器检测适合在启动、登录和进入比赛时采样。检测到以后先记录,再决定是否限制高价值操作,不要直接闪退。闪退会让正常崩溃排查和客服处理都失去线索。
代码段完整性¶
对关键函数做哈希,可以发现一部分 Inline Hook。但必须先解决几个问题:
- 代码地址在 ASLR 后变化,哈希范围要基于模块映射计算。
- 热更新程序集和原生插件有自己的版本,不能共用一份固定哈希。
- 保护壳、JIT、重定位和平台加载器可能修改内存页。
- 只校验一段代码,无法发现 IAT Hook 和运行时方法表修改。
比较实际的做法是按 buildId + 平台 + 架构 生成基线,保存服务器版本,客户端只上报差异页和模块信息。日志里记录采样地址、原始字节范围和对应模块,不要只上报一个 modified=true。
检测频率¶
全量模块枚举、内存页扫描和代码哈希不适合放在每帧。一次扫描可能触发页错误、占用 CPU、造成卡顿,还会和加载、热更、场景切换互相干扰。
可以分阶段采样:
| 时机 | 适合检查 | 注意事项 |
|---|---|---|
| 启动早期 | 模块、调试权限、签名、Root/越狱 | 要避开热更和插件初始化 |
| 登录后 | 设备证明、会话状态、异常输入 | 平台证明失败不等于作弊 |
| 场景切换 | 新加载模块、可执行内存、关键代码页 | 加载器可能临时映射合法模块 |
| 比赛结算 | 行为统计、重复请求、异常结果 | 不在战斗帧里做重扫描 |
采样频率和 CPU 预算要按目标设备测。低端手机上,几毫秒的检测也可能变成明显掉帧。检测逻辑本身要有超时和降级,不能因为扫描失败就阻止正常登录。
检测结果怎么用¶
一个信号只能说明“这里和常见环境不同”。更有用的做法是把版本、设备、模块来源、服务器结果和账号历史放在一起看。
{
"buildId": "1.8.3.20261114.1",
"platform": "windows",
"sessionId": "s_7f2c",
"sample": "match_start",
"signals": [
{
"kind": "unbacked_executable_memory",
"module": null,
"firstSeenTick": 12840
},
{
"kind": "unsigned_module",
"path": "%TEMP%\\overlay.dll"
}
],
"serverChecks": {
"damageRejected": 3,
"movementCorrections": 18
}
}
serverChecks 比客户端信号更重要。移动频繁被校正、伤害经常超出武器上限、领奖请求重复,这些是服务器已经观察到的事实。客户端检测只解释“为什么可能出现这些结果”。
响应可以分级:
- 低风险:记录,继续游戏,不提示具体命中项。
- 中风险:限制交易、排行榜和排位,要求重新验证。
- 高风险且服务器结果异常:进入人工复核,保留回放和请求日志。
- 确认作弊:按账号规则处理,保留申诉入口。
不要只凭一个 Root 标志、一个调试器标志或一个未知模块封号。误封成本通常比漏掉一次作弊更高,而且会让检测规则本身更难调整。
防御顺序¶
- 高价值结果放服务器,客户端只提交输入和必要证据。
- 购买、领奖、交易使用唯一键和事务,防重复发放。
- 安装包、热更清单、代码和资源分别校验来源与完整性。
- 运行时检测按阶段采样,把结果作为风险信号,不作为唯一裁决。
- 检测失败时允许游戏降级运行,但限制高价值操作。
- 日志保留版本、模块来源、服务器校正和请求 ID,便于复盘。
内存加密、混淆和完整性校验都能提高修改成本,但它们都运行在攻击者可以控制的设备上。服务器能拒绝错误结果,才是最终约束。
不建议做的事¶
- 在没有驱动签名、兼容性测试和回滚能力时自研内核反作弊。
- 每帧枚举模块、扫描全部内存或校验整个代码段。
- 用一份固定哈希覆盖多个平台、多个构建和热更版本。
- 检测到调试器就直接崩溃,导致误报时无法排查。
- 把所有封禁条件和证据完整下发到客户端。
- 认为没有 Root、没有越狱或开了混淆就等于安全。