1. 程式人生 > >PE檔案解析 基礎篇

PE檔案解析 基礎篇

PE檔案解析 基礎篇

來源 https://bbs.pediy.com/thread-247114.htm

前言

  • 之前學習了PE格式,為了更好的理解,決定寫一個類似LoadPE的小工具。
  • 編譯器是VS2015,採用MFC框架。
  • 此係列文章採用邊介紹知識點,邊寫程式碼的形式,以免變的無聊喪失興趣。
  • PE知識請參照《加密與解密》第10章
文章有錯誤或則不清楚的地方還請您指出。  

PE檔案格式

 

1.PE檔案基本概念

  • PE檔案是windows系統中遵循PE結構的檔案,比如以.exe   .dll為字尾名的檔案 以及系統驅動檔案。(PE結構框架看下圖)
PE檔案大體分為兩部分,頭(包括下圖中的DOS頭,PE檔案頭,塊表)與主體(塊),
 
  •  PE檔案從磁碟當中像記憶體中的對映,不是簡單的“1對1”的關係,而是“拉長”了。具體的位置表現在塊。 但是磁碟上的資料結構與在記憶體中的結構是一致的。
 

  • 無論PE檔案在磁碟中還是在記憶體中,都少不了地址的概念,理解一下幾個概念至關重要。
       虛擬地址(VA): 在一個程式執行起來的時候,會被載入到記憶體中,並且每個程序都有自己的4GB,這個4GB當中的某個位置叫做**虛擬地址**,由實體地址對映過來的,4GB的空間,並沒有全部被用到。        基地址( Imagebase ):       磁碟中的檔案載入到記憶體當中的時候可以載入到任意位置,而這個位置就是程式的基址。EXE預設的載入基址是400000h,DLL檔案預設基址是10000000h。需要注意的是基地址不是程式的入口點。        相對虛擬地址(RVA):為了避免PE檔案中有確定的記憶體地址,引入了相對虛擬地址的概念。RVA是在記憶體中相對與載入地址(基地址)
的偏移量,所以你可以發現前三個概念的關係 :  虛擬地址(VA)=   基地址+ 相對虛擬地址(RVA)        檔案偏移地址(FOA):當PE檔案儲存在某個磁碟當中的時候,某個資料的位置相對於檔案頭的偏移量。        入口點(OEP):首先明確一個概念就是OEP是一個RVA,,然後使用 OEP + Imagebase == 入口點的VA,通常情況下,OEP指向的不是main函式。    
  • 存了張圖 比較好的解釋了各部分的關係
    接下來依次介紹PE結構框圖的每個部分   2.DOS頭部 每個PE檔案都是以DOS頭開始的,IMAGE_DOS_HEADER 結構如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 (最左邊是檔案頭的偏移量。)  IMAGE_DOS_HEADER STRUCT  +0h  WORD     e_magic         //   MZ(4Dh 5Ah)     DOS可執行檔案標記  +2h      WORD     e_cblp             +4h  WORD     e_cp                          +6h  WORD     e_crlc                       +8h  WORD     e_cparhdr       +0ah     WORD     e_minalloc        +0ch     WORD     e_maxalloc   +0eh     WORD     e_ss            +10h     WORD     e_sp        +12h     WORD     e_csum       +14h     WORD     e_ip         +16h     WORD     e_cs         +18h     WORD     e_lfarlc        +1ah     WORD     e_ovno           +1ch     WORD     e_res[4]         +24h     WORD     e_oemid          +26h     WORD     e_oeminfo     +29h     WORD     e_res2[10]   +3ch     DWORD    e_lfanew      //  RVA     指向PE檔案頭  } IMAGE_DOS_HEADER ENDS
  需要關注的點是結構體的第一個和第二個元素。 e_magic:DOS頭的標記位,值為4D5Ah。ASCII為”MZ“,判斷一個檔案是否為PE檔案是會用 e_lfanew:這是一個RVA,代表了PE檔案頭到基址的偏移量,我們可以用它來找到PE檔案頭的位置。   我們用010editor開啟一個exe檔案      

3.PE檔案頭

  IMAGE_NT_HEADERS STRUCT  結構體
1 2 3 4 5 6 IMAGE_NT_HEADERS STRUCT  { +0h        DWORD     Signature   +4h       IMAGE_FILE_HEADER    FileHeader  +18h      IMAGE_OPTIONAL_HEADER32   OptionalHeader    } IMAGE_NT_HEADERS ENDS
   
  • Signature  欄位
在一個PE檔案中Signature欄位被設定為4550h,ASCII碼為”PE00“。如上圖所示。      
  • IMAGE_FILE_HEADER  結構體
1 2 3 4 5 6 7 8 9 10 struct  IMAGE_FILE_HEADER {      WORD  Machine;  //執行平臺      WORD  NumberOfSections;  //區塊表的個數      DWORD  TimeDataStamp; //檔案建立時間,是從1970年至今的秒數      DWORD  PointerToSymbolicTable; //指向符號表的指標      DWORD  NumberOfSymbols; //符號表的數目      WORD  SizeOfOptionalHeader; //IMAGE_NT_HEADERS結構中OptionHeader成員的大小,對於win32平臺這個值通常是0x00e0      WORD  Characteristics; //檔案的屬性值 }
  在010 Editor上檢視一下    
  • IMAGE_OPTIONAL_HEADER 結構體
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 typedef  struct  _IMAGE_OPTIONAL_HEADER {      //      // Standard fields.        // +18h     WORD     Magic;                    // 標誌字, ROM 映像(0107h),普通可執行檔案(010Bh) +1Ah     BYTE     MajorLinkerVersion;       // 連結程式的主版本號 +1Bh     BYTE     MinorLinkerVersion;       // 連結程式的次版本號 +1Ch     DWORD    SizeOfCode;               // 所有含程式碼的節的總大小 +20h     DWORD    SizeOfInitializedData;    // 所有含已初始化資料的節的總大小 +24h     DWORD    SizeOfUninitializedData;  // 所有含未初始化資料的節的大小 +28h     DWORD    AddressOfEntryPoint;      // 程式執行入口RVA +2Ch     DWORD    BaseOfCode;               // 程式碼的區塊的起始RVA +30h     DWORD    BaseOfData;               // 資料的區塊的起始RVA      //      // NT additional fields.    以下是屬於NT結構增加的領域。      // +34h     DWORD    ImageBase;                // 程式的首選裝載地址 +38h     DWORD    SectionAlignment;         // 記憶體中的區塊的對齊大小 +3Ch     DWORD    FileAlignment;            // 檔案中的區塊的對齊大小 +40h     WORD     MajorOperatingSystemVersion;   // 要求作業系統最低版本號的主版本號 +42h     WORD     MinorOperatingSystemVersion;   // 要求作業系統最低版本號的副版本號 +44h     WORD     MajorImageVersion;        // 可運行於作業系統的主版本號 +46h     WORD     MinorImageVersion;        // 可運行於作業系統的次版本號 +48h     WORD     MajorSubsystemVersion;    // 要求最低子系統版本的主版本號 +4Ah     WORD     MinorSubsystemVersion;    // 要求最低子系統版本的次版本號 +4Ch     DWORD    Win32VersionValue;        // 莫須有欄位,不被病毒利用的話一般為0 +50h     DWORD    SizeOfImage;              // 映像裝入記憶體後的總尺寸 +54h     DWORD    SizeOfHeaders;            // 所有頭 + 區塊表的尺寸大小 +58h     DWORD    CheckSum;                 // 映像的校檢和 +5Ch     WORD     Subsystem;                // 可執行檔案期望的子系統 +5Eh     WORD     DllCharacteristics;       // DllMain()函式何時被呼叫,預設為 0 +60h     DWORD    SizeOfStackReserve;       // 初始化時的棧大小 +64h     DWORD    SizeOfStackCommit;        // 初始化時實際提交的棧大小 +68h     DWORD    SizeOfHeapReserve;        // 初始化時保留的堆大小 +6Ch     DWORD    SizeOfHeapCommit;         // 初始化時實際提交的堆大小 +70h     DWORD    LoaderFlags;              // 與除錯有關,預設為 0  +74h     DWORD    NumberOfRvaAndSizes;      // 下邊資料目錄的項數,這個欄位自Windows NT 釋出以來一直是16 +78h    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];    // 資料目錄表 } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
  重要的有 AddressOfEntryPoint: 也就是上文提到的OEP,程式源入口點。 ImageBase: 預設載入基址, SectionAlignment:  記憶體當中的塊對齊數,一般為0x1000 FileAlignment:磁碟當中塊對齊數,一般為0x200 SizeOfHeaders:所有頭部大小 也就是DOS頭 檔案頭 以及區塊頭的總大小 ,檔案主體相對檔案其實的偏移。 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]:資料目錄表,儲存了各種表的RVA及大小。   來看一下資料目錄的定義
1 2 3 4 IMAGE_DATA_DIRECTORY STRUCT        VirtualAddress     DWORD        ?   ; 資料的起始RVA        Size              DWORD        ?   ; 資料塊的長度 IMAGE_DATA_DIRECTORY ENDS
  在010 Editor上檢視一下     

4.寫程式碼操作一下

  主要解析了DOS頭與PE檔案頭比較重要的欄位,直接放程式碼。  
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 //開啟檔案 m_hFile = CreateFile(      m_DeleFileName,GENERIC_READ,NULL,NULL,OPEN_EXISTING,      FILE_ATTRIBUTE_NORMAL,NULL);   DWORD  dwSize = GetFileSize(m_hFile, NULL);   PBYTE  pBuf =  new  BYTE [dwSize]{};   //讀取 ReadFile(m_hFile,pBuf,dwSize,&dwSize,NULL);   //判斷是否為PE檔案 m_pDos = PIMAGE_DOS_HEADER(pBuf); if  (m_pDos->e_magic!=IMAGE_DOS_SIGNATURE) {      MessageBox(L "不是有效的PE檔案 \n" );      CloseHandle(m_hFile);      m_hFile = NULL;      return ; } m_pNTHeader = PIMAGE_NT_HEADERS(pBuf+m_pDos->e_lfanew); if  (m_pNTHeader->Signature!= IMAGE_NT_SIGNATURE) {      MessageBox(L "不是有效的PE檔案 \n" );      CloseHandle(m_hFile);      m_hFile = NULL;      return ; }     //讀取檔案頭資訊 m_pFileHeader = &(m_pNTHeader->FileHeader);   m_NumberOfSections.Format(L "%X" ,m_pFileHeader->NumberOfSections); m_TimeDateStamp.Format(L "%p" , m_pFileHeader->TimeDateStamp); m_SizeOfOptionalHeader.Format(L "%X" , m_pFileHeader->SizeOfOptionalHeader);   //拓展頭資訊 m_pOptionalHeader = &(m_pNTHeader->OptionalHeader);   m_AddressOfEntryPoint.Format(L "%X" ,m_pOptionalHeader->AddressOfEntryPoint); m_SizeOfHeaders.Format(L "%X" , m_pOptionalHeader->SizeOfHeaders); m_ImageBase.Format(L "%X" , m_pOptionalHeader->ImageBase); m_SizeOfImage.Format(L "%X" , m_pOptionalHeader->ImageBase); m_BaseOfCode.Format(L "%X" , m_pOptionalHeader->BaseOfCode); m_DllCharacteristics.Format(L "%X" , m_pOptionalHeader->DllCharacteristics); m_BaseOfData.Format(L "%X" , m_pOptionalHeader->BaseOfData); m_NumberOfRvaAndSizes.Format(L "%X" , m_pOptionalHeader->NumberOfRvaAndSizes); m_SectionAlignment.Format(L "%X" , m_pOptionalHeader->SectionAlignment); m_FileAlignment.Format(L "%X" , m_pOptionalHeader->FileAlignment); m_CheckSum.Format(L "%X" , m_pOptionalHeader->CheckSum); m_Magic.Format(L "%X" , m_pOptionalHeader->CheckSum); m_Subsystem.Format(L "%X" , m_pOptionalHeader->Subsystem);
      實現的效果如下:     第一部分比較簡單,完整程式碼放到附件。

 

============== End