الهندسة العكسية - Reverse Engineering

إستغلال ثغرات Buffer overflow على أنظمة Linux بإستخدام ret2libc technique لتخطي NX

منذ فترة لم أقم بالكتابة داخل iSecur1ty للإنشغال ببعض الأمور , لذلك أحببت أن أعود للكتابة من جديد من خلال البدأ بسلسلة مقالات جديدة تختص بالـ binary exploitation سوف أقوم من خلالها بشرح العديد من الثغرات وإستغلالها على الأنظمة المُختلفة , وفي هذا المقال إن شاء الله سوف أقوم بشرح كيفية إستغلال ثغرات buffer overflow على أنظمة Linux وتخطي الـ Non executable stack أو ما يُعرف بالـ NX من خلال إستخدام تقنية Return2Libc , وسوف أحاول قدر الإمكان إعادة شرح أساسيات ثغرات الـ buffer overflow بشكل مُلخص أثناء المقال.

مع تطور تقنيات الحماية من ثغرات الـ Memory corruption مع مُرور الزمن تم إستحداث بعض التقنيات الخاصة لمحاولة وقف هذه الثغرات داخل أنظمة التشغيل المُختلفة , ومن أشهر هذه الحمايات :

  1. Address Space Layout Randomization – ASLR
  2. Data Execution Prevention – DEP
  3. Stack canaries
  4. No Execute – NX bit

بهذه السلسلة إن شاء الله سوف أقوم بكتابة مجموعة من المقالات تُختص بتخطي هذه الحمايات على أنظمة Windows و Linux , وسوف أبدأ بهذا المقال بالتحدث عن كيفية تخطي الـ NX bit على أنظمة Linux.

ما هي حماية الـ No Execute – NX bit ؟

قبل البدأ بدراسة كيفية تخطي هذه الحماية لا بُد لنا أن نقوم بمعرفة فوائد هذه الحماية وكيفية عملها , بإختصار في حالتنا هذه الحماية يتم إستخدمها مع الـ stack أثناء عمل compile لـ C program من خلال gcc compiler حيث تمنع هذه الحماية تنفيذ أي instructions موجوده داخل الـ stack الخاص بالبرنامج الخاص بنا , حيث أنه بالحالات التقليدية الخاصة بالـ buffer overflow بعد التحكُم بالـ EIP نقوم بوضع الـ shellcode الخاص بنا داخل الـ stack ومن ثم نقوم بعمل jump إلى الـ stack وبدأ تنفيذ الـ shellcode الخاص بنا , وفي الصور التالية سوف أوضح لكم تأثير الـ NX bit وكيف يُمكن أن يكون مُفعل داخل برنامج مُعين.

قبل البدأ سوف أقوم بإستخدام التالي :

  1. Ubuntu 16.04
  2. GDB with pwndbg

وهذا هو الـ source code الخاص بالبرنامج الخاص بنا :

#include <stdlib.h>
#include <stdio.h>


int main(int argc, char *argv[]){


char buffer[50];

strcpy(buffer, argv[1]);

printf("Done !\n");
printf("Your input is %s\n", buffer);

}

حيث أن هذا البرنامج مُصاب بثغرة buffer overflow وتحدث أثناء إستخدام strcpy function بالسطر رقم 10 لنقل الـ user input للـ buffer array , تحدثنا أكثر عن هذا الموضوع داخل iSecur1ty بشكل مُسبق.

دعونا نقوم الأن بإستخدام الأمر التالي لعمل compile للبرنامج :

gcc stack.c -o stack -m32 -w

أنا أقوم بإستخدام ubuntu x64 لذلك قمت بإستخدام الـ m32 flag لكي يعمل الملف بإستخدام الـ 32bit architecture.

بعد عمل compile للبرنامج سوف تكون هذه النتيجه :

دعونا الأن نقوم بتشغيل الملف التنفيذي stack بإستخدام الـ GDB كالتالي :

سوف نقوم بتشغيل البرنامج من خلال تنفيذ الأمر r داخل الـ gdb لتكون النتيجه كالتالي :

هذا الخطأ (SIGSEGV) طبيعي بسبب عدم إدخال قيمة argv للداله strcpy لنقلها لـ buffer ونحن للأن لم نقم بعمل overflow للبرنامج وذلك لأنني أريد توضيح فكرة عمل الـ NX عموماً

دعونا نقوم الأن بإستخراج الـ process id ودراسة بعض الأمور داخل الـ process الحاليه كما التالي :

الـ process id الخاصه بنا هي 3504 وقد علمنا ذلك كما نرى بعد تنفيذ الأمر info inferior , سوف نقوم بدراسة الـ memory map للـ process الخاصه بنا من خلال قرأة الملف التالي :

/proc/3504/maps

ملف الـ maps يقوم بسرد معلومات الـ virtual memory الخاصه بالـ process الخاصه بنا

مع تغير 3504 للـ process id الخاصه بك لنحصل على التالي :

كما نرى الخانه الخاصه بالصلاحيات الممنوحه للـ stack تحتوي فقط على إمكانية القراءة والكتابة “rw” ولكنها لا تحتوي على صلاحية التنفيذ , لذلك لن نستطيع تنفيذ الـ shellcode الخاص بنا داخل الـ stack.

نستطيع تعطيل الـ NX أثناء عمل compile للبرنامج بإستخدام gcc من خلال الأمر التالي :

الخيار execstack سوف يقوم يُعطي الـ stack صلاحية التنفيذ , وبعد تكرار السابق وعرض ملف الـ maps تكون النتيجه كالتالي :

كما نرى الـ stack بالنهاية أصبح يحمل الـ execution permission وأصبح قادر على تنفيذ ما بداخله.

الأن بعد ما فهمنا كيف تعمل هذه الحماية , دعونا أن نقوم ببدأ تخطيها في الحاله الأولى “مع وجود الـ NX”.

كيف نستطيع إستغلال الثغرة وتخطي الـ NX ؟

بِكُل بساطة نستطيع إستغلال هذه الثغره من خلال تقنية تُسمى Return2libc , حيث أن جميع البرامج التي تعمل على أنظمة Linux في نهاية الأمر تقوم بالإعتماد على libc في الحصول على الـ functions التي يُراد التعامل معها , لذلك هذه التقنية بإختصار تسمح لنا بأن نقوم بإستدعاء وإعادة إستخدام بعض الـ functions من الـ libc بعد التحكُم بالـ EIP الخاص بالبرنامج , حيث أننا سوف نقوم بإستخدام الـ system function وتمرير القيمه /bin/bash لها لكي تقوم بتنفيذها والحصول على shell من خلالها , ونقوم بتمرير الـ exit() function تجنباً لأي مشاكل قد تحصل بعد الرجوع للـ function الخاصه بنا

بالطبع يوجد تقنيات أخرى مُشابهه لتخطي الـ NX مثل الـ ROP chaining والتي سوف أتطرق لها في مقال منفصل إن شاء الله

لذلك بإختصار نحتاج للتالي :

crash - 4) + system_function_address + exit_function_address + bin_sh_address)
  • قمنا بعمل crash – 4 لكي نُمرر الـ system_function_address بدلاً من الـ BBBB.

وهذا سوف يقوم بتغير الـ execution flow الخاص بالبرنامج إلى الداله system ومن ثم تمرير الـ bin_sh لها , لذلك دعونا الأن نقوم بتنفيذ التالي :

  1. إستغلال الثغرة داخل البرنامج
  2. تنفيذ تقنية الـ Return2Libc

دعونا نقوم بإعادة عمل compile للبرنامج بإستخدام الأمر :

gcc stack.c -o stack -m32 -z execstack -fno-stack-protector -mpreferred-stack-boundary=2 -w

لتكون النتيجه كالتالي :

ممتاز , دعونا الأن نقوم بمحاولة عمل crash للبرنامج من خلال إدخال عدد كبير من الأحرف بعد أن نقوم بتشغيل البرنامج من خلال GDB كالتالي :

ومن ثم تشغيل البرنامج من خلال r لنحصل على التالي :

ممتاز , كما نرى أن الـ EIP الخاص بالبرنامج تم الكتابة عليه بالقيمه 41414141 وهي تُساوي القيمة AAAA أي أننا تمكنا من إحداث crash فيه وبإمكاننا السيطرة على EIP لتغير الـ Execution Flow لتنفيذ الهجوم الخاص بنا , وهذه أيضاً صورة لمحتويات الـ stack الذي يؤشر الـ ESP عليه “0xffbb3300” :

نرى أنه يمتلئ بباقي المُدخلات الخاصه بنا والتي تحتوي على حرف الـ A فقط , الأن بالحاله الطبيعيه هي أن نقوم بوضع الـ shellcode لداخل الـ stack ومن ثم عمل jump إليه , ولكننا لن نستطيع أن نقوم بهذا بسبب الـ NX , لذلك دعونا الأن أن نقوم بالتحكم بشكل كامل بالـ EIP لتنفيذ الهجوم , وسوف نقوم بتشغيل السكربت التالي فالبداية لتوليد pattern لمعرفة عدد الـ bytes التي سوف نستخدمها للتحكم بالـ EIP :

#!/usr/bin/python

from pwnlib.util.cyclic import cyclic

print cyclic(200)

نحتاج إلى تحميل pwntools library لتشغيل هذا الكود ونستطيع تحميلها من خلال pip install pwntools

وتشغيل الكود من خلال gdb بإستخدام :

لنحصل على التالي :

توقف التنفيذ عند القيمة 0x616f6161 وهي “aaoa” كما نرى من خلال الـ EIP , ولمعرفة القيمة نستطيع تنفيذ التالي من خلال python :

ممتاز , إذاً بعد 54 byte نستطيع أن نتحكم بالـ EIP , لذلك سوف نقوم بتنفيذ الكود التالي للتحقق من ذلك :

#!/usr/bin/python


crash = "A" * 54 + "B" * 4

print crash

وتنفيذه لنحصل على :

ممتاز جداً , لقد قمنا بالتحكم بالـ EIP بسهوله.

الأن سوف نبدأ بتنفيذ هجوم Return2libc وسوف نحتاج للتالي :

  1. عنوان خاص بالـ system function
  2. عنوان خاص بالنص “/bin/sh”
  3. عنوان خاص الـ exit function

وهذا يعني بأننا سوف نقوم بتحويل الـ EIP إلى system ومن ثم تمرير الـ /bin/bash كـ argument لها , حيث نستطيع معرفة المزيد من خلال تنفيذ الأمر man system كما يلي :

حيث أن system كما نرى تأخذ argument واحده وهي عباره عن الـ command الخاص بنا , لذلك فور تمريرها سوف نقوم بتمرير عنوان bin/bash كما ذكرت.

ولكن قبل جلب العناوين يجب علينا تعطيل الـ ASLR من النظام لكي نتمكن من عمل exploitation كامل لهذا البرنامج , حيث أن ASLR تمنع تثبيت العناوين التي نُريدها لذلك سوف تُشكل لنا مُشكله.

في المقالات القادمه بالسلسله بإذن الله سوف أتطرق لكيفية تخطي الـ ASLR مع الـ NX وسوف أتحدث عن ASLR بشكل أكبر إن شاء الله

نستطيع تعطيل ASLR من خلال كتابة الأمر :

echo 0 > /proc/sys/kernel/randomize_va_space

سوف نبدأ بإيجاد العناوين الخاصه بنا كالتالي :

نبدأ بعنوان system نستطيع الحصول عليه من خلال :

حيث أن العنوان الخاص بها هو 0xf7e3eda0 , لنقوم بتسجيله جانباً للإستخدام اللاحق , الأن لن نستطيع الحصول على /bin/bash من داخل البرنامج نفسه إلا في حالة أن النص بشكل أو بأخر لا بد أن يكون مربوط بالبرنامج إما من خلال نص داخلي أو من خلال الـ environment variable , سوف نقوم بحل هذه المشكلة بشكل ثابت وبشكل مُستقر كُلياً من خلال تنفيذ المعادلة التالية :

binsh_address = libcbase + binsh_string_offset

حيث أننا نستطيع الحصول على الـ libc base address من خلال الأمر التالي :

حيث كما نرى الـ base address الخاص بها هو 0xf7e04000

يُمكن إيجاد binsh_string_offset  من خلال إضافة الـ offset الخاص بالنص إلى 0xf7e04000 , ونستطيع إيجاد الـ offset من خلال :

حيث أننا نُخبر grep بإيجاد النص /bin/sh وطباعة الـ byte offset من خلال -b ومعاملة الملف كـ text لإيجاد المطلوب وكما نرى الـ offset هو 1423883 , وعليه فإن العنوان النهائي هو offset + libcbase

وأخيراً سوف نقوم بإيجاد الـ exit function من خلال :

وكما نرى الـ address الخاص بها هو 0xf7e329d0 , أصبحنا نحتوي جميع مُتطلبات الهجوم والكود النهائي هو كالتالي :

#!/usr/bin/python

import struct

crash = "A" * 54

libc_base = 0xf7e04000

system = struct.pack("I", 0xf7e3eda0)

binsh_string_offset = struct.pack("I", libc_base + 1423883)

exit = struct.pack("I", 0xf7e329d0)

data = crash + system + exit + binsh_string_offset

print data

دعونا نقوم بتشغيل الـ code وعمل debug له للتأكد من جميع القيم كالتالي :

الأمر المُستخدم بالطبع هو :

gdb --args stack $(python exploit.py)

مع وضع الكود النهائي داخل exploit.py

سوف أقوم بوضع break point عند الـ return address الخاص بالـ main function , بعد أن قُمت بعمل disassemble لها , ومن ثم تشغيل البرنامج من خلال r :

نرى أن الـ ret الأن سوف توصلنا للـ system , نقوم بإستكمال التشغيل وعمل step into أو “الذهاب لل instruction التاليه” من خلال الأمر ni كما التالي لنحصل على :

كما نرى في المربع الأحمر الأول نرى أنه بالفعل تم تمرير الـ /bin/sh إلى الـ system function وفي المُربع الثاني يُمكن أن نرى الـ exit function والـ address الخاص بالـ /bin/sh داخل الـ stack.

إذاً كل شيء يعمل بشكل جيد من المفترض , دعونا أن نقوم بإغلاق الـ gdb وتشغيل الـ exploit كما التالي :

كما نرى بالفعل لقد قُمنا بتشغيل الـ payload الخاص بنا كما يجب وقمنا بفتح shell جديد على النظام.

بهذا أكون قد إنتهيت من المقال بشكل كامل , ويُعتبر هذا المقال الجزء الأول إن شاء الله من سلسلة الـ binary exploitation , حيث أيضاً سوف أحاول أن أقوم بشرح بعض التقنيات على real world softwares إن شاء الله في حال تسنّت لي الفُرصه.

في حال كان لديكم أي إستفسارات المرجو إخباري بها في التعليقات.

‫6 تعليقات

  1. زمااااان عن هذه المقالات

    مقال رائع محمد , اشتقنا الى مقالات راقية مثل هذا ….. رائع 🙂

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

زر الذهاب إلى الأعلى