逆向初級-PE(五)

5.1.PE文件結構

1、什麼是可執行文件?
可執行文件(executable fle)指的是可以由作業系統進行載入執行的文件。
可執行文件的格式:
Windows平台:
PE(Portable Executable)文件結構
Linux平台:
ELF(Executable and Linking Format)文件結構
哪些領域會用到PE文件格式:
<1>病毒與反病毒
<2>外掛與反外掛
<3>加殼與脫殼(保護與破解)

<4>無源碼修改功能、軟體漢化等

2、如何識別PE文件

<1> PE文件的特徵(PE指紋)
分別打開.exe .dlI .sys 等文件,觀察特徵前2個位元組。
image
<2>不要僅僅通過文件的後綴名來認定PE文件

5.2.PE文件的兩種狀態

1、PE文件主要結構體
image

  • IMAGE_DOS_HEADER佔64個位元組。
  • DOS Sub:IMAGE_DOS_HEADER尾部的四個位元組指向PE文件的開始位置。IMAGE_DOS_HEADER尾部到PE文件頭開始的中間部分是DOS_Sub部分(大小不固定)
  • PE文件頭標誌:PE頭是前面4個位元組
  • PE文件表頭:IMAGE_FILE_HEADER是20個位元組
  • 擴展PE頭:IMAGE_OPTIONAL_HEADER在32位中佔224個位元組(這個大小是可以修改的)
  • IMAGE_SECTION_HEADER:40個位元組

2、PE文件的兩種狀態
image

5.3.DOS頭屬性說明

IMAGE_DOS_HEADER結構體

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;

主要就看兩個成員

WORD   e_magic;       //PE文件判斷表示   4D5A,ascii是MZ
LONG   e_lfanew;     //存儲PE頭首地址
  • e_magic兩個位元組和e_lfanew四個位元組內容不能修改
  • 開頭e_magic和結尾e_lfanew中間的成員部分可以隨意修改
  • e_lfanew到PE頭文件中間的DOS Stub部分可以隨便修改
    image

5.4.標誌PE頭屬性說明

1、PE頭

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;   //PE標識,佔4位元組
    IMAGE_FILE_HEADER FileHeader;    //標誌PE頭
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;    //擴展PE頭
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

PE標識不能破壞,作業系統在啟動一個程式的時候會檢測這個標識。

2、標準PE頭(佔20位元組)

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;//可以運行在什麼樣的CPU上   任意:0    Intel 386以及後續:14C   x64:8664  
    WORD    NumberOfSections;//表示節的數量
    DWORD   TimeDateStamp;//編譯器填寫的時間戳 與文件屬性裡面(創建時間、修改時間)無關
    DWORD   PointerToSymbolTable;//調試相關
    DWORD   NumberOfSymbols;//調試相關
    WORD    SizeOfOptionalHeader;//可選PE頭的大小(32位PE文件:0xE0  64位PE文件:0xF0)
    WORD    Characteristics;//文件屬性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

Characteristics文件屬性
image
文件屬性
Characteristics值為: 01 0F
轉換為二進位:0000 0001 0000 1111
說明下標0,1,2,3,8有值,根據下標是不是1,然後查看對應的文件屬性
image

5.5.擴展PE頭屬性說明

1、擴展PE頭結構體(總共224位元組)

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    // 
    WORD    Magic;  // 分辨32位程式還是64位,如果32位則10B,64位則20B
    BYTE    MajorLinkerVersion; //鏈接器版本號
    BYTE    MinorLinkerVersion; //鏈接器版本號
    DWORD   SizeOfCode; //所有程式碼節的總和 文件對齊後的大小 編譯器填寫的,無用處
    DWORD   SizeOfInitializedData; //已經初始化數據的節的總大小 文件對齊後的大小   編譯器填寫的,無用處
    DWORD   SizeOfUninitializedData; // 未初始化數據的節的總大小 文件對齊後的大小   編譯器填寫的,無用處
    DWORD   AddressOfEntryPoint; // 程式入口
    DWORD   BaseOfCode; //程式碼開始的基址 編譯器填寫的,無用處
    DWORD   BaseOfData; //數據開始的基址  編譯器填寫的,無用處

    //
    // NT additional fields.
    //

    DWORD   ImageBase; //記憶體鏡像基址
    DWORD   SectionAlignment; //記憶體對齊
    DWORD   FileAlignment; //文件對齊
    WORD    MajorOperatingSystemVersion; //作業系統版本號
    WORD    MinorOperatingSystemVersion; //作業系統版本號
    WORD    MajorImageVersion; //PE文件自身的版本號
    WORD    MinorImageVersion; //PE文件自身的版本號
    WORD    MajorSubsystemVersion; //運行所需要子系統的版本號
    WORD    MinorSubsystemVersion; //運行所需要子系統的版本號
    DWORD   Win32VersionValue; //子系統版本的值,必須為0
    DWORD   SizeOfImage; //記憶體中整個PE文件的映射尺寸,比實際的值大,必須是SectionAlignment整數倍
    DWORD   SizeOfHeaders; //所有的頭+節表按照文件對齊後的大小
    DWORD   CheckSum; //校驗和,可偽造
    WORD    Subsystem; //子系統, 驅動程式(1) 圖形介面(2) DLL(3)
    WORD    DllCharacteristics;	 //文件特性 不是針對DLL文件的
    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;

2、ImageBase和AddressOfEntryPoint

ImageBase; //記憶體鏡像基址
AddressOfEntryPoint; // 程式入口,相對於ImageBase的偏移

實例

程式入口:0193BE
記憶體鏡像:400000

程式真正入口=記憶體鏡像+程式入口=4193BE

image
通過DTDebug確認
image
3、 DllCharacteristics文件特性
image

5.6.PE節表

節表結構體(佔40位元組)

#define IMAGE_SIZEOF_SHORT_NAME 8              
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];  //ASCII字元串 可自定義 只截取8個位元組(佔8位元組)
    union {                     //Misc雙子是該位元組沒有在對齊前的真實尺寸 該值可以不準確(佔4位元組)
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;        //在記憶體中的偏移地址加上ImageBase才是記憶體中的真正地址
    DWORD   SizeOfRawData;		   //節在文件中對齊後的尺寸	
    DWORD   PointerToRawData;      //節區在文件中的偏移
    DWORD   PointerToRelocations;  //調試相關
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;        //節的屬性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
#define IMAGE_SIZEOF_SECTION_HEADER          40

DOS頭64位元組+PE標識4位元組+PE標準頭20位元組+PE擴展頭224位元組,然後就是節表的起始位置,每個節表佔40個位元組
image

5.7.RVA與FOA的轉換

1、RVA(相對虛擬地址)到FOA(文件偏移地址)的轉換:
<1>得到RVA的值:記憶體地址- ImageBase
<2>判斷RVA是否位於PE頭中,如果是: FOA== RVA
<3>判斷RVA位於哪個節:
RVA>=節VirtualAddress
RVA <=節.VirtualAddress +當前節記憶體對齊後的大小
差值= RVA-節VirtualAddress;
<4> FOA=節.PointerToRawData +差值;

如果文件對齊和記憶體對齊的值一樣,則RVA=記憶體地址- ImageBase,F0A=RVA,就可以得出在文件中的地址

5.8.空白區添加程式碼

給程式添加一個MessageBox對話框,步驟

  • 在PE的空白區構造一段程式碼
  • 修改入口地址為新增程式碼的地址
  • 新增程式碼執行後,跳回到入口地址

1、MessageBox的反彙編硬編碼

E8 表示call

6A表示push

9:        MessageBox(0,0,0,0);
00401028 8B F4                mov         esi,esp
0040102A 6A 00                push        0
0040102C 6A 00                push        0
0040102E 6A 00                push        0
00401030 6A 00                push        0
00401032 FF 15 8C 42 42 00    call        dword ptr [__imp__MessageBoxA@16 (0042428c)]
00401038 3B F4                cmp         esi,esp
0040103A E8 31 00 00 00       call        __chkesp (00401070)

2、找到要運行的程式的MessageBoxA的地址

用DTDdbug打開程式,點「E」,找到「USER32.DLL」,按「Ctrl+n」,然後找到MessageBoxA函數的地址
image
構造自己的程式碼,找一段空白區,寫上自己的程式碼

先執行我們要寫的程式碼(彈出資訊框),執行完,然後jmp到程式入口位置

構造要寫入的程式碼

6A 00 6A 00 6A 00 6A 00 E8 00 00 00 00 E9 00 00 00 00

E8表示call 
E8後面的硬編碼 = 要跳轉的地址 - E8指令當前的地址 - 5

要跳轉的MessageBoxA的地址:77D5050B

E8後面的硬編碼 = 77D5050B - (ImageBase+F98)- 5 = 7794F56E
    
程式入口:000193BE
ImageBase:00400000
程式運行入口=ImageBase+程式入口=004193BE
    
E9後面的硬編碼 = 004193BE - 400F9D - 5 = 1841C

最終程式碼
6A 00 6A 00 6A 00 6A 00 E8 6E F5 94 77 E9 1C 84 01 00

image
修改程式入口
image
把入口改成我們自己構造的程式碼的起始位置F90
image

5.9.擴大節

1、為什麼要擴大節

我們可以在任意空白區添加自己的程式碼,但如果添加的程式碼比較多,空白區不夠怎麼辦?

2、擴大節的步驟

<1>分配一塊新的空間,大小為S
<2>將最後-一個節的SizeOfRawData和VirtualSize改成N
N = (SizeOfRawData或者VirtualSize記憶體對齊後的值)+ S
<3>修改SizeOflmage大小

S = 1000

VirtualSize:78B0 當前節記憶體中沒有對齊的實際大小

SizeOfRawData:8000 當前節文件對齊後的大小

N = 8000 + 1000 = 9000
image
修改VirtualSize和SizeOfRawData值
image
擴大節,添加1000h,也就是十進位4096位元組。右鍵–>粘貼–>粘貼零位元組–>4096
image
修改SizeOflmage的值,先記憶體對齊後再加1000
image
SizeOflmage結果為
image

5.10.新增節

1、新增節的步驟:
<1>判斷是否有足夠的空間,可以添加一個節表.
<2>在節表中新增一個成員.
<3>修改PE頭中節的數量.
<4>修改sizeOflmage的大小.
<5>在原有數據的最後,新增一個節的數據(記憶體對齊的整數倍).
<6>修正新增節表的屬性.

2、節表結構

#define IMAGE_SIZEOF_SHORT_NAME 8              
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];  //ASCII字元串 可自定義 只截取8個位元組(佔8位元組)
    union {                     //Misc雙子是該位元組沒有在對齊前的真實尺寸 該值可以不準確(佔4位元組)
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;        //在記憶體中的偏移地址加上ImageBase才是記憶體中的真正地址
    DWORD   SizeOfRawData;		   //節在文件中對齊後的尺寸	
    DWORD   PointerToRawData;      //節區在文件中的偏移
    DWORD   PointerToRelocations;  //調試相關
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;        //節的屬性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
#define IMAGE_SIZEOF_SECTION_HEADER          40

在節表中新增一個節,把.txet節的40個位元組複製粘貼到新增加的節,然後修改新增加節的成員屬性

  • 前8個位元組是節的名字:隨便改個名字
  • 把之前最後一個節的VirtualSize(記憶體中沒有對齊的實際值)改為記憶體對齊後的值
    image
    改為8000
    image
    修改新增加節的VirtualSize和SizeOfRawData,因為新增加的節大小為1000h
    image
    新增加節的VirtualAddress = 上一個節記憶體對齊後的大小+上一個節.VirtualAddress
新增加節
VirtualAddress = 00008000+0002B000 = 00033000
PointerToRawData=VirtualAddress

image
修改sizeOflmage的大小
image
修改為34000
image
在原有數據的最後,新增一個節的數據,新增加節的大小為1000h

先刪除第一個節前面的40個位元組(因為前面新增加了一個節表,數據全部往後推移了40個位元組)
image
在最後面添加1000h位元組
image

5.11.導出表

1、如何查找導出表

擴展PE頭最後一個成員是一個數組(包含16和元素),每個數組對應一個表(每個表佔8位元組),如導出表、導入表等。

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;     //表的起始位置RVA
    DWORD   Size; 				//表的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

2、導出表結構

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics; 		//未使用
    DWORD   TimeDateStamp;			//時間戳
    WORD    MajorVersion;			//未使用
    WORD    MinorVersion;			//未使用
    DWORD   Name;					//指向該導出表文件名字元串
    DWORD   Base;					//導出函數起始序號
    DWORD   NumberOfFunctions;		//所有導出函數的個數
    DWORD   NumberOfNames;			//以函數名字導出的函數個數
    DWORD   AddressOfFunctions;     // RVA from base of image 導出函數地址表RVA
    DWORD   AddressOfNames;         // RVA from base of image 導出函數名稱表RVA
    DWORD   AddressOfNameOrdinals;  // RVA from base of image 導出函數序號表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

3、導出表成員 40位元組

導出表位置,數組DataDirectory[0]
image
起始位置2AD80
image
Name:2ADBC (RVA),然後從2ADBC的位置開始找,到以0結尾,就是導出表的名字
image
NumberOfFunctions:導出函數的個數 2個
image
NumberOfNames:以函數名字導出的函數個數 2個
image
image
AddressOfFunctions:導出函數地址表RVA
image
AddressOfNames:導出函數名稱表RVA
image
AddressOfNameOrdinals:導出函數序號表RVA。序號是兩個位元組,序號的個數跟函數名稱的個數相同

這裡序號為0和1
image
4、參考

  • 總共四個函數
  • 所有導出函數的個數為5,因為序號中間隔了個14沒有。函數個數 = 最大序號 – 最小序號 + 1
  • 以函數名導出的函數個數為3,因為有一個函數沒有名字
  • 把函數地址對應的二進位複製到OD裡面,可以查看到具體是什麼函數
    image

5.12.導入表_確定依賴模組

1、定位導入表

導入表位置,數組DataDirectory[1]

第一個導入表開始的位置:22A10
image
2、導入表結構

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA指向IMAGE_THUNK_DATA結構數組
    };
    DWORD   TimeDateStamp;                  // 時間戳
    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;							//RVA 指向dll名字,該名字以0結尾
    DWORD   FirstThunk;                     // RVA 指向IMAGE_THUNK_DATA結構數組
} IMAGE_IMPORT_DESCRIPTOR;

3、導入表個數

導入表的個數判斷:,每個導入表佔20個位元組,判斷有多少個導入表,以20個0為結尾的位置
image
4、查看依賴的模組名

第一個模組名字
image
查看
image

5.13.導入表_確定依賴函數

1、確定需要導入的函數
image
第一個成員指向的是一張表INT(導入名稱表),INT表裡面每個成員都是結構體IMAGE_THUNK_DATA,大小是4個位元組

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        PBYTE  ForwarderString;
        PDWORD Function;
        DWORD Ordinal;
        PIMAGE_IMPORT_BY_NAME  AddressOfData;
    } u1;
} IMAGE_THUNK_DATA32;

2、INT表裡面的結構體

INT表位置22A88,INT表裡面有多少個成員(4個位元組),就說明依賴當前導入模組多少個函數。結尾標誌:四個位元組都是00
image
INT表
image
3、確定需要導入的函數的名字
image
確定函數名字為ExitThread

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;		//可能為空,編譯器決定,如果不為空,是函數在導出表中的索引
    BYTE    Name[1];	//函數名稱,以0結尾
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

image

5.14.導入表_確定函數地址

PE文件載入前
image
PE文件載入後
image

5.15.重定位表

重定位表的位置(第六個表)

導入表位置,數組DataDirectory[5]

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;     
    DWORD   SizeOfBlock;        
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;