Reverse Code Engineering

逆向工程代码 обратная код машиностроения

Reverse Code Engineering

逆向工程代码 обратная код машиностроения

Reverse Code Engineering

Share what I know............... learn what I don’t

آخرین نظرات

تشخیص ساختارهای برنامه نویسی در اسمبلی - قسمت پنجم

پنجشنبه, ۳۱ ارديبهشت ۱۳۹۴، ۱۰:۴۴ ب.ظ

قبل از اینکه بخوایم انواع فراخوانی رو بررسی کنیم یه توضیح اجمالی در مورد حافظه و stack میدم تا پیش زمینه لازم رو داشته باشید.

حافظه اصلی یا همون رم سیستم برای یک برنامه به 4 قسمت اصلی زیر تقسیم میشه

قسمتی که برای ما در اینجا مهمه Stack هستش.
پشته به عبارت کلی قطعه ای از حافظه هستش که برای نگهداری اطلاعات تابع از جمله متغیرهای محلی , کنترل جریان و... استفاده میشه. با push میتوان عنصری داخل پشته قرار داد و با pop میتوان عنصری از داخل پشته برداشت کرد یا باصطلاح بازیابی کرد.
همونطور که میدونید ساختار پشته بصورت LIFO است یعنی اگه ما 1 , 2 و 3 رو به ترتیب درون پشته قرار دهیم (push) , اولین عنصری که میتونیم برداشت کنیم pop) 3) خواهد بود پس هر عنصری جدید روی عنصر ماقبل خودش قرار میگیره.
در معماری x86 پشته built-in پشتیانی میشه و رجیسترهایی هم که برای این کار هستند ESP و EBP هستند.
رجیستر ESP به بالای پشته اشاره میکنه یعنی آخرین عنصر, و با push یا pop کردن مقدار این رجیستر تغییر می کند.
از EBP هم بعنوان آدرس مبنا مورد استفاده قرار میگیره , تا از این طریق بشه متغیرهای محلی و پارامترها رو پیدا کرد.
پشته از آدرس های بالا به پایین تخصیص داده میشود به این صورت که آخرین عنصر آدرسش کوچکتر از اولین عنصر هستش.


فراخوانی تابع:

تابع قطعه کدی در داخل برنامه اصلی هستش که یک وظیفه خاص رو انجام میده, این تعریف تابع هستش.
حال زمانی که یک تابع در داخل برنامه اصلی فراخوانی میشه, روند اجرایی برنامه بصورت موقت در اختیار تابع قرار میگیره و این تابع قراره یه سری کارها انجام بده و دوباره روند اجرایی رو به برنامه اصلی برگردونه.
بیشتر توابع محتوی prologue (چند خط کد در شروع تابع) هستند. این prologue وظیفه داره تا پشته و رجیسترهارو برای تابع آماده کنه, از طرف دیگه نیاز هستش تا epilogue (چند خط کد در انتهای تابع) هم باشه تا وضعیت پشته و رجیسترهارو به حالت اول برگردونه.

در ادامه ما خلاصه ای از مهمترین کارهایی که برای فراخوانی یک تابع انجام میشه رو لیست کردیم.

1- آرگومانها با استفاده از دستور push روی پشته قرار میگیرند.

2- تابع با استفاده از دستور call memory_location فراخوانی میشه. این دستور باعث میشه تا آدرس دستور جاری (یا همون EIP) برروی پشته قرار بگیرد.
(زمانی که تابع به پایان برسد این مقدار بازیابی (pop) میشه)
حالا EIP به memory_location تغییر می کنه تا تابع اجرا بشه.

3- بوسیله prologue ما فضایی رو برای متغیرهای محلی روی پشته اختصاص میدیم و EBP رو هم روی پشته push میکنیم.

در ادامه...

1- تابع اجرا میشه.

2- بوسیله epilogue, پشته بازیابی میشه.یعنی فضای متغیرهای محلی آزاد شده و EBP که قبلا push شده بود pop میشه

3- با فراخوانی دستور ret از تابع خارج میشم و به تابع اصلی برمی گردیم.به این صورت که :
این دستور pop میکنه آدرس برگشت رو از پشته و اون رو داخل EIP قرار میده و برنامه از دستور بعد از تابع فراخوانی شده ادامه پیدا می کنه.
4- آرگومانهایی که قبلا به تابع فرستاده بودیم از روی پشته پاک میشه مگراینکه باز هم قرار باشه از آنها استفاده دوباره بشه.

آرایش پشته:

همونطور که قبلا هم گفتیم پشته از آدرس های بالا به آدرس های پایین رشد میکنه که در شکل زیر هم مشاهده میکنید.هر زمانی که یک فراخوانی انجام میشه یک قاب پشته (stack frame) جدید ساخته میشه و تابع تا زمانی که به اتمام نرسیده این قاب پشته هم از بین نمیره.


شکل بالا رو با جزییات بیشتر در پایین مشاهده میکنید.

توی شکل زیر, مکان های حافظه اختصاص یافته به عناصر هم نشان داده شدن
ESP به بالای پشته اشاره میکنه و آدرس EBP هم در زمان اجرای تابع ثابت هستش, برای اینکه بتونیم با استفاده از اون به متغیرهای محلی و آرگومان ها دسترسی داشته باشیم.
آرگومانهایی که قبل از فراخوانی روی پشته قرار گرفتن در پایین قاب پشته قرار دارند, بعد از آرگومان ها , آدرس برگشت وجود داره که بصورت اتوماتیک توسط دستور Call روی پشته قرار میگیره.
Old EBP هم که روی پشته قرار گرفته در حقیقت EBP قاب پشته فراخواننده (قاب پشته تابعی که این تابع رو فراخوانی کرده) هستش.



دستورات اصلی کار با پشته push  و pop هستن که به ترتیب برای قرار دادن عنصری روی پشته و برداشتن عنصری از پشته استفاده میشند.
 ولی به جز این دو دستور , دستورات و روشهایی هم برای اینکار وجود داره مثلا:

Mov eax, ss:[esp]


این دستور همانند دستور pop eax هستش.

دستورات pusha (برای رجیسترهای 16 بیتی) و pushad (برای رجیسترهای 32 بیتی)
این دو دستور تمام رجیسترها را روی پشته قرار میدن
 (EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI )
دستورات متقابل اینها هم popa و popad هستن.




قرارداد های فراخوانی:

قراردادهای فراخوانی, روش فراخوانی تابع رو تعیین می کنن.
3 تا قرارداد برای قرار گرفتن پارامترها در پشته یا رجیسترها وجود داره که مشخص میکنه:
آیا فراخواننده مسئول پاک کردن پشته هستش یا تابع فراخوانی شده, زمانی که تابع به اتمام میرسه.
این قراردادها به کامپایلر و دیگر مسائل هم وابسته هستن.
چگونگی پیاده سازی این سه قرار داد هم در کمپایلر ها متفاوت هستش.
ما از عبارت pseudocode برای نشاندادن هر قرار داد فراخوانی استفاده می کنیم.

این سه قرارداد عبارتند از:
cdecl, stdcall,  و  fastcall

کدی که قرار است بررسی کنیم:


int test(int x, int y, int z);

int a, b, c, ret;

 

ret = test(a, b, c);

cdecl :


یکی از رایجترین روش هاست.توی این روش پارامترها از راست به چپ روی پشته قرار میگیرند, فراخوانند مسئول پاک سازی (درست کردن) پشته هستش هنگامی که تابع به اتمام میرسه و مقدار بازگشتی داخل EAX نگهداری می شود.


کد دیس اسمبل شده توی این قرارداد:


push c
push b
push a
call test
add esp, 12
mov ret, eax

قسمت پررنگ شده نشون میده که فراخواننده مسئول پاک سازی پشته هستش.

 

Stdcall:

 

این روش مثل روش قبل هستش با این تفاوت که تابعی که فراخوانی شده مسئول پاک سازی پشته هستش زمانی که تابع به اتمام میرسه.ازینرو مشخصه که دستور پررنگ شده ی بالا دیگه نیاز نیست, چونکه خود تابع پشته رو پاک سازی میکنه.

این قرارداد مورد استفاده توابع API ویندوز هستش.

 

Fastcall:


این قرارداد در کامپایلرها متفاوت هستش, اما در کل مشابه هم هستن. توی این روش, ابتدا تعدادی از آرگومانها (بنوعی 2 تا) از طریق رجیسترهای رایج مثل  EDX و ECX فرستاده میشن و باقی آرگومانها از راست به چپ روی پشته بارگزاری میشن. درضمن تابع در حال فراخوانی مسئول پاک سازی پشته هستش البته درصورت لزوم.

 

 


 

Push vs. Move

بغیر از روش های بالا امکان داره تا کامپایلرها از دستورات متفاوتی برای انجام یک چنین عملیاتی استفاده کنند

 

تابع زیر دو آرگومان میگیره و مقدار برگشتی آن نتیجه محاسبه هستش.و تابع main این تابع را فراخوانی کرده و نتیجه را چاپ میکند:

int adder(int a, int b)
{
   return a+b;
}

void main()
{
   int x = 1;
   int y = 2;

   printf("the function returned the number %d\n", adder(x,y));
}

کد اسمبلی تابع adder در اکثر کامپایلر ها مشابه است.همانطور که میبینید کد زیر arg_0 رو به arg_4 اضافه میکنه و نتیجه رو در EAX نگهداری میکنه.


00401730        push    ebp
00401731        mov     ebp, esp
00401733        mov     eax, [ebp+arg_0]
00401736        add     eax, [ebp+arg_4]
00401739        pop     ebp
0040173A        retn


توی جدول زیر, تفاوت میان calling convention مورد استفاده توسط دو کامپایلر متفاوت رو مشاهده میکنید.
Microsoft Visual Studio  و ( GNU Compiler Collection(GCC
سمت چپ, پارامترهای addr و printf قبل از فراخوانی روی پشته با دستور push قرار داده شدن. در سمت راست, پارامترها قبل از فراخوانی با دستور mov به پشته انتقال داده شدن.
شما باید بعنوان یک آنالیزر, برای هر دو روش آمادگی لازم رو داشته باشید
چونکه ما کنترلی روی کامپایلرها نداریم



نظرات (۰)

هیچ نظری هنوز ثبت نشده است
ارسال نظر آزاد است، اما اگر قبلا در بیان ثبت نام کرده اید می توانید ابتدا وارد شوید.
شما میتوانید از این تگهای html استفاده کنید:
<b> یا <strong>، <em> یا <i>، <u>، <strike> یا <s>، <sup>، <sub>، <blockquote>، <code>، <pre>، <hr>، <br>، <p>، <a href="" title="">، <span style="">، <div align="">
تجدید کد امنیتی