كسر صدع بسيط مع غيدرا - الجزء 2

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

في هذا الجزء ، كما وعدت ، سنتولى تحليل وظيفة _construct_key () ، والتي ، كما اكتشفنا ، مسؤولة عن قراءة الملف الثنائي المنقول إلى البرنامج والتحقق من قراءة البيانات.

الخطوة 5 - نظرة عامة على _construct_key () وظيفة


دعونا نلقي نظرة على القائمة الكاملة لهذه الوظيفة على الفور:

قائمة _construct_key ()
char ** __cdecl _construct_key(FILE *param_1) { int iVar1; size_t sVar2; uint uVar3; uint local_3c; byte local_36; char local_35; int local_34; char *local_30 [4]; char *local_20; undefined4 local_19; undefined local_15; char **local_14; int local_10; local_14 = (char **)__prepare_key(); if (local_14 == (char **)0x0) { local_14 = (char **)0x0; } else { local_19 = 0; local_15 = 0; _text(&local_19,1,4,param_1); iVar1 = _text((char *)&local_19,*(char **)local_14[1],4); if (iVar1 == 0) { _text(local_14[1] + 4,2,1,param_1); _text(local_14[1] + 6,2,1,param_1); if ((*(short *)(local_14[1] + 6) == 4) && (*(short *)(local_14[1] + 4) == 5)) { local_30[0] = *local_14; local_30[1] = *local_14 + 0x10c; local_30[2] = *local_14 + 0x218; local_30[3] = *local_14 + 0x324; local_20 = *local_14 + 0x430; local_10 = 0; while (local_10 < 5) { local_35 = 0; _text(&local_35,1,1,param_1); if (*local_30[local_10] != local_35) { _free_key(local_14); return (char **)0x0; } local_36 = 0; _text(&local_36,1,1,param_1); if (local_36 == 0) { _free_key(local_14); return (char **)0x0; } *(uint *)(local_30[local_10] + 0x104) = (uint)local_36; _text(local_30[local_10] + 1,1,*(size_t *)(local_30[local_10] + 0x104),param_1); sVar2 = _text(local_30[local_10] + 1); if (sVar2 != *(size_t *)(local_30[local_10] + 0x104)) { _free_key(local_14); return (char **)0x0; } local_3c = 0; _text(&local_3c,1,1,param_1); local_3c = local_3c + 7; uVar3 = _text(param_1); if (local_3c < uVar3) { _free_key(local_14); return (char **)0x0; } *(uint *)(local_30[local_10] + 0x108) = local_3c; _text(param_1,local_3c,0); local_10 = local_10 + 1; } local_34 = 0; _text(&local_34,4,1,param_1); if (*(int *)(*local_14 + 0x53c) == local_34) { _text("Markers seem to still exist"); } else { _free_key(local_14); local_14 = (char **)0x0; } } else { _free_key(local_14); local_14 = (char **)0x0; } } else { _free_key(local_14); local_14 = (char **)0x0; } } return local_14; } 


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

  • غريب ()
  • strncmp ()
  • strlen ()
  • ftell ()
  • fseek ()
  • يضع ()

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

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

لذلك ، مباشرة بعد إعلان المتغيرات المحلية ، تسمى وظيفة معينة _prepare_key () :

 key_array = (char **)__prepare_key(); if (key_array == (char **)0x0) { key_array = (char **)0x0; } 

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

ثم يقرأ البرنامج البايتات الأربع الأولى من الملف ويقارن مع القسم المقابل من مجموعة key_array . (الرمز التالي بعد إعادة التسمية ، بما في ذلك المتغير local_19 ، قمت بإعادة تسمية first_4bytes .)

 first_4bytes = 0; /*   4    */ fread2(&first_4bytes,1,4,param_1); /*   key_array[1][0...3] */ iVar1 = strncmp2((char *)&first_4bytes,*(char **)key_array[1],4); if (iVar1 == 0) { ... } 

وبالتالي ، يحدث المزيد من التنفيذ فقط إذا تزامن أول 4 بايت (تذكر هذا). بعد ذلك نقرأ كتلتين من البايت من الملف (ويتم استخدام نفس key_array كمخزن مؤقت لكتابة البيانات):

 fread2(key_array[1] + 4,2,1,param_1); fread2(key_array[1] + 6,2,1,param_1); 

ومرة أخرى - تعمل الوظيفة فقط إذا كان الشرط التالي صحيحًا:

 if ((*(short *)(key_array[1] + 6) == 4) && (*(short *)(key_array[1] + 4) == 5)) { //   ... } 

من السهل أن ترى أن أول من الكتل ثنائية البايت التي تم قراءتها أعلاه يجب أن يكون الرقم 5 ، والثاني يجب أن يكون الرقم 4 (يشغل نوع البيانات باختصار 2 بايت على الأنظمة الأساسية 32 بت).

التالي هو هذا:

 local_30[0] = *key_array; // .. key_array[0] local_30[1] = *key_array + 0x10c; local_30[2] = *key_array + 0x218; local_30[3] = *key_array + 0x324; local_20 = *key_array + 0x430; 

نرى هنا أن المصفوفة local_30 (تم إعلانها كـ char * local_30 [4]) تحتوي على إزاحات لمؤشر key_array . بمعنى ، local_30 عبارة عن صفيف من خطوط العلامات التي من المحتمل أن تتم قراءة بيانات الملف من خلالها. بموجب هذا الافتراض ، قمت بإعادة تسمية local_30 إلى علامات . في هذا القسم من التعليمات البرمجية ، يبدو السطر الأخير مشبوهًا بعض الشيء ، حيث لا يتم تنفيذ تعيين الإزاحة الأخيرة (في الفهرس 0x430 ، أي 1072) بواسطة عنصر العلامات التالي ، ولكن بواسطة متغير local_20 منفصل ( char * ). لكننا سنعرف ذلك حتى الآن ، ولكن الآن - دعنا ننتقل!

بعد ذلك ننتظر دورة:

  i = 0; // local_10   i while (i < 5) { // ... i = i + 1; } 

أي فقط 5 تكرارات من 0 إلى 4 شاملة. في الحلقة ، تبدأ على الفور القراءة من الملف والتحقق من الامتثال لمجموعة العلامات الخاصة بنا:

 char c_marker = 0; //   local_35 /*  .    */ fread2(&c_marker, 1, 1, param_1); if (*markers[i] != c_marker) { /*    -      */ _free_key(key_array); return (char **)0x0; } 

بمعنى ، تتم قراءة البايت التالي من الملف في متغير c_marker (في التعليمة البرمجية decompiled الأصلية - local_35 ) والتحقق من توافقه مع الحرف الأول من عنصر علامات i-th. في حالة عدم تطابق ، يتم إلغاء صفيف key_array ويتم إرجاع مؤشر مزدوج فارغ. علاوة على ذلك ، نرى أن ذلك يتم عندما لا تتطابق بيانات القراءة مع بيانات التحقق.

لكن هنا ، كما يقولون ، "الكلب مدفون". دعونا نلقي نظرة فاحصة على هذه الدورة. لديها 5 تكرارات ، كما اكتشفنا. يمكنك التحقق من هذا إذا كنت تريد من خلال النظر في رمز المجمع:





في الواقع ، يقارن أمر CMP قيمة المتغير local_10 (لدينا بالفعل i ) بالرقم 4 وإذا كانت القيمة أقل من أو تساوي 4 (أمر JLE) ، فسيتم الانتقال إلى التسمية LAB_004017eb ، أي بداية جسم الدورة. أي سيتم استيفاء الشرط لـ i = 0 و 1 و 2 و 3 و 4 - فقط 5 تكرارات! سيكون كل شيء على ما يرام ، ولكن يتم أيضًا فهرسة العلامات بواسطة هذا المتغير في حلقة ، وبعد كل شيء ، يتم الإعلان عن هذه المجموعة مع 4 عناصر فقط:

 char *markers [4]; 

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

 local_20 = *key_array + 0x430; 

تماما مثل ذلك! انظر فقط إلى القائمة الكاملة للوظيفة وحاول العثور على إشارة واحدة على الأقل إلى المتغير local_20 . هي ليست هناك! نستنتج من هذا: يجب أيضًا تخزين هذا الإزاحة في صفيف العلامات ، ويجب أن يحتوي الصفيف نفسه على 5 عناصر. دعونا إصلاحه. انتقل إلى تعريف المتغير ، اضغط Ctrl + L (إعادة كتابة المتغير) وقم بتغيير حجم الصفيف بجرأة إلى 5:



القيام به. قم بالتمرير لأسفل للوصول إلى الرمز لتعيين إزاحة المؤشر إلى علامات - وها! - يختفي متغير إضافي غير مفهوم وتندرج كل الأشياء في مكانها:

 markers[0] = *key_array; markers[1] = *key_array + 0x10c; markers[2] = *key_array + 0x218; markers[3] = *key_array + 0x324; markers[4] = *key_array + 0x430; //   ...   ! 

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

 byte n_strlen1 = 0; //   local_36 /*  .    */ fread2(&n_strlen1,1,1,param_1); if (n_strlen1 == 0) { /*      */ _free_key(key_array); return (char **)0x0; } 

حسنًا ، يجب أن يكون n_strlen1 غير صفري. لماذا؟ سترى الآن ، ولكن في نفس الوقت سوف تفهم لماذا أعطيت هذا المتغير الاسم التالي:

  /*   n_strlen1)  (markers[i] + 0x104) */ *(uint *)(markers[i] + 0x104) = (uint)n_strlen1; /*    (n_strlen1)  (-->  ?) */ fread2(markers[i] + 1,1,*(size_t *)(markers[i] + 0x104),param_1); n_strlen2 = strlen2(markers[i] + 1); //   sVar2 if (n_strlen2 != *(size_t *)(markers[i] + 0x104)) { /*    (n_strlen2)  == n_strlen1 */ _free_key(key_array); return (char **)0x0; } 

أضفت التعليقات التي ينبغي أن يكون كل شيء واضح. تتم قراءة البايتات N_strlen1 من الملف وحفظها كسلسلة من الأحرف (أي سلسلة) في مجموعة العلامات [i] - أي بعد "رمز الإيقاف" المقابل ، والتي تمت كتابتها بالفعل من key_array . حفظ القيمة n_strlen1 في العلامات [i] عند الإزاحة 0x104 (260) لا يلعب أي دور هنا (راجع السطر الأول في الكود أعلاه). في الواقع ، يمكن تحسين هذا الرمز على النحو التالي (وبالتأكيد هذا هو الحال في التعليمات البرمجية المصدر):

 fread2(markers[i] + 1, 1, (size_t) n_strlen1, param_1); n_strlen2 = strlen2(markers[i] + 1); if (n_strlen2 != (size_t) n_strlen1) { ... } 

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

مزيد من المياه هذا الرمز (الذي علق أيضا على الفور):

 uint n_pos = 0; //   local_3c /*  .    */ fread2(&n_pos,1,1,param_1); /*   7 */ n_pos = n_pos + 7; /*     */ uint n_filepos = ftell2(param_1); //   uVar3 if (n_pos < n_filepos) { /* n_pos   >= n_filepos */ _free_key(key_array); return (char **)0x0; } 

لا يزال الأمر أكثر بساطة هنا: نأخذ البايت التالي من الملف ، ونضيف 7 ونقارن القيمة الناتجة مع موضع المؤشر الحالي في دفق الملف الذي تم الحصول عليه بواسطة دالة ftell () . يجب أن لا تقل قيمة n_pos عن موضع المؤشر (أي الإزاحة بالبايت من بداية الملف).

الخط النهائي في الحلقة:

 fseek2(param_1,n_pos,0); 

أي إعادة ترتيب مؤشر الملف (من البداية) إلى الموضع المشار إليه بواسطة n_pos بواسطة الدالة fseek () . حسنا ، نحن نفعل كل هذه العمليات في الحلقة 5 مرات. تنتهي الدالة _construct_key () بالكود التالي:

 int i_lastmarker = 0; //   local_34 /*   4    (int32) */ fread2(&i_lastmarker,4,1,param_1); if (*(int *)(*key_array + 0x53c) == i_lastmarker) { /*    == key_array[0][1340] ...   :) */ puts2("Markers seem to still exist"); } else { _free_key(key_array); key_array = (char **)0x0; } 

وبالتالي ، يجب أن تكون الكتلة الأخيرة من البيانات في الملف قيمة عددية 4 بايت ويجب أن تساوي القيمة في key_array [0] [1340] . في هذه الحالة ، سوف نتلقى رسالة تهنئة في وحدة التحكم. خلاف ذلك ، لا يزال يتم إرجاع مجموعة فارغة دون أي المديح :)

الخطوة 6 - نظرة عامة على وظيفة __prepare_key ()


لدينا وظيفة واحدة فقط لم يتم تجميعها - __prepare_key () . لقد خمنا بالفعل أنه في ذلك يتم إنشاء بيانات التحقق في شكل صفيف key_array ، والتي يتم استخدامها بعد ذلك في دالة _construct_key () للتحقق من البيانات من الملف. يبقى لمعرفة أي نوع من البيانات هناك!

لن أقوم بتحليل هذه الوظيفة بالتفصيل وسأقدم على الفور قائمة كاملة مع تعليقات بعد إعادة تسمية المتغيرات الضرورية:

__Prepare_key () قائمة الوظائف
 void ** __prepare_key(void) { void **key_array; void *pvVar1; /* key_array = new char*[2]; // 2 4-  (char*) */ key_array = (void **)calloc2(1,8); if (key_array == (void **)0x0) { key_array = (void **)0x0; } else { pvVar1 = calloc2(1,0x540); /* key_array[0] = new char[1340] */ *key_array = pvVar1; pvVar1 = calloc2(1,8); /* key_array[1] = new char[8] */ key_array[1] = pvVar1; /* "VOID" */ *(undefined4 *)key_array[1] = 0x404024; /* 5  4 (2- ) */ *(undefined2 *)((int)key_array[1] + 4) = 5; *(undefined2 *)((int)key_array[1] + 6) = 4; /* key_array[0][0] = 'b' */ *(undefined *)*key_array = 0x62; *(undefined4 *)((int)*key_array + 0x104) = 3; /* 'W' */ *(undefined *)((int)*key_array + 0x218) = 0x57; /* 'p' */ *(undefined *)((int)*key_array + 0x324) = 0x70; /* 'l' */ *(undefined *)((int)*key_array + 0x10c) = 0x6c; /* 152 ( ASCII) */ *(undefined *)((int)*key_array + 0x430) = 0x98; /*   = 1122 (int32) */ *(undefined4 *)((int)*key_array + 0x53c) = 0x462; } return key_array; } 


المكان الوحيد الذي يستحق النظر هو هذا الخط:

 *(undefined4 *)key_array[1] = 0x404024; 

كيف أفهم أن هنا يكمن السطر "VOID"؟ الحقيقة هي أن 0x404024 هو العنوان الموجود في مساحة عنوان البرنامج المؤدي إلى قسم .rdata . النقر المزدوج على هذه القيمة يسمح لنا أن نرى بوضوح ما هو موجود:



بالمناسبة ، يمكن فهم الشيء نفسه من رمز المجمّع لهذا الخط:

004015da c7 00 24 MOV dword ptr [EAX], .rdata = 56h V
40 40 00

البيانات المقابلة لخط VOID هي في بداية قسم .rdata (عند إزاحة صفر من العنوان المقابل).

لذلك ، عند الخروج من هذه الوظيفة ، يجب تشكيل صفيف ثنائي الأبعاد بالبيانات التالية:

[0] [0]:'b' [268]:'l' [536]:'W' [804]:'p' [1072]:152 [1340]:1122
[1] [0-3]:"VOID" [4-5]:5 [6-7]:4

الخطوة 7 - تحضير الثنائي للكسر


الآن يمكننا أن نبدأ تجميع الملف الثنائي. جميع البيانات الأولية في أيدينا:
1) بيانات التحقق ("رموز التوقف") ومواقعها في صفيف التحقق ؛
2) تسلسل البيانات في الملف

دعنا نستعيد بنية الملف الذي نبحث عنه وفقًا لخوارزمية الدالة _construct_key () . لذلك ، سيكون تسلسل البيانات في الملف كما يلي:

هيكل الملف
  1. 4 بايت == key_array [1] [0 ... 3] == "VOID"
  2. 2 بايت == key_array [1] [4] == 5
  3. 2 بايت == key_array [1] [6] == 4
  4. بايت واحد == key_array [0] [0] == 'b' (الرمز المميز)
  5. 1 بايت == (طول السطر التالي) == n_strlen1
  6. بايت n_strlen1 == (أي سلسلة) == n_strlen1
  7. 1 بايت == (+7 == الرمز التالي) == n_pos
  8. بايت واحد == key_array [0] [0] == 'l' (الرمز المميز)
  9. 1 بايت == (طول السطر التالي) == n_strlen1
  10. بايت n_strlen1 == (أي سلسلة) == n_strlen1
  11. 1 بايت == (+7 == الرمز التالي) == n_pos
  12. بايت واحد == key_array [0] [0] == 'W' (الرمز المميز)
  13. 1 بايت == (طول السطر التالي) == n_strlen1
  14. بايت n_strlen1 == (أي سلسلة) == n_strlen1
  15. 1 بايت == (+7 == الرمز التالي) == n_pos
  16. بايت واحد == key_array [0] [0] == 'p' (الرمز المميز)
  17. 1 بايت == (طول السطر التالي) == n_strlen1
  18. بايت n_strlen1 == (أي سلسلة) == n_strlen1
  19. 1 بايت == (+7 == الرمز التالي) == n_pos
  20. بايت واحد == key_array [0] [0] == 152 (الرمز المميز)
  21. 1 بايت == (طول السطر التالي) == n_strlen1
  22. بايت n_strlen1 == (أي سلسلة) == n_strlen1
  23. 1 بايت == (+7 == الرمز التالي) == n_pos
  24. 4 بايت == (key_array [1340]) == 1122


من أجل الوضوح ، لقد صنعت في Excel مثل هذا الجهاز اللوحي مع بيانات الملف المطلوب:



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

يمكن تنزيل ملف excel نفسه بالإضافة إلى مواد المصدر الأخرى لهذه المقالة هنا .

ملف ثنائي توليد والتحقق من الصحة


الشيء الوحيد المتبقي هو إنشاء الملف المطلوب بالتنسيق الثنائي وإطعامه باستخدام الكراك. لإنشاء الملف ، كتبت سيناريو بيثون بسيطًا:

البرنامج النصي لإنشاء الملف
 import sys, os import struct import subprocess out_str = ['!', 'I', ' solved', ' this', ' crackme!'] def write_file(file_path): try: with open(file_path, 'wb') as outfile: outfile.write('VOID'.encode('ascii')) outfile.write(struct.pack('2h', 5, 4)) outfile.write('b'.encode('ascii')) outfile.write(struct.pack('B', len(out_str[0]))) outfile.write(out_str[0].encode('ascii')) pos = 10 + len(out_str[0]) outfile.write(struct.pack('B', pos - 6)) outfile.write('l'.encode('ascii')) outfile.write(struct.pack('B', len(out_str[1]))) outfile.write(out_str[1].encode('ascii')) pos += 3 + len(out_str[1]) outfile.write(struct.pack('B', pos - 6)) outfile.write('W'.encode('ascii')) outfile.write(struct.pack('B', len(out_str[2]))) outfile.write(out_str[2].encode('ascii')) pos += 3 + len(out_str[2]) outfile.write(struct.pack('B', pos - 6)) outfile.write('p'.encode('ascii')) outfile.write(struct.pack('B', len(out_str[3]))) outfile.write(out_str[3].encode('ascii')) pos += 3 + len(out_str[3]) outfile.write(struct.pack('B', pos - 6)) outfile.write(struct.pack('B', 152)) outfile.write(struct.pack('B', len(out_str[4]))) outfile.write(out_str[4].encode('ascii')) pos += 3 + len(out_str[4]) outfile.write(struct.pack('B', pos - 6)) outfile.write(struct.pack('i', 1122)) except Exception as err: print(err) raise def main(): if len(sys.argv) != 2: print('USAGE: {this_script.py} path_to_crackme[.exe]') return if not os.path.isfile(sys.argv[1]): print('File "{}" unavailable!'.format(sys.argv[1])) return file_path = os.path.splitext(sys.argv[1])[0] + '.dat' try: write_file(file_path) except: return try: outputstr = subprocess.check_output('"{}" -f "{}"'.format(sys.argv[1], file_path), stderr=subprocess.STDOUT) print(outputstr.decode('utf-8')) except Exception as err: print(err) if __name__ == '__main__': main() 


يأخذ البرنامج النصي المسار إلى التصدعات كمعلمة واحدة ، ثم يقوم بإنشاء ملف ثنائي بالمفتاح في نفس الدليل ويستدعي التصدعات باستخدام المعلمة المقابلة ، وترجمة إخراج البرنامج إلى وحدة التحكم.

لتحويل البيانات النصية إلى ثنائي ، استخدم الحزمة الهيكلية . تتيح لك طريقة pack () كتابة بيانات ثنائية بتنسيق يتم فيه تحديد نوع البيانات ("B" = "byte" ، "i" = int ، وما إلى ذلك) ، ويمكنك أيضًا تحديد التسلسل (">" = "كبير -يانديان "،" <"=" Little-endian "). الترتيب الافتراضي هو Little-endian. لأن لقد قررنا بالفعل في المقالة الأولى أن هذا هو حالنا بالضبط ، ثم نشير فقط إلى النوع.

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

تحقق الإخراج:



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

مواد لهذه المادة.

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


All Articles