7.2 דפוסים באסמבלי הרצאה
הקדמה¶
בהרצאה הקודמת (7.1) למדנו מהי הנדסה הפוכה והכרנו את הכלים. עכשיו נתחיל ללמוד את המיומנות המרכזית ב-RE: זיהוי דפוסים באסמבלי.
כשאתה קורא דיסאסמבלי של תוכנית, אתה לא קורא שורה-שורה - אתה מחפש דפוסים שאתה מזהה. כל מבנה בשפת C (if, for, while, switch, struct, array) מתורגם לדפוס מסוים באסמבלי. ברגע שאתה מזהה את הדפוס, אתה יודע מה הקוד המקורי ב-C עשה.
בהרצאה הזו נעבור על כל הדפוסים העיקריים ונלמד לזהות אותם.
דפוס תנאי - if/else pattern¶
נתחיל עם הדפוס הכי בסיסי. קוד C:
באסמבלי (בהנחה ש-x נמצא ב-edi):
cmp edi, 5
jle .else_branch ; אם x <= 5, קפוץ ל-else
; --- if body ---
jmp .end
.else_branch:
; --- else body ---
.end:
הנקודה הקריטית: התנאי באסמבלי הפוך מהתנאי ב-C. בקוד C כתוב if (x > 5), אבל באסמבלי הקפיצה היא jle (jump if less or equal) - כלומר "קפוץ ל-else אם x קטן-שווה ל-5". זה כי באסמבלי הקפיצה היא מתי לדלג על גוף ה-if.
טבלת ההמרה:
| תנאי ב-C | קפיצה באסמבלי (signed) | קפיצה באסמבלי (unsigned) |
|---|---|---|
> |
jle (דלג אם <=) |
jbe (דלג אם <=) |
>= |
jl (דלג אם <) |
jb (דלג אם <) |
< |
jge (דלג אם >=) |
jae (דלג אם >=) |
<= |
jg (דלג אם >) |
ja (דלג אם >) |
== |
jne (דלג אם לא שווה) |
jne |
!= |
je (דלג אם שווה) |
je |
דוגמה נוספת - if בלי else:
test edi, edi ; בדיקה אם edi == 0
jne .skip ; אם לא אפס - דלג
call do_something
.skip:
; continue...
שימו לב לשימוש ב-test edi, edi במקום cmp edi, 0. הפקודה test מבצעת AND לוגי בין edi לעצמו - התוצאה היא אפס רק אם edi הוא אפס. זו אופטימיזציה נפוצה של הקומפיילר (חוסכת בית בקידוד ההוראה).
דפוס לולאת for - for loop pattern¶
קוד C:
באסמבלי:
mov dword [rbp-4], 0 ; i = 0 (אתחול)
jmp .check ; קפוץ לבדיקת התנאי
.body:
; --- loop body ---
add dword [rbp-4], 1 ; i++ (קידום)
.check:
cmp dword [rbp-4], 10 ; i < 10? (תנאי)
jl .body ; אם כן - חזור לגוף הלולאה
שימו לב למבנה: אתחול, קפיצה לתנאי, גוף, קידום, תנאי, קפיצה חזרה. הקומפיילר שם את בדיקת התנאי בסוף ולא בתחילה. למה? כי ככה הבדיקה מתבצעת גם לפני האיטרציה הראשונה (דרך ה-jmp הראשוני) וגם אחרי כל איטרציה - בלי לשכפל קוד.
עם אופטימיזציה (-O2), הקומפיילר עשוי לפשט:
xor ecx, ecx ; i = 0 (xor עם עצמו = אפס)
.body:
; --- loop body ---
inc ecx ; i++
cmp ecx, 10 ; i < 10?
jl .body ; אם כן - חזור
בגרסה המאופטמת: המשתנה i נשמר באוגר (ecx) ולא על המחסנית, אין קפיצה ראשונית לתנאי (הקומפיילר יודע שהתנאי מתקיים בפעם הראשונה), ו-xor ecx, ecx מאפס את האוגר (אופטימיזציה נפוצה - קידוד קצר יותר מ-mov ecx, 0).
דפוס לולאת while - while loop pattern¶
קוד C:
באסמבלי:
jmp .check
.body:
; --- loop body ---
sub dword [rbp-4], 1 ; x--
.check:
cmp dword [rbp-4], 0 ; x > 0?
jg .body ; אם כן - חזור לגוף
דפוס הwhile דומה מאוד לfor, רק בלי שלב אתחול מובנה. אם יש do-while:
.body:
; --- loop body ---
sub dword [rbp-4], 1 ; x--
cmp dword [rbp-4], 0 ; x > 0?
jg .body ; אם כן - חזור
בdo-while, אין קפיצה ראשונית לתנאי - הגוף תמיד רץ לפחות פעם אחת.
דפוס switch/case - switch pattern¶
ל-switch יש שני מימושים שונים באסמבלי, תלוי בכמות הcase-ים ובערכים שלהם.
אפשרות 1: שרשרת השוואות - if/else chain¶
כשיש מעט case-ים או שהערכים לא רצופים:
cmp edi, 1
je .case_1
cmp edi, 5
je .case_5
cmp edi, 100
je .case_100
jmp .end ; default (או סוף אם אין default)
.case_1:
call func_a
jmp .end
.case_5:
call func_b
jmp .end
.case_100:
call func_c
.end:
זה נראה בדיוק כמו שרשרת if-else if-else.
אפשרות 2: טבלת קפיצות - jump table¶
כשהcase-ים הם ערכים רצופים (או כמעט רצופים), הקומפיילר יוצר טבלת קפיצות:
switch (x) {
case 0: func_a(); break;
case 1: func_b(); break;
case 2: func_c(); break;
case 3: func_d(); break;
}
cmp edi, 3
ja .default ; אם x > 3 (unsigned), זה לא case תקין
lea rax, [rip + .jump_table]
movsxd rdx, dword [rax + rdi*4]
add rax, rdx
jmp rax ; קפיצה עקיפה דרך הטבלה
.jump_table:
.long .case_0 - .jump_table
.long .case_1 - .jump_table
.long .case_2 - .jump_table
.long .case_3 - .jump_table
איך לזהות טבלת קפיצות בדיסאסמבלי:
- יש cmp ואז ja (unsigned above) - בדיקת גבולות
- יש lea שטוען כתובת בסיס
- יש חישוב עם אינדקס (כפולה של הערך הנבדק)
- יש jmp לכתובת שחושבה דינמית (לא כתובת קבועה)
כשאתה רואה jmp rax (קפיצה לכתובת שבאוגר) - זה סימן חזק לטבלת קפיצות.
דפוס קריאות לפונקציות - function call pattern¶
כשהקומפיילר מתרגם קריאה לפונקציה, יש דפוס ברור:
mov edx, 30 ; ארגומנט שלישי -> rdx
mov esi, 20 ; ארגומנט שני -> rsi
mov edi, 10 ; ארגומנט ראשון -> rdi
call calculate
mov [rbp-4], eax ; שמירת ערך ההחזרה (rax) למשתנה מקומי
זיהוי פונקציות ספריה נפוצות¶
ב-RE, הרבה פעמים אתה רואה קריאה לפונקציה דרך PLT ומזהה אותה לפי שם. אבל גם בלי שם, אפשר לזהות פונקציות לפי הארגומנטים שלהן:
זיהוי printf:
lea rdi, [rip+0x...] ; rdi = format string (כתובת ב-.rodata)
mov esi, [rbp-4] ; rsi = ארגומנט ראשון ל-format
xor eax, eax ; al = 0 (מספר ארגומנטי SSE/float)
call printf@plt
המפתח: rdi מצביע למחרוזת ב-.rodata (format string), ולפני הקריאה יש
xor eax, eax (שאומר שאין ארגומנטי floating point).
זיהוי malloc:
המפתח: ארגומנט מספרי אחד ב-rdi, וערך ההחזרה נשמר כמצביע.
זיהוי strcmp:
lea rdi, [rbp-0x20] ; rdi = מחרוזת ראשונה
lea rsi, [rip+0x...] ; rsi = מחרוזת שנייה
call strcmp@plt
test eax, eax ; בדיקת ערך ההחזרה
je .strings_equal ; אם 0 - המחרוזות שוות
המפתח: שני ארגומנטים שנראים כמו כתובות של מחרוזות, ואחרי הקריאה יש בדיקה אם ערך ההחזרה הוא 0.
דפוס גישה לסטראקט - struct access pattern¶
כשיש גישה לשדות של struct, רואים באסמבלי גישה לoffset-ים קבועים מכתובת בסיס:
struct Point {
int x; // offset 0
int y; // offset 4
char *name; // offset 8
};
struct Point p;
p.x = 1;
p.y = 2;
p.name = "origin";
mov dword [rax], 1 ; p.x = 1 (offset 0 מתחילת הstruct)
mov dword [rax+4], 2 ; p.y = 2 (offset 4)
lea rcx, [rip+0x...] ; rcx = כתובת המחרוזת "origin"
mov qword [rax+8], rcx ; p.name = "origin" (offset 8)
איך לזהות struct בדיסאסמבלי:
- יש אוגר שמשמש ככתובת בסיס (כמו rax בדוגמה)
- יש כמה גישות לזיכרון עם offset-ים קבועים מאותו בסיס: [rax], [rax+4], [rax+8]
- הגדלים וה-offset-ים תואמים סוגי נתונים (int = 4 בתים, pointer = 8 בתים)
דוגמה יותר מורכבת - גישה דרך מצביע:
struct Node {
int value; // offset 0
struct Node *next; // offset 8 (padding after int)
};
void print_list(struct Node *head) {
while (head != NULL) {
printf("%d\n", head->value);
head = head->next;
}
}
print_list:
push rbx
mov rbx, rdi ; rbx = head
test rbx, rbx ; head == NULL?
je .end
.loop:
mov esi, [rbx] ; esi = head->value (offset 0)
lea rdi, [rip+0x...] ; rdi = "%d\n"
xor eax, eax
call printf@plt
mov rbx, [rbx+8] ; head = head->next (offset 8)
test rbx, rbx ; head == NULL?
jne .loop
.end:
pop rbx
ret
דפוס גישה למערך - array access pattern¶
גישה למערך באסמבלי משתמשת בחישוב כתובת עם אינדקס:
הנוסחה היא: base + index * scale, כאשר:
- base (rbx) = כתובת תחילת המערך
- index (rcx) = האינדקס i
- scale (4) = גודל כל איבר (4 בתים עבור int)
זה נקרא כתובת SIB (Scale-Index-Base) - מצב כתובת מיוחד של x86 שתוכנן בדיוק בשביל מערכים.
הscale יכול להיות 1, 2, 4, או 8:
- [rbx + rcx*1] - מערך של char/byte
- [rbx + rcx*2] - מערך של short (2 בתים)
- [rbx + rcx*4] - מערך של int/float (4 בתים)
- [rbx + rcx*8] - מערך של long/double/pointer (8 בתים)
דוגמה עם לולאה:
xor eax, eax ; sum = 0
xor ecx, ecx ; i = 0
.loop:
add eax, [rdi + rcx*4] ; sum += arr[i]
inc ecx ; i++
cmp ecx, esi ; i < n?
jl .loop
דפוס באפר על המחסנית - stack buffer pattern¶
כשמוקצה באפר על המחסנית, רואים הקצאת מקום ואז שימוש ב-lea כדי לקבל את הכתובת:
func:
push rbp
mov rbp, rsp
sub rsp, 0x40 ; הקצאת 64 בתים על המחסנית
lea rsi, [rbp-0x40] ; rsi = כתובת הbuffer
lea rdi, [rip+0x...] ; rdi = "%s"
xor eax, eax
call scanf@plt
leave
ret
איך לזהות:
- sub rsp, X - הקצאת מקום על המחסנית (X הוא גודל הbuffer ועוד אולי padding)
- lea reg, [rbp-X] - לקיחת כתובת הbuffer כדי להעביר אותה כארגומנט לפונקציה
- הכתובת מועברת כארגומנט לפונקציות כמו scanf, read, fgets וכו'
זה חשוב במיוחד בהקשר של אבטחה - buffer על המחסנית ליד כתובת החזרה הוא מתכון לstack buffer overflow (נושא שנלמד בהמשך הקורס).
דפוסי אופטימיזציה - optimization patterns¶
כשמקמפלים עם אופטימיזציות (-O2 ומעלה), הקומפיילר מחליף פעולות בגרסאות מהירות יותר. חשוב לזהות אותן כי הן נפוצות מאוד:
כפל בחזקת 2: שימוש ב-shl במקום imul¶
הזזה שמאלה ב-N ביטים שקולה לכפל ב-2^N. זה הרבה יותר מהיר מכפל אמיתי.
כפל בקבועים קטנים: שימוש ב-lea¶
lea eax, [rdi + rdi*2] ; x * 3 = x + x*2
lea eax, [rdi + rdi*4] ; x * 5 = x + x*4
lea eax, [rdi + rdi*8] ; x * 9 = x + x*8
הפקודה lea מחשבת כתובת אבל לא ניגשת לזיכרון - היא פשוט מבצעת חיבור וכפל. הקומפיילר מנצל את זה לחישוב כפל מהיר.
חילוק בחזקת 2: שימוש ב-sar/shr¶
הזזה ימינה ב-N ביטים שקולה לחילוק ב-2^N.
לחילוק signed, הקומפיילר מוסיף תיקון עבור מספרים שליליים:
mov eax, edi
sar eax, 31 ; eax = 0 אם חיובי, -1 אם שלילי
shr eax, 30 ; תיקון עיגול (עבור חילוק ב-4)
add eax, edi
sar eax, 2 ; חילוק ב-4
בדיקה אם אפס: test במקום cmp¶
test eax, eax קצר יותר מ-cmp eax, 0 (חוסך בית) והוא דפוס שתראו כמעט בכל בינארי.
איפוס אוגר: xor במקום mov¶
xor eax, eax קצר יותר מ-mov eax, 0 (2 בתים במקום 5). זה כל כך נפוץ שכל reverser צריך לזהות את זה אוטומטית.
שלילה בוליאנית ובדיקות¶
דוגמה מלאה: שחזור פונקציה מאסמבלי¶
בואו נראה דוגמה שמשלבת כמה דפוסים ונשחזר ממנה קוד C:
mystery_func:
push rbp
mov rbp, rsp
mov dword [rbp-4], edi ; שמירת ארגומנט ראשון
mov dword [rbp-8], 0 ; משתנה מקומי = 0
mov dword [rbp-12], 1 ; משתנה מקומי = 1
jmp .check
.loop:
mov eax, [rbp-12]
add [rbp-8], eax ; sum += i
add dword [rbp-12], 1 ; i++
.check:
mov eax, [rbp-12]
cmp eax, [rbp-4] ; i <= n?
jle .loop
mov eax, [rbp-8] ; ערך החזרה = sum
pop rbp
ret
נפרק את זה:
1. [rbp-4] = הארגומנט הראשון (edi). נקרא לו n
2. [rbp-8] = משתנה מקומי שמתחיל ב-0 ומצטבר. נקרא לו sum
3. [rbp-12] = משתנה מקומי שמתחיל ב-1 ועולה ב-1 כל איטרציה. נקרא לו i
4. יש דפוס של לולאת for: אתחול, קפיצה לתנאי, גוף (חיבור), קידום (i++), תנאי (i <= n)
5. ערך ההחזרה הוא sum
שחזור:
זו פונקציה שמחשבת סכום של כל המספרים מ-1 עד n.
סיכום¶
בהרצאה הזו למדנו לזהות את הדפוסים הבסיסיים באסמבלי x86-64:
- תנאי if/else - cmp ואז קפיצה מותנית (התנאי הפוך מה-C)
- לולאת for - אתחול, קפיצה לתנאי, גוף, קידום, תנאי, קפיצה חזרה
- לולאת while - דומה ל-for בלי אתחול מובנה
- לולאת do-while - תנאי בסוף, בלי קפיצה ראשונית
- switch/case - שרשרת השוואות (מעט case-ים) או טבלת קפיצות (ערכים רצופים)
- קריאות לפונקציות - הכנת ארגומנטים באוגרים, call, קריאת ערך חזרה מ-rax
- גישה לstruct - offset-ים קבועים מכתובת בסיס
- גישה למערך - חישוב base + index * scale (כתובת SIB)
- באפר על המחסנית - sub rsp, lea לכתובת הבאפר
- אופטימיזציות - shl/shr במקום כפל/חילוק, lea לכפל בקבועים, test/xor לבדיקות, xor לאיפוס
בהרצאה הבאה (7.3) נלמד GDB מתקדם - שימוש ב-GDB ככלי הנדסה הפוכה לניתוח דינמי של תוכניות בלי קוד מקור.