第二十六章:逆向工程中的输入处理:键盘与鼠标输入的 Hook、模拟输入、宏的实现
聊到游戏逆向,输入处理这块儿,说实话,是绕不开的硬骨头。你想想看,不管是做外挂、写脚本,还是搞自动化测试,最终都要跟键盘鼠标打交道。我做了这么多年逆向,见过太多人在输入这块儿翻车——要么 Hook 没挂上,要么模拟输入被游戏检测到,直接封号。
今天我就把这块儿掰开了揉碎了讲清楚。键盘鼠标的 Hook、模拟输入、还有宏的实现,咱们一个一个来。
一、输入 Hook 的核心原理
Hook 说白了,就是拦截。你想在游戏收到按键消息之前,先截胡一把。Windows 下有两种主流方式:全局 Hook 和进程内 Hook。
关键点:全局 Hook 用 SetWindowsHookEx,进程内 Hook 用 Detours 或 MinHook。我个人更推荐进程内 Hook,稳定,不容易被反外挂盯上。
1.1 键盘 Hook 实战
先看一个最简单的键盘 Hook 例子。用 SetWindowsHookEx 挂 WH_KEYBOARD_LL,这是低级键盘钩子,不需要注入 DLL。
// 低级键盘钩子回调
LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION) {
KBDLLHOOKSTRUCT *p = (KBDLLHOOKSTRUCT*)lParam;
// 拦截 A 键
if (p->vkCode == 'A') {
// 这里可以做你想做的事
return 1; // 阻止按键传递
}
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
// 安装钩子
HHOOK hHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc,
GetModuleHandle(NULL), 0);
嗯,这里要注意。低级钩子必须在消息循环里跑,不然钩子会被系统自动卸载。我早期做项目时就踩过这个坑——钩子装上了,但程序一跑就掉,查了半天才发现少了消息泵。
1.2 鼠标 Hook 实战
鼠标 Hook 跟键盘类似,用 WH_MOUSE_LL。我记得有一次做 FPS 游戏的自动瞄准,需要拦截鼠标移动事件,然后自己计算角度再模拟移动回去。
LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION) {
MSLLHOOKSTRUCT *p = (MSLLHOOKSTRUCT*)lParam;
if (wParam == WM_MOUSEMOVE) {
// 获取鼠标坐标
int x = p->pt.x;
int y = p->pt.y;
// 这里可以做坐标修正
}
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
个人经验:游戏里做鼠标 Hook,建议用 SendInput 配合 Hook 一起用。单纯 Hook 不模拟,容易被检测到异常行为。
二、模拟输入的几种方式
模拟输入,说白了就是让游戏以为你真的按了键盘或鼠标。Windows 提供了几种 API,但各有坑。
| 方法 | API | 优点 | 缺点 |
|---|---|---|---|
| SendInput | SendInput() | 最接近真实输入 | 部分游戏会检测 |
| keybd_event | keybd_event() | 简单易用 | 已被多数游戏屏蔽 |
| mouse_event | mouse_event() | 兼容性好 | 精度不如 SendInput |
| 驱动级模拟 | IoWrite | 无法被检测 | 需要驱动签名 |
2.1 SendInput 的正确用法
SendInput 是目前最推荐的模拟方式。但很多人用不对,导致游戏不响应。关键是要构造正确的 INPUT 结构体。
// 模拟按下 W 键
INPUT input = {0};
input.type = INPUT_KEYBOARD;
input.ki.wVk = 'W';
input.ki.dwFlags = 0; // 按下
SendInput(1, &input, sizeof(INPUT));
// 模拟释放 W 键
input.ki.dwFlags = KEYEVENTF_KEYUP;
SendInput(1, &input, sizeof(INPUT));
为什么会这样?因为有些游戏会检查按键的按下和释放时间间隔。你如果瞬间按下释放,游戏会认为这是模拟输入。我建议在按下和释放之间加一个随机延迟,比如 50-100ms。
2.2 鼠标模拟的坑
鼠标模拟比键盘复杂。尤其是 FPS 游戏,它们会直接读取原始输入设备的数据,绕过 Windows 的消息队列。
// 模拟鼠标移动
INPUT input = {0};
input.type = INPUT_MOUSE;
input.mi.dx = 100; // 相对移动
input.mi.dy = 50;
input.mi.dwFlags = MOUSEEVENTF_MOVE;
SendInput(1, &input, sizeof(INPUT));
避坑指南:我曾经在某个竞技游戏里用 SendInput 模拟鼠标移动,结果被检测到异常。后来发现游戏用了 Raw Input API,直接读取硬件数据。解决方案是改用驱动级模拟,或者 Hook Raw Input 的处理函数。
三、宏的实现思路
宏,就是一系列预定义的输入序列。比如一键连招、自动拾取、自动射击。实现宏的核心是:记录输入序列,然后按顺序回放。
3.1 简单的宏录制器
我写过一个简单的宏录制器,原理就是 Hook 键盘鼠标事件,把时间戳和按键码存到列表里。
struct MacroEvent {
DWORD time; // 相对时间(ms)
WORD vkCode; // 虚拟键码
bool isDown; // 按下还是释放
};
std::vector<MacroEvent> macroEvents;
// 录制回调
void OnKeyEvent(WORD vk, bool down) {
static DWORD startTime = GetTickCount();
MacroEvent ev;
ev.time = GetTickCount() - startTime;
ev.vkCode = vk;
ev.isDown = down;
macroEvents.push_back(ev);
}
// 回放宏
void PlayMacro() {
for (auto &ev : macroEvents) {
Sleep(ev.time); // 等待到对应时间
INPUT input = {0};
input.type = INPUT_KEYBOARD;
input.ki.wVk = ev.vkCode;
input.ki.dwFlags = ev.isDown ? 0 : KEYEVENTF_KEYUP;
SendInput(1, &input, sizeof(INPUT));
}
}
嗯,这里有个细节。回放时的时间控制,用 Sleep 不够精确。我后来改成了高精度定时器,用 QueryPerformanceCounter 来计时,误差控制在 1ms 以内。
3.2 游戏中的宏检测与反检测
游戏怎么检测宏?说白了就是看输入模式。人类操作有随机性,宏操作太规律了。比如按键间隔固定、鼠标移动轨迹是直线。
反检测的思路就是加随机化。我一般会在宏里加这些随机因素:
- 按键间隔随机抖动 ±20ms
- 鼠标移动路径加贝塞尔曲线
- 偶尔模拟误触(比如不小心按到其他键)
核心原则:宏越像人,越安全。但永远没有绝对的安全,反外挂也在进化。
四、SVG 知识体系图
下面这张图总结了本章的核心知识结构。输入处理分为三大块:Hook、模拟、宏。它们之间互相配合,构成了完整的输入操控方案。
五、实战中的注意事项
最后聊点实战中的坑。我这些年做逆向,在输入处理上栽过不少跟头。
曾经踩过的坑:
- 用 keybd_event 模拟按键,被某款反外挂直接检测到,封号三天。后来换成 SendInput 才解决。
- 宏回放时忘了加随机延迟,按键间隔完全一致,被游戏后台日志记录,人工审核封禁。
- Hook 鼠标移动时,没有处理好坐标转换,导致游戏内视角乱转,差点被队友举报。
说白了,输入处理这块儿,没有银弹。每个游戏都有自己的检测机制,你需要不断调试、测试。我建议你准备一个测试环境,先用小号试,确认没问题再上大号。
我的习惯:每次写新的输入处理代码,都会先用 Cheat Engine 的调试功能跟踪一下游戏的消息处理流程。看看游戏到底是从哪个 API 接收输入的,然后再决定用哪种方式 Hook 或模拟。
嗯,今天就到这儿。输入处理是逆向工程里最实用的技能之一,掌握了它,你就能操控游戏的行为。多练练,多踩坑,自然就熟了。