在玩一个移动MMORPG的多年中,我在反向工程方面积累了一些经验,我想在一系列文章中分享。 示例主题:
- 解析服务器和客户端之间的消息格式。
- 编写侦听应用程序以方便的方式查看游戏流量。
- 使用非HTTP代理服务器进行流量拦截及其修改。
- 到您自己的(“盗版”)服务器的第一步。
在本文中,我将讨论
在服务器和客户端之间解析消息格式 。 有兴趣的,我要猫。
所需工具
为了能够重复下面描述的步骤,您将需要:
- 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数据包,您可以找到传送的消息并创建物品(工艺)。 让我们从第一个开始。 拥有游戏数据,我知道该寻找传送点的价值。 对于测试,我使用了值为
0x2B
,
0x67
和
0x1AF
。 与消息中的值进行比较:
0x2B
,
0x67
和
0x3AF
:
一团糟。 可见两个问题:
- 值不是4个字节,但是大小不同;
- 并非所有值都与文件中的数据匹配,在这种情况下,差值为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);
但是回到主题的创造。 再次出现
D
并再次在更改值前面出现
0x08
。
0x10 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
值之前的
0x0D
,
Y
之前的
0x015
和
Z
之前的
0x1D
)与状态(
0x30
)之前的常量字节(标记),这在含义上与
0x08
和
0x10
相似。 分析了来自其他事件的许多标记后,结果发现它确定了紧随其后的值的类型(前三个位)和语义含义,即 在上面的示例中,如果在保持向量标记(坐标前面的
0x120F
等)的情况下交换向量,则游戏(理论上)通常应解析该消息。 根据此信息,您可以添加几个新类型:
struct Packed { VLQ marker <bgcolor=0xFFBB00>;
现在,我们的运动消息模板已大大减少:
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链接到有用的相关
文章 。