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:
- Analysieren Sie das Nachrichtenformat zwischen Server und Client.
- Schreiben einer Höranwendung, um den Spielverkehr auf bequeme Weise anzuzeigen.
- Abfangen des Datenverkehrs und dessen Änderung mithilfe eines Nicht-HTTP-Proxyservers.
- 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:
- Werte sind nicht 4 Bytes, sondern unterschiedlich groß;
- 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);
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>;
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 .