跳转至

一个更棘手的解析器

不过,取决于游戏架构,找到解析器函数可能没那么简单。考虑一个解析器函数看起来像这样的游戏:

packetHandlers[PACKET_HEALTH_CHANGE] = 
onHealthChange;
packetHandlers[PACKET_MANA_CHANGE] = onManaChange;
void parseNextPacket()
{
    if (!network->packetReady()) return;
    auto packet = network->getPacket();
    auto data = packet->decrypt();
    auto handler = packetHandlers[data->getType()];
    handler->invoke(data->getMessage());
}

由于 parseNextPacket() 函数没有 switch() 语句,在内存中没有明显的方法识别它。除非你非常仔细,否则你很可能会在调用栈上直接爬过它。当游戏的解析器函数像这样时,试图弄清楚解析器函数的样子可能毫无意义。如果你在爬升 recv() 调用栈时没有看到 switch() 语句,你必须记下调用栈上的每个被调用者。

不是从断点向上爬调用栈,而是前往 OllyDbg 栈窗格中 ESP 下方标记为 RETURN 的每个地址。这些是每个被调用者返回每个调用者的返回地址。在每个返回地址处,你需要在 OllyDbg 的反汇编窗格中找到调用者的顶部并记下地址。结果是,你会得到通往 recv() 调用的每个函数调用列表。

接下来,从放置在游戏几个处理程序函数上的断点重复相同的列表制作过程。你可以通过监控它必然会使用的内存找到处理程序函数。例如,生命值变化数据包的处理程序会更新内存中的生命值。使用 OllyDbg,你可以在生命值地址上设置内存写入断点。断点触发时,意味着游戏从处理程序函数更新了生命值。这对大多数由服务器控制的值应该同样有效。服务器会控制任何游戏关键值,比如生命值、魔法值、等级、物品等等。

记录 recv() 和几个处理程序函数的调用栈后,你可以关联它们以定位解析器函数。例如,考虑表 10-1 中三个伪调用栈。

recv() 栈onHealthChange() 栈onManaChange() 栈

这些栈展示了在调用 recv() 以及游戏假想的 onHealthChange() 和 onManaChange() 函数期间内存可能的样子。注意每个函数都源自四个常见函数调用的链(用粗体显示)。最深的公共地址 0xDEADBEEF 是解析器的地址。为了更好地理解这个结构,看看以树视图排列的调用栈,如图 10-1 所示。

0x0BADF00D - recv() 0x14141414 - onManaChange()
0x40404040 - network->getPacket() 0x60606060 - 
handler->invoke()
0x101E1337 - onHealthChange()
0x50505050 - handler->invoke()
0xDEADBEEF - parseNextPacket()
0x20202020 - executeFrame()
0x10101010 - main()

图 10-1 三个调用栈的树视图

每个函数的调用栈都从 0xDEADBEEF 函数分支出来,意味着该函数是所有三个调用的公共起点。示例中的 parseNextPacket() 函数负责调用这些函数,所以它必须是 0xDEADBEEF 处最近的公共祖先。

 这些调用栈是假设的,而且比你通常遇到的简化得多。真实调用栈可能有更多函数调用,比较起来也不那么容易。