virtualqueryex 可识别三类危险内存页:①mem_commit+page_execute_readwrite;②mem_commit+page_readwrite且不在已知模块内;③mem_reserve后被virtualprotectex改权限;需结合enumprocessmodules白名单与区间重叠判断规避win10+ allocationbase模糊化。

怎么用 VirtualQueryEx 发现可疑内存页
游戏运行时,外挂常把代码写进进程内存(比如 MEM_COMMIT | MEM_WRITE 的可写页),或把 DLL 注入后改成可执行(PAGE_EXECUTE_READWRITE)。直接扫全内存太慢,但你可以只查那些「不该可写的可执行页」或「刚分配出来的写页」。
关键不是查所有页,而是盯住三类危险组合:MEM_COMMIT + PAGE_EXECUTE_READWRITE、MEM_COMMIT + PAGE_READWRITE 且基址不在已知模块内、MEM_RESERVE 后立刻 VirtualProtectEx 改权限。
- 调用前先用
EnumProcessModules拿到所有已加载模块的地址范围,存成std::vector<:pair size_t>></:pair> - 遍历内存时用
VirtualQueryEx分块查,每次传入上次返回的lpInformation->BaseAddress + lpInformation->RegionSize继续往下走 - 别在主线程里跑全量扫描——卡顿明显,建议每帧只查 2–3MB,用
std::this_thread::sleep_for(1ms)避免占满 CPU - 注意:Windows 10 1809+ 对
VirtualQueryEx返回的AllocationBase做了随机化模糊,不能直接比对,得结合BaseAddress和大小做区间重叠判断
怎么识别非白名单 DLL 的注入行为
DLL 注入不等于恶意,但游戏里绝大多数非白名单 DLL(比如 GameOverlayRenderer.dll 或 discord-rpc.dll)都是外挂入口。重点不是“有没有新 DLL”,而是“谁在没走正常 LoadLibrary 路径的情况下把它塞进来”。
系统级 API 如 LoadLibraryExA、LoadLibraryW 会触发模块加载回调(LdrRegisterDllNotification),但注入者常用 WriteProcessMemory + CreateRemoteThread 绕过——这类操作不会触发回调,但会在目标进程里留下痕迹。
- 启用
LdrRegisterDllNotification监听所有模块加载,记录FullDllName和LoadedBaseAddress,和预置白名单比对(白名单必须含完整路径,不能只比文件名) - 同时轮询
EnumProcessModules,发现新增模块但没收到对应通知 → 极大概率是远程线程注入 - 检查新增模块的
IMAGE_DOS_HEADER和IMAGE_NT_HEADERS是否完整(注入者有时只拷贝 PE 头+部分节,导致SizeOfImage异常小或节表校验失败) - 注意:Steam Overlay、NVIDIA Freestyle 等合法组件也会动态注入,它们通常带签名且路径固定,别一刀切封禁
nvngx.dll或gameoverlayrenderer64.dll
SetWindowsHookEx 和 WH_KEYBOARD_LL 是不是反作弊该拦的
低级钩子本身不危险,危险的是它被用来拦截输入、篡改按键事件流,或者作为注入跳板(通过 hMod 参数把 DLL 强制载入目标进程)。但直接禁掉所有 WH_KEYBOARD_LL 会导致无障碍软件、录屏工具、键盘宏全部失效,玩家投诉爆炸。
真正要盯的是钩子的宿主模块是否异常:比如一个叫 helper.dll 的无签名 DLL,通过 SetWindowsHookEx(WH_KEYBOARD_LL, ...) 注册后,又没出现在 EnumProcessModules 列表里——说明它被手动映射(manual map)进了进程,这是典型外挂手法。
- 用
GetWinEventHookInfo(需 Windows 10 2004+)或遍历HHOOK句柄对应的线程上下文,反查其所属模块基址 - 对每个注册了
WH_KEYBOARD_LL或WH_MOUSE_LL的钩子,检查其回调函数所在的模块是否在白名单中,且是否通过正常LoadLibrary加载 - 如果钩子来自
ntdll.dll或kernel32.dll内存区域(即回调地址落在这些模块的.text段内),基本可判定为 inline hook 或代码洞,立即告警 - 注意:
SetWindowsHookEx返回的HHOOK是全局句柄,不能仅靠GetModuleHandle查——得用GetMappedFileName+VirtualQueryEx定位真实模块路径
为什么自己实现 ReadProcessMemory 校验容易误杀
很多新手想每秒读一次关键内存地址(比如血量、坐标),对比上一帧值是否被改写。这思路没错,但直接裸调 ReadProcessMemory 会撞上三个硬坑:权限问题、时机问题、混淆问题。
外挂早就不硬改数值了,它改的是计算过程——比如把 player.hp = 100 换成 player.hp = (int)(100 * GetObfuscatedFactor()),你读到的还是 100,但实际逻辑已被劫持;更糟的是,有些内存地址本身受硬件断点保护,连续读多次会触发 STATUS_GUARD_PAGE_VIOLATION 导致游戏崩溃。
- 不要只读单个变量,读整个结构体(比如
PlayerState类的 sizeof),再用 CRC32 校验字段布局一致性 - 避免高频轮询:用
WaitForDebugEvent或SetThreadContext+ 单步异常捕获代替轮询,只在关键函数入口/出口打点 - 对指针字段做二级校验:比如
player->weapon是个指针,就顺便读它指向的vtable前 8 字节,看是否匹配已知合法类的虚表地址 - 注意:某些驱动级外挂(如 EvilLyrics、Cheat Engine 的 Kernel Mode Driver)能拦截
ReadProcessMemory并返回伪造数据,此时必须配合用户态内存扫描 + 内核态 CR3 切换检测交叉验证
最麻烦的永远不是“怎么检测”,而是“怎么区分调试器附加、合法工具注入、和真外挂”——这三个行为在 API 层几乎一模一样,最终得靠行为时序(比如 OpenProcess 后立刻 VirtualAllocEx)、模块签名链、以及是否绕过 PatchGuard 的间接调用痕迹来交叉判断。漏掉任意一环,要么放行太多,要么封禁太多。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











