发送数据包¶
游戏绘制帧之前,它会检查键盘和鼠标输入。当它收到导致动作的输入(比如移动或施放法术)时,它会检查动作是否可能,如果可能,告诉游戏服务器动作已经执行。检查事件并提醒服务器的游戏代码通常看起来像这样:
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(),以下是你可能调用它的方式:
由于机器人不会知道这个游戏函数的实际名称,代码把 0xDEADBEEF 处内存的内容赋给一个方便命名的变量:stepActor。然后,代码像调用任何其他函数一样调用 stepActor()。
如果你有正确的地址、函数原型和参数,这会完美工作;你将能够像拥有游戏源代码一样自动化动作。只需确保从与游戏相同的线程内调用动作函数,否则可能会遇到线程问题。最好的方法是从 Direct3D 的 EndScene() 或 Windows API 的 PeekMessage() 等主要函数上的钩子调用动作,因为这些函数通常只从游戏主线程调用。