كيفية تحليل بروتوكول شبكة المحمول MMORPG

على مدار سنوات من لعب لعبة MMORPG المحمولة ، اكتسبت بعض الخبرة في الهندسة العكسية ، والتي أود مشاركتها في سلسلة من المقالات. المواضيع عينة:

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

في هذه المقالة ، سأناقش تحليل تنسيق الرسالة بين الخادم والعميل . مهتمة ، أطلب القط.

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


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

  • الكمبيوتر الشخصي (فعلت في Windows 7/10 ، ولكن قد يعمل MacOS أيضًا إذا كانت العناصر أدناه متوفرة هناك) ؛
  • Wireshark لتحليل الحزمة ؛
  • 010 محرر لتحليل الحزم حسب القالب (اختياري ، لكن يسمح لك بوصف تنسيق الرسالة بسرعة وسهولة) ؛
  • الجهاز المحمول نفسه مع اللعبة.

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

إعراب تنسيق الرسالة بين الخادم والعميل


للبدء ، نحتاج إلى رؤية حركة مرور الجهاز المحمول. من السهل جدًا القيام بذلك (على الرغم من أنني وصلت إلى هذا القرار الواضح لفترة طويلة جدًا): على جهاز الكمبيوتر الخاص بنا ، نقوم بإنشاء نقطة وصول Wi-Fi ، والاتصال بها من جهاز محمول ، وتحديد الواجهة المطلوبة في Wireshark - ولدينا كل حركة المرور على الهاتف المحمول أمام أعيننا.

بعد دخول اللعبة والانتظار لبعض الوقت حتى تتوقف الطلبات غير المرتبطة بخادم اللعبة نفسه ، يمكنك مراقبة الصورة التالية:


في هذه المرحلة ، يمكننا بالفعل استخدام عوامل تصفية Wireshark لرؤية الحزم فقط بين اللعبة والخادم ، وكذلك فقط مع الحمولة النافعة:

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

إذا كنت تقف في مكان هادئ ، بعيدًا عن اللاعبين الآخرين و NPC ، ولا تفعل شيئًا ، يمكنك مشاهدة الرسائل المتكررة باستمرار من الخادم والعميل (الحجم 76 و 84 بايت ، على التوالي). في حالتي ، تم إرسال الحد الأدنى لعدد الحزم المختلفة على شاشة اختيار الأحرف.


يشبه تكرار الطلب من العميل إلى ping. دعنا نأخذ بعض الرسائل للتحقق (3 مجموعات ، أعلاه عبارة عن طلب من عميل ، أسفله استجابة خادم):


أول ما يلفت انتباهك هو هوية الحزم. 8 بايت إضافية في الاستجابة عند تحويلها إلى النظام العشري تشبه إلى حد كبير الطابع الزمني بالثواني: 5CD008F8 16 = 1557137656 10 (من الزوج الأول). نتحقق من الساعة - نعم ، إنها كذلك. تطابق وحدات البايت الأربع السابقة آخر 4 بايتات في الطلب. عند الترجمة ، نحصل على: A4BB 16 = 42171 10 ، وهو أيضًا مشابه جدًا للوقت ، ولكن بالميلي ثانية. يتزامن ذلك تقريبًا مع الوقت منذ إطلاق اللعبة ، وعلى الأرجح هو.

يبقى للنظر في أول 6 بايت من الطلب والاستجابة. من السهل ملاحظة اعتماد قيمة البايتات الأربع الأولى من الرسالة (دعنا نسمي هذه المعلمة L ) على حجم الرسالة: استجابة الخادم أكثر من 8 بايت ، زادت قيمة L أيضًا بمقدار 8 ، ومع ذلك ، فإن حجم الحزمة يزيد بمقدار 6 بايت من قيمة L في كلتا الحالتين. يمكنك أيضًا ملاحظة أن وحدتي بايت بعد L يحتفظان بقيمتهما في الطلبات المقدمة من العميل ومن الخادم ، وبالنظر إلى أن قيمتها تختلف من جانب واحد ، يمكننا القول بثقة أن هذا هو رمز الرسالة C (سيتم تحديد رموز الرسائل المرتبطة على الأرجح بالتتابع). الهيكل العام واضح بما فيه الكفاية لكتابة قالب الحد الأدنى ل 010Editor:

  • أول 4 بايت - L - حجم البيانات الفعلية للرسائل ؛
  • 2 بايت التالي - رمز رسالة C ؛
  • الحمولة نفسها.

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

وبالتالي ، تنسيق رسالة ping العميل: إرسال وقت ping المحلي؛ تنسيق استجابة الخادم: إرسال نفس الوقت ووقت إرسال الاستجابة بالثواني. لا يبدو صعبا ، أليس كذلك؟

دعونا نحاول أن نجعل مثالا أكثر تعقيدا. يقف في مكان هادئ ويختبئ حزم ping ، يمكنك العثور على رسائل النقل الفضائي وإنشاء عنصر (حرفي). لنبدأ مع أول واحد. امتلاك بيانات اللعبة ، كنت أعرف قيمة نقطة النقل الفضائي التي يجب البحث عنها. بالنسبة للاختبارات ، استخدمت نقاطًا ذات قيم 0x2B و 0x6B و 0x1AF و 0x1AF . قارن مع القيم في الرسائل: 0x2B ، 0x6B ، 0x6B و 0x3AF :


الفوضى. مشكلتان مرئيتان:

  1. القيم ليست 4 بايت ، ولكن بأحجام مختلفة ؛
  2. لا تتطابق جميع القيم مع البيانات من الملفات ، وفي هذه الحالة يكون الفرق هو 128.

بالإضافة إلى ذلك ، عند المقارنة بتنسيق ping ، يمكنك ملاحظة بعض الاختلافات:

  • 0x08 غير مفهومة قبل القيمة المتوقعة ؛
  • قيمة 4 بايت ، 4 أقل من L (دعنا نسميها D هذا الحقل لا يظهر في جميع الرسائل ، وهو غريب بعض الشيء ، ولكن حيث هو ، L - 4 = D الحفاظ على L - 4 = D من ناحية ، للرسائل مع بنية بسيطة (مثل بينغ) ليست مطلوبة ، ولكن من ناحية أخرى - يبدو عديم الفائدة).

أعتقد أن البعض منكم ربما كان قد خمن بالفعل سبب عدم تطابق القيم المتوقعة ، لكنني سأستمر. دعونا نرى ما يحدث في الحرفة:


لا تتوافق القيمتان 14183 و 14285 أيضًا مع 28391 و 28621 الفعلي ، ولكن الفرق هنا أكبر بكثير من 128. بعد اختبارات عديدة (بما في ذلك مع أنواع أخرى من الرسائل) اتضح أنه كلما زاد العدد المتوقع ، زاد الفرق بين القيمة في الحزمة. الأمر الغريب هو أن القيم حتى 128 بقيت في حد ذاتها. حصلت عليه ، ما الأمر؟ الموقف الواضح هو بالنسبة لأولئك الذين واجهوا هذا بالفعل ، وبغض النظر ، اضطررت إلى تفكيك هذا "التشفير" لمدة يومين (في النهاية ، ساعد تحليل القيم في شكل ثنائي في "القرصنة"). يسمى السلوك الموصوف أعلاه " كمية الطول المتغير" - تمثيل لعدد يستخدم عددًا غير محدد من البايتات ، حيث تحدد البت الثامن من البايت (بتات المتابعة) وجود البايتة التالية. من الوصف ، من الواضح أن قراءة VLQ ممكنة فقط بترتيب Little-Endian. من قبيل الصدفة ، كل القيم في الحزم في هذا الترتيب.

الآن بعد أن عرفنا كيفية الحصول على القيمة الأولية ، يمكننا كتابة قالب للنوع:

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

ووظيفة تحويل مجموعة من البايتات إلى قيمة عددية:

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

لكن العودة إلى إنشاء الموضوع. مرة أخرى D يظهر ومرة ​​أخرى 0x08 أمام القيمة المتغيرة. تشبه وحدات البايت الأخيرة من رسالة 0x10 0x01 للريبة عدد عناصر صياغة ، حيث يكون 0x10 دور مشابه 0x08 ولكن لا يزال غير مفهومة. ولكن الآن يمكنك كتابة قالب لهذا الحدث:

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

والتي سوف تبدو مثل هذا:


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

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

النتيجة البصرية:


تبين أن الثلاثة الآخرون هم إحداثيات الموقع ، أما الثلاثة الصفراء ، على الأرجح ، فتبين المكان الذي تبحث فيه الشخصية ومتجهة سرعته ، وآخرها هو حالة الشخصية. يمكنك ملاحظة بايت ثابت (علامات) بين قيم الإحداثيات ( 0x0D قبل قيمة X ، 0x015 قبل Y و 0x1D قبل Z ) وقبل الحالة ( 0x30 ) ، والتي تشبه بشكل 0x1D للريبة معنى 0x08 و 0x10 . بعد تحليل العديد من العلامات من الأحداث الأخرى ، اتضح أنه يحدد نوع القيمة التي تتبعها (البتات الثلاثة الأولى) والمعنى الدلالي ، أي في المثال أعلاه ، إذا قمت 0x120F المتجهات مع الحفاظ على 0x120F ( 0x120F أمام الإحداثيات ، وما إلى ذلك) ، فيجب عادةً تحليل اللعبة (نظريًا) للرسالة. بناءً على هذه المعلومات ، يمكنك إضافة نوعين جديدين:

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

الآن تم تخفيض قالب رسالة الحركة بشكل كبير:

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

هناك نوع آخر قد نحتاجه في المقالة التالية وهو السطور التي تسبقها قيمة Packed بحجمها:

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

الآن ، مع العلم بتنسيق نموذج الرسالة ، يمكنك كتابة تطبيق الاستماع الخاص بك لراحة تصفية الرسائل وتحليلها ، ولكن هذا هو موضوع المقال التالي.

محدثًا: شكرًا على التلميح بأن بنية الرسالة الموضحة أعلاه هي بروتوكول التخزين المؤقت ، وأيضًا Tatikoma لارتباطه بمقال مفيد ذي صلة.

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


All Articles