如何解析移动MMORPG网络协议

在玩一个移动MMORPG的多年中,我在反向工程方面积累了一些经验,我想在一系列文章中分享。 示例主题:

  1. 解析服务器和客户端之间的消息格式。
  2. 编写侦听应用程序以方便的方式查看游戏流量。
  3. 使用非HTTP代理服务器进行流量拦截及其修改。
  4. 到您自己的(“盗版”)服务器的第一步。

在本文中,我将讨论在服务器和客户端之间解析消息格式 。 有兴趣的,我要猫。

所需工具


为了能够重复下面描述的步骤,您将需要:

  • PC(我在Windows 7/10上使用过,但如果有以下各项,MacOS也可以使用);
  • Wireshark用于数据包分析;
  • 010Editor用于按模板解析数据包(可选,但是允许您快速轻松地描述消息格式);
  • 移动设备本身与游戏。

另外,非常希望从手边的游戏中获得可读数据,例如带有其标识符的物体,生物等的列表。 这极大地简化了对包中关键点的搜索,有时它使您可以在恒定的数据流中过滤所需的消息。

解析中 服务器和客户端之间的消息格式


首先,我们需要查看移动设备的流量。 做到这一点非常简单(尽管我很长一段时间以来都做出了这个明显的决定):在我们的PC上,我们创建了一个Wi-Fi接入点,从移动设备连接到它,在Wireshark中选择所需的接口-这样我们就可以看到所有的移动流量。

进入游戏并等待一段时间后,与游戏服务器本身无关的请求将停止,您可以观察以下图片:


在此阶段,我们已经可以使用Wireshark过滤器来仅查看游戏和服务器之间的数据包,以及仅包含有效负载的数据包:

tcp && tcp.payload && tcp.port == 44325 

如果您站在一个安静的地方,远离其他播放器和NPC,却什么也不做,则可以看到服务器和客户端不断重复发送消息(分别为76和84字节)。 就我而言,在字符选择屏幕上发送了最少数量的不同软件包。


客户端发出请求的频率与ping非常相似。 让我们以几则消息进行验证(3组,上面是客户端的请求,下面是服务器的响应):


引起您注意的第一件事是包装的标识。 转换为十进制系统时,响应中的另外8个字节与以秒为单位的时间戳非常相似: 5CD008F8 16 = 1557137656 10 (来自第一对)。 我们检查时钟 -是的。 前4个字节与请求中的后4个字节匹配。 翻译时,我们得到: A4BB 16 = 42171 10 ,这也与时间非常相似,但以毫秒为单位。 它与游戏发布以来的时间大致吻合,很可能是这样。

仍然需要考虑请求和响应的前6个字节。 很容易注意到消息的前四个字节的值(我们称此参数L )与消息的大小有关:服务器的响应超过8个字节, L的值也增​​加了8个,但是,在两种情况下,数据包的大小都比L的值大了6个字节。 您还可以注意到, L后面的两个字节在来自客户端和服务器的请求中都保留了它们的值,并且鉴于它们的值相差一个,我们可以确信地说这是消息代码C (相关的消息代码很可能会确定按顺序)。 总体结构很清晰,足以为010Editor编写一个最小的模板:

  • 前4个字节L消息有效负载大小;
  • 接下来的2个字节-C-消息代码;
  • 有效载荷本身。

 struct Event { uint payload_length <bgcolor=0xFFFF00, name="Payload Length">; ushort event_code <bgcolor=0xFF9988, name="Event Code">; byte payload[payload_length] <name="Event Payload">; }; 

因此,客户端ping消息的格式为:发送本地ping时间; 服务器响应格式:发送时间和发送响应的时间相同,以秒为单位。 似乎并不困难,对吧?

让我们尝试使示例更复杂。 站在一个安静的地方并隐藏ping数据包,您可以找到传送的消息并创建物品(工艺)。 让我们从第一个开始。 拥有游戏数据,我知道该寻找传送点的价值。 对于测试,我使用了值为0x2B0x670x1AF 。 与消息中的值进行比较: 0x2B0x670x3AF


一团糟。 可见两个问题:

  1. 值不是4个字节,但是大小不同;
  2. 并非所有值都与文件中的数据匹配,在这种情况下,差值为128。

此外,与ping格式进行比较时,您会注意到一些区别:

  • 预期值之前0x08理解的0x08
  • 一个4字节的值,比L小4(我们称它为D此字段并非出现在所有消息中,这有点奇怪,但在此位置,保留了L - 4 = D的依赖关系。一方面,对于具有一个简单的结构(例如ping)不是必需的,但另一方面-它看起来没用)。

我想,有些人可能已经猜到了预期值不匹配的原因,但我会继续。 让我们看看工艺中正在发生什么:


14183和14285的预期值也并不对应于实际的28391和28621,但是此处的差值已经比128大得多。经过多次测试(包括其他类型的消息),结果是预期数越大,数据包中的值差越大。 奇怪的是,它们自己最多保留128个值。 知道了,怎么了? 显而易见的情况是对于那些已经遇到过这种情况的人,在不知不觉中,我不得不将这种“代码”分解了两天(最后,对二进制形式的值进行分析有助于“黑客”活动)。 上述行为称为可变长度数量 -表示使用不确定数量的字节的数字,其中字节的第八位(连续位)确定下一个字节的存在。 根据描述,很明显,仅以Little-Endian顺序读取VLQ是可能的。 巧合的是,数据包中的所有值都按该顺序排列。

现在我们知道如何获取初始值,我们可以为该类型编写一个模板:

 struct VLQ { local char size = 1; while(true) { byte obf_byte; if ((obf_byte & 0x80) == 0x80) { size++; } else { break; } } FSeek(FTell() - size); byte bytes[size]; local uint64 _ = FromVLQ(bytes, size); }; 

并将字节数组转换为整数值的函数:

 uint64 FromVLQ(byte bytes[], char size) { local uint64 source = 0; local int i = 0; local byte x; for (i = 0; i < size; i++) { x = bytes[i]; source |= (x & 0x7F) * Pow(2, i * 7); //   <<   , ..     ,  uint32,        uint64 if ((x & 0x80) != 0x80) { break; } } return source; }; 

但是回到主题的创造。 再次出现D并再次在更改值前面出现0x080x10 0x01消息的最后两个字节可疑地类似于制作项目的数量,其中0x10的作用类似于0x08但仍然难以理解。 但是现在您可以为该事件编写模板:

 struct CraftEvent { uint data_length <bgcolor=0x00FF00, name="Data Length">; byte marker1; VLQ craft_id <bgcolor=0x00FF00, name="Craft ID">; byte marker2; VLQ quantity <bgcolor=0x00FF00, name="Craft Quantity">; }; 

看起来像这样:


而且,这些只是简单的例子。 解析角色移动的事件将更加困难。 我们希望看到什么信息? 角色的座标至少要包括他所处的位置,速度和状态(站立,奔跑,跳跃等)。 由于消息中没有可见的行,因此状态很可能通过enum描述。 通过枚举选项,同时将它们与游戏文件中的数据进行比较,以及通过大量测试,您可以使用此繁琐的模板找到三个XYZ向​​量:

 struct MoveEvent { uint data_length <bgcolor=0x00FF00, name="Data Length">; byte marker; VLQ move_time <bgcolor=0x00FFFF>; FSkip(2); byte marker; float position_x <bgcolor=0x00FF00>; byte marker; float position_y <bgcolor=0x00FF00>; byte marker; float position_z <bgcolor=0x00FF00>; FSkip(2); byte marker; float direction_x <bgcolor=0x00FFFF>; byte marker; float direction_y <bgcolor=0x00FFFF>; byte marker; float direction_z <bgcolor=0x00FFFF>; FSkip(2); byte marker; float speed_x <bgcolor=0x00FFFF>; byte marker; float speed_y <bgcolor=0x00FFFF>; byte marker; float speed_z <bgcolor=0x00FFFF>; byte marker; VLQ character_state <bgcolor=0x00FF00>; }; 

视觉效果:


绿色三号是位置的坐标,黄色三号最有可能显示角色的位置和速度矢量,最后一个是角色的状态。 您会注意到坐标值( X值之前的0x0DY之前的0x015Z之前的0x1D )与状态( 0x30 )之前的常量字节(标记),这在含义上与0x080x10相似。 分析了来自其他事件的许多标记后,结果发现它确定了紧随其后的值的类型(前三个位)和语义含义,即 在上面的示例中,如果在保持向量标记(坐标前面的0x120F等)的情况下交换向量,则游戏(理论上)通常应解析该消息。 根据此信息,您可以添加几个新类型:

 struct Packed { VLQ marker <bgcolor=0xFFBB00>; //    VLQ! local uint size = marker.size; //       ( , )          switch (marker._ & 0x7) { case 1: double v; size += 8; break; //     case 5: float v; size += 4; break; default: VLQ v; size += v.size; break; } }; struct PackedVector3 { Packed marker <name="Marker">; Packed x <name="X">; Packed y <name="Y">; Packed z <name="Z">; }; 

现在,我们的运动消息模板已大大减少:

 struct MoveEvent { uint data_length <bgcolor=0x00FF00, name="Data Length">; Packed move_time <bgcolor=0x00FFFF>; PackedVector3 position <bgcolor=0x00FF00>; PackedVector3 direction <bgcolor=0x00FF00>; PackedVector3 speed <bgcolor=0x00FF00>; Packed state <bgcolor=0x00FF00>; }; 

在下一篇文章中,我们可能需要的另一种类型是在其行的Packed值之前的行:

 struct PackedString { Packed length; char str[length.v._]; }; 

现在,了解了示例消息格式,您可以编写侦听应用程序,以方便过滤和分析消息,但这是下一篇文章的主题。

更新:感谢aml提示上述消息结构是Protocol Buffer ,也感谢Tatikoma链接到有用的相关文章

Source: https://habr.com/ru/post/zh-CN451512/


All Articles