تا اینجا دیدیم که چطور ممکنه دو تا Pointer به یه آدرس از حافظه اشاره کنن حالا میخوایم یه حالت مهمتر رو بررسی کنیم
فرض کنید یه تابع داریم که یه مقدار رو توی حافظه تغییر میده و یه تابع دیگه همون مقدار رو از حافظه میخونه
نکته اینجاست که ممکنه این دو تا تابع از Pointer های متفاوتی استفاده کنن ولی هر دو به یه آدرس مشترک دسترسی داشته باشن
این موضوع توی مهندسی معکوس خیلی مهمه، چون ممکنه با بررسی هر تابع بهتنهایی متوجه ارتباط بین اونها نشیم
یه مثال ساده با
C
void set_value(int *p)
{
*p = 100;
}
int get_value(int *q)
{
return *q;
}
int main()
{
int value = 10;
set_value(&value);
int result = get_value(&value);
return result;
}
اینجا چه اتفاقی میفته؟
اول مقدار
value برابر 10 هستبعد آدرس
value به تابع set_value داده میشهتابع
set_value مقدار موجود در اون آدرس رو به 100 تغییر میدهبعد همون آدرس به تابع
get_value داده میشه و مقدار جدید از حافظه خونده میشهدر نهایت
result برابر 100 میشهنکته اصلی اینه که Pointerهای
p و q اسم و متغیرهای متفاوتی دارن اما در این مثال هر دو به همون خانه حافظه دسترسی دارنپس ارتباط بین این دو تابع از طریق حافظه شکل گرفته، نه از طریق مقدار برگشتی یکی از اونها
حالا این موضوع توی اسمبلی چه شکلیه؟
فرض کنید تابع
set_value به این شکل کامپایل شدهset_value:
mov dword ptr [rdi], 100
ret
این مثال بر اساس قرارداد فراخوانی System V AMD64 نوشته شده که معمولا توی برنامههای 64 بیتی لینوکس استفاده میشه
طبق این قرار داد آرگومان اول تابع داخل رجیستر
RDI قرار میگیرهحالا این دستور رو بررسی کنیم
mov dword ptr [rdi], 100
این دستور عدد 100 رو در آدرسی مینویسه که داخل
RDI قرار دارهدقت کنید که مقدار
RDI با مقداری که داخل حافظه ذخیره شده فرق دارهRDIآدرس رو نگه میداره و عبارت
[rdi] به داده ای اشاره میکنه که در اون آدرس قرار دارهحالا تابع
get_value رو ببینیدget_value:
mov eax, dword ptr [rdi]
ret
اینجا مقدار چهار بایتی موجود در آدرس
RDI خونده میشه و داخل EAX قرار میگیره از اونجایی که مقدار برگشتی تابع از نوع int هست این مقدار در رجیستر EAX برگردونده میشه پس یه تابع توی حافظه مینویسه و تابع دیگه از همون حافظه میخونه البته به شرطی که آدرسی که به هر دو تابع داده شده به همون محل حافظه اشاره کنهحالا توی مهندسی معکوس چطور پیداش کنیم؟
وقتی داری یه فایل باینری رو توی Ghidra یا Disassembler بررسی میکنید این مراحل رو دنبال کنید
مرحله اول:
آرگومانهای تابع رو بررسی کنید
ببینید تابع چه Pointer هایی دریافت میکنه و از اونها برای خوندن یا نوشتن حافظه استفاده میکنه یا نه
مرحله دوم:
دنبال دستورهای نوشتن حافظه بگردید
مثلا دستور زیر رو در نظر بگیرید
mov dword ptr [rdi], 100
این دستور نشون میده تابع از آدرسی که داخل
RDI قرار داره برای نوشتن داده استفاده میکنهمرحله سوم:
تابعهای دیگه رو بررسی کنید
ببینید تابع های دیگه همون آدرس یا Pointer مرتبطی رو دریافت میکنن و بعدا از اون حافظه مقداری میخونن یا نه
فقط دنبال اسم یکسان متغیرها یا رجیسترها نباشید ممکنه آدرس از طریق رجیستر دیگهای منتقل بشه داخل ساختار ذخیره بشه یا با چند مرحله محاسبه به دست بیاد
مرحله چهارم:
ارتباط بین تابع ها رو دنبال کنید
فقط به اسم تابع یا مقدار برگشتی اون توجه نکنیدممکنه یه تابع حافظه رو تغییر بده و هیچ مقداری برنگردونه ولی اثر تغییرش چند مرحله بعد داخل تابع دیگهای دیده بشه
برای همین باید مسیر انتقال Pointer ها و خوندن و نوشتن حافظه رو دنبال کنید
یه نکته مهم درباره Must Alias و May Alias
همیشه نمیتونیم از روی شباهت آرگومان ها مطمئن بشیم که دو Pointer به یه آدرس اشاره میکنن
اینجا دو مفهوم مهم داریم
Must Aliasیعنی طبق اطلاعات و شرایطی که بررسی کردیم دو Pointer حتما به یه محل حافظه اشاره میکنن
May Aliasیعنی ممکنه دو Pointer به یه محل حافظه اشاره کنن ولی اطلاعات موجود برای اثبات قطعی این موضوع کافی نیست
این تفاوت توی تحلیل برنامه اهمیت زیادی داره اگه دو Pointer رو اشتباهی یکی فرض کنیم ممکنه ترتیب تغییرات حافظه و در نتیجه مسیر واقعی اجرای برنامه رو اشتباه تحلیل کنیم
تمرین:
کد زیر رو بررسی کنید
C
void change(int *p)
{
*p = 50;
}
void read_value(int *q)
{
int x = *q;
}
int main()
{
int a = 10;
int b = 20;