Home .NET Creating a listening application to view mobile MMORPG traffic

Creating a listening application to view mobile MMORPG traffic

by admin

This is the second part of a series of articles about parsing mobile MMORPG network traffic.Sample topics of the series :

  1. Parsing the message format between the server and the client.
  2. Writing a listening application to view game traffic in a convenient way.
  3. Traffic capturing and modification using non-HTTP proxy.
  4. First steps to your own ("pirate") server.

In this part I will describe how to create a sniffer that will allow us to filter events according to their type and source, display information about the message and save them for analysis, and I will also go into the game executable ("binary") to find some supporting information and add support for Protocol Buffers into the appendix. Those who are interested, please see under.

Tools required

To be able to repeat the steps described below, you will need :

  • Wireshark for packet analysis;
  • NET;
  • library PcapDotNet For working with WinPcap;
  • library protobuf-net for working with Protocol Buffers.

Writing a listening application

As we recall from previous article , the game communicates via TCP, and within a session it does so only with one server and on one port. To be able to analyze the traffic of the game we need to perform the following tasks:

  • Intercept mobile device packets;
  • filter out the game packets;
  • add the data of the next packet to the buffer for further processing;
  • get game events from the buffers as it fills up.

These actions are implemented in the class Sniffer which uses the PcapDotNet library to capture packets. In method Sniff we pass the IP address of the adapter (in fact it’s the address of the PC from which Wi-Fi is distributed to the mobile device, within the same network), the IP address of the mobile device and the IP address of the server. Due to the volatility of the last two (after months of monitoring different platforms and servers, it turned out that the server is selected from a pool of ~50 servers, each of which has another 5-7 possible ports) I pass only the first three octets. The use of this filtering can be seen in the method 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;}}

Great, now we have two buffers with packet data from the client and the server. Recall the format of events between the game and the server :

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

From this you can create an event class 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;}}

Class Event will be used as a base class for all game events. Here is an example of a class for an 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.";}}

Now that we have the event class, we can add methods to Sniffer :

private void ProcessBuffer(ref List<byte> buffer){if (buffer.Count> 0){while (Active){if (buffer.Count > 4) // The first 4 bytes in the event contain the size of the payload ...{var eventLength = BitConverter.ToInt32(buffer.Take(4).ToArray(), 0) + 6; // ... so the event size is the P.N. size + first 4 bytes + 2 bytes of the event codeIf (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{// Create an instance of the event class (for example, <code> Ping</code> ).var className = "Events." + eventType;Type t = Type.GetType(className);ev = (Event)Activator.CreateInstance(t, data);}catch (Exception){// If there is no special class, we create an instance of the base one.ev = new Event(data, direction);}finally{}}_eventCallback?.Invoke(ev);return ev;}

Let’s create a form class that will run a wiretap :

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.", "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));}}

Done! Now you can add a couple of tables to manage the list of events (it fills _eventTypesFilter ) and live view (main table events_table ). For example, I filtered by the following criteria (method FilterEvent ):

  • Displaying events from the client;
  • showing events from the server;
  • displaying of unknown events;
  • display of selected known events.

Examining the executable file of the game

While it is now possible to analyze game events without problems, there is a lot of manual work to be done to determine not only the meaning of all the event codes, but also the payload structure, which will be quite difficult, especially if it varies depending on some of the fields. I decided to look for some information in the game’s executable file. Since the game is cross-platform (available on Windows, iOS and Android), the following options are available for analysis :

  • exe file (I have it located at C:/Program Files/WindowsApps/%appname%/;
  • iOS binary which is encrypted by Apple, but having JailBreak you can get the decrypted version using Crackulous or similar;
  • Android shared object library.Located at /data/data/%app-vendor-name%/lib/.

Having no idea what architecture to choose for Android and iOS, I started with the .exe file.Load the binary into IDA, see the choice of architectures.
Creating a listening application to view mobile MMORPG traffic
The aim of our search is some very useful strings, so decompiling assembler is not in our plans, but we choose "executable 80386" just in case, because "Binary File" and "MS-DOS executable" obviously don’t fit. Clicking "OK", wait for the file to load into the database, and it is desirable to wait until the end of the analysis of the file. The end of the analysis can be seen in the status bar at the bottom left will be the following state :
Creating a listening application to view mobile MMORPG traffic
Go to the Strings tab (View/Open subviews/Strings or Shift + F12 ). The string generation process can take some time. In my case ~47k strings were found. The string locations have a prefix of the form data , rdata and others In my case, all the "interesting" lines were in the section rdata which size was ~44.5k records. Looking through the table you can see :

  • error messages and request segments in the login phase;
  • error lines and initialization information for the game, game engine, and interfaces;
  • a lot of garbage;
  • list of game tables on the client side;
  • game engine values used in the game;
  • list of effects;
  • huge list of interface localization keys;
  • etc.

Finally, near the end of the table is what we were looking for.
Creating a listening application to view mobile MMORPG traffic
This is a list of event codes between the client and the server. This may make life easier for us when parsing the network protocol of the game. But let’s not stop there! We need to check if we can somehow get the numeric value of the event code. We see the "familiar" codes from the previous article CMSG_PING and SMSG_PONG with codes 30 ( 1E 16 ) and31 ( 1F 16 ) respectively. Double-click on a line to jump to that location in the code.
Creating a listening application to view mobile MMORPG traffic
Indeed, right after the string code values comes the sequence 0x10 0x1E and 0x10 0x1F Great, so we can parse the whole table and get a list of events and their numeric value, which will make the protocol even easier to parse.
Unfortunately, the Windows version of the game is very much behind the mobile versions, and therefore the information from the .exe is not up to date, and although it can help, do not rely entirely on it. Next I decided to look into the Android dynamic library because I saw on one forum that it contained a lot of meta-information about classes, unlike the iOS binary. But alas, searching through the value file CMSG_PING did not give any results.
Without hope, I do the same search in the iOS binary – unbelievably, it has the same data as the .exe! Load the file into the IDA.
Creating a listening application to view mobile MMORPG traffic
I choose the first offered variant, because I’m not sure which one I need. Again we wait for the end of analysis of the file (the binary is almost 4 times larger than .exe, so the analysis time increased too, of course). We open the window with strings, which this time appeared to be 51k. Via Ctrl + F search for CMSG_PING and… we don’t find it. If you enter the code character by character, you will see the following result :
Creating a listening application to view mobile MMORPG traffic
For some reason, IDA has compiled the entire object Opcode.proto into one line. By double-clicking on this place in the code, we see that the structure is described the same way as in the .exe file, so we can cut it out and convert it into Enum
This is where it’s finally worth remembering, as in the comments to last week’s article aml suggested that the game’s message structure is an implementation of Protocol Buffers If you look closely at the code in the binary file, you can see that the description Opcode is also in this format.
Creating a listening application to view mobile MMORPG traffic
Let’s write a template parser for 010Editor to get all code values.
Updated Packed* type code for 010Editor Small changes to the types contain a field label check to skip missing ones.

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._); // Output the value to the console for insertion into 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;

The result is something like this :
Creating a listening application to view mobile MMORPG traffic
It gets more interesting! Noticed pb in the object description? We should look for other lines, in case there are many more such objects?
Creating a listening application to view mobile MMORPG traffic
The results are extremely unexpected. The game’s executable file seems to describe many data types, including enumerations and message formats between the server and the client. Here is an example of a type description describing the position of an object in the world :
Creating a listening application to view mobile MMORPG traffic
A quick search revealed two large places with type descriptions, although a closer examination will surely reveal other small places. After cutting them out, I wrote a small C# script to separate the descriptions by file (similar in structure to the event code list description) – so it’s easier to analyze them in 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;}}

I will not go into detail about the format of the description of the structures, because either it is specific to the game in question, or it is common in Protocol Buffers format (if you know for sure, please specify in comments). From what I could find :

  • the description also comes in the format Protocol Buffers ;
  • the description of each field contains its name, number, and data type, for which its own type table was used :
    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;}};
  • if the data type is an enumeration or a structure, then it was followed by a link to the desired object.

And the last thing we have to do is to use this information in our listening application: parse messages with the library protobuf-net Connect the library via NuGet, add Using ProtoBuf; and you can create classes to describe messages. Let’s take one of the examples from the last article: character movement. The explicit description of the format when the segments are highlighted looks like this :
Creating a listening application to view mobile MMORPG traffic
The debug output allows you to create a short description from this :

Field 1 (Type 13): timeField 2 (Struct .pb.CxGS_Vec3): positionField 3 (UInt64): guidField 4 (Struct .pb.CxGS_Vec3): directionField 5 (Struct .pb.CxGS_Vec3): speedField 6 (UInt32): stateField 10 (UInt32): flagField 11 (Float): y_speedField 12 (Boolean): is_flyingField 7 (UInt32): emote_idField 9 (UInt32): emote_durationField 8 (Boolean): emote_loop

Now you can create a corresponding class using the library 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}";}}

For comparison, here is the template for the same event from last week’s article :

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

When inheriting a class Event we can override the method ParseData by deserializing the package data :

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

That’s it. The next step is to redirect the game traffic to our proxy server in order to inject, spoof and cut packets.

You may also like