على مدار سنوات من لعب لعبة MMORPG المحمولة ، اكتسبت بعض الخبرة في الهندسة العكسية ، والتي أود مشاركتها في سلسلة من المقالات. المواضيع عينة:
- تحليل تنسيق الرسالة بين الخادم والعميل.
- كتابة تطبيق استماع لعرض حركة مرور اللعبة بطريقة مريحة.
- اعتراض حركة المرور وتعديله باستخدام خادم وكيل بخلاف HTTP.
- الخطوات الأولى لخادمك ("المقرصن").
في هذه المقالة ، سأناقش
تحليل تنسيق الرسالة بين الخادم والعميل . مهتمة ، أطلب القط.
الأدوات المطلوبة
لتتمكن من تكرار الخطوات الموضحة أدناه ، ستحتاج إلى:
- الكمبيوتر الشخصي (فعلت في 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
:
الفوضى. مشكلتان مرئيتان:
- القيم ليست 4 بايت ، ولكن بأحجام مختلفة ؛
- لا تتطابق جميع القيم مع البيانات من الملفات ، وفي هذه الحالة يكون الفرق هو 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);
لكن العودة إلى إنشاء الموضوع. مرة أخرى
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>;
الآن تم تخفيض قالب رسالة الحركة بشكل كبير:
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 لارتباطه بمقال مفيد ذي صلة.