So analysieren Sie das mobile MMORPG-Netzwerkprotokoll

Im Laufe der Jahre, in denen ich ein mobiles MMORPG gespielt habe, habe ich einige Erfahrungen im Reverse Engineering gesammelt, die ich in einer Reihe von Artikeln teilen möchte. Beispielthemen:

  1. Analysieren Sie das Nachrichtenformat zwischen Server und Client.
  2. Schreiben einer Höranwendung, um den Spielverkehr auf bequeme Weise anzuzeigen.
  3. Abfangen des Datenverkehrs und dessen Änderung mithilfe eines Nicht-HTTP-Proxyservers.
  4. Die ersten Schritte zu Ihrem eigenen ("Raubkopien") Server.

In diesem Artikel werde ich das Parsen des Nachrichtenformats zwischen dem Server und dem Client erläutern . Interessiert frage ich nach Katze.

Erforderliche Werkzeuge


Um die unten beschriebenen Schritte wiederholen zu können, benötigen Sie:

  • PC (habe ich unter Windows 7/10 gemacht, aber MacOS funktioniert möglicherweise auch, wenn die folgenden Elemente dort verfügbar sind);
  • Wireshark für die Paketanalyse;
  • 010Editor zum Parsen von Paketen nach Vorlage (optional, ermöglicht jedoch die schnelle und einfache Beschreibung des Nachrichtenformats);
  • das mobile Gerät selbst mit dem Spiel.

Darüber hinaus ist es sehr wünschenswert, lesbare Daten aus dem Spiel zur Hand zu haben, z. B. eine Liste von Objekten, Kreaturen usw. mit ihren Kennungen. Dies vereinfacht die Suche nach Schlüsselpunkten in Paketen erheblich und ermöglicht es Ihnen manchmal, die gewünschte Nachricht in einem konstanten Datenstrom zu filtern.

Parsen Nachrichtenformat zwischen Server und Client


Um loszulegen, müssen wir den Datenverkehr des Mobilgeräts sehen. Dies ist ganz einfach (obwohl ich diese offensichtliche Entscheidung schon sehr lange getroffen habe): Auf unserem PC erstellen wir einen Wi-Fi-Zugangspunkt, stellen von einem mobilen Gerät aus eine Verbindung her, wählen die gewünschte Schnittstelle in Wireshark aus - und wir haben den gesamten mobilen Verkehr vor Augen.

Nachdem Sie das Spiel betreten und einige Zeit gewartet haben, damit Anforderungen, die nicht mit dem Spielserver selbst zusammenhängen, gestoppt werden, können Sie das folgende Bild sehen:


Zu diesem Zeitpunkt können wir bereits Wireshark-Filter verwenden, um nur Pakete zwischen dem Spiel und dem Server sowie nur die Nutzdaten anzuzeigen:

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

Wenn Sie an einem ruhigen Ort stehen, fern von anderen Spielern und dem NPC, und nichts tun, können Sie ständig wiederholte Nachrichten vom Server und Client sehen (Größe 76 bzw. 84 Byte). In meinem Fall wurde die Mindestanzahl verschiedener Pakete auf dem Zeichenauswahlbildschirm gesendet.


Die Häufigkeit der Anforderung vom Client ist der von Ping sehr ähnlich. Nehmen wir ein paar Nachrichten zur Überprüfung (3 Gruppen, oben eine Anfrage eines Clients, unten die Antwort eines Servers):


Das erste, was auffällt, ist die Identität der Pakete. Die 8 zusätzlichen Bytes in der Antwort bei der Konvertierung in das Dezimalsystem sind dem Zeitstempel in Sekunden sehr ähnlich: 5CD008F8 16 = 1557137656 10 (vom ersten Paar). Wir schauen auf die Uhr - ja, das ist es. Die vorherigen 4 Bytes stimmen mit den letzten 4 Bytes in der Anforderung überein. Bei der Übersetzung erhalten wir: A4BB 16 = 42171 10 , was ebenfalls der Zeit sehr ähnlich ist, jedoch in Millisekunden. Es stimmt ungefähr mit der Zeit seit dem Start des Spiels überein, und höchstwahrscheinlich ist es das auch.

Es bleiben die ersten 6 Bytes der Anforderung und Antwort zu berücksichtigen. Es ist leicht zu bemerken, dass der Wert der ersten vier Bytes der Nachricht (nennen wir diesen Parameter L ) von der Größe der Nachricht abhängt: Die Antwort vom Server beträgt mehr als 8 Bytes, der Wert von L ebenfalls um 8 erhöht, die Paketgröße ist jedoch in beiden Fällen um 6 Bytes größer als der Wert von L Sie können auch feststellen, dass die beiden Bytes nach L ihren Wert sowohl bei Anforderungen vom Client als auch vom Server beibehalten. Da sich ihr Wert um eins unterscheidet, können wir mit Sicherheit sagen, dass dies der Nachrichtencode C (die zugehörigen Nachrichtencodes werden höchstwahrscheinlich bestimmt nacheinander). Die allgemeine Struktur ist klar genug, um eine minimale Vorlage für 010Editor zu schreiben:

  • erste 4 Bytes - L - Nachrichtennutzlastgröße;
  • nächste 2 Bytes - C - Nachrichtencode;
  • Nutzlast selbst.

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

Daher das Format der Client-Ping-Nachricht: lokale Ping-Zeit senden; Server-Antwortformat: Senden Sie dieselbe Zeit und Uhrzeit des Sendens der Antwort in Sekunden. Es scheint nicht schwierig zu sein, oder?

Versuchen wir, ein Beispiel komplizierter zu machen. Wenn Sie an einem ruhigen Ort stehen und die Ping-Pakete verstecken, können Sie Nachrichten teleportieren und Gegenstände (Fahrzeuge) erstellen. Beginnen wir mit dem ersten. Ich besaß die Spieldaten und wusste, nach welchem ​​Wert des Teleportpunkts ich suchen musste. Für Tests habe ich Punkte mit den Werten 0x2B , 0x67 , 0x6B und 0x1AF . Vergleichen Sie mit den Werten in den Nachrichten: 0x2B , 0x67 , 0x6B und 0x3AF :


Das Durcheinander. Zwei Probleme sind sichtbar:

  1. Werte sind nicht 4 Bytes, sondern unterschiedlich groß;
  2. Nicht alle Werte stimmen mit den Daten aus den Dateien überein. In diesem Fall beträgt der Unterschied 128.

Beim Vergleich mit dem Ping-Format können Sie außerdem einen Unterschied feststellen:

  • unverständlich 0x08 vor dem erwarteten Wert;
  • Ein 4-Byte-Wert, 4 kleiner als L (nennen wir es D Dieses Feld erscheint nicht in allen Nachrichten, was etwas seltsam ist, aber wo es ist, bleibt die Abhängigkeit L - 4 = D erhalten. Zum einen für Nachrichten mit eine einfache Struktur (wie Ping) ist nicht erforderlich, aber auf der anderen Seite - es sieht nutzlos aus).

Ich denke, einige von Ihnen hätten den Grund für die Nichtübereinstimmung der erwarteten Werte bereits erraten können, aber ich werde fortfahren. Mal sehen, was im Handwerk passiert:


Die erwarteten Werte von 14183 und 14285 entsprechen ebenfalls nicht den tatsächlichen 28391 und 28621, aber der Unterschied ist hier bereits viel größer als 128. Nach vielen Tests (auch bei anderen Arten von Nachrichten) stellte sich heraus, dass der Unterschied zwischen dem Wert im Paket umso größer ist, je größer die erwartete Anzahl ist. Was seltsam war, war, dass die Werte bis zu 128 für sich blieben. Verstanden, was ist los? Die offensichtliche Situation ist für diejenigen, die dies bereits erlebt haben, und ich musste diese „Chiffre“ unwissentlich zwei Tage lang zerlegen (am Ende half die Analyse der Werte in binärer Form beim „Hacken“). Das oben beschriebene Verhalten wird als Variable Length Quantity bezeichnet - eine Darstellung einer Zahl, die eine unbestimmte Anzahl von Bytes verwendet, wobei das achte Bit eines Bytes (Fortsetzungsbit) das Vorhandensein des nächsten Bytes bestimmt. Aus der Beschreibung geht hervor, dass das Lesen von VLQ nur in Little-Endian-Reihenfolge möglich ist. Zufälligerweise sind alle Werte in den Paketen in dieser Reihenfolge.

Nachdem wir nun wissen, wie der Anfangswert ermittelt wird, können wir eine Vorlage für den Typ schreiben:

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

Und die Funktion, ein Array von Bytes in einen ganzzahligen Wert umzuwandeln:

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

Aber zurück zur Schaffung des Themas. Wieder erscheint D und wieder 0x08 vor dem sich ändernden Wert. Die letzten zwei Bytes der 0x10 0x01 Nachricht ähneln verdächtig der Anzahl der Handwerksgegenstände, wobei 0x10 eine ähnliche Rolle wie 0x08 aber immer noch unverständlich ist. Aber jetzt können Sie eine Vorlage für dieses Ereignis schreiben:

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

Welches würde so aussehen:


Und dennoch waren dies einfache Beispiele. Es wird schwieriger sein, das Ereignis der Bewegung des Charakters zu analysieren. Welche Informationen erwarten wir? Zumindest die Koordinaten des Charakters, wo er hinschaut, Geschwindigkeit und Zustand (Stehen, Laufen, Springen usw.). Da in der Nachricht keine Zeilen sichtbar sind, wird der Status höchstwahrscheinlich durch enum . Durch Auflisten der Optionen, gleichzeitiges Vergleichen mit den Daten aus den Spieledateien sowie durch zahlreiche Tests können Sie drei XYZ-Vektoren mithilfe dieser umständlichen Vorlage finden:

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

Visuelles Ergebnis:


Die grünen drei erwiesen sich als die Koordinaten des Ortes, die gelben drei zeigen höchstwahrscheinlich, wohin der Charakter schaut und der Vektor seiner Geschwindigkeit, und die letzte einzelne ist der Zustand des Charakters. Sie können konstante Bytes (Markierungen) zwischen den Koordinatenwerten ( 0x0D vor dem X Wert, 0x015 vor Y und 0x1D vor Z ) und vor dem Zustand ( 0x30 ) 0x30 , die in ihrer Bedeutung verdächtig ähnlich zu 0x08 und 0x10 . Nachdem viele Marker von anderen Ereignissen analysiert worden waren, stellte sich heraus, dass sie die Art des darauf folgenden Wertes (die ersten drei Bits) und die semantische Bedeutung bestimmen, d. H. Wenn Sie im obigen Beispiel die Vektoren 0x120F während Sie ihre Markierungen beibehalten ( 0x120F vor den Koordinaten usw.), sollte das Spiel (theoretisch) normalerweise die Nachricht analysieren. Basierend auf diesen Informationen können Sie einige neue Typen hinzufügen:

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

Jetzt wurde unsere Vorlage für Bewegungsnachrichten erheblich reduziert:

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

Ein weiterer Typ, den wir im nächsten Artikel möglicherweise benötigen, sind die Zeilen, denen der Packed Wert ihrer Größe vorangestellt ist:

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

Wenn Sie das Beispielnachrichtenformat kennen, können Sie Ihre Abhöranwendung schreiben, um Nachrichten bequem zu filtern und zu analysieren. Dies ist jedoch das Thema für den nächsten Artikel.

Upd: danke aml für den Hinweis, dass die oben beschriebene Nachrichtenstruktur Protocol Buffer ist , und auch Tatikoma für einen Link zu einem nützlichen verwandten Artikel .

Source: https://habr.com/ru/post/de451512/


All Articles