با Ghidra
تا اینجا Backward Slicing و Forward Slicing رو یاد گرفتیم
حالا وقتشه همین مفاهیم رو روی یه باینری واقعی اجرا کنیم
هدف اینه که وقتی یه تابع شلوغ و پر از متغیر و دستور دیدیم لازم نباشه کل تابع رو زیر و رو کنیم
فقط مسیر دادهای که برامون مهمه رو جدا میکنیم و همون رو دنبال میکنیم
یه برنامه ساده برای آزمایش
مثلا این کد رو داریم
C
#include <stdio.h>
int check(int input) {
int a = input + 5;
int b = a * 3;
int junk = 900;
junk ^= 123;
int result = b - 7;
return result;
}
int main() {
int x = 10;
printf("%d\n", check(x));
return 0;
}
`اگه input برابر 10 باشه محاسبات این شکلی پیش میرن
input = 10
↓
a = 10 + 5 = 15
↓
b = 15 × 3 = 45
↓
result = 45 - 7 = 38
در نهایت return مقدار 38 رو برمیگردونه
ولی این وسط یه متغیر دیگه هم داریم
junk = 900
junk ^= 123
این مقدار هیچ تاثیری روی خروجی نداره
و دقیقا همین چیزیه که میخوایم با Program Slicing پیدا کنیم
باینری رو وارد Ghidra میکنیم
اول برنامه رو کامپایل میکنیم و فایل اجرایی رو داخل Ghidra باز میکنیم
بعد میریم سراغ تابع
check
اینجا دو بخش برای ما خیلی مهم هستن
Decompiler
Function Graph
Decompiler
کمک میکنه منطق کلی تابع رو راحت تر بفهمیم
Function Graph
هم کمک میکنه مسیرهای مختلف اجرای تابع و ارتباط بین Basic Blockها رو ببینیم
ولی حواسمون باشه
Decompiler
خودش حقیقت مطلق نیست
چیزی که میبینیم بازسازی Ghidra از باینریه
برای تحلیل جدی باید در صورت نیاز برگردیم به Assembly و خود Data Flow رو بررسی کنیم
اول Return Value رو پیدا میکنیم
فرض میکنیم Decompiler چیزی شبیه این نشون بده
C
int check(int input)
{
int a;
int b;
int junk;
int result;
a = input + 5;
b = a * 3;
junk = 900;
junk = junk ^ 123;
result = b - 7;
return result;
}
حالا سوال اصلی اینه
چه چیزهایی روی return تاثیر گذاشتن
اینجا از Backward Slicing شروع میکنیم
از Return به عقب برمیگردیم
آخرین قسمت اینه
lua
return result;
پس اولین چیزی که باید بررسی کنیم result هست
result اینجا ساخته شده
result = b - 7;
پس وابستگی ما فعلا اینه
return
↑
result
↑
b
حالا میپرسیم b از کجا اومده
b = a * 3;
پس
return
↑
result
↑
b
↑
a
حالا a
a = input + 5;
در نتیجه میرسیم به
return
↑
result
↑
b
↑
a
↑
input
اگه محاسبات رو هم داخل مسیر بذاریم
input
↓
+5
↓
×3
↓
-7
↓
return
پس Slice مربوط به خروجی این برنامه تقریبا همین مسیره
حالا Junk Code رو بررسی کنیم
این قسمت رو داریم
C
int junk = 900;
junk ^= 123;
ولی هیچ جا نتیجه
junk وارد محاسبه result نشدهپس مسیرش این شکلیه
junk
↓
junk ^ 123
↓
X
و هیچ مسیری به
result ندارهدر نتیجه وقتی هدفمون
returnهست
junk داخل Backward Slice مربوط به
return قرار نمیگیرهاینجاست که میبینیم Slicing چرا مفیده
به جای اینکه تمام دستورهای تابع رو هم وزن ببینیم
فقط دستورهایی رو نگه میداریم که به داده موردنظر ما وابستگی دارن
همین کار رو روی Assembly انجام بدیم
فرض میکنیم کامپایلر تابع رو تقریبا به این شکل تبدیل کرده
mov eax, edi
add eax, 5
imul eax, 3
sub eax, 7
ret
حالا از پایین به بالا نگاه میکنیم
دستور
ret مقدار برگشتی تابع رو برمیگردونهداخل این مثال مقدار خروجی داخل
EAX قرار دارهپس میریم دستور قبل
sub eax, 7
یعنی مقدار
EAX هنوز روی خروجی تاثیر دارهقبل از اون
imul eax, 3
باز هم همون مقدار
EAX رو داریم که وارد محاسبه بعدی میشهبعد
add eax, 5
باز هم
EAX بخشی از مسیر داده استو در اخر
mov eax, edi
اینجا مشخص میشه مقدار اولیه از
EDI وارد این مسیر شدهپس Slice ما در Assembly میشه
EDI
↓
EAX
↓
EAX + 5
↓
EAX × 3
↓
EAX - 7
↓
RET
اینجا دیگه داریم Data Flow واقعی رو در سطح Instruction دنبال میکنیم
یه نکته خیلی مهم
در باینری واقعی معمولا دیگه خبری از اسمهایی مثل اینا نیست
input
result
a
b
ممکنه فقط چیزایی مثل این ببینیم
RAX
RBX
RCX
RDX
[rbp-0x20]
[rbp-0x18]
اینجاست که کار ما شروع میشه
باید خودمون رابطه بین این داده ها رو بازسازی کنیم
مثلا
mov eax, [rbp-20h]
add eax, 5
imul eax, 3
mov [rbp-18h], eax
میتونیم این مسیر رو ذهنی تبدیل کنیم به