创建一个监听应用程序以查看移动MMORPG流量

这是有关移动MMORPG网络流量分析的系列文章的第二部分。 样本循环主题:

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

在这一部分中,我将描述侦听应用程序(嗅探器)的创建,该侦听器将使我们能够按事件的类型和来源过滤事件,显示有关消息的信息并有选择地将其保存以进行分析,以及略微进入游戏的可执行文件(“二进制文件”)支持信息,并向应用程序添加协议缓冲区支持。 有兴趣的,我要猫。

所需工具


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

  • Wireshark用于数据包分析;
  • .NET
  • PcapDotNet库,用于与WinPcap一起使用;
  • protobuf-net库,用于处理协议缓冲区。

编写听力应用程序


上一篇文章中我们还记得,游戏通过TCP协议进行通信,并且在会话框架中,它仅通过一个服务器和一个端口进行通信。 为了能够分析游戏流量,我们需要执行以下任务:

  • 拦截移动设备的数据包;
  • 过滤游戏包;
  • 将下一个数据包的数据添加到缓冲区进行进一步处理;
  • 从填充的缓冲区中获取游戏事件。

这些动作在Sniffer类中实现,该类使用PcapDotNet库来拦截数据包。 在Sniff方法中,我们传递适配器的IP地址(实际上,这是为同一网络内的移动设备分配Wi-Fi的PC的地址),移动设备的IP地址和服务器IP地址。 由于最后两个不一致(在监视不同平台和服务器几个月后,事实证明该服务器是从约50个服务器的池中选择的,每个服务器仍具有5-7个可能的端口),我仅传输前三个八位位组。 在IsTargetPacket方法中可以看到此过滤的使用。

 public class Sniffer { private byte[] _data = new byte[4096]; public bool Active { get; set; } = true; private string _adapterIP; private string _target; private string _server; private List<byte> _serverBuffer; private List<byte> _clientBuffer; private LivePacketDevice _device = null; private PacketCommunicator _communicator = null; private Action<Event> _eventCallback = null; public void Sniff(string ip, string target, string server) { _adapterIP = ip; _target = target; _server = server; _serverBuffer = new List<byte>(); _clientBuffer = new List<byte>(); IList<LivePacketDevice> allDevices = LivePacketDevice.AllLocalMachine; for (int i = 0; i != allDevices.Count; ++i) { LivePacketDevice device = allDevices[i]; var address = device.Addresses[1].Address + ""; if (address == "Internet " + _adapterIP) { _device = device; } } _communicator = _device.Open(65536, PacketDeviceOpenAttributes.Promiscuous, 1000); _communicator.SetFilter(_communicator.CreateFilter("ip and tcp")); new Thread(() => { Thread.CurrentThread.IsBackground = true; BeginReceive(); }).Start(); } private void BeginReceive() { _communicator.ReceivePackets(0, OnReceive); do { PacketCommunicatorReceiveResult result = _communicator.ReceivePacket(out Packet packet); switch (result) { case PacketCommunicatorReceiveResult.Timeout: continue; case PacketCommunicatorReceiveResult.Ok: OnReceive(packet); break; } } while (Active); } public void AddEventCallback(Action<Event> callback) { _eventCallback = callback; } private void OnReceive(Packet packet) { if (Active) { IpV4Datagram ip = packet.Ethernet.IpV4; if (IsTargetPacket(ip)) { try { ParseData(ip); } catch (ObjectDisposedException) { } catch (EndOfStreamException e) { Console.WriteLine(e); } catch (Exception) { throw; } } } } private bool IsTargetPacket(IpV4Datagram ip) { var sourceIp = ip.Source.ToString(); var destIp = ip.Destination.ToString(); return (sourceIp != _adapterIP && destIp != _adapterIP) && ( (sourceIp.StartsWith(_target) && destIp.StartsWith(_server)) || (sourceIp.StartsWith(_server) && destIp.StartsWith(_target)) ); } private void ParseData(IpV4Datagram ip) { TcpDatagram tcp = ip.Tcp; if (tcp.Payload != null && tcp.PayloadLength > 0) { var payload = ExtractPayload(tcp); AddToBuffer(ip, payload); ProcessBuffers(); } } private byte[] ExtractPayload(TcpDatagram tcp) { int payloadLength = tcp.PayloadLength; MemoryStream ms = tcp.Payload.ToMemoryStream(); byte[] payload = new byte[payloadLength]; ms.Read(payload, 0, payloadLength); return payload; } private void AddToBuffer(IpV4Datagram ip, byte[] payload) { if (ip.Destination.ToString().StartsWith(_target)) { foreach (var value in payload) _serverBuffer.Add(value); } else { foreach (var value in payload) _clientBuffer.Add(value); } } private void ProcessBuffers() { ProcessBuffer(ref _serverBuffer); ProcessBuffer(ref _clientBuffer); } private void ProcessBuffer(ref List<byte> buffer) { // TODO } public void Suspend() { Active = false; } public void Resume() { Active = true; } } 

太好了,现在我们有两个缓冲区,包含来自客户端和服务器的数据包数据。 回顾游戏和服务器之间事件的格式:

 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">; }; 

基于此,您可以创建一个Event事件类:

 public enum EventSource { Client, Server } public enum EventTypes : ushort { Movement = 11, Ping = 30, Pong = 31, Teleport = 63, EnterDungeon = 217 } public class Event { public uint ID; public uint Length { get; protected set; } public ushort Type { get; protected set; } public uint DataLength { get; protected set; } public string EventType { get; protected set; } public EventSource Direction { get; protected set; } protected byte[] _data; protected BinaryReader _br = null; public Event(byte[] data, EventSource direction) { _data = data; _br = new BinaryReader(new MemoryStream(_data)); Length = _br.ReadUInt32(); Type = _br.ReadUInt16(); DataLength = 0; EventType = $"Unknown ({Type})"; if (IsKnown()) { EventType = ((EventTypes)Type).ToString(); } Direction = direction; } public virtual void ParseData() { } public bool IsKnown() { return Enum.IsDefined(typeof(EventTypes), Type); } public byte[] GetPayload(bool hasDatLength = true) { var payloadLength = _data.Length - (hasDatLength ? 10 : 6); return new List<byte>(_data).GetRange(hasDatLength ? 10 : 6, payloadLength).ToArray(); } public virtual void Save() { var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Packets", EventType); Directory.CreateDirectory(path); File.WriteAllBytes(path + $"/{ID}.dump", _data); } public override string ToString() { return $"Type {Type}. Data length: {Length}."; } protected ulong ReadVLQ(bool readFlag = true) { if (readFlag) { var flag = _br.ReadByte(); } ulong vlq = 0; var i = 0; for (i = 0; ; i += 7) { var x = _br.ReadByte(); vlq |= (ulong)(x & 0x7F) << i; if ((x & 0x80) != 0x80) { break; } } return vlq; } } 

Event类将用作游戏中所有事件的基类。 这是Ping事件的示例类:

 public class Ping : Event { private ulong _pingTime; public Ping(byte[] data) : base(data, EventSource.Client) { EventType = "Ping"; DataLength = 4; _pingTime = _br.ReadUInt32(); } public override string ToString() { return $"Pinging server at {_pingTime}ms."; } } 

现在我们有了事件类,我们可以向Sniffer添加方法了:

 private void ProcessBuffer(ref List<byte> buffer) { if (buffer.Count > 0) { while (Active) { if (buffer.Count > 4) //  4        ... { var eventLength = BitConverter.ToInt32(buffer.Take(4).ToArray(), 0) + 6; // ...    -   .. +  4  + 2    if (eventLength >= 6 && buffer.Count >= eventLength) { var eventData = buffer.Take(eventLength).ToArray(); var ev = CreateEvent(eventData, direction); buffer.RemoveRange(0, eventLength); continue; } } break; } } } private Event CreateEvent(byte[] data, EventSource direction) { var ev = new Event(data, direction); var eventType = Enum.GetName(typeof(EventTypes), ev.Type); if (eventType != null) { try { //     (, <code>Ping</code>). var className = "Events." + eventType; Type t = Type.GetType(className); ev = (Event)Activator.CreateInstance(t, data); } catch (Exception) { //     -   . ev = new Event(data, direction); } finally { } } _eventCallback?.Invoke(ev); return ev; } 

创建一个将触发窃听的表单类:

 public partial class MainForm : Form { private Sniffer _sniffer = null; private List<Event> _events = new List<Event>(); private List<ushort> _eventTypesFilter = new List<ushort>(); private bool _showClientEvents = true; private bool _showServerEvents = true; private bool _showUnknownEvents = false; private bool _clearLogsOnRestart = true; private uint _eventId = 1; private void InitializeSniffer() { _sniffer = new Sniffer(); _sniffer.AddEventCallback(NewEventThreaded); _sniffer.Sniff("192.168.137.1", "192.168.137.", "123.45.67."); } private void NewEventThreaded(Event ev) { events_table.Invoke(new NewEventCallback(NewEvent), ev); } public delegate void NewEventCallback(Event ev); private void NewEvent(Event ev) { ev.ID = _eventId++; _events.Add(ev); LogEvent(ev); } private void LogEvent(Event ev) { if (FilterEvent(ev)) { var type = ev.GetType(); events_table.Rows.Add(1); events_table.Rows[events_table.RowCount - 1].Cells[0].Value = ev.ID; events_table.Rows[events_table.RowCount - 1].Cells[1].Value = ev.EventType; events_table.Rows[events_table.RowCount - 1].Cells[2].Value = Enum.GetName(typeof(EventSource), ev.Direction); events_table.Rows[events_table.RowCount - 1].Cells[3].Value = ev.ToString(); } } private void ReloadEvents() { events_table.Rows.Clear(); events_table.Refresh(); foreach (var ev in _events) { LogEvent(ev); } } private bool FilterEvent(Event ev) { return ( (ev.Direction == EventSource.Client && _showClientEvents) || (ev.Direction == EventSource.Server && _showServerEvents) ) && (_eventTypesFilter.Contains(ev.Type) || (!ev.IsKnown() && _showUnknownEvents)); } } 

做完了! 现在,您可以添加几个表来管理事件列表(通过它填充_eventTypesFilter )并实时查看(主表events_table )。 例如,我按以下条件过滤( FilterEvent方法):

  • 显示来自客户的事件;
  • 显示来自服务器的事件;
  • 显示未知事件;
  • 显示选定的已知事件。

学习游戏可执行文件


尽管现在可以毫无问题地分析游戏中的事件,但是要进行大量的人工工作不仅要确定所有事件代码的含义,还要确定有效载荷的结构,这将非常困难,尤其是当它根据某些领域而变化时。 我决定在游戏的可执行文件中寻找一些信息。 由于游戏是跨平台的(在Windows,iOS和Android上可用),因此可以使用以下选项进行分析:

  • .exe文件(我位于路径C:/程序文件/ WindowsApps /%appname%/;
  • Apple加密的二进制iOS文件,但是使用JailBreak可以使用Crackulous或类似工具获取解密的版本;
  • Android共享库 它位于路径/ data / data /%app-vendor-name%/ lib /。

不知道要为Android和iOS选择哪种体系结构,我从一个.exe文件开始。 我们将二进制文件加载到IDA中,我们看到了架构的选择。



我们搜索的目的是一些非常有用的行,这意味着计划中不包括反汇编程序,但是为了防万一,请选择“可执行80386”,因为“二进制文件”和“ MS-DOS可执行文件”选项显然不合适。 单击“确定”,等待文件加载到数据库中,建议等待文件分析完成。 通过在左下方的状态栏中显示以下状态,可以发现分析结束:



转到“字符串”选项卡(“查看/打开子视图/字符串”或Shift + F12 )。 线路生成过程可能需要一些时间。 就我而言,找到了约47k条线。 字符串位置的地址的前缀为.data.rdata 形式。 就我而言,所有“有趣”行都位于.rdata节中,其大小约为44.5k条记录。 查看表格,您可以看到:

  • 登录阶段的错误消息和查询段;
  • 游戏,游戏引擎,界面的错误字符串和初始化信息;
  • 很多垃圾;
  • 客户端的游戏桌清单;
  • 游戏中游戏引擎的使用价值;
  • 效果清单;
  • 接口本地化键的大量列表;

最后,我们正在寻找接近表格末尾的内容。



这是客户端和服务器之间的事件代码的列表。 解析游戏的网络协议时,这可以简化我们的生活。 但是我们不会止步于此! 有必要检查是否有可能以某种方式获得事件代码的数值。 我们从以前的代码CMSG_PINGSMSG_PONG看到“熟悉的”,分别是代码30( 1E 16 )和31( 1F 16 )。 双击该行以转到代码中的该位置。



实际上,紧接在代码的字符串值之后的是0x10 0x1E0x10 0x1F的序列。 好吧,这意味着您可以解析整个表并获取事件及其数值的列表,这将进一步简化协议的分析。

不幸的是,游戏的Windows版本比移动版本落后很多版本,因此.exe中的信息无关紧要,尽管它可以提供帮助,但您不应完全依赖它。 接下来,我决定使用Android研究动态库,正如我在一个论坛上看到的那样,与iOS二进制文件不同,该论坛包含许多有关类的元信息。 但可惜的是,在CMSG_PINGCMSG_PING进行的搜索未返回结果。

毫无希望,我正在iOS二进制文件中进行相同的搜索-令人难以置信,但事实证明与.exe中的数据相同! 将文件上传到IDA。



我选择第一个建议的选项,因为我不确定需要哪个。 同样,我们正在等待文件分析的结束(二进制文件.exe的大小几乎增加了4倍,分析时间当然也增加了)。 我们打开一个带线的窗口,这次结果是51k。 通过Ctrl + F我们寻找CMSG_PING而...我们找不到它。 逐字符输入代码,您会注意到以下结果:



由于某种原因,IDA将整个Opcode.proto对象放在一行中。 双击代码中的该位置,可以看到该结构的描述与.exe文件中的描述相同,因此可以对其进行剪切并将其转换为Enum

最后,值得回想一下aml在上一篇文章的评论中如何建议游戏的消息结构是Protocol Buffers的实现。 如果仔细查看二进制文件中的代码,您会发现Opcode说明也采用这种格式。



我们将为010Editor编写一个解析器模板,以获取所有代码值。

更新了010Editor的打包*类型代码
较小的类型更改包含字段标签验证以跳过丢失的标签。

 uint PeekTag() { if (FTell() == FileSize()) { return 0; } Varint tag; FSkip(-tag.size); return tag._ >> 3; } struct Packed (uint fieldNumber) { if (PeekTag() != fieldNumber) { break; } Varint key <bgcolor=0xFFBB00>; local uint wiredType = key._ & 0x7; local uint field = key._ >> 3; local uint size = key.size; switch (wiredType) { case 1: double value; size += 8; break; case 5: float value; size += 4; break; default: Varint value; size += value.size; break; } }; struct PackedString(uint fieldNumber) { if (PeekTag() != fieldNumber) { break; } Packed length(fieldNumber); char str[length.value._]; }; 


 struct Code { Packed size(2) <bgcolor=0x00FF00>; PackedString code_name(1) <bgcolor=0x00FF00>; Packed code_value(2) <bgcolor=0x00FF00>; Printf("%s = %d,\n", code_name.str, code_value.value._); //        Enum }; struct Property { Packed size(5) <bgcolor=0x00FF00>; PackedString prop_name(1) <bgcolor=0x00FF00>; while (FTell() - 0x176526B - prop_name.length.value._ < size.value._) { Code codes <name="Codes">; } }; struct { FSkip(0x176526B); PackedString object(1) <bgcolor=0x00FF00>; PackedString format(2) <bgcolor=0x00FFFF>; Property prop; } file; 

结果是这样的:



更有趣! 在对象说明中注意到了pb ? 有必要寻找其他行,突然有很多这样的对象?



结果非常出乎意料。 显然,游戏的可执行文件描述了多种类型的数据,包括服务器与客户端之间的枚举和消息格式。 这是一个描述类型的示例,该类型描述了对象在世界上的位置:



快速搜索发现了两个带有类型描述的大地方,尽管仔细检查可能会发现其他小地方。 切掉它们之后,我用C#编写了一个小脚本,以按文件将描述分开(结构上类似于事件代码列表的描述)-在010Editor中分析它们更容易。

 class Program { static void Main(string[] args) { var br = new BinaryReader(new FileStream("./BinaryFile.partX", FileMode.Open)); while (br.BaseStream.Position < br.BaseStream.Length) { var startOffset = br.BaseStream.Position; var length = ReadVLQ(br, out int size); var tag = br.ReadByte(); var eventName = br.ReadString(); br.BaseStream.Position = startOffset; File.WriteAllBytes($"./parsed/{eventName}", br.ReadBytes((int)length + size + 1)); } } static ulong ReadVLQ(BinaryReader br, out int size) { var flag = br.ReadByte(); ulong vlq = 0; size = 0; var i = 0; for (i = 0; ; i += 7) { var x = br.ReadByte(); vlq |= (ulong)(x & 0x7F) << i; size++; if ((x & 0x80) != 0x80) { break; } } return vlq; } } 

我将不分析用于详细描述结构的格式,因为 它是针对特定游戏的,还是Protocol Buffers普遍接受的格式(如果有人确定,请在评论中指出)。 从我能发现的:

  • 描述也以Protocol Buffers格式出现;
  • 每个字段的描述均包含其名称,数字和数据类型,并为其使用了自己的类型表:
     string TypeToStr (uint type) { switch (type) { case 2: return "Float"; case 4: return "UInt64"; case 5: return "UInt32"; case 8: return "Boolean"; case 9: return "String"; case 11: return "Struct"; case 14: return "Enum"; default: local string s; SPrintf(s, "%Lu", type); return s; } }; 
  • 如果数据类型是枚举或结构,则存在指向所需对象的链接。

好吧,对我们来说,剩下的最后一件事就是使用在侦听应用程序中收到的信息:使用protobuf-net库解析消息。 通过NuGet连接库, using ProtoBuf;添加using ProtoBuf; 您可以创建用于描述消息的类。 以上一篇文章为例:角色移动。 突出显示细分时,格式的格式说明如下所示:



调试输出使您可以从中创建简短描述:

 Field 1 (Type 13): time Field 2 (Struct .pb.CxGS_Vec3): position Field 3 (UInt64): guid Field 4 (Struct .pb.CxGS_Vec3): direction Field 5 (Struct .pb.CxGS_Vec3): speed Field 6 (UInt32): state Field 10 (UInt32): flag Field 11 (Float): y_speed Field 12 (Boolean): is_flying Field 7 (UInt32): emote_id Field 9 (UInt32): emote_duration Field 8 (Boolean): emote_loop 

现在,您可以使用protobuf-net库创建适当的类。

 [ProtoContract] public class MoveInfo : ProtoBufEvent<MoveInfo> { [ProtoMember(3)] public ulong GUID; [ProtoMember(1)] public ulong Time; [ProtoMember(2)] public Vec3 Position; [ProtoMember(4)] public Vec3 Direction; [ProtoMember(5)] public Vec3 Speed; [ProtoMember(6)] public ulong State; [ProtoMember(7, IsRequired = false)] public uint EmoteID; [ProtoMember(8, IsRequired = false)] public bool EmoteLoop; [ProtoMember(9, IsRequired = false)] public uint EmoteDuration; [ProtoMember(10, IsRequired = false)] public uint Flag; [ProtoMember(11, IsRequired = false)] public float SpeedY; [ProtoMember(12)] public bool IsFlying; public override string ToString() { return $"{GUID}: {Position}"; } } 

为了进行比较,这是上一篇文章中相同事件的模板:

 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>; }; 

在继承Event类时,我们可以通过反序列化包数据来覆盖ParseData方法:

 class CMSG_MOVE_INFO : Event { private MoveInfo _message; [...] public override void ParseData() { _message = MoveInfo.Deserialize(GetPayload()); } public override string ToString() { return _message.ToString(); } } 

仅此而已。 下一步是将游戏流量重定向到我们的代理服务器,以进行注入,欺骗和削减程序包。

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


All Articles