跳转至

内存修改与代码注入

一次数值搜索

金币显示 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 标志、一个调试器标志或一个未知模块封号。误封成本通常比漏掉一次作弊更高,而且会让检测规则本身更难调整。

防御顺序

  1. 高价值结果放服务器,客户端只提交输入和必要证据。
  2. 购买、领奖、交易使用唯一键和事务,防重复发放。
  3. 安装包、热更清单、代码和资源分别校验来源与完整性。
  4. 运行时检测按阶段采样,把结果作为风险信号,不作为唯一裁决。
  5. 检测失败时允许游戏降级运行,但限制高价值操作。
  6. 日志保留版本、模块来源、服务器校正和请求 ID,便于复盘。

内存加密、混淆和完整性校验都能提高修改成本,但它们都运行在攻击者可以控制的设备上。服务器能拒绝错误结果,才是最终约束。

不建议做的事

  • 在没有驱动签名、兼容性测试和回滚能力时自研内核反作弊。
  • 每帧枚举模块、扫描全部内存或校验整个代码段。
  • 用一份固定哈希覆盖多个平台、多个构建和热更版本。
  • 检测到调试器就直接崩溃,导致误报时无法排查。
  • 把所有封禁条件和证据完整下发到客户端。
  • 认为没有 Root、没有越狱或开了混淆就等于安全。