در این سری قراره با ساختار فایل های PE آشنا بشیم و به عنوان نمونه نحوه Resolveشدن آدرس Symbolها در DLL ها رو بررسی کنیم. بزارید خود مساله رو بیشتر بازش کنم:
میدونیم DLL ها یا Dynamic Link Library ها کتابخونه هایی هستن که بصورت Static (مثل یسری کتابخونه هایی که توسط لینکر بعد از کامپایل، پیوست خود برنامه میشن) به برنامه link نشدن و قراره ویندوز زمانی که برنامه رو اجرا میکنه، فایل DLL رو هم لود کنه و بیاره تو فضای حافظه پروسس برنامه مون، تا برنامه بتونه تکه کد هایی رو ازش فراخوانی کنه.
نکته اینجاست که توی فضای حافظه، DLL ها همیشه توی یک آدرس مشخصی لود نمیشن. حالا فرض کنید برنامه شما قراره یه جا یک تابع از داخل یکی از این DLL ها، مثلاً Kernel32 فراخوانی کنه. پس باید در سطح اسمبلی یک دستور Call یا Jmpبه همراه آدرسی از حافظه که قراره فراخوانی بشه، نوشته بشه. اما از اونجا که این DLL ها مکان از قبل مشخص شده ای ندارن، چه آدرسی اینجا قرار میگیره؟ در کل چطوری آدرس اون تابع رو پیدا و اون رو فراخوانی میکنه؟
در مقاله امروز قراره با بررسی ساختار فایل های PE، در انتها این سوال رو به راحتی خوردن یک لیوان شیرکاکائو جواب بدیم:

قبل اینکه بریم سراغ اصل ماجرا، باید بگم که توی این مقاله از مفاهیمی استفاده شده که لازمه اونارو بلد باشید تا بتونید مطلب رو کامل متوجه بشید. از جمله:
- Virtual Address (VA)
- Relative Virual Address (RVA)
- Address Space Layout Randomization (ASLR)
اول از همه باید بدونیم که اصلی ترین و کامل ترین Document (از این به بعد بهش میگم مستند) مربوط به ساختار فایل های PE، مستند خود مایکروسافت هست:
https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#general-concepts
اما از اونجایی که هدف ما بیشتر مسلط شدن به ساختار فایل های PE و تحلیل و بررسی اونها هست، میخوایم از ابزار های مختلفی استفاده کنیم تا خودمون بخشی از ماجرا رو کشف کنیم. ابزارهایی از جمله نرم افزار 010 Editor که در ادامه توضیح میدم که از هر سر انگشت این نرم افزار یه قابلیت میباره که کار مارو در تحلیل ساختار فایل های مختلف بسیار آسون تر میکنه.
برای ادامه آنالیزمون یک برنامه Hello World ساده کامپایل کردم و اون رو توی نرم افزار 010 Editor باز کردم.

موقع باز کردن این پنجره رو میبینید که میگه Binary Template مربوط به Exe رو نصب کنید. اما Binary Template(bt) ها چی هستن؟ Binary Template ها یکسری اسکریپت هایی هستن که دقیقاً کاری که ما میخوایم بصورت دستی انجام بدیمو انجام میدن. وقتی اسکریپت مربوط به exe رو روی فایل خودمون اجرا کنیم، ساختار فایل رو بصورت Parse شده بشکل زیر بهمون نشون میده:

خب حالا بریم سراغ چینش اطلاعات توی فایل:
64 بایت اول فایل DOS Header هست. این هدر از MS-DOS به یادگار مونده، یعنی فایل های اجرایی MS-DOS این هدر رو داشتن و برای سازگاری فایل های اجرایی اونها با ویندوز، این هدر توی ویندوز هم هست.
فهرست این اطلاعات رو توی شکل بالا میبینید. اما چندتا موردش که برامون اهمیت داره رو بررسی میکنیم:


اما بیاید File Header رو اول بررسی کنیم که حاوی چه اطلاعاتی هست:


بریم سراغ Optional Header. این هدر برای فایل های Exe واقعا Optionalیا اختیاری نیست و بودنش لازمه. اختیاری بودنش تو اسم برا اینه که Object File ها نیازی به این هدر ندارن.



بیاید باهم ImageBase و BaseOfCode رو بررسی کنیم. برای همین فایل اجرایی رو توسط IDA باز میکنم. حالا فایل اجرایی ما روتوی VA نشون میده و آدرس ها همه مجازی ان:

میبینید که اولین جایی که کد ما، یعنی اولین دستور قسمت Code، که "sub rsp,28h;" هست از آدرس 0x140001000شروع شده. یعنی ImageBase+BaseOfCode. (دقت کنید اطلاعات قبل دستور sub همه کامنت ان که توسط خود IDA اضافه شده)
برنامه رو یکبار برای debug اجرا میکنم. میبینید که آدرس ها همه تغییر کردن و دیگه آدرس اولین دستور ما 0x140001000 نیست و به یه عدد تصادفی دیگه ای تغییر کرد:

اما چرا؟
دلیلش اینه که قابلیت ASLR برای فایل اجرایی مون فعاله. این قابلیت که یک Mitigationهست باعث میشه بجای اینکه فایل ما دقیقاً توی ImageBase لود بشه، توی یه فضای تصادفی دیگه از حافظه بارگزاری بشه.
برای همین یکبار دیگه برنامه رو با غیرفعال کردن قابلیت ASLR کامپایل میکنم. برای اینکار توی Visual Studioاز قسمت Project -> Properties -> Linker -> Advanced -> Randomized Base Addressو مقدارش رو روی Noقرار میدم. حالا اگر برنامه رو دوباره اجرا کنید میبیند آدرس برنامه در حافضه مجازی تغییری نکرده (هرچند آدرس مجازی بقیه DLL هایی که لود میشن باز تصادفی انتخاب میشه، چون ما ASLR فقط برای برنامه خودمون غیرفعال کردیم، برای اونها هنوز فعاله)

توی ستون آخر Type هر کدوم از Data Directory ها مشخصه.
برای نمونه چندتا از Directory های بالا رو بیاید بررسی کنیم.
بریم سراغ بررسی Import Table. توی تصویر میبیند که آدرس مجازیش 0x2964هست. یعنی تو IDA باید تو آدرس ImageBase + 0x2964 یعنی 0x140002964 باید ببینیمش:

درسته، این آدرس ساختار IMAGE_IMPORT_DESCRIPTOR هست برای MSVCP140.dll، یعنی اولین خونه از جدول Import Table. ساختار مربوط به بقیه DLLها هم به ترتیب بعدش اومده. مشخصه اندازه هرکدوم 0x14 هست.
این آدرس مجازی این Descriptor ها توی حافظه بود. اما چطوری میتونیم آدرس اون ها رو توی خود فایل بدست بیاریم؟
ما الان آدرس RVA مربوط به اولین IMPORT_DESCRIPTOR رو داریم. برای اینکه آدرس اون رو توی فایل بدست بیاریم چیکار کنیم؟ به این آدرس FOA یا File Offset میگن.
میتونیم نحوه محاسبه شو توی Exe.bt یعنی اسکریپتی که خود 010 Editor ازش استفاده میکنه ببینیم:

میبینید که ابتدا داره VA مربوط به Section ای که Import Descriptor توش قرار میگیره رو پیدا میکنه.
هر Section، یک PointerToRawData داره که نشون میده اون Section توی فایل مون کجا قرار میگیره.
توی فرمول مشخصه که اختلاف RVA رو با آدرس مجازی Section ای که Descriptor توش قرار میگیره رو به آدرس Section توی فایل اضافه میکنه و اینطوری FOA بدست میاد.
توی تصویر قبل میبینید که خود ادیتور محاسبه کرده و توی ستون کامنت ها نوشته: .rdata FOA = 0x1D64


ببینید این دقیقاً همون Import Descriptor ای هست که ما دنبالش بودیم (خود ادیتور پارس کرده).

میبینیم که درسته و توی 0x21C2 اسم DLLمون قرار گرفته. توی IDAهم اگر روی dd rva aMsvcp140 کلید اینتر رو بزنید شمارو میبره به آدرسی از حافظه مجازی که Name بهش اشاره میکنه.

آیتم مهم بعدی برای ما First Thunk هست. این فیلد یه RVA هست به یه آرایه از تمام Importهایی که از این DLLانجام شده. هر خونه آرایه دقیقاً 8 بایت هست که خودش دوباره یه RVA به اسم اون تابع یا symbolای که importشده.
مثلاً بریم توی FOA فیلد First Thunk از Kernel32.dll. توی قسمت Comment خودش محاسبه کرده: 0x1400

میبینید که هر 8 بایت حاوی یه پوینتره، اما یکی از این 8 بایت ها صفر هست که این نشون دهنده انتهای آرایه هست.
بعدش آرایه بعدی شروع میشه که مربوط به Import Descriptor بعدی هست.
یکی از این پوینتر های RVA رو برای نمونه تبدیل به FOA میکنم تا ببینیم چه خبره، مثلاً اولی:
0x30CA – 0x2000 + 0x1400 = 0x24CA

میبینیم که درسته و رسیدیم به اسامی توابع Kernel32. 0x24CA دقیقاً بایتی هست که انتخاب شده.
اما یه مشکلی هست!!
قبل از اسم RtlCaptureContext که اسم تابع Kernel32 هست، دوتا بایت اضافه است!
درسته، چون هر Entry فقط یه اسم نیست! یه ساختاری هست به این شکل:
struct IMAGE_IMPORT_BY_NAME { short Hint; char Name[1]; }
که اولین فیلدش از نوع short یعنی 2 بایت هست. ولی کاربردش چیه؟ در ادامه میگم.
حالا رسیدیم به اسامی تک تک توابع و البته کلی تر، Symbol هایی که فایل اجرایی مون داره از DLLهای مختلف Importمیکنه.
اما هنوز مشکل داریم!!!
پس آدرس خود توابع کجاست؟
اینا که فقط اسامی توابعه، پس برنامه از کجا میفهمه که خود تابع کجاست تا اجراش کنه؟!
مگه نگفتیم که DLL ها همیشه توی یک آدرس مشخصی لود نمیشن؟ پس ما از قبل نمیتونیم آدرس Symbol هارو مشخص کنیم. پس طبیعیه آدرسی از توابع توی فایل نباشه.
پس برنامه ما چطوری باید توابعی که Import کرده رو پیدا کنه؟
باید بگم که برنامه ما نیست که این آدرس هارو پیدا میکنه! ویندوز قراره این آدرس هارو بدست بیاره یا اصطلاحاً Resolve کنه.
اگر دوباره یه نگاهی به IMAGE_IMPORT_DESCRIPTOR بندازید، میبینید یه پارامتر دیگه هست به اسم OriginalFirstThunk. میبینیم که مقدارش با FirstThunk متفاوته، یعنی هرکدوم به یه آدرس متفاوت اشاره میکنه. اما اگر به آدرس FOA اون توی فایل بریم میبینیم که اون هم یه آرایه دقیقاً مثل FirstThunk هست و حتی مقادیرشونم باهم یکیه!
خب چرا یه دسته اطلاعات مشابه رو دو جا نوشته؟!
چون کاربرد اینا متفاوته! OriginalFirstThunk به آرایه ای اشاره میکنه که بهش Import Lookup Table یا ILTمیگن، و FirstThunkبه آرایه ای اشاره میکنه (هر چند هر دو مقادیرشون یکیه) که بهش Import Address Tableگفته میشه.
ویندوز موقع اجرای برنامه ها، ابتدا DLL رو توی حافظه لود میکنه، بعد یه Linker داره که به ازای تک تک Import Symbol ها قراره آدرس اونهارو Resolve کنه و توی حافظه بنویسه. چطوری Resolve میکنه؟
همونطور که از اسم Import Lookup Table مشخصه، از این جدول استفاده میکنه و با استفاده از اسم هر Symbol، توی DLL مربوطه، توی بخش Export Directory(بالاتر درموردش صحبت کردیم) دنبال اون Symbol میگرده. بیاید Kernel32.dll رو توی 010 Editor باز کنیم:

میبینید توی Export Directory یه فیلد داره به اسم AddressOfNames که یه پوینتر RVA به اسامی Function ها هست. مسیر رو خلاصه میکنیم و شما پایین تر میبینید که یه تعداد ساختار IMAGE_EXPORT_BY_NAME هست که هر کدوم دوباره یه رشته کاراکتر حاوی اسم Function مربوطه هست. بصورت موازی یه آرایه دیگه هست که به ازای هر اسم اشاره میکنه به آدرس خود اون تابع که در نهایت قراره از اون استفاده بشه.
اما آیا لینکر ویندوز تک به تک توی اسامی این توابع میگرده تا هر کدوم از Symbolهایی که میخواد رو پیدا کنه؟ این که خیلی طول میکشه!!
باید بگیم در بدترین حالت بله!
اما اون فیلد short Hint که توی ساختار IMAGE_IMPORT_BY_NAME بود قراره کمک کنه تا حتی الامکان این کار سریع تر انجام شه! چطوری؟
این فیلد طبق اسمش راهنمایی میکنه که این تابع توی جدول Export های DLL مورد نظر، احتمالاً چندمین مورد هست. ولی چون ممکنه توابع جدیدی به فایل DLL اضافه بشه و فایل DLL به مرور زمان تغییر کنه، ممکنه این Hint یه زمانی دیگه درست نباشه! پس ویندوز اول Hint رو بررسی میکنه و اگر اسم تابعی که دنبالش میگرده با اسم اون تابع توی جدول Export یکی نباشه، ناچار باید فرآیند جستجو کردن تابع رو انجام بده.
اما در انتها ...
وقتی که لینکر آدرس هارو پیدا کرد، دیگه دست به ILT نمیزنه! و اونهارو توی Import Address Table قرار میده.
.
.
حالا زمانی که برنامه در حال اجراست، میتونه اطمینان داشته باشه که توابعی که میخواد آدرسشون توی IAT هست.

سعی داشتم توی یک سناریو بخشی از اطلاعاتی که توی هدر و بخش های دیگه PE فایل ها بود رو توضیح بدم، اما این فقط بخشی از اطلاعات یک PE فایل بود. قراره این سری ادامه دار باشه!
و چندتا لینک خوب: