לדלג לתוכן

8.1 קבצים ארוזים ו UPX פתרון

פתרון - קבצים ארוזים ו-UPX

נעבור תרגיל-תרגיל. נזהה את האריזה, נפרק בשתי דרכים, ונחלץ את הדגל. הדגל של האתגר flag הוא:

UPX...? sounds like a delivery service :)

עכשיו נראה איך מגיעים אליו.

פתרון תרגיל 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 - לזהות שהאתגר ארוז

בודקים את סוג הקובץ:

file ./flag
flag: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header

שתי מילים כאן מסגירות המון: statically linked (בינארי סטטי, מתאים לאריזה), ו-no section header (אין טבלת סקשנים - חריג מאוד לבינארי מהודר רגיל). מנסים לראות מחרוזות:

strings ./flag | head -40

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

readelf -S ./flag     # warning / "There are no sections in this file."
readelf -l ./flag     # two large LOAD segments, entry inside the second

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

strings ./flag | grep -i upx
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 תקין ופורקים:

upx -t ./flag                       # tested OK
upx -d ./flag -o ./flag.unpacked
                       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 - לחלץ את הדגל

הדרך המהירה עובדת מיד:

strings ./flag.unpacked | grep -i "delivery"
UPX...? sounds like a delivery service :)

זה הדגל. אבל שווה לראות למה הוא שם, כי זו הדרך היסודית. מפרקים את main:

objdump -d ./flag.unpacked | grep -A 25 '<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:

gdb ./flag.unpacked
pwndbg> p (char*) flag
$1 = 0x496628 "UPX...? sounds like a delivery service :)"

או בלי סמלים: x/s על הכתובת שאליה flag מצביע.

למה זה עבד: הדגל הוא קבוע ב-.rodata. ברגע שהאריזה ירדה, strings חושף כל מחרוזת בקובץ, כולל אותו. פירוק האריזה הפך משימה שנראתה בלתי אפשרית (ג'יבריש דחוס) לטריוויאלית (strings | grep).

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

פתרון תרגיל 4 - פריקה ידנית מהזיכרון

עכשיו נחלץ את אותו הדגל מהקובץ הארוז המקורי flag, בלי upx -d בכלל. פותחים ב-gdb ונותנים ל-stub לפרוק:

gdb ./flag
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
strings /tmp/d.bin | grep -i delivery   # UPX...? sounds like a delivery service :)

למה זה עבד: ה-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

חילוץ הסוד בכל זאת, בשיטה הידנית:

gdb ./secret.corrupt
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. פריקה ידנית מהזיכרון עמידה לזה לגמרי.