إنشاء تطبيق استماع لعرض حركة مرور MMORPG على الهاتف المحمول

هذا هو الجزء الثاني من سلسلة من المقالات حول تحليل حركة مرور شبكة MMORPG المحمول. مواضيع حلقة عينة:

  1. تحليل تنسيق الرسالة بين الخادم والعميل.
  2. كتابة تطبيق استماع لعرض حركة مرور اللعبة بطريقة مريحة.
  3. اعتراض حركة المرور وتعديله باستخدام خادم وكيل بخلاف HTTP.
  4. الخطوات الأولى لخادمك ("المقرصن").

في هذا الجزء سوف أصف إنشاء تطبيق استماع (sniffer) ، والذي سيتيح لنا تصفية الأحداث حسب نوعها ومصدرها ، وعرض معلومات حول الرسالة وحفظها بشكل انتقائي للتحليل ، وكذلك الدخول في الملف القابل للتنفيذ للعبة ("ثنائي") قليلاً معلومات الدعم وإضافة دعم بروتوكول مخازن إلى التطبيق. مهتمة ، أطلب القط.

الأدوات المطلوبة


لتتمكن من تكرار الخطوات الموضحة أدناه ، ستحتاج إلى:

  • Wireshark لتحليل الحزمة ؛
  • . NET.
  • مكتبة PcapDotNet للعمل مع WinPcap ؛
  • مكتبة protobuf-net للعمل مع مخازن بروتوكول.

كتابة طلب الاستماع


كما نتذكر من المقالة السابقة ، تتواصل اللعبة عبر بروتوكول TCP ، وفي إطار الجلسة تقوم بذلك مع خادم واحد فقط ومنفذ واحد. لتكون قادرًا على تحليل حركة مرور اللعبة ، نحتاج إلى تنفيذ المهام التالية:

  • اعتراض الحزم لجهاز محمول ؛
  • حزم لعبة تصفية.
  • إضافة بيانات الحزمة التالية إلى المخزن المؤقت لمزيد من المعالجة ؛
  • الحصول على أحداث اللعبة من المخازن المؤقتة كما يتم ملؤها.

يتم تطبيق هذه الإجراءات في فئة Sniffer ، والتي تستخدم مكتبة PcapDotNet لاعتراض الحزم. في طريقة Sniff ، ننقل عنوان IP للمحول (في الواقع ، هذا هو عنوان الكمبيوتر الذي يتم توزيع Wi-Fi منه على الجهاز المحمول داخل نفس الشبكة) ، وعنوان 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; } } 

سيتم استخدام فئة الأحداث كفئة أساسية لجميع الأحداث في اللعبة. فيما يلي فئة مثال لحدث 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 تعبئة _eventTypesFilter ) وعرضها في الوقت الفعلي (الجدول الرئيسي events_table ). على سبيل المثال ، لقد قمت FilterEvent حسب المعايير التالية (طريقة FilterEvent ):

  • عرض الأحداث من العميل ؛
  • عرض الأحداث من الخادم ؛
  • عرض أحداث غير معروفة ؛
  • عرض الأحداث المعروفة المختارة.

تعلم اللعبة القابلة للتنفيذ


على الرغم من أنه من الممكن الآن تحليل أحداث اللعبة دون مشاكل ، إلا أن هناك قدرًا هائلاً من العمل اليدوي ليس فقط لتحديد معنى جميع رموز الأحداث ، ولكن أيضًا بنية الحمولة الصافية ، والتي ستكون صعبة للغاية ، خاصةً إذا تغيرت وفقًا لبعض الحقول. قررت البحث عن بعض المعلومات في الملف القابل للتنفيذ للعبة. نظرًا لأن اللعبة مشتركة بين الأنظمة الأساسية (متوفرة على Windows و iOS و Android) ، فإن الخيارات التالية متاحة للتحليل:

  • .exe ملف (لقد وجدت على المسار C: / Program Files / WindowsApps /٪ appname٪ /؛
  • ملف ثنائي لنظام iOS تم تشفيره بواسطة Apple ، ولكن مع JailBreak يمكنك الحصول على النسخة المشفرة باستخدام Crackulous أو ما شابه ؛
  • مكتبة أندرويد المشتركة إنه موجود على المسار / البيانات / البيانات /٪ app-vendor-name٪ / lib /.

مع عدم وجود فكرة عن البنية التي يجب أن تختارها لنظامي Android و iOS ، فقد بدأت مع ملف exe. نحمّل الثنائي في المؤسسة الدولية للتنمية ، ونرى اختيار البنى.



الغرض من بحثنا هو بعض الأسطر المفيدة للغاية ، مما يعني أن إلغاء تجميع المجمّع غير مدرج في الخطط ، لكن في الحالة ، حدد "80806 القابل للتنفيذ" ، لأن خيارات "الملف الثنائي" و "MS-DOS القابل للتنفيذ" غير مناسبة بشكل واضح. انقر فوق "موافق" ، انتظر حتى يتم تحميل الملف في قاعدة البيانات ، وينصح بالانتظار حتى يكتمل تحليل الملف. يمكن العثور على نهاية التحليل بحقيقة أنه في شريط الحالة في أسفل اليسار ستكون هناك الحالة التالية:



انتقل إلى علامة تبويب "السلاسل" (عرض / فتح مقاطع فرعية / سلاسل أو Shift + F12 ). قد تستغرق عملية إنشاء الخط بعض الوقت. في حالتي ، تم العثور على خطوط 47k ~. تحتوي عناوين موقع السلاسل على بادئة للنموذج .data و. .rdata وغيرها . في حالتي ، كانت جميع الخطوط "المثيرة للاهتمام" موجودة في قسم .rdata ، وكان حجمها حوالي 44.5 ألف سجل. من خلال الاطلاع على الجدول ، يمكنك رؤية:

  • رسائل الخطأ وشرائح الاستعلام أثناء مرحلة تسجيل الدخول ؛
  • سلاسل الخطأ ومعلومات التهيئة للعبة ، محرك اللعبة ، واجهات ؛
  • الكثير من القمامة
  • قائمة بجداول اللعبة على جانب العميل ؛
  • القيم المستخدمة لمحرك اللعبة في اللعبة ؛
  • قائمة الآثار ؛
  • قائمة ضخمة من مفاتيح توطين الواجهة ؛
  • إلخ

أخيرًا ، يأتي ما كنا نبحث عنه أقرب إلى نهاية الجدول.



هذه قائمة برموز الأحداث بين العميل والخادم. هذا يمكن أن تبسيط حياتنا عند تحليل بروتوكول الشبكة للعبة. لكننا لن نتوقف عند هذا الحد! من الضروري التحقق من إمكانية الحصول على القيمة العددية لرمز الحدث بطريقة أو بأخرى. نرى "مألوفًا" من أكواد المقالات السابقة CMSG_PING و SMSG_PONG ، مع الأكواد 30 ( 1E 16 ) و 31 ( 1F 16 ) ، على التوالي. انقر نقرًا مزدوجًا على السطر للانتقال إلى هذا المكان في الكود.



بالفعل ، مباشرة بعد سلسلة قيم الرموز تسلسل 0x10 0x1E و 0x10 0x1F . حسنًا ، هذا يعني أنه يمكنك تحليل الجدول بأكمله والحصول على قائمة بالأحداث وقيمتها العددية ، مما يزيد من تبسيط تحليل البروتوكول.

لسوء الحظ ، فإن إصدار Windows من اللعبة يتخلف عن الإصدارات المحمولة من خلال الكثير من الإصدارات ، وبالتالي فإن المعلومات من .exe ليست ذات صلة ، وعلى الرغم من أنها يمكن أن تساعد ، يجب ألا تعتمد عليها بالكامل. بعد ذلك ، قررت دراسة المكتبة الديناميكية باستخدام Android ، كما رأيت في أحد المنتديات التي تحتوي على الكثير من المعلومات الوصفية حول الفصول الدراسية ، على عكس ثنائيات iOS. ولكن للأسف ، لم CMSG_PING البحث في CMSG_PING قيمة CMSG_PING النتائج.

دون أمل ، أقوم بإجراء نفس البحث في iOS الثنائي - لا يمكن تصديقه ، لكن البيانات الموجودة هناك هي نفسها كما هي في .exe! قم بتحميل الملف إلى المؤسسة الدولية للتنمية.



اخترت الخيار الأول المقترح ، لأنني لست متأكدًا من الخيار المطلوب. مرة أخرى ، نحن ننتظر نهاية تحليل الملف (حجم الملف الثنائي أكبر بحوالي 4 أضعاف .exe ، وقت التحليل ، بالطبع ، يزداد أيضًا). نفتح نافذة مع خطوط ، والتي تحولت هذه المرة إلى 51k. من خلال Ctrl + F نبحث عن CMSG_PING و ... لم نعثر عليه. عند إدخال رمز الحرف بحرف ، يمكنك ملاحظة هذه النتيجة:



لسبب ما ، وضعت المؤسسة الدولية للتنمية كائن Opcode.proto بأكمله في سطر واحد. انقر نقرًا مزدوجًا على هذا المكان في الكود ونرى أن الهيكل موصوف بالطريقة نفسها كما في الملف. exe ، بحيث يمكنك قصه وتحويله إلى Enum .

أخيرًا ، تجدر الإشارة إلى أنه ، في التعليقات على المقال السابق ، اقترح aml أن بنية رسالة اللعبة عبارة عن تطبيق لبروتوكولات التخزين المؤقت . إذا نظرت عن كثب إلى الشفرة في ملف ثنائي ، يمكنك أن ترى أن وصف 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; ويمكنك إنشاء فئات لوصف الرسائل. خذ مثالاً واحداً من مقالة سابقة: حركة الشخصيات. يبدو الوصف المنسق للتنسيق عند تمييز الأجزاء كما يلي:



يسمح لك إخراج التصحيح بإنشاء وصف قصير من هذا:

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

عند ParseData فئة 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/ar457480/


All Articles