8.1 קבצים ארוזים ו UPX פתרון
פתרון - קבצים ארוזים ו-UPX¶
נעבור תרגיל-תרגיל. נזהה את האריזה, נפרק בשתי דרכים, ונחלץ את הדגל. הדגל של האתגר flag הוא:
עכשיו נראה איך מגיעים אליו.
פתרון תרגיל 0 (חימום) - לבנות ולפרוק קובץ ארוז משלכם¶
התוכנית ששמרנו כ-secret.c:
#include <stdio.h>
#include <string.h>
int main(void) {
char buf[64];
strcpy(buf, "FLAG{this_string_was_hidden_by_upx}");
puts("packed demo running");
printf("len=%zu\n", strlen(buf)); // keeps buf alive without exposing it
return 0;
}
מהדרים, מוודאים שהסוד גלוי, אורזים, ומוודאים שהוא נעלם:
gcc -O0 -static -o secret secret.c
strings secret | grep -i flag # FLAG{this_string_was_hidden_by_upx}
upx --best -o secret.packed secret
strings secret.packed | grep -i flag # (empty - gone)
strings secret.packed | grep -i upx # reveals UPX! and the signatures
ופורקים בחזרה:
upx -d secret.packed -o secret.unpacked
strings secret.unpacked | grep -i flag # FLAG{this_string_was_hidden_by_upx}
למה זה עבד: הדחיסה הפכה את .rodata (שם יושבת המחרוזת) לגוש בלתי קריא, ולכן strings על הקובץ הארוז לא ראה אותה. upx -d שחזר את הבינארי המקורי במלואו, והמחרוזת חזרה. עכשיו יש לנו תחושת בטן לכל שלב, ואפשר לגשת לאתגר האמיתי.
פתרון תרגיל 1 - לזהות שהאתגר ארוז¶
בודקים את סוג הקובץ:
שתי מילים כאן מסגירות המון: statically linked (בינארי סטטי, מתאים לאריזה), ו-no section header (אין טבלת סקשנים - חריג מאוד לבינארי מהודר רגיל). מנסים לראות מחרוזות:
לבינארי סטטי בגודל מכובד היינו מצפים לאלפי מחרוזות. במקום זה מקבלים חופן קטן וג'יבריש. בודקים סקשנים מול מקטעים:
readelf -S ./flag # warning / "There are no sections in this file."
readelf -l ./flag # two large LOAD segments, entry inside the second
מריצים את סקריפט האנטרופיה מההרצאה ורואים קפיצה חדה לאזור רחב סביב 7.9 סיביות לבית - הגוש הדחוס. ולבסוף, ההוכחה החד-משמעית:
UPX!
$Info: This file is packed with the UPX executable packer http://upx.sf.net $
$Id: UPX 3.08 Copyright (C) 1996-2011 the UPX Team. All Rights Reserved. $
למה זה עבד: כל אחד מהסימנים לבדו רק מרמז, אבל ביחד הם ודאות. חתימות ה-UPX הן ההוכחה הישירה - הכלי משאיר אותן בכל קובץ שהוא אורז. מסקנה: הקובץ ארוז ב-UPX.
איך להכליל: כשאין חתימת טקסט נוחה (ארזים אחרים לא בהכרח משאירים), נשענים על שילוב של אנטרופיה גבוהה, מיעוט מחרוזות, והיעדר סקשנים. כלי כמו die מזהה עשרות ארזים לפי חתימות בינאריות.
פתרון תרגיל 2 - לפרוק את הקובץ¶
מאשרים שזה UPX תקין ופורקים:
Ultimate Packer for eXecutables
File size Ratio Format Name
-------------------- ------ ----------- -----------
883745 <- 335288 37.94% linux/amd64 flag.unpacked
Unpacked 1 file.
עכשיו הקובץ הפרוק שונה לגמרי:
file ./flag.unpacked # ELF 64-bit, statically linked, WITH section header table
readelf -S ./flag.unpacked # .text .rodata .data ... all the sections came back
strings ./flag | wc -l # very few
strings ./flag.unpacked | wc -l # much more
למה זה עבד: כותרת ה-UPX בתוך הקובץ שמרה את כל מה שהפורק צריך (גודל מקורי, פרמטרי דחיסה, OEP), אז upx -d פשוט הריץ את אלגוריתם הפריקה ההפוך ושחזר את הבינארי המקורי בייט-בבייט.
פתרון תרגיל 3 - לחלץ את הדגל¶
הדרך המהירה עובדת מיד:
זה הדגל. אבל שווה לראות למה הוא שם, כי זו הדרך היסודית. מפרקים את main:
<main>:
push rbp
mov rbp,rsp
sub rsp,0x10
mov edi,0x496658 ; "I will malloc() and strcpy the flag there. take it."
call <puts>
mov edi,0x64 ; 100
call <malloc>
mov QWORD PTR [rbp-0x8],rax ; ptr = malloc(100)
mov rdx,QWORD PTR [rip+...] ; loads the global pointer flag
mov rax,QWORD PTR [rbp-0x8]
mov rsi,rdx ; strcpy source = the string flag points to
mov rdi,rax ; destination = the allocated buffer
call <strcpy>
התוכנית מקצה 100 בתים, ומעתיקה אליהם את המחרוזת שעליה מצביע המשתנה הגלובלי flag. היא אף פעם לא מדפיסה אותה למסך - רק מדפיסה את ההודעה "I will malloc()...". לכן, אילו הקובץ נשאר ארוז, לא היינו רואים את הדגל בשום מקום. בקובץ הפרוק אפשר לקרוא אותו ישירות ב-gdb:
או בלי סמלים: x/s על הכתובת שאליה flag מצביע.
למה זה עבד: הדגל הוא קבוע ב-.rodata. ברגע שהאריזה ירדה, strings חושף כל מחרוזת בקובץ, כולל אותו. פירוק האריזה הפך משימה שנראתה בלתי אפשרית (ג'יבריש דחוס) לטריוויאלית (strings | grep).
איך להכליל: כשהדגל בנוי דינמית ולא יושב כמחרוזת אחת רציפה, strings לא יעזור, ואז חוזרים לקריאת ה-main ומבינים איך הוא מורכב - בדיוק כמו שעשינו כאן דרך ה-strcpy.
פתרון תרגיל 4 - פריקה ידנית מהזיכרון¶
עכשיו נחלץ את אותו הדגל מהקובץ הארוז המקורי flag, בלי upx -d בכלל. פותחים ב-gdb ונותנים ל-stub לפרוק:
pwndbg> starti
Starting program: .../flag
Program stopped at the entry point (inside the UPX stub)
pwndbg> catch syscall write
Catchpoint 1 (syscall 'write')
pwndbg> continue
Continuing.
I will malloc() and strcpy the flag there. take it.
Catchpoint 1 (call to syscall write)
עצרנו על ה-write הראשון (זה שהדפיס את ההודעה). בשלב הזה ה-stub כבר סיים לפרוק את כל הבינארי המקורי לזיכרון, כולל .rodata עם הדגל. מחפשים:
pwndbg> search "delivery"
[anon] 0x496628 'UPX...? sounds like a delivery service :)'
pwndbg> x/s 0x496628
0x496628: "UPX...? sounds like a delivery service :)"
ואם לא יודעים מראש איזו מילה לחפש, משליכים את הזיכרון ומריצים strings:
pwndbg> info proc mappings
0x400000 0x4c8000 ... the region of the original binary
pwndbg> dump memory /tmp/d.bin 0x400000 0x4c8000
למה זה עבד: ה-stub של UPX חייב לשחזר את המקור המלא לזיכרון לפני שהוא קופץ ל-OEP, אחרת התוכנית לא תרוץ. הבחירה לעצור על write נותנת ערובה שהפריקה הסתיימה - קריאת המערכת הזו היא כבר חלק מהתוכנית המקורית, אחרי ה-OEP. מכאן הכול פרוש בזיכרון וקריא.
איך להכליל: השיטה הזו עובדת מול כל ארז, לא רק UPX, וגם כשאין לנו כלי פירוק ייעודי. העיקרון קבוע: תן ל-stub לפרוק, עצור בנקודה שבטוח אחרי הפריקה (קריאת מערכת, breakpoint על ה-OEP, או פשוט אחרי שהתוכנית הדפיסה משהו), ואז קרא מהזיכרון.
פתרון תרגיל 5 (העמקה) - כשהכותרת שובשה¶
מדגימים את הטריק על העותק מתרגיל 0. קודם מאתרים את מספר הקסם ומשבשים בית אחד בעותק:
cp secret.packed secret.corrupt
grep -abo 'UPX!' secret.corrupt # shows the offsets of every 'UPX!'
# corrupt one byte of one occurrence (e.g. at the second offset) using a hex editor or printf:
printf '\x00' | dd of=secret.corrupt bs=1 seek=<OFFSET> count=1 conv=notrunc
עכשיו הקובץ עדיין רץ, אבל upx -d נשבר:
./secret.corrupt # runs normally, prints the message
upx -d secret.corrupt # upx: secret.corrupt: NotPackedException / CantUnpackException
חילוץ הסוד בכל זאת, בשיטה הידנית:
pwndbg> starti
pwndbg> catch syscall write
pwndbg> continue
pwndbg> search "FLAG"
[anon] 0x... 'FLAG{this_string_was_hidden_by_upx}'
הסוד יצא, אף שהכלי הרשמי סירב.
למה זה עבד: upx -d בודק את החתימה כדי לוודא שזה קובץ UPX תקין לפני שהוא מפרק, ולכן בית מקולקל אחד מספיק כדי לעצור אותו. ה-stub, לעומת זאת, לא בודק את החתימה - הוא פשוט מריץ את קוד הפריקה. לכן פריקה מהזיכרון עמידה לחלוטין לשיבוש הכותרת.
איך להכליל: כדי להחזיר את upx -d לעבודה, מספיק לתקן את הבית ששיברנו בחזרה למקור. איך יודעים איזה בית? משווים לקובץ תקין שאורז באותה גרסת UPX (upx --version), או מזהים שהחתימה UPX! אמורה להיות בדיוק ארבעת הבתים האלה. בעולם ה-CTF זה תרחיש נפוץ: מזהים מה שובש, מתקנים, ואז upx -d עובד - או פשוט מדלגים על הכלי ופורקים ידנית.
סיכום הפתרון¶
- זיהוי:
fileהראה בינארי סטטי בלי סקשנים,strings | grep -i upxחשף את החתימות. הקובץ ארוז ב-UPX. - פריקה אוטומטית:
upx -tאישר,upx -d ./flag -o ./flag.unpackedשחזר את הבינארי המקורי בפקודה אחת. - חילוץ הדגל:
strings ./flag.unpackedמוצא אותו מיד, וקריאתmainמסבירה למה - התוכנית עושהstrcpyמהמשתנה הגלובליflag. הדגל:UPX...? sounds like a delivery service :). - פריקה ידנית: מריצים תחת gdb,
catch syscall writeמבטיח שהפריקה הסתיימה, ואזsearchבזיכרון אוdump memoryו-stringsחושפים את הדגל בלי לפרוק את הקובץ כלל. - שיבוש כותרת: משבש בית אחד ב-
UPX!שובר אתupx -dאבל לא את ה-stub. פריקה ידנית מהזיכרון עמידה לזה לגמרי.