AFL Under the hood (Part 1)

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


מה קורה ב afl-gcc ו afl-as?

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

למעשה, afl-gcc הוא לא יותר ממעטפת קטנה, שמוסיפה \ עורכת כמה פרמטרים לgcc וגורמת לו להשתמש באסמבלר אחר, שהוא afl-as – שם קורה הקסם האמיתי והאינסטרומנטציה נוספת אל התוכנית שלנו. הקלט שמועבר אל afl-as הוא קוד אסמבלי שעבר קומפילציה של gcc (יש לזכור שכשאומרים קומפילציה לעיתים מתכוונים להפוך קוד לתוצר הסופי, executable, אך לתהליך זה יש כמה תחנות בדרך: compiler->assembler->linker. אנחנו בתחנה האמצעית ועלינו להפוך קוד אסמבלי להוראות מכונה שהמעבד מבין).

החלק המעניין מתחיל בפונקציה add_instrumentation ב afl-as.c. הפונקציה בודקת תחילה אם אנחנו נמצאים בtext section, כלומר אנחנו בחלק שמכיל קוד ולא מידע (data section) לכן במקום זה אפשר להוסיף אינסטרומנטציה. (כשאני אומר אינסטרומנטציה – אני למעשה מתכוון לקוד אסמבלי קטן)

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

·         אם השורה מתחילה בהוראת קפיצה בתנאי (jz, jle, jg) – סימן שאנחנו נמצאים ב"צומת" שמוביל לשני ענפים שונים – במקרה כזה נוסיף אינסטרומנטציה ישר אחרי הקפיצה, קטע קוד זה רץ במידה והתנאי לא מתקיים ולא קפצנו (ענף ראשון בצומת).

·         אם השורה מתחילה ב”.L” ואז מספר (כלומר .L0: .L1: .L2: ) סימן שהגענו לlabel שמסמן יעד של קפיצה. זה רמז של GCC שעוזר לנו להבין לאן קופץ קוד אחרי קפיצה בתנאי. במקרה כזה נוסיף אינסטרומנטציה ישר אחרי הlabel, קטע קוד זה רץ במידה והתנאי כן מתקיים (ענף שני בצומת).

רק כדי להבין כמה פשוט זה נעשה, נסתכל על קטע הקוד הבא (שורות 372-381 afl-as.c)


    if (line[0] == '\t') {
      if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) {
        fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
                R(MAP_SIZE));
        ins_lines++;
      }
  
  

המערך line מכיל את השורה שכרגע אנו נמצאים בה בקוד האסמבלי של התוכנה שלנו. אם אחרי טאב (\t) מתחילה ההוראה באות j – סימן שזו קפיצה. אך אם האות הבאה היא m, ההוראה היא jmp – וזו קפיצה ללא תנאי. לא נוסיף אינסטרומנטציה לקפיצה ללא תנאי כי אין פה צומת ואין פה שום דבר מעניין. אם האות השנייה היא לא m סימן שזו קפיצה בתנאי (je, jne, jg) ונוסיף אינסטרומנטציה – כלומר נכתוב לקובץ באותה נקודה כמה הוראות אסמבלי שנמצאות במשתנה trampoline_fmt_ , ומעבירים בו גם מספר רנדומלי כלשהו כפרמטר (R בקטע קוד הנ"ל זה מאקרו לrandom). את ההוראות שנמצאות בתוך המשתנה הזה נבין תכף.


אחרי שafl-as סיים להוסיף את האינסטרומטציה איפה שהיה צריך – הוא מוסיף בסוף עוד קוד אסמבלי שנמצא בmain_payload – הקוד הזה מכיל כל מיני פונקציות לתפקוד של AFL.

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

·         לקרוא ישירות מקוד המקור, כי כל הקוד שנוסף לתוכנה כתוב ב afl-as.h

·         לקחת את התוכנה שלנו אחרי קומפילציה של afl-gcc ולנתח אותה בעצמינו בdisassembler.

אלך על הדרך השנייה משום שקוד המקור כתוב בסינטקס של AT&T והוא מסורבל בגלל כל מיני preprocessor statements (ifndef, define stuff).

אשתמש בכלי הפשוט והאהוב, objdump, כי לא צריך יותר מזה במקרה שלנו (אנחנו רק צריכים לקרוא את האסמבלי).

כל הפונקציות של AFL (מקורן בmain_payload בafl-as.h) מתחילות ב __afl  לכן לא תהיה בעיה למצוא אותן: __afl_maybe_log, __afl_store, __afl_return, __afl_setup, __afl_setup_first, __afl_forkserver, __afl_fork_wait_loop, __afl_fork_resume, __afl_die, __afl_setup_abort


Trampoline

כל הפונקציות הבאות כתובות בסוף הtext section. אך אנו יודעים גם שבקוד שלנו נוספו הוראות לפני כן, האינסטרומנטציה עצמה של הענפים בקוד – ההוראות שהיו בתוך המשתנה trampoline_fmt. בואו נראה דוגמה: 


      1398:	lea    rsp,[rsp-0x98]
    13a0:	mov    QWORD PTR [rsp],rdx
    13a4:	mov    QWORD PTR [rsp+0x8],rcx
    13a9:	mov    QWORD PTR [rsp+0x10],rax
    13ae:	mov    rcx,0x18fa
    13b5:	call   1538 <__afl_maybe_log>
    13ba:	mov    rax,QWORD PTR [rsp+0x10]
    13bf:	mov    rcx,QWORD PTR [rsp+0x8]
    13c4:	mov    rdx,QWORD PTR [rsp]
    13c8:	lea    rsp,[rsp+0x98]
  

קטע הקוד הזה חוזר על עצמו (למעט הערך שמועבר לrcx) אחרי כל קפיצה בתנאי ואחרי כל ענף חדש בקוד. קטע הקוד הזה לא היה קיים בתוכנית אם הייתי מקמפל אותה עם קומפיילר רגיל, וגם השורה שקודמת לקטע הזה כוללת את ההוראה jne – כלומר זו אינסטרומנטציה שafl-as הוסיף כי הוא זיהה קפיצה.

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

הערך שבתוך rcx הוא מספר רנדומלי בין 0 ל65535. המספר הזה הוא "המזהה הייחודי" של הענף שאנו נמצאים בו בקוד - כך AFL יודע אם הגענו לענף חדש או לא. אם נחזור לשורה בafl-as.c שבה הוא כותב את הtrampoline_fmt לקובץ נראה שמועבר פרמטר לפונקציה. הפרמטר הזה נכתב בתוך הקובץ משום שtrampoline_fmt הוא format string. אם נציץ באמת בקוד המקור, בafl-as.h בהגדרה של trampoline_fmt נראה

movq $0x%08x, %%rcx

שלכאן מגיע הפרמטר לפונקציה fprintf.

 

את הפונקציות הבאות אתאר בקצרה מכיוון שהן קטנות ועושות דבר מאוד פשוט:

afl_setup_abort: הפונקציה הזו משנה את הערך של המשתנה __afl_setup_failure מ0 ל1 כדי לסמן שהsetup נכשל, משחזרת את כל הרגיסטרים שהיה להם גיבוי במחסנית, משחזרת את רגיסטר הflags וחוזרת למי שקרא לה.

afl_return: משחזרת את רגיסטר הflags, וחוזרת למי שקרא לה (עושה return פשוט).

afl_die: קוראת לexit()


את הפונקציות האחרות ננתח לפי הסדר הלוגי שבו הן פועלות.

__afl_maybe_log:


  0000000000001538 <__afl_maybe_log>:
    1538:	lahf   
    1539:	seto   al
    153c:	mov    rdx,QWORD PTR [rip+0x2add]        # 4020 <__afl_area_ptr>
    1543:	test   rdx,rdx
    1546:	je     1568 <__afl_setup>
  

הפונקציה הזאת נקראת בכל פעם שמגיעים לענף בקוד שלנו (ה"טרמפולינה" שלנו).

2 ההוראות הראשונות שבה מגבות את הflags רגיסטר לתוך ax, ולאחר מכן בודקת אם המשתנה __afl_area_ptr הוא 0. במידה וכן, נקפוץ לafl_setup. במידה ולא, הפונקציה תמשיך לשורה הבאה לafl_store.

 

__afl_setup:


0000000000001568 <__afl_setup>:
    1568:	cmp    BYTE PTR [rip+0x2ac9],0x0        # 4038 <__afl_setup_failure>
    156f:	jne    1560 <__afl_return>
    1571:	lea    rdx,[rip+0x2ae0]        # 4058 <__afl_global_area_ptr>
    1578:	mov    rdx,QWORD PTR [rdx]
    157b:	test   rdx,rdx
    157e:	je     1589 <__afl_setup_first>
    1580:	mov    QWORD PTR [rip+0x2a99],rdx        # 4020 <__afl_area_ptr>
    1587:	jmp    1548 <__afl_store>

משווים את הערך שבמשתנה __afl_setup_failure ל0. במידה והוא שונה מ0 קופצים ל __afl_return. אפשר להניח שבדיקה זו בודקת אם הsetup כבר נכשל פעם, ואם כן, נצא ישר מהפונקציה במקום לעבור אותו שוב (את ההנחות אפשר לבסס ע"י קריאה של ההערות בקוד המקור afl-as.h). נמשיך עם קריאה של המצביע __afl_global_area_ptr. אם הערך של המצביע הוא 0 נקפוץ ל __afl_setup_first. אם לא, נשמור את הערך הזה גם ב __afl_area_ptr  ונקפוץ ל __afl_store. נניח ש__afl_global_area_ptr אמור להכיל מצביע כלשהו, אם הוא NULL, כנראה שזו פעם ראשונה שאנו מריצים את הsetup ולכן צריך לקרוא ל __afl_setup_first. הפונקציה נראית בערך כך בפסודו קוד:


  afl_setup:
    if (afl_setup_failure) {
        goto afl_return;
    }
    if (afl_global_area_ptr == NULL) {
        goto afl_setup_first;
    }
    afl_area_ptr = afl_global_area_ptr;
    goto afl_store;
  


__afl_setup_first:


    0000000000001589 <__afl_setup_first>:
    1589:	lea    rsp,[rsp-0x160]
    1591:	mov    QWORD PTR [rsp],rax
    1595:	mov    QWORD PTR [rsp+0x8],rcx
    159a:	mov    QWORD PTR [rsp+0x10],rdi
    159f:	mov    QWORD PTR [rsp+0x20],rsi
    15a4:	mov    QWORD PTR [rsp+0x28],r8
    15a9:	mov    QWORD PTR [rsp+0x30],r9
    15ae:	mov    QWORD PTR [rsp+0x38],r10
     .....
    164a:	push   r12
    164c:	mov    r12,rsp
    164f:	sub    rsp,0x10
    1653:	and    rsp,0xfffffffffffffff0
    1657:	lea    rdi,[rip+0x2c1]        # 191f <.AFL_SHM_ENV>
    165e:	call   10f0 <getenv@plt>
    1663:	test   rax,rax
    1666:	je     184e <__afl_setup_abort>
    166c:	mov    rdi,rax
    166f:	call   1180 <atoi@plt>
    1674:	xor    rdx,rdx
    1677:	xor    rsi,rsi
    167a:	mov    rdi,rax
    167d:	call   1170 <shmat@plt>
    1682:	cmp    rax,0xffffffffffffffff
    1686:	je     184e <__afl_setup_abort>
    168c:	mov    rdx,rax
    168f:	mov    QWORD PTR [rip+0x298a],rax        # 4020 <__afl_area_ptr>
    1696:	lea    rdx,[rip+0x29bb]        # 4058 <__afl_global_area_ptr>
    169d:	mov    QWORD PTR [rdx],rax
    16a0:	mov    rdx,rax
    

הפונקציה שומרת לעצמה 0x160 בתים על המחסנית, לשם היא מכניסה את ערכי כל הרגיסטרים, כנראה גיבוי שתשחזר בהמשך. אחר כך טוענת את הכתובת .AFL_SHM_ENV שמכילה מחרוזת, משתנה סביבה. הקריאה לgetenv בודקת אם קיים משתנה סביבה עם השם הזה ומחזירה לנו מצביע אליו. אם לא, נקפוץ ל __afl_setup_abort  לבטל את כל מה שעשינו. את המצביע שקיבלנו מgetenv נעביר לפונק' atoi שתמיר לנו את המחרוזת למספר – ואת המספר שקיבלנו נעביר כפרמטר לפונקציה shmat. אם הערך שחוזר הוא -1, נקפוץ ל setup_abort. הערך שחוזר הוא מצביע ונשמור אותו ב__afl_global_area_ptr  וב __afl_area_ptr.


כדי להבין מה קרה פה נצטרך להבין מה זה shm

Shared memory 

כשמו כן הוא, זה מנגנון לתקשורת בין תהליכים ע"י זיכרון משותף ביניהם. כדי לקבל זיכרון משותף נקרא לפונקציה shmget ונקבל ID מיוחד לשימוש פונקציות של shm (כמו file descriptor אבל רק לשימוש של המנגנון shm). כדי לקבל מצביע לזיכרון המשותף ולקרוא \ לכתוב אליו, משתמשים בפונקציה shmat עם הID שקיבלנו (אפשר לגשת לזיכרון הזה באמצעות הID מכל תהליך שנרצה). המידע של התוכנית שלנו עובר לפאזר באמצעות זיכרון משותף, אבל איך מלכתחילה יש לתוכנית שלנו את הID לזיכרון המשותף? התשובה היא באמצעות משתנה סביבה __AFL_SHM_ID שיוצר הפאזר ושם בתוכו את הID לזיכרון המשותף. הקוד הבא בafl-fuzz.c:


  shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);
  if (shm_id < 0) PFATAL("shmget() failed");
  atexit(remove_shm);
  shm_str = alloc_printf("%d", shm_id);
  if (!dumb_mode) setenv(SHM_ENV_VAR, shm_str, 1);
	

הקריאה ל shmget מבקשת מהמערכת ליצור סגמנט זיכרון משותף חדש בגודל MAP_SIZE (משתנה של הפאזר שהוגדר כ-64KB).

הsetup בתוכנית שלנו קורא לgetenv כדי לקרוא את משתנה הסביבה, לאחר מכן מעביר את המחרוזת לatoi כי מדובר בID שהוא מספר ולבסוף קריאה לshmat עם הID הזה – והערך החוזר הוא מצביע לזיכרון המשותף שנשמר ב __afl_area_ptr.

פסודו קוד של afl_setup_first:


  afl_setup_first: 
    char registers_backup[0x160];
    BackupRegisters(registers_backup);
    char* afl_shm_env = getenv(AFL_SHM_ENV); // getenv("AFL_SHM_ID");
    if (afl_shm_env == NULL) {
        goto afl_setup_abort;
    }
    int shm_id = atoi(afl_shm_env);
    void* shared_mem_ptr = shmat(shm_id, NULL, 0);
    if (shared_mem_ptr == -1) {
        goto afl_setup_abort;
    }
    afl_global_area_ptr = shared_mem_ptr;
    afl_area_ptr = shared_mem_ptr;
    goto afl_forkserver;
	

אעצור בנקודה זו כדי שהפוסט לא יהפוך להיות ארוך מדי ופעם הבאה נמשיך מהנקודה בה עצרנו – forkserver.

סיכום

בפוסט זה למדנו שafl-as וafl-gcc הם מעטפת קטנה לקומפיילר ולאסמבלר שכל מה שהם עושים זה לשבץ כמה שורות קוד אסמבלי בתוכנה שלנו. עברנו על תהליך הsetup של התוכנה לקראת פאזינג – שכולל יצירת סגמנט זיכרון משותף בין הפאזר לתוכנה שלנו. בפוסט הבא נלמד יותר על התקשורת בין הפאזר לתוכנה שלנו, על forkserver ועל תפקיד הזיכרון המשותף.

Comments

Popular posts from this blog

AFL מבוא לפאזינג עם