第二十六章:逆向工程中的输入处理:键盘与鼠标输入的 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、模拟、宏。它们之间互相配合,构成了完整的输入操控方案。

输入处理知识体系 输入处理 输入 Hook 模拟输入 宏实现 全局 Hook 进程内 Hook 低级钩子 SendInput keybd_event 驱动级模拟 录制回放 随机化 反检测 三者配合:Hook 拦截 → 模拟输入 → 宏自动化

五、实战中的注意事项

最后聊点实战中的坑。我这些年做逆向,在输入处理上栽过不少跟头。

曾经踩过的坑:

  • 用 keybd_event 模拟按键,被某款反外挂直接检测到,封号三天。后来换成 SendInput 才解决。
  • 宏回放时忘了加随机延迟,按键间隔完全一致,被游戏后台日志记录,人工审核封禁。
  • Hook 鼠标移动时,没有处理好坐标转换,导致游戏内视角乱转,差点被队友举报。

说白了,输入处理这块儿,没有银弹。每个游戏都有自己的检测机制,你需要不断调试、测试。我建议你准备一个测试环境,先用小号试,确认没问题再上大号。

我的习惯:每次写新的输入处理代码,都会先用 Cheat Engine 的调试功能跟踪一下游戏的消息处理流程。看看游戏到底是从哪个 API 接收输入的,然后再决定用哪种方式 Hook 或模拟。

嗯,今天就到这儿。输入处理是逆向工程里最实用的技能之一,掌握了它,你就能操控游戏的行为。多练练,多踩坑,自然就熟了。

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