ساختار فایل های PE - لیوان اول - نحوه Resolve شدن Import Address ها

در این سری قراره با ساختار فایل های 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 این هدر رو داشتن و برای سازگاری فایل های اجرایی اونها با ویندوز، این هدر توی ویندوز هم هست.

فهرست این اطلاعات رو توی شکل بالا میبینید. اما چندتا موردش که برامون اهمیت داره رو بررسی میکنیم:

  • MZSignature - دو بایت (WORD) اول هدر یا کل فایل، که همیشه برابر “MZ” هست و این یه نشونه تشخیص فایل های Exe هست.
  • E_lfanew آخرین فیلد توی هدر DOS هست که به آفست NT Header اشاره میکنه (جلوتر توضیحش میدم). همینطور که توی شکل میبینید اندازه اش Long یعنی 4 بایت هست. اسمی که 010 Editor برا این فیلد انتخاب کرده، AddressOfNewExeHeader هست. یعنی هدر Exe های جدید (بعد DOS)
  • 168 بایت بعدی (برای برنامه های مختلف ممکنه متفاوت باشه)، یعنی از 0x40 تا 0xE8، اسمش DOS Stubهست. یعنی یک قطعه کد قابل اجرا توی DOS. برای نرم افزار های ویندوزی که بصورت عادی کامپایل میکنیم، توسط کامپایلر یه کد پیشفرضی اینجا قرار میگیره که موقع اجرا توی DOS، فقط یه متنی رو چاپ میکنه که "This program cannot be run in DOS mode".
  • NTHeader هدر اصلی فایل های PE بعد از MS-DOSهست که آفست اون ثابت نیست (چون اندازه DOS Stub متغیره) و آدرسش توی E_lfanew ذخیره میشه. طبق تصویر (ستون type) میبیند NTHeader یک Struct از نوع struct IMAGE_NT_HEADERS هست. توی سری های بعدی از اینا استفاده میکنیم و یه پارسر برای فایل های Exe مینویسیم. NTHeader خودش سه تا قسمت داره:
  • Signature – چهار بایت (Double WORD) ابتدای این هدر که برابر 0x4550هست. بصورت اسکی، میشه PE.
  • File Header – خودش یک ساختار از نوع struct IMAGE_FILE_HEADER هست که جلوتر وارد ساختارش میشیم.
  • Optional Header - خودش یک ساختار از نوع struct IMAGE_FILE_OPTIONAL_HEADER هست که اینم جلوتر بررسی میکنیم.

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

  • Machine – پردازنده ای که برنامه براش کامپایل شده. این فیلد از نوع enum IMAGE_MACHINE هست. میخوام دقیق تر نوع این فیلد رو بررسی کنم. روی گزینه Machine راست کلیک میکنم و گزینه Goto Type Definitionرو انتخاب میکنم. توی تصویر میبیند که تمام مقادیری که enum IMAGE_MACHINE میتونه داشته باشه توی Exe.btتعریف شده:
  • Number Of Sections – تعداد (از نوع WORD) Section های برنامه که توی یک قسمت دیگه فقط روی این مورد تمرکز میکنیم.
  • TimeDateStamp – تاریخ ایجاد فایل (معمولاً کامپایل)
  • SizeOfOptionalHeader – از اونجاییکه اندازه Optional Header متغیره، اندازه اش اینجا بصورت WORD قرار میگیره.

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

  • اولین مورد توی این هدر، Magic Number هست که نشون میده فایل 32 بیتی هست یا 64 بیتی. تصویر سمت راست binary template مربوط به 010 editor هست و تصویر سمت چپ از مستند مایکروسافته.
Microsoft Document
Microsoft Document
  • AddressOfEntrypoint – آدرس اولین دستور یا instructionای رو نشون میده که موقع اجرای برنامه قراره اجرا از اونجا شروع بشه.
  • ImageBase – این آدرس به ما میگه که موقع اجرای برنامه، وقتی که Memory Manager کرنل ویندوز یه Virtual Memory برای Process ما ساخت، خود فایل اجرایی برنامه، کجای Virtual Memory باید لود (اصطلاحاً Map) بشه. معمولا این آدرس بصورت پیشفرض برای فایل های exe 0x400000 و برای فایل های DLL هم 0x10000000 هست.
  • BaseOfCode – آدرس جایی که Code section شروع میشه، البته بصورت RVA.

بیاید باهم 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 فقط برای برنامه خودمون غیرفعال کردیم، برای اونها هنوز فعاله)

  • FileAlignment و SectionAlignmentرو توی یه قسمت دیگه بررسی میکنیم.
  • قسمت آخری که بسیار هم مهمه، DataDirectory ها هست. این فیلد یه آرایه از Directory ها هست. Directory چیه؟ هر Directory خودش یه بخش مهم دیگه ایه که حاوی اطلاعات طبقه بندی شده ای از ویژگی های فایل هست. تعداد کل Directory ها دقیقاً 16 تاست و هر کدوم یه ساختار از نوع IMAGE_DATA_DIRECTORY هست که ساختارشو توی تصویر میبینید. حاوی دوتا فیلد، Virtual Address و Size (که هرکدوم از نوع DWORD هستن، روی هم 8 بایت. پس اندازه کل DataDirArray میشه 16*8 = 128 یا 0x80 که توی تصویر میبینید). Virtual Address اشاره به آدرس مجازی Table (البته لزوماً همه Directoryها از نوع Tableنیستن) اصلی حاوی اطلاعات هست.

توی ستون آخر Type هر کدوم از Data Directory ها مشخصه.

برای نمونه چندتا از Directory های بالا رو بیاید بررسی کنیم.

  • اولی Export هست که یه جدول از نوع IMAGE_DIRECTORY_ENTRY_EXPORT هست. این جدول حاوی فهرست تمام توابعیه که قراره Exportو توسط بقیه فایل ها استفاده بشن. اما دقت کنید. مقدار VirtualAddress و Size هردو صفر هست. این به این معناست که این فایل، اصلا Export Table نداره. یعنی اصلا چیزی Export نکرده.
  • دومی Import هست که این هم یه جدولی هست که نوعش توی تصویر معلومه، و حاوی تمام DLL هایی هست که برنامه قراره در طول اجرا ازشون استفاده کنه. پس ویندوز از این جدول قراره متوجه بشه که چه DLLهایی رو باید Loadکنه توی حافظه.
  • بعدی Import Address Table هست که آدرس توابعی که از هرکدام از DLL های توی جدول Import استفاده شده، توی این جدول نوشته میشه.
  • آخرین Directory هم Reserved هست که مقادیر اون همیشه صفر هست و در واقع Terminating Entry یعنی نشونه اتمام 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 ازش استفاده میکنه ببینیم:

تابع RVA2FOA از فایل EXE.bt
تابع RVA2FOA از فایل EXE.bt

میبینید که ابتدا داره VA مربوط به Section ای که Import Descriptor توش قرار میگیره رو پیدا میکنه.

هر Section، یک PointerToRawData داره که نشون میده اون Section توی فایل مون کجا قرار میگیره.

توی فرمول مشخصه که اختلاف RVA رو با آدرس مجازی Section ای که Descriptor توش قرار میگیره رو به آدرس Section توی فایل اضافه میکنه و اینطوری FOA بدست میاد.

توی تصویر قبل میبینید که خود ادیتور محاسبه کرده و توی ستون کامنت ها نوشته: .rdata FOA = 0x1D64

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

  • Name - اسم DLL ای هست که Import کرده ولی بشکل یک پوینتر RVA. توی کامنت میبینیم نوشته .rdata FOA = 0x21C2 -> MSVCP140.dll. هم FOA اش رو نوشته و اینکه توی بخش rdataقرار گرفته.

میبینیم که درسته و توی 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 فایل بود. قراره این سری ادامه دار باشه!

و چندتا لینک خوب: