7.1 הקדמה להנדסה הפוכה פתרון
פתרון - הקדמה להנדסה הפוכה¶
פתרון תרגיל 1 - זיהוי קובץ ומחרוזות¶
א. זיהוי הקובץ:
/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 כי אין שמות פונקציות פנימיות
ב. המחרוזות הראשונות:
בין המחרוזות נראה דברים כמו:
- שמות של אפשרויות (flags): --color, --sort, --all, --long
- הודעות שגיאה: cannot access, invalid option
- שמות של פורמטים: across, commas, long, single-column
- מחרוזות של עזרה (help text)
כל אלה עוזרים להבין שזו תוכנית שמציגה תוכן של תיקיות עם אפשרויות תצוגה שונות.
ג. השוואת מספר מחרוזות:
הפקודה 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;
}
קומפילציה:
א. מציאת 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:
הארגומנט הראשון (rdi) הוא כתובת של מחרוזת ב-.rodata.
עבור הקריאה ל-malloc:
הארגומנט הראשון (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:
הארגומנט הראשון (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;
}
קומפילציה:
א. מציאת כל הפונקציות:
שלוש פונקציות: hidden_func, visible_func, ו-main. הפונקציה hidden_func קיימת בקוד אבל main לא קורא לה.
גם פקודה זו מראה את אותן פונקציות.
ב. מציאת המחרוזת:
המחרוזת נמצאת.
ג. איתור המחרוזת בדיסאסמבלי:
בדיסאסמבלי של 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.
אפשר לוודא:
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:
ב. פתיחה ב-GDB:
התוכנית נעצרת בתחילת main. בלי -g לא נראה קוד C, אבל נוכל לראות אסמבלי.
ג. דיסאסמבלי של main:
זיהוי הקריאות:
- call atoi@plt - המרת הארגומנט ממחרוזת למספר. לפני הקריאה, rdi מכיל את argv[1]
- call factorial - חישוב העצרת. לפני הקריאה, edi מכיל את המספר שהומר
- call printf@plt - הדפסת התוצאה. לפני הקריאה, rdi מכיל את כתובת format string, esi מכיל את המספר, edx מכיל את התוצאה
ד. מעקב אחרי factorial:
התוכנית תיעצר בכניסה ל-factorial. נבדוק את הארגומנט:
הארגומנט הראשון הוא 5.
התוכנית תיעצר שוב ב-factorial (כי זו פונקציה רקורסיבית):
עכשיו הארגומנט הוא 4. אם נמשיך לעשות continue, נראה שהארגומנט יורד: 5, 4, 3, 2, 1.
אפשר גם לראות את מחסנית הקריאות:
פתרון תרגיל 5 - ניתוח עם strace ו-ltrace¶
א. ניתוח strace של echo:
בפלט נראה (בין השאר):
- הfile descriptor הוא 1, שזה stdout (כזכור מפרק 5.5 - fd 0 הוא stdin, fd 1 הוא stdout, fd 2 הוא stderr)
- המחרוזת שנכתבת היא "hello world\n"
- 12 הוא אורך המחרוזת (11 תווים + newline)
ב. ניתוח strace של factorial:
בהתחלה נראה טעינת ספריות:
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 עם fd=1 (stdout) מדפיסה את התוצאה.
ג. ניתוח ltrace של factorial:
פלט:
רואים את קריאות ה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 שימושי להבנת הלוגיקה ברמה גבוהה יותר (אילו פונקציות נקראות ועם אילו ארגומנטים).