PE Mimarisi
PE Nedir?
PE(Portable Executable) yani taşınılabilir yürütülebilir dosyalar Windows sistemler arasında uyumluluk sorunu yaşamadan taşınıp çalıştırılabilen dosyalardır. Taşınabilir olması için tüm cihazlar için ortak bir dil/mimari tanımlanması gerekmektedir, bir cihazda “A” anlamına gelen veri diğer cihazda da “A” anlamına gelmelidir. Burada da ortaya bir mimari çıkıyor. Örneğin bir taşınabilir dosyanın 0x24 adresinde ImageBase verisinin taşındığı bütün cihazlar tarafından bilinmektedir ve ona göre yorumlanıp dosya çalıştırılmaktadır. Bu yazıda genel hatlarıyla bir PE yapısında neler bulunduğuna ve bunların ne anlamlara geldiğine bakılacaktır.
“Portable Executable (PE) biçimi, Windows işletim sistemlerinin 32 bit ve 64 bit sürümlerinde kullanılan yürütülebilir dosyalar, DLL’ler vb. için bir dosya biçimidir. PE biçimi, Windows işletim sistemi yükleyicisinin kapsüllenmiş yürütülebilir kodu yönetmesi için gerekli bilgileri içeren bir veri yapısıdır. Buna; bağlantı için dinamik kitaplık referansları, API dışa aktarma(export table) ve içe aktarma (import table) tabloları, kaynak yönetimi verileri(resources) ve iş parçacığı yerel depolama (TLS) verileri dahildir. NT işletim sistemlerinde PE formatı EXE, DLL, SYS (cihaz sürücüsü), MUI ve diğer dosya türleri için kullanılır.”[1]
Yukarıdaki görselde bir PE yapısının detaylı yapısı Ange Albertini [2] tarafından görselleştirilmiştir. Günümüzde çeşitli araçlar bu yapıyı analiz edip uygun çıktıyı analistlere sunmaktadır. PEBear, CFF Explorer, PEStudio vb. bu araçlara örnek olarak verilebilir. Peki buradaki bilgiler bizim için neden önemlidir ? İlerleyen kısımlarda bahsedeceğimiz “Raw Size ve Virtual Size arasındaki fark yüksek olursa ne anlama gelmektedir?” gibi konularda bize yol göstermektedir ve belirli kanıtlar sunmaktadır.
Virtual Adress (VA) vs Relative Virtual Adress (RVA)
Virtual Adress, bir uygulamaya atanmış hafıza adresidir. Cihaz üzerinde çalıştırılan uygulamalar direkt olarak fiziksel hafızaya erişemez, yalnızca sanal olarak oluşturulan hafızaya erişebilir. Cihazların RAM’lerinin yönetimi için oluşturulan bu sanallaştırma yapısı uygulamalara esneklik sağlayarak kendi hafızalarını yönetme konusunda kolaylık sağlar ve aynı zamanda fiziksel hafıza üzerinde güvenlik sağlar.
Relative Virtual Adress, iki Virtual Adress arasındaki farktır. Virtual Adress hafıza üzerindeki uygulamanın gerçek adresini gösterir fakat Relative Virtual Adress Image Base’i referans alır. Yani şu denklem karşımıza çıkar => RVA= VA - Image Base. Örnek vermek gerekirse; Uygulamamızın Image Base’i 0x400000 (genellikle user mode uygulamaların default Image Base adresidir) olsun, Virtual Adress’imiz de 0x00401000 olsun. Bu durumda RVA değerimiz 0x00001000 olmaktadır.
DOS Headers
DOS Header 64 byte boyutundaki bir veri yapısıdır ve çalıştırılabilir bir dosyanın en başında bulunur. Dosyanın işlevini etkilemeyen bu kısım dosyanın uyumluluk sorunu oluşturmaması için oluşturulmuştur. MS-DOS üzerinde çalıştırıldığında gerçek program yerine MS-DOS Stub mesajı olan “This program cannot be run in DOS mode” yazısı gösterilmektedir. Bu header olmadan herhangi bir PE dosyası çalışmayacaktır. 64 byte boyutundaki veri içeriği şu şekildedir:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
[3]
Burada bizim için önemli 2 adet değer bulunmaktadır;
- e_magic: DOS Header’ının ilk elemanıdır ve sabit olarak 0x5A4D (MZ) değerine sahiptir. Bu dosyanın bize PE dosyası olduğunu ifade etmektedir.
- e_lfanew: DOS Header’ının son elemanıdır ve NT Headerlarının başlangıç adresini tutmaktadır.
COFF File Header
Bir PE dosyasının başlangıcında veya imzasının hemen ardından gelen kısımdır.
Offset | Boyut | İsim | Açıklama |
---|---|---|---|
0 | 2 | Machine | Hedef cihazın tipini belirtir. (Bkz. Machine Types) |
2 | 2 | NumberOfSections | İmaj üzerindeki section boyutu |
4 | 4 | TimeDateStamp | Dosyanın ne zaman oluşturulduğunu gösteren 00:00 1 Ocak 1970’ten bu yana geçen saniye sayısının low (sağ) 32 biti. |
8 | 4 | PointerToSymbolTable | COFF sembol tablosunun dosya ofseti veya COFF sembol tablosu mevcut değilse sıfır. COFF hata ayıklama bilgisi kullanımdan kaldırıldığı için bu değer bir görüntü için sıfır olmalıdır. |
12 | 4 | NumberOfSymbols | Sembol tablosundaki entry sayısı. Bu veriler, sembol tablosunu hemen takip eden string tablosunu bulmak için kullanılabilir. COFF hata ayıklama bilgisi kullanımdan kaldırıldığı için bu değer bir görüntü için sıfır olmalıdır. |
16 | 2 | SizeOfOptionalHeader | OptionalHeader kısmının boyutunu belirtir. |
18 | 2 | Characteristics | Dosyanın özelliklerini belirten değerlerini içeren kısım |
[4]
File Characteristics kısmındaki değerler ise şu şekildedir:
Optional Headers
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
WORD 16 bit (2 byte) işaretsiz sayı, DWORD 32 bit (4 byte) işaretsiz sayıyı ifade etmektedir.
Genel olarak önemli veriler şunlardır:
Magic: İmaj dosyasının türünü belirtir. 0x010B ise 32-bit, 0x020B ise 64-bit imaj dosyası olduğunu belirtir.
SizeOfCode: Çalıştırılabilir kodların toplam boyutunu belirtir.
SizeOfInitializedData: Tanımlanırken değer atanmış verilerin bulunduğu kısmın boyutunu belirtir.(Genelde .data sectionında bulunurlar)
SizeOfUninitializedData: Tanımlanırken değer atanmamış verilerin bulunduğu kısmın boyutunu belirtir.(.bss sectionında bulunurlar)
AddressOfEntryPoint: Bu değer çalıştırılabilir dosyaların Entry Point(başlangıç noktası) Relative Virtual Adress değerini, DLL dosyalarının ise DllMain fonksiyonunun Relative Virtual Adress değerini tutar. Genellikle .text sectionında bir yer belirtir. Sectionlarda ileride bahsedeceğiz fakat buraya şöyle ufak bir not düşülebilir: Text section’ı dışında bir noktadaki EntryPoint şüphelidir!.
ImageBase: PE formatının yükleneceği adresin değerini tutar. Genel olarak 32-bit mimari için çalıştırılabilir dosyalar için bu kısımda 0x00400000 değeri bulunur. Derleyicinizin Linker ayarlarından bu kısım değiştirebilirsiniz.
[5]
Section Table
Section tanımlamalarının yapıldığı kısımdır. Optional Header’dan hemen sonra bulunur (varsa) çünkü headerlarda bu kısmı işaret eden bir veri yoktur. Section tablosunun konumu headerların bittiği yerde başlamaktadır.
Bu tablodaki bulunan section sayısı File Header kısmında belirtilen NumberOfSections değerinde saklanmaktadır. Her bir section header’ı (tablonun her bir entry değeri) 40 byte’tır ve aşağıdaki şekildedir:
Offset | Boyut | İsim | Açıklama |
---|---|---|---|
0 | 8 | Name | 8 byte, UTF-8 ve boş karakterler NULL olarak doldurulan bir değerdir. 8 byte’tan uzun isimler için slash (/) karakteri ve sonrasında string tablosundaki bu ismin string değerinin saklandığı offset değeri içerir. |
8 | 4 | VirtualSize | Section hafızaya yüklendiğindeki boyutu. Bu değer SizeOfRawData değerinden büyükse section’ın boş kısımları 0’lar ile doldurulmuştur. |
12 | 4 | VirtualAdress | Çalıştırılabilir dosyalar için section hafızaya yüklendiğinde Image Base ile relatif olan adresin değerini tutar. |
16 | 4 | SizeOfRawData | Yürütülebilir imajlar için bu, Optional Header’da bulunan FileAlignment’ın katı olmalıdır. Bu, VirtualSize’dan küçükse, bölümün geri kalanı sıfırla doldurulmuştur. SizeOfRawData alanı yuvarlanır fakat VirtualSize alanı yuvarlanmadığından, SizeOfRawData’nın VirtualSize’dan büyük olabilir. Bir bölüm yalnızca başlatılmamış(uninitialized) veriler içeriyorsa, bu alan sıfır olmalıdır. |
20 | 4 | PointerToRawData | COFF (Common Object File Format) dosyası içindeki sectionın ilk sayfasının pointer değeri. Çalıştırılabilir dosyalar için bu Optional Header’daki FileAlignment’ın katı olmalıdır. Bir section yalnızca başlatılmamış(uninitialized) veriler içeriyorsa, bu alan sıfır olmalıdır. |
24 | 4 | PointerToRelocations | Section için relocation entry değerlerinin başlangıcının pointer değeri. Çalıştırılabilir dosyalar için veya yer değiştirme yoksa bu değer sıfırdır. |
28 | 4 | PointerToLinenumbers | Section için satır numarası girişlerinin başının pointer değeri. COFF satır numarası yoksa bu değer sıfırdır. COFF hata ayıklama bilgisi(debug information) kullanımdan kaldırıldığı için bu değer bir görüntü için sıfır olmalıdır. |
32 | 2 | NumberOfRelocations | Section için relocation entry’lerinin sayısı. Çalıştırılabilir dosyalar için bu değer sıfırdır. |
34 | 2 | NumberOfLinenumbers | Section için satır numarası entry’lerinin sayısı. COFF hata ayıklama bilgisi(debug information) kullanımdan kaldırıldığı için bu değer bir dosya için sıfır olmalıdır. |
36 | 4 | Characteristics | Bölümün izinlerinin bulunduğu kısımdır. Bu değerler için Bkz. Microsoft Dokümantasyon |
[6]
Sections
Sectionlar dosyanın asıl verilerini içeren kısımlardır. Section Header kısmından hemen sonra başlamaktadır. Çalıştırılabilir kodlar, değişkenler, dosya hakkında bilgiler vs. bu kısımlarda bulunur.
Harici olarak section tanımlanabilir fakat özel amaçlı ön tanımlı sectionlar vardır. Bu özel sectionlara Microsoft Dokümantasyonundan ulaşabilirsiniz. Önemli sectionlardan bazıları ise şunlardır :
- .text: Dosyanın çalıştırılabilir kodlarının bulunduğu kısımdır.
- .data: Kod içerisinde tanımlanmış(initialize) verileri içerir.
- .bss: Kod içerisinde tanımlanmamış(uninitialized) verileri içerir.
- .rdata: Read-only tanımlanmış(initialize) verileri içerir.
- .edata: Export tablosunu içerir.
- .idata: Import tablosunu içerir.
- .rsrc: Dosya tarafından kullanılan kaynakları (icon, fotoğraf, gömülü binaries) içerir.
- .tls: Thread Local Storage, threadler için saklama alanı sağlar.
Örnek
Örnek bir dosya içerisinde import edilen DLL ve bu DLL’ler içinden import edilen fonksiyonların nasıl bulunacağına bakalım.
Öncelikle Optional Header’ın son değeri olan Data Directories içerisinden Import Directory RVA değerine bakıyoruz. Bu değer bize dosyanın hafızaya yüklendiğinde Import tablosunun adresini vermekte. Örneğin program hafızanın 0x0040000 adresine yüklendi, Import Directory’miz 0x004< ImportDirectoryRVA > adresinde bulunuyor. Fakat dosya içerisinde ( file offset ) bu tabloyu nasıl bulabiliriz ?
Buradaki değerin hangi section içerisinde olduğunu bulmamız gerekiyor. Bunun için sectionların Virtual Offset değerlerine bakıyoruz.
- Yeşil kutu içindeki değer Name
- Kırmızı kutu içindeki değer Virtual Offset
- Sarı kutu içindeki değer Raw Offset içermektedir.
Burada karşımızda Section Table yapısı çıkmakta. Burada 12. offset değeri bu section’ın Virtual Adress’ini ifade etmekte. Yani buradaki sectionlar şu şekilde olmaktadır:
İsim | Raw Offset | Virtual Offset |
---|---|---|
.text | 0x400 | 0x1000 |
.rdata | 0x1200 | 0x2000 |
.data | 0x1E00 | 0x3000 |
.rsrc | 0x2000 | 0x4000 |
.reloc | 0x2200 | 0x5000 |
Bizim bakmamız gereken şey ise Import Directory değerimiz hangi Virtual Offset aralığında? Değerimiz => 0x263C olduğu için buradan bizim Import tablomuzun .rdata section’ında olduğunu anlıyoruz. Burada kullanacağımız formül ise şu:
File Offset= Raw Offset + RVA - Virtual Offset örneğimize uygularsak;
File Offset= 0x1200 + 0x263C - 0x2000 => 0x183C , adrese baktığımızda ise bizi IMAGE_IMPORT_DESCRIPTOR struct yapısı karşılamakta.
public struct IMAGE_IMPORT_DESCRIPTOR
{
[FieldOffset(0)]
public uint Characteristics;
[FieldOffset(0)]
public uint OriginalFirstThunk;
[FieldOffset(4)]
public uint TimeDateStamp;
[FieldOffset(8)]
public uint ForwarderChain;
[FieldOffset(12)]
public uint Name;
[FieldOffset(16)]
public uint FirstThunk;
}
Buradaki struct yapısına baktığımızda her biri 20 byte’tan oluşmakta ve 12-16 arası bulunan byte’lar import edilen DLL’in isminin bulunduğu adresi içermekte, yukarıdaki görsele baktığımızda seçili kısımdaki 4. 4’lü byte değerimiz 0x27C6 olduğunu görüyoruz. Tekrar File Offset değerini hesaplamamız gerekmekte. RVA(0x27C6) + Raw Offset(0x1200) - Virtual Offset(0x2000) => 0x19C6.
Ve bu DLL içerisinden import edilen API’ların bulunduğu kısım ise OriginalFirstThunk kısmında bulunmakta yani IMAGE_IMPORT_DESCRIPTOR yapısındaki 0-4 arası byte değerlerine bakacağız. Örneğimizde bu değer 0x26F0 olmaktadır. Tekrar File Offset değerini hesaplamamız gerekmekte. RVA(0x26F0) + Raw Offset(0x1200) - Virtual Offset(0x2000) => 0x18F0.
Burada bulunan değerler ise IMAGE_THUNK_DATA struct yapısında bulunur.
[StructLayout(LayoutKind.Explicit)]
public struct IMAGE_THUNK_DATA32
{
[FieldOffset(0)]
public uint ForwarderString;
[FieldOffset(0)]
public uint Function;
[FieldOffset(0)]
public uint Ordinal;
[FieldOffset(0)]
public uint AddressOfData;
}
[StructLayout(LayoutKind.Explicit)]
public struct IMAGE_THUNK_DATA64
{
[FieldOffset(0)]
public ulong ForwarderString;
[FieldOffset(0)]
public ulong Function;
[FieldOffset(0)]
public ulong Ordinal;
[FieldOffset(0)]
public ulong AddressOfData;
}
Her bir API’ın ismi 4byte boyutunda RVA olarak saklanır, bir DLL’in thunk verileri bittiğinde ise 0x00000000 değeri görülür. Örneğin çağırılan 1 ve 2. fonksiyonlara bakalım. Öncelikle tekrar File Offset değerini hesaplıyoruz:
1. API için File Offest: RVA(0x27B8) + Raw Offset(0x1200) - Virtual Offset(0x2000) = 0x19B8
2. API için File Offest: RVA(0x2BB2) + Raw Offset(0x1200) - Virtual Offset(0x2000) = 0x1DB2
Bu şekilde tüm çağrılan DLL ve API’ları tespit edebiliriz.
Eleştiri/düzeltme/öneri için lütfen iletişim adreslerimden bana ulaşınız. Yorumlarınız benim için değerli :)
Referans
[1] en[.]wikipedia.org/wiki/Portable_Executable
[2] github[.]com/corkami/pics/tree/master/binary/pe101
[3] 0xrick[.]github.io/win-internals/pe3/
[4] learn[.]microsoft.com/en-us/windows/win32/debug/pe-format#coff-file-header-object-and-image
[5] 0xrick[.]github.io/win-internals/pe4/#optional-header-image_optional_header
[6] learn[.]microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers