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 - לקרוא את החולשה ולנסח את הפרימיטיב¶
לפני שתוקפים, תבינו למה זה עובד.
- פתחו את NVD בערך CVE-2013-2028 וקראו את התקציר. אילו גרסאות nginx vulnerable? מה סוג החולשה ומה הווקטור?
- בקוד ה-harness, עקבו אחרי המסע של הערך
size: מה יקרה אםlineהוא"ffffffff"? מה הערך שלsizeכ-int? האם הוא עובר את הבדיקהsize > sizeof(buf)? מה קורה כשהוא מועבר ל-readכ-size_t? - נסחו במשפט אחד את הפרימיטיב המדויק שהחולשה נותנת.
רמז: 0xffffffff כ-int הוא -1. השוואה חתומה -1 > 256 היא שקר, אז אין הגבלה. read מקבל size_t, ו--1 הופך ל-0xffffffffffffffff.
תרגיל 2 - שחזור הקריסה ומציאת ה-offset¶
- התחברו לשירות עם
nc 127.0.0.1 1337או pwntools, שלחו שורת גודל שלילית (ffffffff) ואז המון בתים, וּודאו שהחיבור נופל (ה-worker קרס). - כתבו סקריפט pwntools ששולח
cyclic(400)אחרי שורת הגודל, ומצאו את ה-offset עד כתובת החזרה עםcyclic_find. - ודאו שליטה מלאה: שלחו
p64(0xdeadbeefcafebabe)בדיוק ב-offset והריצו תחת gdb (docker execעם gdb, או הריצו את הבינארי מקומית). ראו ש-RIP נעשה0xdeadbeefcafebabe. - הריצו
checksec ./chunkserverורשמו את ההגנות. מה מהן מקל עליכם, ומה חסר לכם בגלל שהוא כבוי?
רמז: כדי להפעיל את הoverflow בכלל, שורת הגודל חייבת לגרום ל-size להיות שלילי. גודל חיובי גדול פשוט ייחתך ל-256 על ידי הבדיקה.
תרגיל 3 - leak libc וחישוב הבסיס¶
הגנת ASLR דלוקה, אז אתם צריכים כתובת libc אמיתית מזמן ריצה.
- מצאו את
elf.plt['puts'],elf.got['puts']ו-elf.symbols['main']ב-pwntools. - בנו chain ROP שקוראת
puts(&puts@got)ואז חוזרת ל-main. השתמשו ב-rop.call('puts', [elf.got['puts']])ו-rop.call('main'). - שלחו, קראו את שורת הleak, ופענחו עם
u64(p.recvline().strip().ljust(8, b'\x00')). - חשבו
libc.address = leaked - libc.symbols['puts']וּודאו שהבסיס נגמר ב-000.
רמז: הבינארי אינו PIE, אז כתובות ה-GOT, ה-PLT ו-main קבועות ואפשר להשתמש בהן ישירות. החזרה ל-main נותנת סיבוב שני של הלולאה - לכן אפשר לתקוף אחרי שדלפתם. סנכרנו את הפלט עם p.recvuntil(b'chunk> ') לפני כל שליחה.
תרגיל 4 - קבלת shell עם ret2libc¶
- אחרי חישוב הבסיס, בנו chain שנייה:
system("/bin/sh")בכתובות המחושבות. השתמשו ב-next(libc.search(b'/bin/sh\x00'))וב-libc.symbols['system']. - אל תשכחו גאדג'ט
retבודד לalignment המחסנית לפניsystem. - שלחו את ה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¶
- הריצו
one_gadget ./libc.so.6וקבלו את רשימת המועמדים עם האילוצים שלהם. - בחרו מועמד עם אילוץ מסוג
[rsp+0xXX] == NULL(בשליטתכם דרך padding אפסים), והחליפו את chain ה-systemבקפיצה בודדת אליו. - אם המועמד הראשון לא עובד, נסו את הבא. תעדו איזה מהם הצליח ולמה.
רמז: אחרי הoverflow, rsp מצביע על מה שכתבתם מיד אחרי כתובת הגאדג'ט. רפדו במספיק בתי אפס כדי לספק את האילוץ [rsp+0x40] == NULL.
תרגיל 6 (בונוס) - שחזור על nginx האמיתי ו-porting¶
- בנו קונטיינר נפרד עם nginx 1.4.0 מהמקור (ראו את ה-Dockerfile בהרצאה), שלחו בקשת POST עם
Transfer-Encoding: chunkedוגודל chunk זדוני, וּודאו ב-error.logשה-worker קיבל signal 11. זה שחזור הקריסה על היעד האמיתי. - הריצו
checksecעל הבינארי של nginx. אילו הגנות נוספות פעילות שם ולא היו ב-harness? רשמו לפחות שתיים. - שאלת תכנון: אילו leaks נוספות תצטרכו כדי לתקוף את nginx האמיתי, ומדוע העובדה שה-worker חי לאורך זמן (ומשרת בקשות רבות) עוזרת לכם? כתבו את מפת הדרך ל-exploit מלא, גם בלי לממש אותה.
רמז: nginx נבנה עם canary ועם PIE. הcanary דורש leak כדי לכתוב אותו בחזרה במקום, וה-PIE דורש leak בסיס קוד. הכתובות (canary, ASLR, בסיס PIE) יציבות לאורך חיי ה-worker, אז leak אחת מספיקה לכל הבקשות שאחריה.