Struct Layout
و Offset Analysis در Reverse Engineering
وقتی Source Code رو داشته باشیم فهمیدن Struct خیلی راحت تره
مثلا:
struct User {
int id;
int age;
char *name;
};
اینجا دقیقا میدونیم هر Field چیه و کجا قرار گرفته.
ولی توی Reverse Engineering معمولا خبری از این کد نیست
کامپایلر Struct رو تبدیل کرده به دستورهای Machine Code و Assembly و چیزی که ما میبینیم ممکنه فقط این باشه:
mov eax, [rdi+0x4]
mov rax, [rdi+0x8]
اینجا دیگه کامپایلر نمیاد بهمون بگه:
+0x04 = age
+0x08 = name
پس باید خودمون از روی نحوه استفاده از Memory بفهمیم هر Offset احتمالا مربوط به چه Field ی بوده
این کار یکی از بخشهای مهم برای بازسازی ساختارهای ناشناخته است
Offset
دقیقا چیه؟
فرض کنید آدرس شروع یک Object این باشه:
0x1000
حالا توی Assembly داریم:
mov eax, [rdi+0x4]
اگر RDI برابر 0x1000 باشه CPU میره سراغ:
0x1000 + 0x4 = 0x1004
یعنی:
Object Base
|
v
0x1000
+0x00
+0x04 <-- accessed
+0x08
+0x10
اون +0x4 همون Offset هست
پس وقتی توی RE میبینید:
[Register + Offset]
یکی از اولین سوالها اینه:
این Register به کجا اشاره میکنه؟
این Offset چه Field ی رو نشون میده؟
در خیلی از مواقع چیزی که داریم دنبالش میگردیم همینه:
Base Address + Offset
|
v
Field
حالا یک Struct ساده رو ببینیم
مثلا:
struct User {
int id;
int age;
char *name;
};
در یک محیط 64 بیتی معمولی که int چهار بایت و Pointer هشت بایت باشه Layout مفهومی میتونه این شکلی باشه:
Offset Size Field
+0x00 4 bytes id
+0x04 4 bytes age
+0x08 8 bytes name
پس وقتی Compiler میخواد age رو بخونه:
mov eax, [rdi+0x4]
و برای خوندن name:
mov rax, [rdi+0x8]
ما از روی همین Offsetها میتونیم کم کم Layout Struct رو بازسازی کنیم
البته این Layout به ABI و Alignment هم وابسته است پس همیشه نباید یک Layout رو برای همه Binary ها قطعی فرض کنیم
Read و Write
رو از هم تشخیص بده
این دستور:
mov eax, [rdi+0x4]
یعنی مقدار موجود در Object + 0x4 خونده شده
پس:
read field at +0x4
ولی این:
mov [rdi+0x4], eax
یعنی مقدار جدیدی در Object + 0x4 نوشته شده
پس:
write field at +0x4
این تفاوت توی RE خیلی مهمه
مثلا اگر در چند جای برنامه ببینیم:
mov eax, [rdi+0x4]
و جای دیگه:
add dword ptr [rdi+0x4], 1
میتونیم حدس بزنیم که +0x4 احتمالا یک مقدار عددی یا چیزی با رفتار عددی هست
چرا؟
چون برنامه داره مستقیما روی اون Memory به شکل یک مقدار 32 بیتی عملیات انجام میده
اندازه Memory Access هم سرنخ میده
مثلا:
mov eax, [rdi+0x4]
اینجا داریم یک مقدار 4 بایتی میخونیم.
در مقابل:
mov rax, [rdi+0x8]
یک مقدار 8 بایتی خونده میشه
در یک Binary 64 بیتی این مقدار میتونه یک Pointer یا یک مقدار 64 بیتی باشه
حالا اگر بعدش ببینیم:
mov rax, [rdi+0x8]
mov rcx, [rax]
اینجا یک سرنخ مهم داریم
مقداری که از:
Object + 0x8
خوندیم بعدا به عنوان Address استفاده شده
پس احتمال Pointer بودنش بیشتر میشه:
+0x8 = pointer ?
ولی فعلا علامت سؤال رو نگه دارید 😁
در Reverse Engineering بهتره با یک Instruction سریع نتیجه قطعی نگیریم
Assembly
به تنهایی خیلی چیزها رو نمیگه باید Context رو هم ببینیم
دنبال Offsetهای تکراری بگرد
یکی از روشهای خیلی خوب اینه که یک Offset رو در چند تابع مختلف بررسی کنیم
مثلا:
Function A:
mov eax, [rdi+0x4]
و:
Function B:
cmp dword ptr [rdi+0x4], 0
و:
Function C:
add dword ptr [rdi+0x4], 1
اگر RDI در این توابع واقعا به Object مشابهی اشاره کنه احتمال زیادی وجود داره که:
+0x4
یک Field مشترک از همون Struct باشه
حالا مثلا میبینیم:
mov rax, [rdi+0x8]
و جای دیگه:
call [rdi+0x8]
اینجا باید بیشتر بررسی کنیم
ممکنه +0x8 یک Pointer باشه
ممکنه Function Pointer باشه
یا ممکنه بخشی از یک Object Layout خاص باشه
پس فقط از روی Offset اسمگذاری نکنید
رفتار اون Field رو هم بررسی کنید
Base Pointer
رو درست پیدا کنید
قبل از اینکه بریم سراغ Offset ها باید بفهمیم Register پایه واقعا به چی اشاره میکنه
مثلا:
mov eax, [rdi+0x4]
ممکنه RDI به یک Struct اشاره کنه
ولی صرفا چون اسمش RDI هست نمیتونیم چنین نتیجهای بگیریم
باید مسیر مقدار رو دنبال کنیم
مثلا:
mov rdi, rbx
call process_user
Post #1843
46