حل Crackme بسيط لسيجا ميجا محرك

مرحبا بالجميع



على الرغم من تجربتي الواسعة في عكس الألعاب لـ Sega Mega Drive ، إلا أنني لم أقرر مطلقًا حلها ، ولم يوجهوا إلي عبر الإنترنت. ولكن ، في اليوم الآخر كان هناك شجاعة مضحكة أراد حلها. أشارككم القرار ...


وصف


يمكن تنزيل وصف المهمة والروم نفسه هنا .


على الرغم من أن قائمة الموارد تقول Hydra ، فإن المعيار الفعلي من بين أدوات تصحيح الأخطاء وعكس الألعاب في Sega هو Smd Ida Tools . لديه كل ما تحتاجه لحل هذا كريم:


  • رم محمل لإيدا
  • المصحح
  • عرض وتغيير ذاكرة الوصول العشوائي / VDP الذاكرة
  • عرض معلومات كاملة تقريبا على VDP

نسقط أحدث إصدار في الإضافات لـ Ide ونبدأ في النظر إلى ما لدينا.


قرار


يبدأ إطلاق أي لعبة من ألعاب Shogi بتنفيذ متجه Reset . يمكن العثور على مؤشر لها في DWORD الثاني من بداية الروم.




نرى عدة وظائف مجهولة الهوية تبدأ من العنوان 0x27A . دعونا نرى ما هناك.


sub_2EA ()



من خلال تجربتي الخاصة ، سأقول إن هذا عادة ما يشبه وظيفة انتظار مقاطعة VBLANK لإكمالها. لنرى أين توجد مكالمات إلى المتغير byte_FF0026 :



نرى أنه تم تعيين صفر بت في مقاطعة VBLANK . لذلك نحن نسمي المتغير vblank_ready ، والوظيفة التي يتم التحقق منها هي wait_for_vblank .


sub_60E ()


بعد ذلك ، يتم استدعاء الدالة sub_60E بواسطة الكود. دعونا نرى ما هو موجود:



ما يكتبه الأمر الأول إلى VDP_CTRL هو أمر تحكم VDP . لمعرفة ما تفعله ، نقف على هذا الأمر J المفتاح J :



نرى أن الإدخال في CRAM (المكان الذي يتم تخزين اللوحات فيه) يتم تهيئته. هذا يعني أن كل شفرة الوظيفة اللاحقة تحدد ببساطة نوعًا من اللوحة الأولية. وفقا لذلك ، يمكن أن يسمى وظيفة init_cram .


sub_71A ()



نرى أنه تم نقل بعض الأوامر مرة أخرى إلى VDP_CTRL ، ثم اضغط مرة أخرى على J واكتشف أن هذا الأمر يبدأ التسجيل في ذاكرة الفيديو:



علاوة على ذلك ، لفهم ما يتم نقله إلى ذاكرة الفيديو ، فإنه لا معنى له. لذلك ، ندعو ببساطة الدالة load_vdp_data .


sub_C60 ()


يحدث الشيء نفسه تقريبًا كما في الوظيفة السابقة ، لذلك ، وبدون الدخول في التفاصيل ، ندعو ببساطة الدالة load_vdp_data2 .


sub_8DA ()


هناك بالفعل المزيد من التعليمات البرمجية. وإلى جانب ذلك ، تسمى وظيفة أخرى في هذه الوظيفة. لننظر إلى هناك - في sub_D08 .


sub_D08 ()



نرى أنه في السجل D0 ، VDP_CTRL الأمر الخاص بـ VDP_CTRL ، في D1 - القيمة التي سيتم بها ملء VRAM ، وفي D2 و D3 - عرض الملء وارتفاعه (لأنه يتضح من دورتين: داخلي وخارجي). استدعاء دالة fill_vram_by_addr.


sub_8DA ()


نعود إلى الوظيفة السابقة. بمجرد إرسال القيمة في سجل D0 كأمر لـ VDP_CTRL ، اضغط المفتاح J على القيمة. نحصل على:



مرة أخرى ، من تجربة عكس الألعاب إلى Sega ، يمكنني القول أن هذا الأمر يهيئ تسجيل بلاطات التعيين. العناوين التي تبدأ بـ $Fxxx ، $Exxx ، $Dxxx ، $Cxxx في 90٪ من الحالات ستكون عناوين مناطق لها نفس التعيينات. ما هي التعيينات:
هذه هي القيم التي يمكنك من خلالها تحديد مكان عرض هذا التجانب أو ذاك على الشاشة (المربع مربع 8x8 × 8x8 بكسل).


لذلك يمكن استدعاء init_tile_mappings كـ init_tile_mappings .


sub_CDC ()



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


أيضًا ، نرى أنه في هذه الوظيفة يتم تهيئة word_FF000A : word_FF000A و word_FF000C . من تجربتي الخاصة (نعم ، يقرر) سأقول أنه إذا كان هناك متغيرين قريبين في مساحة العنوان وترتبطا بالتعيين ، فسيكونان في معظم الحالات إحداثيات بعض الكائنات (على سبيل المثال ، sprite). لذلك ، أقترح الاتصال بهم sprite_pos_x و sprite_pos_y . الخطأ في x و y مسموح به منذ مزيد من تحت التصحيح سيكون من السهل إصلاح.


VBLANK


نظرًا لأن الحلقة مستمرة في التعليمات البرمجية ، يمكننا افتراض أننا قد انتهينا من التهيئة الأساسية. الآن يمكنك إلقاء نظرة على مقاطعة VBLANK .



نرى أن هناك متغيرين يزدادان (وهو أمر غريب ، في قائمة الارتباطات لكل منهما فارغ تمامًا). ولكن نظرًا لأنه يتم تحديثها مرة واحدة لكل إطار ، يمكنك الاتصال بهم timer1 و timer2 .


بعد ذلك ، sub_2FE الدالة sub_2FE . دعونا نرى ما هو موجود:


sub_2FE ()



وهناك - العمل مع منفذ IO_CT1_DATA (المسؤول عن ذراع التحكم الأول). يتم تحميل عنوان المنفذ في السجل A0 ، ويتم تمريره إلى دالة sub_310 . نذهب إلى هناك:


sub_310 ()



تجربتي تساعدني مرة أخرى. إذا رأيت الكود الذي يعمل مع ذراع التحكم ، ومتغيرين في الذاكرة ، فسيخزن أحدهما pressed keys ، والثاني يحمل held keys ، أي مجرد الضغط وعقد المفاتيح. لذلك دعونا ندعو هذه المتغيرات: pressed_keys و held_keys . ومن ثم يمكن استدعاء update_joypad_state باسم update_joypad_state .


sub_2FE ()


استدعاء الوظيفة كما read_joypad .


حلقة معالج


الآن يبدو كل شيء أكثر وضوحًا:



لذلك تستجيب هذه الدورة للمفاتيح المضغوطة وتنفذ الإجراءات المقابلة. دعنا نذهب من خلال كل وظيفة تسمى في الحلقة.


sub_4D4 ()



هناك الكثير من التعليمات البرمجية. لنبدأ مع أول وظيفة تسمى: sub_60C .


sub_60C ()


إنها لا تفعل شيئًا - قد يبدو الأمر كذلك في البداية. مجرد العودة من الوظيفة الحالية هي rts . لكن لأن تحدث القفزات فقط ( bsr ) ، مما يعني أن rts bsr إلى حلقة المعالج. أود أن أسمي هذه الوظيفة كـ retn_to_loop .


sub_4D4 ()


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



بعد ذلك ، لدينا كمية كبيرة من التعليمات البرمجية التي تعالج بطريقة ما sprite_pos_y و sprite_pos_y ، والتي يمكن أن تقول شيئًا واحدًا فقط - وهذا ضروري لعرض التحديد sprite حول الحرف المحدد في الأبجدية.


حتى الآن يمكنك تسمية الوظيفة بأمان كـ update_selection . دعنا ننتقل.



يتحقق الكود من تحديد بت بعض المفاتيح المضغوطة ، ويستدعي وظائف معينة. دعنا ننظر إليهم.


sub_D28 ()



نوع من السحر الشاماني. أولاً ، يتم أخذ WORD من متغير word_FF0018 ، ثم يتم تنفيذ تعليمة واحدة مثيرة للاهتمام:


 bsr.w *+4 

هذا الأمر ببساطة ينتقل إلى التعليمات التي تتبعه.


التالي هو سحر آخر:


 move.l d0,(sp) rts 

يتم وضع القيمة في السجل D0 في الجزء العلوي من المكدس. تجدر الإشارة إلى أنه بالنسبة إلى Shogi وكذلك بالنسبة إلى x86 ، يتم وضع عنوان المرسل من الوظيفة عند استدعاؤه في الجزء العلوي من الحزمة. وفقًا لذلك ، يضع التعليمة الأولى بعض العناوين في الأعلى ، والثاني يرفعها من المكدس ويجعلها تمر بمرحلة انتقالية. خدعة جيدة .


أنت الآن بحاجة إلى فهم ماهية هذه القيمة في المتغير ، والتي تمر بعد ذلك. ولكن أولاً ، دعنا نسمي هذا المتغير jmp_addr .


وسيتم تسمية الوظائف التالية:


  • sub_D38 : goto_to_d0
  • sub_D28 : jump_to_var_addr

jmp_addr


معرفة أين يتم ملء هذا المتغير. نحن ننظر إلى قائمة المراجع:



لا يوجد سوى مكان واحد للكتابة إلى هذا المتغير. دعنا ننظر إليه.


sub_3A4 ()



هنا ، اعتمادًا على إحداثيات العفريت (تذكر أن هذا هو على الأرجح عنوان الحرف المحدد) ، يتم إدخال هذه القيمة أو تلك. نرى القسم التالي من الكود:



يتم نقل القيمة الحالية إلى اليمين بمقدار 4 بت ، ويتم وضع قيمة جديدة في البايت المنخفض ، ويتم إدخال النتيجة في المتغير مرة أخرى. من الناحية النظرية ، jmp_addr متغير jmp_addr بنا بتخزين الأحرف التي يمكننا إدخالها على شاشة إدخال المفتاح. لاحظ أيضًا أن حجم المتغير هو WORD .


في الواقع ، يمكن استدعاء update_jmp_addr .


sub_414 ()


الآن لدينا وظيفة واحدة فقط اليسار في الحلقة ، والتي لم يتم التعرف عليها. ويسمى sub_414 .



يشبه الكود رمز دالة update_jmp_addr ، فقط في النهاية لدينا sub_45E دالة sub_45E . دعنا ننظر هناك.


sub_45E ()



نرى أنه تم إدخال الرقم #$4B1E2003 في سجل D0 ، والذي يتم إرساله بعد ذلك إلى VDP_CTRL ، مما يعني أننا نتعامل مع أمر تحكم VDP آخر. نضغط على J ، نتلقى أمر تسجيل في المنطقة مع تعيين $Cxxx .


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


قم بتشغيل المصحح


سأستخدم مصحح الأخطاء من Smd Ida Tools ، لكن في الواقع ، ستكون بعض Gens KMod أو Gens ReRecording كافية. الشيء الرئيسي هو أن هناك ميزة مع عرض العناوين في الذاكرة.



تم تأكيد نظريتي. لذلك يمكن الآن key_length .


هناك متغير آخر: dword_FF0010 ، والذي يستخدم أيضًا فقط في الوظيفة الحالية ، ومحتوياته ، بعد إضافته إلى الأمر الأولي في D0 (أذكر أن هذا كان الرقم #$4B1E2003 ) ، يتم إرساله إلى VDP_CTRL . دون التفكير add_to_vdp_cmd ، قمت بتسمية المتغير add_to_vdp_cmd .


ماذا تفعل هذه الوظيفة؟ لدي افتراض أنها ترسم الحرف الذي تم إدخاله. التحقق من ذلك بسيط - عن طريق تشغيل مصحح الأخطاء ومقارنة الحالة قبل استدعاء وظيفة sub_45E وبعد:


إلى:



بعد:



كنت على حق - هذه الوظيفة ترسم الشخصية التي تم إدخالها. نحن نسميها do_draw_input_char ، والوظيفة التي تسميها ( sub_414 ) هي draw_input_char .


ماذا الان


دعنا نتحقق الآن من أن المتغير الذي jmp_addr عليه jmp_addr يقوم بالفعل بتخزين المفتاح الذي تم إدخاله. سوف نستخدم نفس Memory Watch :



كما ترون ، كان التخمين صحيحا. ماذا يعطينا هذا؟ يمكننا القفز إلى أي عنوان. لكن أي واحد؟ في قائمة الوظائف يتم فرز كل شيء بعد كل شيء:



ثم بدأت للتو التمرير خلال الكود حتى وجدت هذا:



شهدت العين المدربة تسلسل $4E, $75 في نهاية البايتات غير المخصصة. هذا هو شفرة تشغيل تعليمة rts ، أي العودة من وظيفة. لذلك يمكن أن تكون هذه البايتات غير المخصصة رمز بعض الوظائف. دعنا نحاول تعيينهم كرمز ، اضغط C :



من الواضح ، هذا هو رمز الوظيفة. يمكنك أيضًا الضغط على P لجعل الكود وظيفة. تذكر هذا الاسم: sub_D3C .


ثم ينشأ الفكر: ماذا لو قفزت على sub_D3C ؟ هذا يبدو جيدًا ، على الرغم من أن قفزة واحدة هنا لن تكون كافية بالتأكيد ، لأن لم تكن هناك روابط word_FF0020 لمتغير word_FF0020 .


ثم جاءت فكرة أخرى لي: ماذا لو بحثنا عن رمز آخر غير مخصص؟ افتح مربع حوار Binary search (Alt + B) ، وأدخل التسلسل 4E 75 فيه ، حدد مربع Find all occurrences :



انقر فوق " لبدء البحث ، وحصلنا على النتائج التالية.



قد يحتوي مكانان آخران على الأقل في الروم على رمز وظيفة ، تحتاج إلى التحقق منه. نقر على أول الخيارات ، وقم بالتمرير قليلاً ، ونرى مرة أخرى سلسلة من البايتات غير المحددة. تدل عليها كدالة؟ نعم! اضغط P حيث تبدأ وحدات البايت:



باردة! الآن لدينا وظيفة sub_34C . نحاول تكرار نفس الشيء مع آخر الخيارات التي تم العثور عليها ، و ... نحصل على المشكله. هناك الكثير من البايتات قبل 4E 75 بحيث لا يكون واضحًا من أين تبدأ الوظيفة. ومن الواضح أن هذه البايتات أعلاه ليست كلها رمزًا ، لأن الكثير من وحدات البايت المكررة.


تحديد بداية الوظيفة


سيكون من الأسهل بالنسبة لنا العثور على بداية الوظيفة إذا وجدنا أين تنتهي البيانات. كيف نفعل ذلك؟ في الواقع ليست معقدة على الإطلاق:


  1. نلتفت قبل بداية البيانات (سيكون هناك رابط لهم من الكود)
  2. نتبع الرابط ونبحث عن دورة يجب أن يظهر بها حجم هذه البيانات
  3. بمناسبة الصفيف

لذلك ، نقوم بتنفيذ الفقرة الأولى ...:



... ونرى على الفور أنه في دورة من move.l 4 بايت من البيانات يتم نسخها في وقت واحد (لأن move.l ) إلى VDP_DATA . التالي نرى الرقم 2047 . في البداية ، قد يبدو أن الحجم النهائي للصفيف هو 2047 * 4 ، لكن الحلقة المستندة إلى dbf تنفذ +1 للتكرار أكثر ، لأن القيمة المقارنة الأخيرة ليست 0 ، لكن -1 .


الإجمالي: حجم المصفوفة 2048 * 4 = 8192 . تشير إلى بايت كصفيف. للقيام بذلك ، انقر فوق * وحدد الحجم:



نلتف إلى نهاية المصفوفة ، ونرى أن هناك بايت ، وهي بالضبط بايت الكود:




الآن لدينا وظيفة sub_2D86 ، ولدينا كل شيء لحل هذه الكراك! دعنا نرى ما تقوم به الوظيفة الجديدة.


sub_2D86 ()


ويضع القيمة #$4147 في السجل D1 sub_34C الدالة sub_34C . ألقِ نظرة عليها.


sub_34C ()



نرى أنه يتم هنا طرح قيمة المتغير word_FF0020 . إذا نظرت إلى الروابط jmp_addr به ، jmp_addr مكانًا آخر يحدث فيه السجل في هذا المتغير ، وسيكون هذا هو بالضبط المكان الذي أردت القفز فيه من خلال متغير jmp_addr . هذا يؤكد حدس sub_D3C بالتأكيد بحاجة إلى القفز إلى sub_D3C .


لكن ما حدث بعد ذلك كان كسولًا جدًا بالنسبة لي لفهمه ، لذا رميت رمًا إلى GHIDRA ، ووجدت هذه الوظيفة ، ونظرت إلى الكود الذي تم فك ترجمته:


 void FUN_0000034c(void) { ushort in_D1w; short sVar1; ushort *puVar2; if (((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,in_D1w ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; } 

نرى أن المتغير الذي يحمل الاسم الغريب in_D1w ، وكذلك المتغير DAT_00ff0020 ، والذي يشبه عنوانه word_FF0020 المذكورة word_FF0020 .


يخبرنا in_D1w أن هذه القيمة مأخوذة من السجل D1 ، أو بالأحرى من نصف WORD الأصغر ، وتعيين السجل D1 الوظيفة التي تتجاوزه. تذكر #$4147 ؟ لذلك تحتاج إلى تعيين هذا السجل كوسيطة إدخال للدالة.


للقيام بذلك ، في الإطار الذي يحتوي على الشفرة المفسرة ، انقر بزر الماوس الأيمن على اسم الوظيفة ، وحدد عنصر قائمة Edit Function Signature :



للإشارة إلى أن الوظيفة تأخذ وسيطة من خلال سجل معين ، أي ليس بالطريقة القياسية لاتفاقية الاتصال الحالية ، يجب عليك تحديد Use Custom Storage والنقر فوق الرمز مع إضافة خضراء :



يظهر موضع وسيطة الإدخال الجديد. نضغط عليه مرتين ، ونحصل على مربع حوار يشير إلى نوع وسيطة الوسيطة:



في التعليمة البرمجية decompiled ، نرى أن in_D1w هي من نوع ushort ، مما يعني أننا ushort في حقل type. ثم انقر فوق الزر " Add :



سيظهر موضع للإشارة إلى وسيطة الوسيطة ، نحتاج إلى تحديد سجل D1w في Location ، وانقر فوق OK :



سوف تأخذ الشفرة المحولة الشكل:


 void FUN_0000034c(ushort param_1) { short sVar1; ushort *puVar2; if (((ushort)(param_1 ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(param_1 ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,param_1 ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; } 

نحن param_1 أن قيمة param_1 لدينا ثابتة ، وتم تمريرها بواسطة وظيفة الاتصال ، وهي تساوي #$4147 . إذن ما قيمة DAT_00ff0020 ؟ نحن نعتبر:


 0x4147 ^ DAT_00ff0020 ^ 0x5e4e = 0x5a5a 0x4147 ^ DAT_00ff0020 ^ 0x4a44 = 0x4e50 

لأن xor - العملية قابلة للعكس ، يمكن تشاجر جميع الأرقام الثابتة مع بعضها البعض والحصول على القيمة المطلوبة للمتغير DAT_00ff0020 .


 DAT_00ff0020 = 0x4147 ^ 0x5e4e ^ 0x5a5a = 0x4553 DAT_00ff0020 = 0x4147 ^ 0x4a44 ^ 0x4e50 = 0x4553 

اتضح أن قيمة المتغير يجب أن تكون 0x4553 . يبدو أنني رأيت بالفعل مكانًا حيث يتم تعيين مثل هذه القيمة ...



الاستنتاجات والقرار


نتوصل إلى النتائج التالية:


  1. تحتاج أولاً إلى الانتقال إلى العنوان 0x0D3C ، لذلك تحتاج إلى إدخال الرمز 0D3C
  2. انتقل إلى الوظيفة في 0x2D86 ، والتي تحدد قيمة D1 للتسجيل #$4147 ، لهذا تحتاج إلى إدخال الكود 2D86

بشكل تجريبي ، نكتشف المفتاح الذي يجب الضغط عليه للتحقق من المفتاح الذي تم إدخاله: B نحن نحاول:



شكرا لك

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


All Articles