Ini adalah bagian kedua dari serangkaian artikel tentang analisis lalu lintas jaringan MMORPG seluler. Contoh topik loop:
- Format pesan parsing antara server dan klien.
- Menulis aplikasi mendengarkan untuk melihat lalu lintas game dengan cara yang nyaman.
- Intersepsi lalu lintas dan modifikasinya menggunakan server proxy non-HTTP.
- Langkah pertama ke server Anda sendiri ("bajakan").
Pada bagian ini saya akan menjelaskan pembuatan aplikasi pendengaran (sniffer), yang akan memungkinkan kita untuk menyaring acara berdasarkan jenis dan sumbernya, menampilkan informasi tentang pesan dan menyimpannya secara selektif untuk dianalisis, dan juga masuk ke file permainan yang dapat dieksekusi ("biner") sedikit. mendukung informasi dan menambahkan dukungan
Protokol Buffer ke aplikasi. Tertarik, saya minta kucing.
Alat Dibutuhkan
Untuk dapat mengulangi langkah-langkah yang dijelaskan di bawah ini, Anda perlu:
- Wireshark untuk analisis paket;
- .NET
- Pustaka PcapDotNet untuk bekerja dengan WinPcap;
- perpustakaan protobuf-net untuk bekerja dengan Protokol Buffer.
Menulis Aplikasi Mendengarkan
Seperti yang kita ingat dari
artikel sebelumnya , game berkomunikasi melalui protokol TCP, dan dalam kerangka sesi melakukan hal ini hanya dengan satu server dan satu port. Untuk dapat menganalisis lalu lintas game, kita perlu melakukan tugas-tugas berikut:
- paket mencegat perangkat seluler;
- saring paket game;
- tambahkan data paket selanjutnya ke buffer untuk diproses lebih lanjut;
- Dapatkan acara game dari buffer saat dihuni.
Tindakan ini diimplementasikan di kelas
Sniffer
, yang menggunakan perpustakaan PcapDotNet untuk mencegat paket. Dalam metode
Sniff
, kami meneruskan alamat IP adaptor (pada kenyataannya, ini adalah alamat PC dari mana Wi-Fi didistribusikan untuk perangkat seluler dalam jaringan yang sama), alamat IP perangkat seluler, dan alamat IP server. Karena ketidakkonsistenan dari dua yang terakhir (setelah berbulan-bulan memonitor berbagai platform dan server, ternyata server dipilih dari kumpulan ~ 50 server, yang masing-masing masih memiliki 5-7 kemungkinan port), saya hanya mengirimkan tiga oktet pertama. Penggunaan pemfilteran ini terlihat dalam metode
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) {
Hebat, sekarang kami memiliki dua buffer dengan paket data dari klien dan server. Ingat format acara antara game dan server:
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">; };
Berdasarkan ini, Anda dapat membuat kelas acara Acara:
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; } }
Kelas
Event
akan digunakan sebagai kelas dasar untuk semua acara dalam game. Berikut adalah contoh kelas untuk acara
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."; } }
Sekarang kita memiliki kelas acara, kita dapat menambahkan metode ke
Sniffer
:
private void ProcessBuffer(ref List<byte> buffer) { if (buffer.Count > 0) { while (Active) { if (buffer.Count > 4)
Buat kelas bentuk yang akan memicu penyadapan:
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)); } }
Selesai! Sekarang Anda dapat menambahkan beberapa tabel untuk mengelola daftar acara (melaluinya
_eventTypesFilter
diisi) dan melihat secara real time (tabel utama
events_table
). Misalnya, saya memfilter menurut kriteria berikut (metode
FilterEvent
):
- perlihatkan acara dari klien;
- tampilkan acara dari server;
- tampilan peristiwa yang tidak diketahui;
- Tampilkan acara yang diketahui diketahui.
Mempelajari eksekusi game
Meskipun sekarang mungkin untuk menganalisis peristiwa permainan tanpa masalah, ada sejumlah besar pekerjaan manual untuk menentukan tidak hanya arti dari semua kode acara, tetapi juga struktur payload, yang akan sangat sulit, terutama jika itu berubah tergantung pada beberapa bidang. Saya memutuskan untuk mencari beberapa informasi dalam file permainan yang dapat dieksekusi. Karena game ini bersifat lintas platform (tersedia untuk Windows, iOS dan Android), opsi berikut tersedia untuk analisis:
- File .exe (saya temukan di jalur C: / Program Files / WindowsApps /% appname% /;
- file iOS biner yang dienkripsi oleh Apple, tetapi dengan JailBreak Anda bisa mendapatkan versi yang didekripsi menggunakan Crackulous atau sejenisnya;
- Pustaka bersama Android Itu terletak di jalur / data / data /% app-vendor-name% / lib /.
Karena tidak tahu arsitektur mana yang harus dipilih untuk Android dan iOS, saya mulai dengan file .exe. Kami memuat biner ke dalam IDA, kami melihat pilihan arsitektur.

Tujuan pencarian kami adalah beberapa baris yang sangat berguna, yang berarti dekompilasi assembler tidak termasuk dalam rencana, tetapi untuk berjaga-jaga, pilih "executable 80386", karena opsi "Binary File" dan "MS-DOS executable" jelas tidak cocok. Klik "OK", tunggu sampai file dimuat ke dalam database, dan disarankan untuk menunggu sampai analisis file selesai. Akhir analisis dapat ditemukan oleh fakta bahwa di bilah status di kiri bawah adalah status berikut:

Pergi ke tab Strings (Lihat / Buka subviews / Strings atau
Shift + F12
). Proses pembuatan garis mungkin memakan waktu. Dalam kasus saya, ~ 47k baris ditemukan. Alamat lokasi string memiliki awalan berupa
.rdata
,
.rdata
, dan
lainnya . Dalam kasus saya, semua baris "menarik" ada di bagian
.rdata
, yang ukurannya ~ 44.5k catatan. Melihat melalui tabel Anda dapat melihat:
- pesan kesalahan dan segmen permintaan selama fase login;
- string kesalahan dan informasi inisialisasi untuk game, mesin game, antarmuka;
- banyak sampah;
- daftar tabel permainan di sisi klien;
- nilai-nilai yang digunakan mesin game dalam game;
- daftar efek;
- daftar besar kunci lokalisasi antarmuka;
- dll.
Akhirnya, semakin dekat ke ujung meja datang apa yang kita cari.

Ini adalah daftar kode acara antara klien dan server. Ini dapat menyederhanakan hidup kita saat mengurai protokol jaringan gim. Tapi kami tidak akan berhenti di situ! Penting untuk memeriksa apakah mungkin untuk mendapatkan nilai numerik dari kode acara. Kita melihat "familiar" dari kode artikel sebelumnya
CMSG_PING
dan
SMSG_PONG
, masing-masing dengan kode 30 (
1E 16
) dan 31 (
1F 16
). Klik dua kali pada baris untuk pergi ke tempat ini dalam kode.

Memang, segera setelah nilai string kode adalah urutan
0x10 0x1E
dan
0x10 0x1F
. Nah, itu berarti Anda dapat mem-parsing seluruh tabel dan mendapatkan daftar peristiwa dan nilai numeriknya, yang selanjutnya akan menyederhanakan analisis protokol.
Sayangnya, versi Windows dari gim ini ketinggalan banyak versi seluler, dan oleh karena itu informasi dari .exe tidak relevan, dan meskipun dapat membantu, Anda tidak boleh bergantung sepenuhnya pada gim tersebut. Selanjutnya, saya memutuskan untuk mempelajari perpustakaan dinamis dengan Android, seperti yang saya lihat di satu forum bahwa di sana, tidak seperti binari iOS, berisi banyak meta-informasi tentang kelas. Namun sayang, pencarian di
CMSG_PING
nilai
CMSG_PING
tidak memberikan hasil.
Tanpa harapan, saya melakukan pencarian yang sama di biner iOS - sulit dipercaya, tetapi data di sana ternyata sama dengan di .exe! Unggah file ke IDA.

Saya memilih opsi yang diusulkan pertama, karena saya tidak yakin mana yang dibutuhkan. Sekali lagi, kami menunggu akhir dari analisis file (biner hampir 4 kali lebih besar dalam ukuran .exe, waktu analisis, tentu saja, juga meningkat). Kami membuka jendela dengan garis, yang kali ini ternyata 51k. Melalui
Ctrl + F
kita mencari
CMSG_PING
dan ... kita tidak menemukannya. Memasukkan karakter kode dengan karakter, Anda dapat melihat hasil ini:

Untuk beberapa alasan, IDA meletakkan seluruh objek
Opcode.proto
dalam satu baris. Klik dua kali pada tempat ini dalam kode dan lihat bahwa strukturnya dijelaskan dengan cara yang sama seperti pada file .exe, sehingga Anda dapat memotongnya dan mengonversinya menjadi
Enum
.
Akhirnya, perlu diingat kembali bagaimana, dalam komentar pada artikel sebelumnya,
aml menyarankan bahwa struktur pesan dari permainan adalah implementasi dari
Protokol Buffer . Jika Anda melihat dengan cermat kode dalam file biner, Anda dapat melihat bahwa deskripsi
Opcode
juga dalam format ini.

Kami akan menulis templat parser untuk 010Editor untuk mendapatkan semua nilai kode.
Diperbarui Dikemas * Jenis Kode untuk 010EditorPerubahan tipe kecil berisi validasi label bidang untuk melewati yang hilang.
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._);
Hasilnya kira-kira seperti ini:

Lebih menarik! Apakah Anda memperhatikan
pb
dalam deskripsi objek? Akan perlu untuk mencari garis lain, tiba-tiba ada banyak objek seperti itu?

Hasilnya sangat tidak terduga. Tampaknya, file yang dapat dieksekusi permainan tersebut menjelaskan banyak jenis data, termasuk enumerasi dan format pesan antara server dan klien. Berikut adalah contoh deskripsi tipe yang menggambarkan posisi suatu objek di dunia:

Pencarian cepat mengungkapkan dua tempat besar dengan deskripsi jenis, meskipun pemeriksaan lebih dekat mungkin akan mengungkapkan tempat-tempat kecil lainnya. Setelah memotongnya, saya menulis skrip kecil di C # untuk memisahkan deskripsi dengan file (dalam struktur ini mirip dengan deskripsi daftar kode acara) - lebih mudah untuk menganalisisnya di 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; } }
Saya tidak akan menganalisis format untuk menggambarkan struktur secara terperinci, karena baik itu khusus untuk game yang dimaksud, atau itu adalah format yang diterima secara umum di
Protocol Buffers
(jika ada yang tahu pasti, harap cantumkan dalam komentar). Dari apa yang bisa saya deteksi:
- deskripsi juga datang dalam format
Protocol Buffers
; - deskripsi setiap bidang berisi nama, nomor, dan tipe datanya, yang digunakan tabel jenisnya sendiri:
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; } };
- jika tipe data adalah enumerasi atau struktur, maka ada tautan ke objek yang diinginkan.
Nah, hal terakhir yang tersisa bagi kita adalah menggunakan informasi yang diterima dalam aplikasi pendengaran kita: pesan parse menggunakan pustaka
protobuf-net
. Hubungkan perpustakaan melalui NuGet, tambahkan
using ProtoBuf;
dan Anda bisa membuat kelas untuk menggambarkan pesan. Ambil satu contoh dari artikel sebelumnya: pergerakan karakter. Deskripsi format yang diformat saat menyorot segmen terlihat seperti ini:

Output debug memungkinkan Anda untuk membuat deskripsi singkat dari ini:
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
Sekarang Anda dapat membuat kelas yang sesuai menggunakan perpustakaan
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}"; } }
Sebagai perbandingan, berikut adalah templat untuk acara yang sama dari artikel sebelumnya:
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>; };
Saat mewarisi kelas
Event
, kita bisa mengganti metode
ParseData
dengan
ParseData
deserialisasi data paket:
class CMSG_MOVE_INFO : Event { private MoveInfo _message; [...] public override void ParseData() { _message = MoveInfo.Deserialize(GetPayload()); } public override string ToString() { return _message.ToString(); } }
Itu saja. Langkah selanjutnya adalah mengarahkan lalu lintas game ke server proxy kami untuk tujuan injeksi, spoofing, dan memotong paket.