לדלג לתוכן

7.1 הקדמה להנדסה הפוכה פתרון

פתרון - הקדמה להנדסה הפוכה

פתרון תרגיל 1 - זיהוי קובץ ומחרוזות

א. זיהוי הקובץ:

file /bin/ls

/bin/ls: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=..., for GNU/Linux 3.2.0, stripped

תשובות:
- זהו קובץ 64 ביט (ELF 64-bit)
- הוא מקושר דינמית (dynamically linked) - תלוי בספריות חיצוניות
- הוא stripped - הוסרו ממנו טבלת הסמלים ומידע הדיבוג. זה מקשה על RE כי אין שמות פונקציות פנימיות

ב. המחרוזות הראשונות:

strings /bin/ls | head -50

בין המחרוזות נראה דברים כמו:
- שמות של אפשרויות (flags): --color, --sort, --all, --long
- הודעות שגיאה: cannot access, invalid option
- שמות של פורמטים: across, commas, long, single-column
- מחרוזות של עזרה (help text)

כל אלה עוזרים להבין שזו תוכנית שמציגה תוכן של תיקיות עם אפשרויות תצוגה שונות.

ג. השוואת מספר מחרוזות:

strings /bin/ls | wc -l

1850

strings /bin/cat | wc -l

320

הפקודה ls מכילה הרבה יותר מחרוזות מ-cat. זה הגיוני - ls היא תוכנית מורכבת הרבה יותר, עם אפשרויות תצוגה רבות, מיון, צבעים, וכו'. cat היא תוכנית פשוטה שבעיקר קוראת קבצים וכותבת ל-stdout.


פתרון תרגיל 2 - זיהוי קריאות לפונקציות בדיסאסמבלי

הקוד:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    printf("Hello World\n");
    char *buf = malloc(64);
    strcpy(buf, "test data");
    printf("buf = %s\n", buf);
    free(buf);
    return 0;
}

קומפילציה:

gcc -o hello hello.c

א. מציאת main:

objdump -d -M intel hello | grep -A 30 "<main>"

ב. הקריאות שmain מבצע (הפלט ישתנה בין מערכות, אבל הרעיון דומה):

call   puts@plt           ; הקומפיילר ממיר printf("...\n") ל-puts
call   malloc@plt
call   strcpy@plt
call   printf@plt
call   free@plt

שימו לב: הקומפיילר לפעמים ממיר printf("Hello World\n") ל-puts("Hello World") כאופטימיזציה, כי printf עם מחרוזת פשוטה שמסתיימת ב-\n ובלי format specifiers שקולה ל-puts.

ג. זיהוי ארגומנטים:

עבור הקריאה ל-puts:

lea    rdi, [rip+0x...]    ; rdi = כתובת המחרוזת "Hello World"
call   puts@plt

הארגומנט הראשון (rdi) הוא כתובת של מחרוזת ב-.rodata.

עבור הקריאה ל-malloc:

mov    edi, 0x40            ; rdi = 64 (גודל ההקצאה)
call   malloc@plt

הארגומנט הראשון (rdi/edi) הוא 0x40 = 64 בתים. שימו לב לשימוש ב-edi (32 ביט) במקום rdi - כי הערך 64 נכנס ב-32 ביט, והקומפיילר חוסך בית.

עבור הקריאה ל-strcpy:

mov    rdi, rax             ; rdi = הכתובת שmalloc החזיר (המצביע ליעד)
lea    rsi, [rip+0x...]     ; rsi = כתובת המחרוזת "test data" (המקור)
call   strcpy@plt

הארגומנט הראשון (rdi) הוא הbuffer שהוקצה, והשני (rsi) הוא כתובת המחרוזת המקורית.

עבור הקריאה ל-free:

mov    rdi, [rbp-0x8]       ; rdi = המצביע ששמרנו (buf)
call   free@plt

הארגומנט הראשון (rdi) הוא המצביע שקיבלנו מ-malloc.


פתרון תרגיל 3 - מציאת מחרוזת מוסתרת

הקוד:

#include <stdio.h>

void hidden_func(void) {
    puts("You found the secret!");
}

void visible_func(void) {
    puts("This is visible");
}

int main(void) {
    visible_func();
    return 0;
}

קומפילציה:

gcc -o secret secret.c

א. מציאת כל הפונקציות:

nm secret | grep " T "

0000000000001149 T hidden_func
0000000000001163 T visible_func
000000000000117d T main

שלוש פונקציות: hidden_func, visible_func, ו-main. הפונקציה hidden_func קיימת בקוד אבל main לא קורא לה.

objdump -t secret | grep "\.text"

גם פקודה זו מראה את אותן פונקציות.

ב. מציאת המחרוזת:

strings secret | grep -i secret

You found the secret!

המחרוזת נמצאת.

ג. איתור המחרוזת בדיסאסמבלי:

objdump -d -M intel secret

בדיסאסמבלי של hidden_func:

0000000000001149 <hidden_func>:
    1149:  f3 0f 1e fa          endbr64
    114d:  55                   push   rbp
    114e:  48 89 e5             mov    rbp,rsp
    1151:  48 8d 05 b0 0e 00 00 lea    rax,[rip+0xeb0]
    1158:  48 89 c7             mov    rdi,rax
    115b:  e8 d0 fe ff ff       call   1030 <puts@plt>
    1160:  90                   nop
    1161:  5d                   pop    rbp
    1162:  c3                   ret

רואים ש-hidden_func טוענת כתובת מ-.rodata לתוך rdi וקוראת ל-puts. הכתובת rip+0xeb0 מצביעה למחרוזת "You found the secret!" ב-.rodata.

אפשר לוודא:

objdump -s -j .rodata secret

Contents of section .rodata:
 2000 01000200 596f7520 666f756e 64207468  ....You found th
 2010 65207365 63726574 21005468 69732069  e secret!.This i
 2020 73207669 7369626c 6500                s visible.


פתרון תרגיל 4 - סיור ב-GDB בלי קוד מקור

הקוד:

#include <stdio.h>
#include <stdlib.h>

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

int main(int argc, char **argv) {
    if (argc != 2) {
        printf("Usage: %s <number>\n", argv[0]);
        return 1;
    }
    int num = atoi(argv[1]);
    int result = factorial(num);
    printf("%d! = %d\n", num, result);
    return 0;
}

קומפילציה בלי -g:

gcc -o factorial factorial.c

ב. פתיחה ב-GDB:

gdb ./factorial
(gdb) break main
(gdb) run 5

התוכנית נעצרת בתחילת main. בלי -g לא נראה קוד C, אבל נוכל לראות אסמבלי.

ג. דיסאסמבלי של main:

(gdb) set disassembly-flavor intel
(gdb) disassemble main

זיהוי הקריאות:
- call atoi@plt - המרת הארגומנט ממחרוזת למספר. לפני הקריאה, rdi מכיל את argv[1]
- call factorial - חישוב העצרת. לפני הקריאה, edi מכיל את המספר שהומר
- call printf@plt - הדפסת התוצאה. לפני הקריאה, rdi מכיל את כתובת format string, esi מכיל את המספר, edx מכיל את התוצאה

ד. מעקב אחרי factorial:

(gdb) break factorial
(gdb) run 5

התוכנית תיעצר בכניסה ל-factorial. נבדוק את הארגומנט:

(gdb) info registers rdi
rdi            0x5

הארגומנט הראשון הוא 5.

(gdb) continue

התוכנית תיעצר שוב ב-factorial (כי זו פונקציה רקורסיבית):

(gdb) info registers rdi
rdi            0x4

עכשיו הארגומנט הוא 4. אם נמשיך לעשות continue, נראה שהארגומנט יורד: 5, 4, 3, 2, 1.

אפשר גם לראות את מחסנית הקריאות:

(gdb) backtrace

#0  factorial ()
#1  factorial ()
#2  factorial ()
#3  main ()


פתרון תרגיל 5 - ניתוח עם strace ו-ltrace

א. ניתוח strace של echo:

strace /bin/echo "hello world" 2>&1 | head -30

בפלט נראה (בין השאר):

write(1, "hello world\n", 12)      = 12

  • הfile descriptor הוא 1, שזה stdout (כזכור מפרק 5.5 - fd 0 הוא stdin, fd 1 הוא stdout, fd 2 הוא stderr)
  • המחרוזת שנכתבת היא "hello world\n"
  • 12 הוא אורך המחרוזת (11 תווים + newline)

ב. ניתוח strace של factorial:

strace ./factorial 5

בהתחלה נראה טעינת ספריות:

openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF..."..., 832) = 832
mmap(NULL, 2136936, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f...
...
close(3)                             = 0

זה הdynamic linker שטוען את libc.so.6 לזיכרון.

הדפסת התוצאה:

write(1, "5! = 120\n", 9)           = 9

קריאת המערכת write עם fd=1 (stdout) מדפיסה את התוצאה.

ג. ניתוח ltrace של factorial:

ltrace ./factorial 5

פלט:

atoi("5")                            = 5
printf("%d! = %d\n", 5, 120)        = 9

רואים את קריאות הlibc:
- atoi("5") - המרת הארגומנט למספר, מחזיר 5
- printf("%d! = %d\n", 5, 120) - הדפסת התוצאה עם הformat string ושני ארגומנטים

שימו לב: הפונקציה factorial עצמה לא מופיעה ב-ltrace כי היא לא פונקציית ספריה - היא פונקציה פנימית של התוכנית.

ד. השוואה בין strace ל-ltrace:

היבט strace ltrace
מה מציג קריאות מערכת (syscalls) קריאות לפונקציות ספריה
רמת הפירוט נמוכה - write, read, open גבוהה - printf, malloc, strcmp
טעינת ספריות רואים את openat/mmap לא רואים
פונקציות libc רואים רק את הsyscall שמאחוריהן (write) רואים את הפונקציה עצמה (printf) עם הארגומנטים
פונקציות פנימיות לא רואים לא רואים (רק פונקציות ספריה)

strace שימושי להבנת האינטראקציה עם מערכת ההפעלה (אילו קבצים נפתחים, אילו חיבורי רשת נפתחים), בעוד ltrace שימושי להבנת הלוגיקה ברמה גבוהה יותר (אילו פונקציות נקראות ועם אילו ארגומנטים).