11、内存与进程检测(服务器辅助):服务器下发检测指令、客户端内存扫描上报、进程白名单机制

聊到反外挂,内存和进程检测这块,可以说是最经典的攻防阵地了。我个人习惯把这类检测叫做“服务器辅助型检测”——说白了,服务器不直接扫描你的内存,它只负责下指令、收结果、做裁决。真正动手的,是客户端的一个安全模块。

为什么要这么设计?你想想看,如果服务器直接去读客户端的内存,那网络开销得多大?而且每个客户端的操作系统、内存布局都不一样,服务器根本管不过来。所以,把扫描任务下放到客户端,服务器只做“裁判”,这才是工程上可行的方案。

11.1 服务器下发检测指令

服务器怎么通知客户端去扫描?不是靠心跳包里带个flag就完事了。我在项目中遇到过,有些外挂会直接篡改心跳包,把检测指令给过滤掉。所以,指令下发必须做到“不可跳过、不可伪造”。

我常用的做法是这样的:

  • 指令嵌入协议流:检测指令不单独成包,而是混在正常的游戏协议里。比如,在移动同步包的最后4个字节,塞一个加密的检测指令ID。
  • 指令时效性:每条指令带一个时间戳,超过500ms未响应,服务器直接判定异常。
  • 指令签名:用服务器私钥对指令内容签名,客户端用公钥验签。这样外挂就算截获了指令,也没法伪造一条假的。

核心要点:指令下发不是“发出去就完事了”,必须做到“发必达、达必验、验必果”。

举个例子,服务器下发一条“扫描进程列表”的指令,大概长这样:

// 服务器端伪代码
function sendScanCommand(client, commandType) {
    let cmd = {
        type: commandType,          // 0x01 = 扫描进程列表
        timestamp: Date.now(),
        nonce: randomBytes(8),      // 防重放
        signature: null
    };
    cmd.signature = rsaSign(cmd.type + cmd.timestamp + cmd.nonce);
    client.send(cmd);
}

11.2 客户端内存扫描上报

客户端收到指令后,安全模块开始干活。这里有个坑——你直接扫描整个进程内存,性能开销太大了。我见过有些游戏,一扫描就卡顿半秒,玩家直接骂娘。

所以,内存扫描必须“精准打击”。我一般会维护一个敏感内存特征库,只扫描那些外挂常驻的内存区域:

  • 特征码扫描:比如外挂的DLL注入后,内存里会有特定的字符串或字节序列。我们提前提取好特征码,扫描时做模式匹配。
  • 内存保护属性检查:正常代码段是“可执行、不可写”。如果发现某块内存是“可执行+可写”,那基本就是外挂动态生成的代码。
  • 模块基址校验:检查每个加载的DLL的基址是否和PE文件里的一致。不一致?大概率是被HOOK了。

我的经验:扫描频率别太高。我曾经把扫描间隔设成1秒一次,结果玩家CPU占用率直接飙到15%。后来改成“随机间隔+事件触发”,平均5~10秒一次,效果就好多了。

扫描结果怎么上报?直接发明文肯定不行。我建议这样:

// 客户端上报数据结构
{
    "cmdId": "0x01",
    "result": {
        "processList": [
            {"name": "cheat.exe", "pid": 1234, "path": "C:\\cheat\\cheat.exe"}
        ],
        "memoryRegions": [
            {"base": 0x400000, "size": 4096, "flags": "RWX", "hash": "a1b2c3d4"}
        ]
    },
    "signature": "客户端私钥签名",
    "timestamp": 1700000000
}

注意,上报数据一定要签名。不然外挂可以伪造一个“一切正常”的结果来糊弄服务器。

11.3 进程白名单机制

进程白名单,说白了就是“只允许这些进程运行,其他的统统干掉”。这个机制在服务器辅助下,可以做得非常强硬。

我参与过一个项目,白名单机制是这么设计的:

  1. 服务器维护白名单库:里面存了所有允许的进程名、数字签名哈希、文件路径。
  2. 客户端定时枚举进程:每隔几秒,客户端枚举当前所有进程,把进程名、签名信息上报给服务器。
  3. 服务器做裁决:服务器比对上报的进程列表和白名单库。发现不在白名单里的进程,直接下发“踢下线”指令。

注意:白名单不是万能的。有些外挂会把自己的进程名改成“svchost.exe”这种系统进程名来伪装。所以,光靠进程名不够,必须加上数字签名校验。

我曾经遇到过一种外挂,它把自己的DLL注入到游戏进程里,然后进程名还是游戏本身的exe。白名单根本拦不住。后来我们加了一条规则:检查游戏进程的模块列表,只允许加载白名单里的DLL。这才把问题解决。

嗯,这里要注意,白名单机制不能太死板。比如玩家开了个“语音聊天工具”,它的进程名不在白名单里,你直接把人踢了,那玩家不得骂街?所以,白名单要分等级:

等级 处理方式 适用场景
L0 直接踢下线 已知外挂进程
L1 弹窗警告,记录日志 未知进程,但行为可疑
L2 仅记录,不处理 常见工具进程(如输入法、录屏软件)

11.4 整体架构图

说了这么多,我画张图帮你理一理整体流程。这张图展示了服务器、客户端安全模块、外挂三者之间的交互关系:

游戏服务器 指令生成器 (签名+时间戳) 白名单库 (进程名+签名哈希) 裁决引擎 (比对+决策) 结果处理器 (踢下线/警告/记录) 游戏客户端 安全模块 (指令解析+验签) 内存扫描器 (特征码+属性检查) 进程枚举器 (枚举+签名采集) 上报模块 (加密+签名上报) 外挂程序 内存篡改 (修改数值/代码) 进程伪装 (改名/注入DLL) 指令拦截 (过滤检测指令) 下发检测指令 上报扫描结果 篡改内存 进程伪装 拦截指令 正常流程 上报流程 外挂攻击

从图里你能看到,整个流程是闭环的:服务器下指令 → 客户端扫描 → 客户端上报 → 服务器裁决。外挂可以在任何一个环节搞破坏,但我们的每一层都有对应的防御措施。

避坑指南:我曾经犯过一个错——让客户端直接把扫描结果明文上报。结果外挂抓包一看,哦,原来你查的是这几个内存地址,直接把我特征码改了。后来我学乖了,上报数据必须加密,而且特征码要动态混淆,每次扫描用的特征码都不一样。

好了,内存与进程检测这块,核心思路就是“服务器指挥,客户端执行,结果上交给裁判”。别想着客户端能完全防住外挂,那不可能。但只要你的检测指令够隐蔽、扫描够精准、白名单够严格,就能把大部分外挂挡在门外。


公众号:蓝海资料掘金营,微信deep3321