Reverse Engineering מתקדם
Reverse Engineering מתקדם — IDA Pro, Ghidra ו-Anti-Debug
Reverse Engineering (RE) הוא אמנות הניתוח של קוד בינארי ללא קוד מקור. שיעור זה מלמד שימוש ב-IDA Pro ו-Ghidra לניתוח קוד, זיהוי פגיעויות ב-Assembly, ועקיפת טכניקות Anti-Debug שמתוכנות מגינות בהן.
- IDA Pro — ניווט, Graph View, פונקציות, Xrefs, Pseudocode
- Ghidra — חלופה חינמית ל-IDA, Decompiler, Scripting
- ניתוח Assembly — זיהוי לולאות, תנאים, קריאות פונקציה
- Anti-Debug Techniques — IsDebuggerPresent, Timing Checks, Exception-based
- עקיפת Anti-Debug — Patching, Plugin-ים, ScyllaHide
- Obfuscation ו-Packing — UPX, VMProtect, זיהוי ועקיפה
- Keygen ו-License Checks — ניתוח ועקיפה
- RE Workflow — מקצה לקצה עם דוגמה מעשית
IDA Pro — הכלי הסטנדרטי לRE
IDA Pro הוא הDisassembler/Debugger המקצועי ביותר בתעשייה. הוא הופך Binary לAssembly קריא, מזהה פונקציות אוטומטית, ומייצר Pseudocode (Hex-Rays Decompiler).
ניווט בסיסי ב-IDA
; ─── Windows חשובים ב-IDA ────────────────────────────────
; IDA View (Disassembly) : קוד Assembly — Text Mode
; Graph View : תרשים זרימה — Graph Mode (Space)
; Functions Window : רשימת כל הפונקציות (Shift+F3)
; Strings Window : כל המחרוזות בBinary (Shift+F12)
; Imports : פונקציות מיובאות מDLL/libc
; Exports : פונקציות מיוצאות
; ─── קיצורי מקלדת חיוניים ─────────────────────────────────
; F5 → Decompile לPseudocode (Hex-Rays)
; G → קפוץ לכתובת
; N → שנה שם (Rename) לפונקציה/משתנה
; ; → הוסף תגובה (Comment)
; X → Xrefs — מי קורא לפונקציה זו?
; D / A → הגדר Data / ASCII string
; Space → עבור בין Text Mode ל-Graph Mode
; Ctrl+F → חיפוש טקסט
; ─── זיהוי פונקציות חשובות ────────────────────────────────
; IDA מזהה אוטומטית: main, WinMain, printf, strcpy...
; פונקציות לא מזוהות: sub_401234 (לפי כתובת)
; שנה שם: N → הקלד שם חדש → ב-Xrefs יופיע השם בכל מקוםGraph View — קריאת זרימת קוד
; ─── קריאת Graph View ───────────────────────────────────────
; כל בלוק = Basic Block (קוד רציף ללא קפיצות)
; חץ ירוק = JMP/JE שמתקיים (True branch)
; חץ אדום = JMP שלא מתקיים (False branch)
; חץ כחול = JMP ללא תנאי (Unconditional)
; ─── דוגמה: if/else בAssembly ────────────────────────────
; C code: if (x > 5) { do_a(); } else { do_b(); }
; Assembly (32bit):
cmp eax, 5 ; השווה eax ל-5
jle .else_branch ; אם eax <= 5 → קפוץ לelse
call do_a ; ← True branch
jmp .end
.else_branch:
call do_b ; ← False branch
.end:
...
; בGraph View:
; [cmp + jle] → ירוק → [do_b block]
; ↓ אדום (Fall-through)
; [do_a block]
; ↓
; [end block]cmp eax, 10
jg loc_PASS
call fail_func
jmp loc_END
loc_PASS:
call pass_func
loc_END:בGraph View, איזה חץ מוביל ל-pass_func?Ghidra — RE חינמי ועוצמתי
Ghidra הוא כלי RE בקוד פתוח של ה-NSA. חינמי, תומך בארכיטקטורות רבות, ומכיל Decompiler מובנה. בCTF ובגאמא — Ghidra הוא כלי סטנדרטי.
הבדלים בין Ghidra ל-IDA
| תכונה | IDA Pro | Ghidra |
|---|---|---|
| מחיר | יקר מאוד ($$$) | חינמי ו-Open Source |
| Decompiler | Hex-Rays (מצוין) | מובנה (טוב מאוד) |
| ארכיטקטורות | רוב הנפוצות | רבות מאוד כולל נדירות |
| Scripting | IDAPython | Java + Python |
| Collaboration | מוגבל | מובנה (Server mode) |
| שימוש בגאמא/CTF | כן | כן — נפוץ מאוד |
Workflow ב-Ghidra
# ─── פתיחת Binary ב-Ghidra ────────────────────────────────
# 1. File → New Project → Import File → בחר binary
# 2. Double-click → Analysis Options → Analyze
# 3. CodeBrowser נפתח
# ─── חלונות עיקריים ───────────────────────────────────────
# Symbol Tree → רשימת פונקציות, imports, exports
# Listing → Assembly (כמו IDA View)
# Decompile → Pseudocode (אוטומטי בצד)
# Functions → כל הפונקציות עם שמות
# ─── Scripting ב-Ghidra (Java/Python) ─────────────────────
# Script Manager → New Script → Python
from ghidra.program.model.listing import *
fm = currentProgram.getFunctionManager()
for func in fm.getFunctions(True):
if "check" in func.getName().lower():
print(f"Found: {func.getName()} @ {func.getEntryPoint()}")
# ─── חיפוש מחרוזות ────────────────────────────────────────
# Search → For Strings → מוצא "password", "flag{", "Wrong!"
# לחץ על מחרוזת → Show References → מי משתמש בה?! Ghidra vs IDA — מתי להשתמש בכל אחד ›
בחר IDA כשיש: ניתוח עמוק של binary מסחרי, צורך ב-Debugger מובנה מלא, plugin-ים ספציפיים (למשל Keypatch, BinDiff).
בחר Ghidra כשיש: CTF (חינמי וזמין), ארכיטקטורות נדירות (ARM, MIPS, PowerPC), צורך בScripting מתקדם, שיתוף פעולה בצוות.
int check_serial(char *input) {
if (strlen(input) != 16) return 0;
for (int i=0; i<16; i++) {
if (input[i] != secret[i] ^ 0x42) return 0;
}
return 1;
}מה צריך לעשות כדי למצוא ה-Serial?; Function sub_401050 (32bit)
push ebp
mov ebp, esp
mov eax, [ebp+8] ; arg1 → eax
imul eax, eax, 3 ; eax = eax * 3
add eax, 7 ; eax = eax + 7
pop ebp
retמה עושה פונקציה זו (ב-Pseudocode)?Anti-Debug Techniques: הגנות מפני ניתוח
תוכנות מגינות על עצמן בטכניקות Anti-Debug שמזהות debugger ומשנות התנהגות. כRE, עליך לזהות ולעקוף אותן.
IsDebuggerPresent — הפשוטה ביותר
; ─── IsDebuggerPresent ────────────────────────────────────
; Windows API: קורא ל-IsDebuggerPresent() מkernel32.dll
; מחזיר 1 אם debugger מחובר, 0 אחרת
; Assembly בIDA:
call IsDebuggerPresent
test eax, eax ; בדוק אם eax != 0
jz short .no_debugger ; 0 = אין debugger → המשך
call exit_or_corrupt ; ← אם debugger → יציאה!
.no_debugger:
; קוד רגיל
; ─── עקיפה ────────────────────────────────────────────────
; שיטה 1: Patch — שנה 'jz' ל-'jmp' (תמיד קפוץ לno_debugger)
; שיטה 2: ScyllaHide plugin (IDA/x64dbg) — מסתיר debugger
; שיטה 3: ב-GDB: set *(BYTE*)(IsDebuggerPresent+N) = 0x31
; שיטה 4: Python script שמ-patch את הcheck בזמן ריצה
; ─── NtGlobalFlag — בדיקה עמוקה יותר ───────────────────
; PEB.NtGlobalFlag = 0x70 כשdebugger מחובר (Windows)
; בדיקה ב-Assembly:
mov eax, fs:[0x30] ; PEB
mov eax, [eax + 0x68] ; NtGlobalFlag
and eax, 0x70
cmp eax, 0x70
je .debugger_detectedTiming Checks ו-Exception-based Anti-Debug
; ─── Timing Check ──────────────────────────────────────────
; Debugger → Single-step → איטי משמעותית מריצה רגילה
; תוכנה מודדת זמן → debugger = זמן גבוה → זיהוי
; C code:
DWORD t1 = GetTickCount();
// code...
DWORD t2 = GetTickCount();
if (t2 - t1 > THRESHOLD) { /* debugger detected */ }
; ─── RDTSC — Timestamp Counter ────────────────────────────
; הוראת Assembly שמחזירה מונה זמן מדויק מאוד:
rdtsc ; EDX:EAX = timestamp
mov esi, eax ; שמור t1
; ... קוד ...
rdtsc
sub eax, esi ; t2 - t1
cmp eax, 500 ; אם > 500 cycles → debugger
ja .detected
; עקיפה: Patch את הcheck (jbe/jae) → תמיד "אין debugger"
; או ScyllaHide שמרמה את RDTSC
; ─── Exception-based ─────────────────────────────────────
; INT 3 (0xCC) = Breakpoint exception
; Debugger: תופס את ה-exception ומחזיר → לא קורסת
; ללא debugger: Exception handler של הprocess מטפל
; בודק: האם exception הגיע ל-handler שלי או נתפס?
__try {
__asm { int 3 } // אם debugger → לא יגיע לexcept
// ← הגענו כאן = אין debugger
} __except(EXCEPTION_EXECUTE_HANDLER) {
// ← הגענו כאן = אין debugger! (exception לא נתפס ע"י debugger)
normal_execution();
}call IsDebuggerPresent
test eax, eax
jnz loc_EXIT ; ← כתובת 0x401200
; קוד רגיל...
loc_EXIT:
call ExitProcessמה צריך לעשות ב-IDA כדי לעקוף בדיקה זו (Patching)?Obfuscation ו-Packing: הגנות על קוד
Packing ו-Obfuscation מקשים על ניתוח Binary. Packers כמו UPX דוחסים/מצפינים את הקוד; Obfuscators מסבכים את הלוגיקה. עליך לזהות ולפרוק אותם.
זיהוי Packed Binary
# ─── זיהוי Packing ───────────────────────────────────────
# כלי: file, strings, PEiD, Detect-It-Easy (DIE)
file ./binary
# ELF 64-bit LSB executable (אם רגיל)
# UPX compressed data (אם packed!)
strings ./binary | head -20
# Binary רגיל: הרבה strings מוכרות (API names, messages)
# Packed: מעט strings, "UPX0", "UPX1", entropy גבוה
# ─── אנטרופיה ─────────────────────────────────────────────
# Binary רגיל: entropy 5-6 bits/byte (לא דחוס)
# Packed/Encrypted: entropy 7.5-8 bits/byte (כמו random!)
# כלי: binwalk -E binary → מציג entropy per section
# ─── UPX — הנפוץ ביותר ───────────────────────────────────
upx -d ./packed_binary # Unpack אוטומטי!
# לא תמיד עובד אם הUPX Modified/Custom
# ─── Manual Unpacking (OEP Finding) ──────────────────────
# 1. בGDB/x64dbg: הרץ binary
# 2. Packed code מריץ Stub שמפרק לזיכרון
# 3. בסוף Stub: JMP לOEP (Original Entry Point)
# 4. שים BP על jmp/push-ret בסוף Stub → dump memory
# 5. כלי: OllyDumpEx, x64dbg Plugin dump; Unpacking Stub (simplified):
mov ecx, 0x1000 ; size
mov esi, packed_data ; source
mov edi, 0x401000 ; destination
.loop:
lodsb ; AL = *esi++
xor al, 0x55 ; decrypt byte
stosb ; *edi++ = AL
loop .loop ; ecx--
jmp 0x401000 ; ← OEP!מה אם נרצה ל-Dump את הbinary המפורק לאחר XOR?License Checks ו-Keygens: Cracking 101
בCTF ובגאמא, Cracking Tasks כוללות ניתוח קוד License Check ומציאת Key תקין (Keygen) או Patching לעקיפת הבדיקה. זה מסכם את כל מה שלמדנו בשיעור.
Flow של ניתוח License Check
; ─── דוגמה: License Check פשוטה ────────────────────────────
; C code שב-IDA מראה:
; int check_key(char *key) {
; if (strlen(key) != 12) return 0;
; if (key[0] != 'G') return 0;
; int sum = 0;
; for (int i=1; i<12; i++) sum += key[i];
; return sum == 0x3E8; // 1000
; }
; ─── Keygen (Python) ─────────────────────────────────────
# מגבלות: אורך=12, key[0]='G', sum(key[1:12])=1000
# פתרון: 11 תווים שסכומם = 1000
# 1000 / 11 = ~90 (0x5A = 'Z')
# 11 * 90 = 990 → חסר 10 → key[1] = chr(90+10) = 'd' = 100
# key = 'G' + 'd' + 'Z'*10 = 'GdZZZZZZZZZZ'
# בדיקה: ord('d') + 10*ord('Z') = 100 + 10*90 = 1000 ✓
# ─── Patching (עקיפה) ─────────────────────────────────────
; בIDA: מצא את ה-check_key, שנה:
; return sum == 0x3E8;
; ב-Assembly: cmp eax, 0x3E8 → jnz .fail
; Patch: שנה jnz (75 XX) ל-jmp (EB XX) → תמיד עובר
; או: שנה הbyte של jnz ל-NOP NOP (90 90)input[0] ^ input[1] == 0x41. מה זוג תווים תקין?cmp eax, 1 / jne loc_FAIL. מה הPatch הפשוט ביותר?; check_flag(char *input):
; 1. strlen(input) == 8 → אחרת return 0
; 2. for i in 0..7:
; input[i] -= 3 ; כל תו מוקטן ב-3
; 3. strcmp(input, "FLAG") ; אחרי הפחתה!
; 4. אם זהה → return 1מה הFlag (הinput) הנכון?RE Workflow מלא + סיכום
הנה ה-Workflow המלא ל-RE Challenge — מקצה לקצה. שמור אותו בראש לכל CTF ומיון.
# ─── RE Workflow מלא ─────────────────────────────────────────
# שלב 1: Static Recon
file ./binary
strings ./binary | grep -i "flag\|pass\|correct\|wrong\|key"
# → מחרוזות חשודות?
# שלב 2: זיהוי Packing
# כלי: Detect-It-Easy, binwalk -E
# אם packed → upx -d / dynamic unpack
# שלב 3: Static Analysis (IDA/Ghidra)
# א. פתח ב-IDA/Ghidra → Functions list
# ב. חפש: main, check, verify, validate, compare
# ג. Strings → Xrefs → הפונקציות שמשתמשות ב"Correct!"
# ד. F5 לPseudocode → הבן את הלוגיקה
# ה. Rename: פונקציות + משתנים חשובים
# שלב 4: Anti-Debug Bypass
# זיהוי: IsDebuggerPresent, RDTSC, INT3 checks
# עקיפה: ScyllaHide / Patch / Manual NOP
# שלב 5: Dynamic Analysis (GDB/x64dbg)
# שים BP על check function
# קרא ארגומנטים, עקוב אחרי הלוגיקה
# Memory dump אם צריך
# שלב 6: Keygen / Patch
# Keygen: הבן את האלגוריתם → כתוב Python שמייצר Input תקין
# Patch: NOP את ה-jne/jz → תמיד "Correct"
# שלב 7: Verify
# הרץ עם הInput/Patch → קבל Flag!- IDA Pro: F5=Pseudocode, X=Xrefs, N=Rename, Graph View קריאת זרימה
- Ghidra: חינמי, Decompiler מובנה, Scripting ב-Python/Java, חזק ל-CTF
- Assembly Reading: cmp/jg/jle = תנאים, Graph arrows: ירוק=True, אדום=False
- Anti-Debug: IsDebuggerPresent, Timing/RDTSC, INT3/SEH, PEB flags
- עקיפה: ScyllaHide, Patching (NOP), TitanHide, xor eax,eax לפני check
- Packing: UPX strings, entropy גבוה = packed. upx -d לפריקה
- OEP Finding: BP על JMP האחרון של Stub → Dump memory
- License Checks: חפש check function → Keygen (הפוך את האלגוריתם) או Patch
- String Encryption: Strings חסרות בStatic אך נראות בRuntime → Runtime Decryption
- RE Workflow: Recon → Unpack → Static → Anti-Debug → Dynamic → Keygen/Patch
call IsDebuggerPresent ; addr 0x401020
test eax, eax ; addr 0x401025
jnz loc_EXIT ; addr 0x401027
; ... קוד הצלחה ...
loc_EXIT:
call ExitProcessבGDB, איך עוקפים את הcheck ללא Patching?; check(char *key):
; if len(key) != 5 → return 0
; if key[0] != 'A' → return 0
; result = 0
; for i in 1..4:
; result ^= key[i]
; if result != 0x52 → return 0
; return 1מה Key תקין?תרגול נוסף — קריפטוגרפיה: Hash ו-HTTPS
שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.
Hash ל-Encryption?MD5 אינו בטוח לסיסמאות?אתגר מעשי — Anti-Debugging — זיהוי ב-RE
שיא השיעור: להבין את הפגיעות, לזהות אותה בקוד ובבינארי, ולהקשיח מולה. ההגשה נבדקת אוטומטית מול בינאריים אמיתיים.
שאלות תרגול ותשובות (21)
שאלה 1.1 — IDA Basics · מה עושה לחיצה על F5 ב-IDA Pro?
- א. מריצה את ה-Binary עצמו בתוך סביבת הניפוי המובנית של IDA Pro
- ב. מבצעת חיפוש של כל המחרוזות (strings) הקיימות בתוך הקובץ הנתון
- ג. מייצרת Pseudocode דמוי-C מה-Assembly (Hex-Rays Decompiler)
- ד. שומרת את הפרויקט הנוכחי על הדיסק יחד עם כל ההערות שנוספו לו
✓ תשובה: ג) — F5 = Decompile. Hex-Rays Decompiler הופך Assembly לPseudocode דמוי-C. זה מקצר ניתוח מדרמטית. שים לב: הPseudocode לא זהה ל-C המקורי — IDA מנסה לשחזר אותו.
שאלה 1.2 — Graph View · cmp eax, 10 jg loc_PASS call fail_func jmp loc_END loc_PASS: call pass_func loc_END:בGraph View, איזה חץ מוביל ל-pass_func?
cmp eax, 10 jg loc_PASS call fail_func jmp loc_END loc_PASS: call pass_func loc_END:
- א. החץ האדום (False): הענף שנלקח כאשר תנאי הקפיצה אינו מתקיים כלל
- ב. החץ הירוק (True): jg מתקיים כאשר eax גדול מ-10 ומגיע ל-pass_func
- ג. החץ הכחול (Unconditional): קפיצה שאינה תלויה כלל בשום תנאי שהוא
- ד. אין כלל חץ מתאים, מכיוון שמדובר כאן ב-Fall-through רציף בתוך הקוד
✓ תשובה: ב) חץ ירוק — jg (Jump if Greater) מתקיים כשeax > 10. ב-IDA: חץ ירוק = תנאי מתקיים = jump taken. jg loc_PASS → ירוק → loc_PASS → pass_func. אם eax ≤ 10: Fall-through (חץ אדום) → fail_func.
שאלה 1.3 — Xrefs · מצאנו פונקציה בשם sub_4012A0. לחצנו X. מה נראה?
# ─── פתיחת Binary ב-Ghidra ────────────────────────────────
# 1. File → New Project → Import File → בחר binary
# 2. Double-click → Analysis Options → Analyze
# 3. CodeBrowser נפתח
# ─── חלונות עיקריים ───────────────────────────────────────
# Symbol Tree → רשימת פונקציות, imports, exports
# Listing → Assembly (כמו IDA View)
# Decompile → Pseudocode (אוטומטי בצד)
# Functions → כל הפונקציות עם שמות
# ─── Scripting ב-Ghidra (Java/Python) ─────────────────────
# Script Manager → New Script → Python
from ghidra.program.model.listing import *
fm = currentProgram.getFunctionManager()
for func in fm.getFunctions(True):
if "check" in func.getName().lower():
print(f"Found: {func.getName()} @ {func.getEntryPoint()}")
# ─── חיפוש מחרוזות ────────────────────────────────────────
# Search → For Strings → מוצא "password", "flag{", "Wrong!"
# לחץ על מחרוזת → Show References → מי משתמש בה?- א. קוד ה-Assembly המלא של הפונקציה, החל מכתובת ההתחלה שלה בזיכרון
- ב. ה-Pseudocode דמוי-C של הפונקציה כפי שמייצר אותו ה-Decompiler
- ג. רשימה של כל ה-Pseudocode הקיים בכל הפונקציות שבתוך ה-Binary כולו
- ד. Cross-References: רשימת כל המקומות בקוד שקוראים אל הפונקציה הזו
✓ תשובה: ד) Xrefs — X = Cross-References. מציג: "Function sub_4012A0 is called from: main+0x3F, sub_401180+0x12...". חיוני להבנת Context — אם מצאת check_password, Xrefs מראה מי קורא לה ומתי.
שאלה 2.1 — Ghidra Basics · ב-CTF קיבלת binary ללא קוד מקור. מה הצעד הראשון ב-Ghidra?
- א. Import ← Analyze ← חפש מחרוזות כמו "flag"/"password" ← Xrefs ← נתח
- ב. לחפש מיד פעולות XOR בתוך קוד ה-Assembly של הקובץ הנתון לניתוח
- ג. להריץ את ה-binary באופן מיידי כדי לראות את התנהגותו בפועל תחילה
- ד. לכתוב Script אוטומטי כבר בתחילת הניתוח, עוד לפני שבודקים דבר
✓ תשובה: א) — Workflow סטנדרטי: Import → Analyze → חפש מחרוזות מעניינות → Xrefs → נתח פונקציות. מחרוזות כמו "Correct!", "Wrong!", "flag{" מובילות ישירות לפונקציות ה-Check הרלוונטיות.
שאלה 2.2 — Decompiler · מצאת פונקציה check_serial. הDecompiler מראה: int check_serial(char *input) { if (strlen(input) != 16) return 0; for (int i=0; i<16; i++) { if (input[i] != secret[i] ^ 0x42) return 0; } return 1; }מה צריך לעשות כדי למצוא ה-Serial?
int check_serial(char *input) {
if (strlen(input) != 16) return 0;
for (int i=0; i<16; i++) {
if (input[i] != secret[i] ^ 0x42) return 0;
}
return 1;
}- א. הרץ Brute Force על כל 16 התווים עד שאחד הצירופים יתקבל כתקין
- ב. הכנס 16 תווים כלשהם, שכן הבדיקה מוודאת רק את אורך הקלט שנשלח
- ג. השתמש בטכניקת SQL Injection כדי לעקוף את בדיקת ה-Serial הזו כאן
- ד. מצא את המערך secret[] ב-Binary ובצע XOR לכל בית עם הערך 0x42
✓ תשובה: ד) — הקוד בודק: input[i] == secret[i] ^ 0x42. כלומר: input[i] = secret[i] ^ 0x42. מצא secret[] ב-Ghidra (Data section / .rodata), XOR כל byte עם 0x42, וקבל ה-Serial! הב-XOR הוא Symmetric: (x ^ k) ^ k = x.
שאלה 2.3 — Dry Run: Assembly לC · ; Function sub_401050 (32bit) push ebp mov ebp, esp mov eax, [ebp+8] ; arg1 → eax imul eax, eax, 3 ; eax = eax * 3 add eax, 7 ; eax = eax + 7 pop ebp retמה עושה פונקציה זו (ב-Pseudocode)?
; Function sub_401050 (32bit) push ebp mov ebp, esp mov eax, [ebp+8] ; arg1 → eax imul eax, eax, 3 ; eax = eax * 3 add eax, 7 ; eax = eax + 7 pop ebp ret
- א. int f(int x) { return x + 3 * 7; }
- ב. int f(int x) { return x * 3 + 7; }
- ג. int f(int x) { return (x + 7) * 3; }
- ד. int f(int x) { return x * 7 + 3; }
✓ תשובה: ב) x * 3 + 7 — mov eax, [ebp+8] = arg1. imul eax, eax, 3 → eax = arg1 * 3. add eax, 7 → eax = arg1*3 + 7. ret → מחזיר eax. בPseudocode: return x * 3 + 7.
שאלה 3.1 — IsDebuggerPresent · call IsDebuggerPresent test eax, eax jnz loc_EXIT ; ← כתובת 0x401200 ; קוד רגיל... loc_EXIT: call ExitProcessמה צריך לעשות ב-IDA כדי לעקוף בדיקה זו (Patching)?
call IsDebuggerPresent test eax, eax jnz loc_EXIT ; ← כתובת 0x401200 ; קוד רגיל... loc_EXIT: call ExitProcess
- א. שנה את ה-jnz (0x75) ל-NOP NOP (0x90 0x90) כדי שהקפיצה ל-EXIT תבוטל
- ב. מחק לחלוטין את השורה call IsDebuggerPresent מתוך קוד הפונקציה כאן
- ג. שנה את ה-jnz ל-jz, כלומר הפוך את תנאי הקפיצה למצב ההפוך לגמרי ממנו
- ד. הוסף את הפקודה XOR eax, eax עוד לפני ה-test כדי לאפס את הרגיסטר
✓ תשובה: א) — jnz = Jump if Not Zero = קפוץ אם debugger. NOP את הjnz (2 bytes → 0x90 0x90) → לא יקפוץ לEXIT → ימשיך לקוד הרגיל תמיד. גם: xor eax,eax לפני test → eax=0 → jnz לא מתקיים.
שאלה 3.2 — Timing Check · מדוע Timing Check מזהה debugger?
- א. מכיוון שכל תוכנת Debugger צורכת הרבה יותר זיכרון RAM מן הרגיל
- ב. מכיוון שה-Debugger משנה את תדר השעון (Clock) של המעבד עצמו כאן
- ג. Single-Step ו-Breakpoints גורמים לעיכובים, וההפרש הגדול בזמן מסגיר
- ד. מכיוון שה-Debugger שולח מנות (packets) אל הרשת במהלך הניפוי
✓ תשובה: ג) — בריצה רגילה: loop של 100 פעולות = ~100-300 cycles. בDebugger עם Breakpoint: עצירה, עיבוד Debug event, המשך → אלפי cycles. RDTSC מדויק מאוד → מזהה את ההפרש.
שאלה 3.3 — INT 3 · מה ייחודי בטכניקת Anti-Debug המשתמשת ב-INT 3?
# ─── זיהוי Packing ─────────────────────────────────────── # כלי: file, strings, PEiD, Detect-It-Easy (DIE) file ./binary # ELF 64-bit LSB executable (אם רגיל) # UPX compressed data (אם packed!) strings ./binary | head -20 # Binary רגיל: הרבה strings מוכרות (API names, messages) # Packed: מעט strings, "UPX0", "UPX1", entropy גבוה # ─── אנטרופיה ───────────────────────────────────────────── # Binary רגיל: entropy 5-6 bits/byte (לא דחוס) # Packed/Encrypted: entropy 7.5-8 bits/byte (כמו random!) # כלי: binwalk -E binary → מציג entropy per section # ─── UPX — הנפוץ ביותר ─────────────────────────────────── upx -d ./packed_binary # Unpack אוטומטי! # לא תמיד עובד אם הUPX Modified/Custom # ─── Manual Unpacking (OEP Finding) ────────────────────── # 1. בGDB/x64dbg: הרץ binary # 2. Packed code מריץ Stub שמפרק לזיכרון # 3. בסוף Stub: JMP לOEP (Original Entry Point) # 4. שים BP על jmp/push-ret בסוף Stub → dump memory # 5. כלי: OllyDumpEx, x64dbg Plugin dump
- א. INT 3 היא הוראת Breakpoint; אם ה-Debugger תפס את החריגה - זוהה
- ב. היא תמיד גורמת לקריסה מיידית של התוכנית, ללא כל דרך למנוע זאת
- ג. היא בודקת כתובות זיכרון מסוימות כדי לאתר בהן נוכחות של Debugger
- ד. ההוראה INT 3 משנה את דגלי ה-CPU (flags) בדרך שמסגירה את ה-Debugger
✓ תשובה: א) — SEH (Structured Exception Handling): כשINT 3 מתרחש, debugger תופס אותו ראשון ומחזיר — ה-handler לא רץ. בלי debugger: Exception מגיע ל-handler → קוד רגיל. הbranch שלא רץ = debugger זוהה.
שאלה 4.1 — זיהוי Packing · פתחת binary ב-IDA ורואה רק פונקציה אחת בתחילה עם הרבה XOR ו-MOV פעולות, ו-strings מציג "UPX0", "UPX1". מה המסקנה?
- א. ה-Binary כתוב כולו בשפת Assembly טהורה, ומשום כך הוא נראה כך כעת
- ב. ה-Binary דחוס עם UPX; הפונקציה הנראית היא Stub לפריקה בזמן ריצה
- ג. ה-Binary מוצפן בהצפנת AES, ולכן לא ניתן כלל לקרוא את תוכנו הפנימי
- ד. קובץ ה-Binary פשוט פגום או משובש, ולכן אין כל טעם להמשיך ולנתחו
✓ תשובה: ב) — UPX0/UPX1 הם שמות ה-sections של UPX. הפונקציה היחידה = Unpacking Stub שמפרק את הקוד האמיתי לזיכרון ואז JMP לOEP. פתרון: upx -d filename → binary מפורק לניתוח רגיל.
שאלה 4.2 — String Encryption · ב-IDA לא מצאת strings כמו "correct" או "wrong". אבל בRuntime (בGDB) ראית אותן. מה זה אומר?
- א. ה-IDA אכן מצא את המחרוזות, אך פשוט בחר שלא להציג אותן למשתמש
- ב. המחרוזות מוצפנות ב-Binary ומפוענחות בזמן ריצה; מצא את פונקציית הפענוח
- ג. תוכנת ה-GDB מדמיינת מחרוזות שאינן קיימות כלל בתוך הקובץ הזה
- ד. ה-Binary כותב את המחרוזות אל הדיסק במהלך הריצה, ולכן הן נראות
✓ תשובה: ב) — String Encryption נפוץ: מחרוזות שמורות כ-XOR/RC4/AES, פונקציה מפענחת אותן לפני שימוש. ב-IDA: חפש פונקציות שמחזירות char*/string, עם לולאה על byte array. בRuntime: שים BP לאחר הDecryption ו-dump הזיכרון.
שאלה 4.3 — Dry Run: Unpacking Stub · ; Unpacking Stub (simplified): mov ecx, 0x1000 ; size mov esi, packed_data ; source mov edi, 0x401000 ; destination .loop: lodsb ; AL = *esi++ xor al, 0x55 ; decrypt byte stosb ; *edi++ = AL loop .loop ; ecx-- jmp 0x401000 ; ← OEP!מה אם נרצה ל-Dump את הbinary המפורק לאחר XOR?
; Unpacking Stub (simplified):
mov ecx, 0x1000 ; size
mov esi, packed_data ; source
mov edi, 0x401000 ; destination
.loop:
lodsb ; AL = *esi++
xor al, 0x55 ; decrypt byte
stosb ; *edi++ = AL
loop .loop ; ecx--
jmp 0x401000 ; ← OEP!- א. הרץ את הפקודה upx -d כדי לפרוק את הקובץ הדחוס באופן אוטומטי לגמרי
- ב. הרץ את התוכנית ללא Debugger כלל, וכך הפריקה תתבצע מעצמה אל הדיסק
- ג. בצע Patch לפעולת ה-XOR שבתוך הלולאה כדי לבטל את הפענוח כליל
- ד. שים Breakpoint על ה-JMP האחרון (ה-OEP), ואז בצע Dump מ-0x401000
✓ תשובה: ד) — Workflow: BP על jmp OEP → הרץ → BP מתפוצץ → הקוד מפורק. בx64dbg: Plugin → OllyDumpEx → Dump. בGDB: dump binary memory output.bin 0x401000 0x402000. כעת ניתח output.bin ב-IDA/Ghidra.
שאלה 5.1 — Keygen · ניתחת פונקציה שבודקת: input[0] ^ input[1] == 0x41. מה זוג תווים תקין?
input[0] ^ input[1] == 0x41
- א. 'A' ו-'\x00': שכן 0x41 ^ 0x00 שווה ל-0x41, וכך גם כל זוג מתאים אחר
- ב. 'A' ו-'A': שכן 0x41 ^ 0x41 שווה ל-0, ולא לערך 0x41 שנדרש כאן
- ג. 'A' ו-'B': שכן 0x41 ^ 0x42 שווה ל-0x03, ולא לערך 0x41 שנדרש כאן
- ד. אי אפשר לפתור זאת כלל, מכיוון שאין שום זוג תווים שמקיים את התנאי
✓ תשובה: א) — 0x41 ^ 0x00 = 0x41. או: 0x00 ^ 0x41 = 0x41. גם: input[0]='A'(0x41), input[1]=0x00 → 0x41^0x00=0x41 ✓. XOR הוא Symmetric: אם a^b=c, אז a=b^c. בחר input[0] כלשהו, חשב input[1] = input[0] ^ 0x41.
שאלה 5.2 — Patching · מצאת ב-IDA: cmp eax, 1 / jne loc_FAIL. מה הPatch הפשוט ביותר?
cmp eax, 1 / jne loc_FAIL
- א. שנה את פקודת ה-cmp לפקודת add כדי לשבש בכך את תוצאת ההשוואה כאן
- ב. מחק את הפונקציה כולה מן ה-Binary כדי לדלג על בדיקת התקינות הזו
- ג. שנה את cmp eax, 1 ל-cmp eax, 0 כדי להשוות את הרגיסטר מול ערך אחר
- ד. שנה את ה-jne (0x75) ל-NOP NOP (0x90 0x90) כדי שהקפיצה ל-FAIL תבוטל
✓ תשובה: ד) — jne = Jump if Not Equal. NOP-ification (שינוי ל-0x90 0x90): הcpu מריץ NOP ומשם ממשיך לקוד הבא — הצלחה תמיד. ב-IDA: Edit → Patch program → Change bytes. ד) גם עובד אבל פחות נקי.
שאלה 5.3 — Dry Run מלא: Crackme · ; check_flag(char *input): ; 1. strlen(input) == 8 → אחרת return 0 ; 2. for i in 0..7: ; input[i] -= 3 ; כל תו מוקטן ב-3 ; 3. strcmp(input, "FLAG") ; אחרי הפחתה! ; 4. אם זהה → return 1מה הFlag (הinput) הנכון?
; check_flag(char *input): ; 1. strlen(input) == 8 → אחרת return 0 ; 2. for i in 0..7: ; input[i] -= 3 ; כל תו מוקטן ב-3 ; 3. strcmp(input, "FLAG") ; אחרי הפחתה! ; 4. אם זהה → return 1
- א. "FLAG" עם ריפוד אפסים לאורך 8: המחרוזת FLAG עצמה בתוספת בתי אפס
- ב. "FLAG" באורך 4: המחרוזת המקורית עצמה, אף שהאורך הנדרש הוא 8 תווים
- ג. "CCCC": ארבעה תווים זהים שמפוענחים כולם לאותו התו לאחר החיסור ב-3
- ד. "IODJ" ועוד 4 בתים: כי 'I','O','D','J' פחות 3 נותנים 'F','L','A','G'
✓ תשובה: ד) — Dry Run: input[i] -= 3, אחר כך strcmp ל-"FLAG". כלומר: input_before - 3 = 'F','L','A','G'. input_before = 'F'+3=0x49='I', 'L'+3=0x4F='O', 'A'+3=0x44='D', 'G'+3=0x4A='J'. אורך חייב 8, אז: "IODJ" + 4 bytes נוספים (הstrcmp בודק 4 בלבד).
גאמא סגנון א׳ — Workflow · קיבלת binary ב-CTF. strings מראה רק "UPX0" ו-"UPX1". IDA מראה פונקציה אחת עם XOR loop. מה הצעד הנכון?
- א. לנתח לעומק את לולאת ה-XOR בתוך IDA כדי להבין את אלגוריתם הפענוח
- ב. לשלוח הרבה קלטים שונים אל ה-Binary ולבדוק כיצד הוא מגיב לכל אחד
- ג. upx -d ל-Binary; אם נכשל: x64dbg, שים BP על ה-JMP, ו-Dump מן ה-OEP
- ד. לחפש פגיעויות (vulnerabilities) בתוך לולאת ה-XOR של קוד הפריקה הזה
✓ תשובה: ג) — Packed binary: הניתוח הסטטי חסר תועלת כי הקוד האמיתי מוסתר. תמיד: Unpack קודם! upx -d הוא הכי מהיר. אם Custom UPX (Modified): Dynamic analysis → OEP dump → נתח ב-IDA.
גאמא סגנון ב׳ — Assembly + Anti-Debug · call IsDebuggerPresent ; addr 0x401020 test eax, eax ; addr 0x401025 jnz loc_EXIT ; addr 0x401027 ; ... קוד הצלחה ... loc_EXIT: call ExitProcessבGDB, איך עוקפים את הcheck ללא Patching?
call IsDebuggerPresent ; addr 0x401020 test eax, eax ; addr 0x401025 jnz loc_EXIT ; addr 0x401027 ; ... קוד הצלחה ... loc_EXIT: call ExitProcess
- א. שים BP אחרי ה-test, וכשהוא נעצר בצע set $eax = 0 כדי שלא יקפוץ ל-EXIT
- ב. הרץ את התוכנית לגמרי ללא GDB, וכך הבדיקה פשוט לא תזהה את הניפוי
- ג. שנה את שם התהליך (process) כדי שה-IsDebuggerPresent לא יזהה אותו
- ד. בטל את מנגנון ה-ASLR במערכת כדי שכל הכתובות יהיו קבועות וידועות
✓ תשובה: א) — Runtime bypass ב-GDB: break *0x401025 → run → (BP) → set $eax = 0 → continue. test eax, eax עם eax=0 → ZF=1 → jnz לא מתקיים → ממשיך לקוד הצלחה! ללא שינוי קובץ.
גאמא סגנון ג׳ — Dry Run Keygen · ; check(char *key): ; if len(key) != 5 → return 0 ; if key[0] != 'A' → return 0 ; result = 0 ; for i in 1..4: ; result ^= key[i] ; if result != 0x52 → return 0 ; return 1מה Key תקין?
; check(char *key): ; if len(key) != 5 → return 0 ; if key[0] != 'A' → return 0 ; result = 0 ; for i in 1..4: ; result ^= key[i] ; if result != 0x52 → return 0 ; return 1
- א. "A\x52\x00\x00\x00": key[0] הוא 'A' ו-XOR של השאר נותן 0x52 כנדרש
- ב. "AAAAA": חמישה תווים זהים שה-XOR של ארבעת האחרונים אינו נותן 0x52
- ג. "ARRR\x52": ארבעה תווים אחרי ה-A שה-XOR שלהם כלל אינו שווה ל-0x52
- ד. "ABCDE": חמישה תווים עוקבים שה-XOR של ארבעת האחרונים אינו 0x52 כלל
✓ תשובה: א) — צריך key[0]='A', אורך 5, ו-key[1]^key[2]^key[3]^key[4] = 0x52. ד) "A\x52\x00\x00\x00": 0x52^0^0^0 = 0x52 ✓. שאר האפשרויות נותנות 0: א) 'A'^'A'^'A'^'A'=0. ב) 'R'^'R'^'R'^'\x52' = 0x52^0x52^0x52^0x52 = 0 (ארבעה ערכים זהים מתבטלים). ג) 'B'^'C'^'D'^'E'=0. רק ד) מגיע ל-0x52.
שאלה 6.1 [קל] · מה ההבדל בין Hash ל-Encryption?
Hash
- א. Hash הפיך תמיד: הנחה שגויה, אף שדווקא Encryption הוא ההפיך עם מפתח, בעוד Hash הוא חד-כיווני ואי אפשר לשחזר ממנו את הקלט המקורי
- ב. Hash הוא חד-כיווני ואי אפשר לשחזר ממנו את המקור, ואילו Encryption הוא דו-כיווני וניתן לפענח את הטקסט המקורי בעזרת המפתח המתאים
- ג. שניהם דו-כיווניים: הנחה שגויה, אף ש-Hash הוא חד-כיווני ואינו ניתן לשחזור, ולכן רק Encryption הוא הדו-כיווני שניתן לפענח עם מפתח
- ד. Encryption חד-כיווני: הנחה שגויה, אף שדווקא Hash הוא החד-כיווני, ו-Encryption הפיך עם המפתח, ולכן התיאור בתשובה זו מהופך לחלוטין כן
✓ תשובה נכונה: ב — Hash: חד-כיווני, אורך קבוע, לאימות שלמות/סיסמאות. Encryption: דו-כיווני עם מפתח, לסודיות. אין 'פענוח Hash' — רק Brute Force.
שאלה 6.2 [בינוני] · מדוע MD5 אינו בטוח לסיסמאות?
MD5
- א. MD5 איטי מדי: הנחה שגויה, אף שדווקא המהירות הגבוהה של MD5 היא הבעיה כי היא מזרזת Brute Force, ולכן הוא אינו איטי אלא מהיר מדי לשימוש זה כן
- ב. MD5 הפיך בקלות: הנחה שגויה, אף ש-MD5 הוא חד-כיווני ואינו הפיך, הבעיה היא מהירותו הגבוהה ומתקפות ה-Collision ולא היפוך ישיר של ה-Hash כן
- ג. MD5 ארוך מדי: הנחה שגויה, אף שאורך ה-Hash אינו הבעיה, החולשה נובעת ממהירות החישוב שמאיצה Brute Force ומקיומן של התנגשויות ידועות
- ד. MD5 מהיר מאוד ולכן מאפשר לתוקף לנסות מיליוני Hashes בשנייה במתקפת כוח, ובנוסף ידועות בו מתקפות Collision, ולכן הוא אינו בטוח לסיסמאות
✓ תשובה נכונה: ד — MD5 מהיר → מיליוני ניסיונות בשנייה ב-Brute Force/Rainbow. גם Collisions ידועות. לסיסמאות: bcrypt/argon2 (איטיים בכוונה) + Salt.
שאלה 6.3 [קשה] · HTTPS משתמש גם ב-Symmetric וגם ב-Asymmetric. למה בשניהם?
c — תהליך יכול להיות traced פעם אחת; אם התוכנית עושה PTRACE_TRACEME על עצמה, דיבאגר שינסה להיצמד ייכשל. זו טכניקת anti-debugging נפוצה.
- א. רק Asymmetric בשימוש: הנחה שגויה, אף ש-Asymmetric משמש לחילוף מפתח בלבד בגלל איטיותו, ואילו הנתונים עצמם מוצפנים ב-Symmetric המהיר יותר
- ב. Asymmetric משמש לחילופי המפתח (Key Exchange) בלבד כי הוא איטי, ואילו Symmetric כמו AES מצפין את הנתונים עצמם כי הוא מהיר בהרבה ממנו
- ג. Symmetric לחילוף מפתח: הנחה שגויה, אף שדווקא Asymmetric משמש לחילוף המפתח בתחילת החיבור, ו-Symmetric מצפין את הנתונים בהמשך הפעולה
- ד. שניהם מצפינים נתונים: הנחה שגויה, אף שרק Symmetric מצפין את גוף הנתונים, בעוד Asymmetric משמש רק לחילוף המפתח הראשוני בזכות אבטחתו כן
✓ תשובה נכונה: ב — Asymmetric (RSA/ECDHE): איטי, משמש ל-Key Exchange בטוח בתחילת TLS. Symmetric (AES): מהיר, מצפין את כל שאר התעבורה. השילוב = מהיר ובטוח.