לדלג לתוכן

10.5 כתיבת exploit ל CVE אמיתי תרגול

תרגול - כתיבת exploit ל-CVE אמיתי

בתרגול הזה תעברו את כל מסלול ה-n-day מקצה לקצה: תבנו קונטיינר עם harness שמשחזר את הפרימיטיב של החולשה CVE-2013-2028 (שגיאת סימן שמובילה לoverflow buffer על המחסנית ב-nginx), תשחזרו את הקריסה, תבנו שליטה ב-RIP, תעקפו ASLR עם leak libc, ותסגרו ב-shell מרחוק. עבדו לפי הסדר - כל תרגיל בונה על הקודם. המטרה היא לא רק exploit עובד, אלא הבנה של כל שלב בשרשרת.

הכנה - הקונטיינר וה-harness

צרו את קובץ ה-harness שמשחזר את הפרימיטיב. הוא קורא שורת גודל chunk לתוך משתנה חתום, מבצע בדיקת גבולות שנראית בטוחה, ואז גולש:

// chunkserver.c
#include <stdio.h>
#include <unistd.h>

static long read_line(char *dst, long max) {
    long i = 0; char c;
    while (i < max - 1) {
        if (read(0, &c, 1) != 1) break;
        if (c == '\n') break;
        dst[i++] = c;
    }
    dst[i] = 0;
    return i;
}

static int parse_chunk_size(const char *s) {
    int size = 0;                         // signed - this is the root of the vulnerability
    for (const char *p = s; *p; p++) {
        int v; char c = *p;
        if (c >= '0' && c <= '9') v = c - '0';
        else if (c >= 'a' && c <= 'f') v = c - 'a' + 10;
        else if (c >= 'A' && c <= 'F') v = c - 'A' + 10;
        else break;
        size = size * 16 + v;
    }
    return size;
}

static void discard_body(void) {
    char buf[256];
    char line[64];
    write(1, "chunk> ", 7);
    read_line(line, sizeof(line));
    int size = parse_chunk_size(line);
    if (size > (int)sizeof(buf))          // a signed check - negative bypasses it
        size = sizeof(buf);
    read(0, buf, size);                   // the overflow
}

int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);
    for (;;)
        discard_body();
    return 0;
}

והקונטיינר:

# Dockerfile
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y gcc socat && rm -rf /var/lib/apt/lists/*
WORKDIR /chal
COPY chunkserver.c .
RUN gcc -fno-stack-protector -no-pie -O0 -o chunkserver chunkserver.c
EXPOSE 1337
CMD ["socat", "TCP-LISTEN:1337,reuseaddr,fork", "EXEC:./chunkserver"]

בונים, מריצים, ומושכים את ה-libc של המטרה ואת הבינארי (תצטרכו את שניהם בסקריפט):

docker build -t chunkchal .
docker run -d --name chunk -p 1337:1337 chunkchal
docker cp chunk:/lib/x86_64-linux-gnu/libc.so.6 .
docker cp chunk:/chal/chunkserver .

ודאו ש-ASLR דלוק על המארח (cat /proc/sys/kernel/randomize_va_space צריך להיות 2) - זה כל האתגר.

תרגיל 1 - לקרוא את החולשה ולנסח את הפרימיטיב

לפני שתוקפים, תבינו למה זה עובד.

  1. פתחו את NVD בערך CVE-2013-2028 וקראו את התקציר. אילו גרסאות nginx vulnerable? מה סוג החולשה ומה הווקטור?
  2. בקוד ה-harness, עקבו אחרי המסע של הערך size: מה יקרה אם line הוא "ffffffff"? מה הערך של size כ-int? האם הוא עובר את הבדיקה size > sizeof(buf)? מה קורה כשהוא מועבר ל-read כ-size_t?
  3. נסחו במשפט אחד את הפרימיטיב המדויק שהחולשה נותנת.

רמז: 0xffffffff כ-int הוא -1. השוואה חתומה -1 > 256 היא שקר, אז אין הגבלה. read מקבל size_t, ו--1 הופך ל-0xffffffffffffffff.

תרגיל 2 - שחזור הקריסה ומציאת ה-offset

  1. התחברו לשירות עם nc 127.0.0.1 1337 או pwntools, שלחו שורת גודל שלילית (ffffffff) ואז המון בתים, וּודאו שהחיבור נופל (ה-worker קרס).
  2. כתבו סקריפט pwntools ששולח cyclic(400) אחרי שורת הגודל, ומצאו את ה-offset עד כתובת החזרה עם cyclic_find.
  3. ודאו שליטה מלאה: שלחו p64(0xdeadbeefcafebabe) בדיוק ב-offset והריצו תחת gdb (docker exec עם gdb, או הריצו את הבינארי מקומית). ראו ש-RIP נעשה 0xdeadbeefcafebabe.
  4. הריצו checksec ./chunkserver ורשמו את ההגנות. מה מהן מקל עליכם, ומה חסר לכם בגלל שהוא כבוי?

רמז: כדי להפעיל את הoverflow בכלל, שורת הגודל חייבת לגרום ל-size להיות שלילי. גודל חיובי גדול פשוט ייחתך ל-256 על ידי הבדיקה.

תרגיל 3 - leak libc וחישוב הבסיס

הגנת ASLR דלוקה, אז אתם צריכים כתובת libc אמיתית מזמן ריצה.

  1. מצאו את elf.plt['puts'], elf.got['puts'] ו-elf.symbols['main'] ב-pwntools.
  2. בנו chain ROP שקוראת puts(&puts@got) ואז חוזרת ל-main. השתמשו ב-rop.call('puts', [elf.got['puts']]) ו-rop.call('main').
  3. שלחו, קראו את שורת הleak, ופענחו עם u64(p.recvline().strip().ljust(8, b'\x00')).
  4. חשבו libc.address = leaked - libc.symbols['puts'] וּודאו שהבסיס נגמר ב-000.

רמז: הבינארי אינו PIE, אז כתובות ה-GOT, ה-PLT ו-main קבועות ואפשר להשתמש בהן ישירות. החזרה ל-main נותנת סיבוב שני של הלולאה - לכן אפשר לתקוף אחרי שדלפתם. סנכרנו את הפלט עם p.recvuntil(b'chunk> ') לפני כל שליחה.

תרגיל 4 - קבלת shell עם ret2libc

  1. אחרי חישוב הבסיס, בנו chain שנייה: system("/bin/sh") בכתובות המחושבות. השתמשו ב-next(libc.search(b'/bin/sh\x00')) וב-libc.symbols['system'].
  2. אל תשכחו גאדג'ט ret בודד לalignment המחסנית לפני system.
  3. שלחו את הpayload השני בסיבוב השני (אחרי החזרה ל-main) וקבלו shell. ודאו שהוא אינטראקטיבי (id, ls).

רמז: rop2 = ROP(libc) אחרי שהצבתם libc.address, ואז rop2.find_gadget(['ret']) ו-rop2.call(libc.symbols['system'], [binsh]). אם ה-shell לא נפתח והתוכנית קורסת בתוך libc - זו מלכודת ה-movaps, הוסיפו/הזיזו את גאדג'ט ה-ret.

תרגיל 5 - גרסת one_gadget

  1. הריצו one_gadget ./libc.so.6 וקבלו את רשימת המועמדים עם האילוצים שלהם.
  2. בחרו מועמד עם אילוץ מסוג [rsp+0xXX] == NULL (בשליטתכם דרך padding אפסים), והחליפו את chain ה-system בקפיצה בודדת אליו.
  3. אם המועמד הראשון לא עובד, נסו את הבא. תעדו איזה מהם הצליח ולמה.

רמז: אחרי הoverflow, rsp מצביע על מה שכתבתם מיד אחרי כתובת הגאדג'ט. רפדו במספיק בתי אפס כדי לספק את האילוץ [rsp+0x40] == NULL.

תרגיל 6 (בונוס) - שחזור על nginx האמיתי ו-porting

  1. בנו קונטיינר נפרד עם nginx 1.4.0 מהמקור (ראו את ה-Dockerfile בהרצאה), שלחו בקשת POST עם Transfer-Encoding: chunked וגודל chunk זדוני, וּודאו ב-error.log שה-worker קיבל signal 11. זה שחזור הקריסה על היעד האמיתי.
  2. הריצו checksec על הבינארי של nginx. אילו הגנות נוספות פעילות שם ולא היו ב-harness? רשמו לפחות שתיים.
  3. שאלת תכנון: אילו leaks נוספות תצטרכו כדי לתקוף את nginx האמיתי, ומדוע העובדה שה-worker חי לאורך זמן (ומשרת בקשות רבות) עוזרת לכם? כתבו את מפת הדרך ל-exploit מלא, גם בלי לממש אותה.

רמז: nginx נבנה עם canary ועם PIE. הcanary דורש leak כדי לכתוב אותו בחזרה במקום, וה-PIE דורש leak בסיס קוד. הכתובות (canary, ASLR, בסיס PIE) יציבות לאורך חיי ה-worker, אז leak אחת מספיקה לכל הבקשות שאחריה.