跳转至

发送数据包

游戏绘制帧之前,它会检查键盘和鼠标输入。当它收到导致动作的输入(比如移动或施放法术)时,它会检查动作是否可能,如果可能,告诉游戏服务器动作已经执行。检查事件并提醒服务器的游戏代码通常看起来像这样:

void processInput() {
    do {
        auto input = getNextInput();
        if (input.isKeyboard())
            processKeyboardInput(input);
        // 处理其他输入类型(例如鼠标)
}
void processKeyboardInput(input) {
    if (input.isKeyPress()) {
        if (input.getKey() == 'W')
            step(FORWARD);
        else if (input.getKey() == 'A')
            step(BACKWARD);
        // 处理其他按键(例如 S  D
    }
}
void step(int direction) {
    if (!map->canWalkOn(player->position))
        return;
    playerMovePacket packet(direction);
    network->send(packet);
}

processInput() 函数每帧被调用。函数遍历待处理输入,并把不同类型的输入分发给相关处理程序。本例中,收到键盘输入时,它被分发给 processKeyboardInput() 函数。这个处理程序然后检查键是 W 还是 S,如果是,就调用 step() 向相应方向移动玩家。

由于 step() 用于执行动作,它被称为动作函数(actor function)。调用动作函数称为执行(actuation)。你可以从机器人直接调用游戏的动作函数来执行动作,完全绕过输入层。

不过,在调用动作之前,你必须找到它的地址。为此,你可以把 OllyDbg 附加到游戏,打开命令行,输入 bp send。这会在 send() 函数上放置断点,该函数用于通过网络发送数据。玩游戏时,每当你迈出一步、施放法术、拾取战利品或做任何其他事情,断点都会触发,你可以记下调用栈中的每个函数。

 游戏应该在你玩游戏时做任何事情都调用 send()。注意每次 send() 断点命中前你做了什么,这会让你粗略了解每次调用向服务器传达什么动作,最终了解你找到的动作负责什么。

有了几个不同的调用栈,你可以比较它们定位动作函数。要看如何发现动作函数,让我们比较图 10-3 中两个带注释的调用栈。

0x70707070  send()
0x60606060  connection->send()
0x50505050  network->send()
0xDEADBEEF  step() 0x0BADF00D  castSpell()
0x40404040  processKeyboardInput()
0x30303030  processInput()
0x20202020  doFrame()
0x10101010  main()

图 10-3 两个动作函数调用栈的树视图

像这两个栈一样,你找到的调用栈在顶部应该相同,共享几个负责通用网络传输的常见函数。它们在底部也应该相同,因为每次调用 send() 都应该源自 processInput() 函数。不过,每个栈在这些相同区域之间应该有一些独特的函数,这些就是你要找的动作函数。通常,感兴趣的函数紧挨在常见网络调用下方。本例中,两个动作是 step() 和 castSpell() 函数。

破解同一款游戏一段时间后,你会知道动作函数在调用栈中离 send() 调用多远。例如,在图 10-3 中,动作恰好发生在 send() 调用之前三次调用。知道这一点,你可以在 send() 断点命中时在 OllyDbg 中爬升栈(ctrl-F9 后跟 F8)三次,进入发送数据的动作函数内部。

找到动作函数后,你可以从注入的 DLL 调用它。如果你在 0xDEADBEEF 找到 step(),以下是你可能调用它的方式:

typedef void _step(int direction);
auto stepActor = (_step*)0xDEADBEEF;
stepActor(FORWARD);

由于机器人不会知道这个游戏函数的实际名称,代码把 0xDEADBEEF 处内存的内容赋给一个方便命名的变量:stepActor。然后,代码像调用任何其他函数一样调用 stepActor()。

如果你有正确的地址、函数原型和参数,这会完美工作;你将能够像拥有游戏源代码一样自动化动作。只需确保从与游戏相同的线程内调用动作函数,否则可能会遇到线程问题。最好的方法是从 Direct3D 的 EndScene() 或 Windows API 的 PeekMessage() 等主要函数上的钩子调用动作,因为这些函数通常只从游戏主线程调用。