↓
+5
↓
×3
↓
[rbp-18h]
حالا باید ببینیم
[rbp-18h] بعدا کجا استفاده میشهاگر در اخر به چیزی شبیه این برسیم
mov eax, [rbp-18h]
ret
میفهمیم این مقدار بخشی از مسیر دادهای هست که به خروجی تابع میرسه
Forward و Backward
رو با هم ترکیب کنیم
اینجا کار جالبتر میشه
فرض میکنیم ورودی تابع در
EDI قرار دارهاز ورودی شروع میکنیم
Forward Slicing
EDI
↓
EAX
↓
ADD
↓
IMUL
↓
SUB
↓
RETURN
حالا از طرف دیگه از
RETURN برمیگردیم عقبBackward Slicing
RETURN
↑
SUB
↑
IMUL
↑
ADD
↑
EAX
↑
EDI
اگر این دو تحلیل به یه مسیر مشترک برسن
اطمینان بیشتری داریم که مسیر اصلی Data Flow رو درست پیدا کردیم
EDI
↓
ADD
↓
IMUL
↓
SUB
↓
RETURN
داخل Ghidra به چی نگاه کنیم
وقتی دارید Program Slicing رو روی یه تابع واقعی انجام میدید چند چیز خیلی مهمن
Decompiler
Function Graph
References
Register Usage
Memory References
Variable Definitions
Variable Uses
ولی دو سوال از همه مهمترن
این مقدار کجا ساخته میشه
و
این مقدار بعدا کجا استفاده میشه
این دو سوال ساده به نظر میرسن ولی پایه خیلی از تحلیلهای جدی Reverse Engineering هستن
هر بار یه Register یا Memory Location دیدید باید همین رابطه رو دنبال کنید
Definition
↓
Value
↓
Use
↓
New Value
↓
Next Use
یه مثال کمی واقعیتر
فرض کنیم این تابع رو داریم
C++
int verify(int input) {
int x = input ^ 0x55;
int y = x + 0x1234;
int noise = 777;
noise *= 4;
int z = y ^ 0xAA;
return z == 0x5678;
}
اینجا هدف ما فهمیدن شرط اخره
lua
return z == 0x5678;
پس از
return به عقب برمیگردیماول
zبعد
yبعد
xو در اخر
inputمسیر میشه
return
↑
comparison
↑
z
↑
y
↑
x
↑
input
اگر محاسبات رو هم اضافه کنیم
input
↓
XOR 0x55
↓
+ 0x1234
↓
XOR 0xAA
↓
compare 0x5678
↓
return
ولی این قسمت
noise = 777
noise *= 4
هیچ ارتباطی با
z و شرط نهایی ندارهپس برای تحلیل این شرط فعلا میتونه خارج از Slice ما قرار بگیره
چرا این موضوع برای ما مهمه
چون توی برنامه واقعی ممکنه یه تابع صدها یا حتی هزاران Instruction داشته باشه
ولی چیزی که واقعا دنبالش هستیم ممکنه فقط این باشه
Input
↓
Transformation
↓
Comparison
↓
Decision
Slicing
کمک میکنه همین مسیر رو از بین حجم زیادی از کد جدا کنیم
مخصوصا وقتی با چیزهایی مثل این روبهرو میشیم
Obfuscation
Junk Code
Function Complexity
Chain of Calculations
Multiple Conditions
ممکنه ظاهر تابع خیلی شلوغ باشه
ولی وقتی Data Flow رو دنبال کنیم بفهمیم فقط چند دستور واقعا روی نتیجهای که برامون مهمه تاثیر دارن
تمرین:
این تابع رو بررسی کنید
int verify(int input) {
int a = input ^ 0x31;
int junk1 = 1234;
junk1 += 55;
int b = a * 7;
int junk2 = 999;
junk2 ^= 0x44;
int c = b - 12;
return c == 0x500;
}
دو تا Slice استخراج کنید
اول Backward Slice از
returnیعنی از اینجا شروع کنید
return
↑
?
↑
?
↑
?
↑
input
بعد Forward Slice رو از
input پیدا کنیدinput
↓
?
↓
?
↓
?
↓
comparison
↓
return
در اخر باید بتونید مسیر اصلی رو تقریبا به این شکل دربیارید
input
↓
XOR 0x31
↓
× 7
↓
- 12
↓
comparison 0x500
↓
return
و این دو قسمت
junk1
junk2
نباید وارد Slice مربوط به این خروجی بشن چون روی نتیجه نهایی تاثیری ندارن
حالا یه مرحله مهمتر هم وجود داره
همین تحلیل رو روی Assembly انجام بدید اونجا دیگه خبری از اسمهای تمیز و قشنگ
input و result نیستباید خودتون Register ها و Memory Location ها رو دنبال کنید و Data Flow رو از روی Instruction ها بازسازی کنید
اینجاست که تفاوت بین فقط خوندن Decompiler و واقعا تحلیل کردن باینری کم کم مشخص میشه
@reverseengine