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個字節。
<2>不要僅僅通過文件的后綴名來認定PE文件
5.2.PE文件的兩種狀態
1、PE文件主要結構體
- 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文件的兩種狀態
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部分可以隨便修改
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文件屬性
文件屬性
Characteristics值為: 01 0F
轉換為二進制:0000 0001 0000 1111
說明下標0,1,2,3,8有值,根據下標是不是1,然后查看對應的文件屬性
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
通過DTDebug確認
3、 DllCharacteristics文件特性
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個字節
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函數的地址
構造自己的代碼,找一段空白區,寫上自己的代碼
先執行我們要寫的代碼(彈出信息框),執行完,然后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
修改程序入口
把入口改成我們自己構造的代碼的起始位置F90
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
修改VirtualSize和SizeOfRawData值
擴大節,添加1000h,也就是十進制4096字節。右鍵-->粘貼-->粘貼零字節-->4096
修改SizeOflmage的值,先內存對齊后再加1000
SizeOflmage結果為
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(內存中沒有對齊的實際值)改為內存對齊后的值
改為8000
修改新增加節的VirtualSize和SizeOfRawData,因為新增加的節大小為1000h
新增加節的VirtualAddress = 上一個節內存對齊后的大小+上一個節.VirtualAddress
新增加節
VirtualAddress = 00008000+0002B000 = 00033000
PointerToRawData=VirtualAddress
修改sizeOflmage的大小
修改為34000
在原有數據的最后,新增一個節的數據,新增加節的大小為1000h
先刪除第一個節前面的40個字節(因為前面新增加了一個節表,數據全部往后推移了40個字節)
在最后面添加1000h字節
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]
起始位置2AD80
Name:2ADBC (RVA),然后從2ADBC的位置開始找,到以0結尾,就是導出表的名字
NumberOfFunctions:導出函數的個數 2個
NumberOfNames:以函數名字導出的函數個數 2個
AddressOfFunctions:導出函數地址表RVA
AddressOfNames:導出函數名稱表RVA
AddressOfNameOrdinals:導出函數序號表RVA。序號是兩個字節,序號的個數跟函數名稱的個數相同
這里序號為0和1
4、參考
- 總共四個函數
- 所有導出函數的個數為5,因為序號中間隔了個14沒有。函數個數 = 最大序號 - 最小序號 + 1
- 以函數名導出的函數個數為3,因為有一個函數沒有名字
- 把函數地址對應的二進制復制到OD里面,可以查看到具體是什么函數
5.12.導入表_確定依賴模塊
1、定位導入表
導入表位置,數組DataDirectory[1]
第一個導入表開始的位置:22A10
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為結尾的位置
4、查看依賴的模塊名
第一個模塊名字
查看
5.13.導入表_確定依賴函數
1、確定需要導入的函數
第一個成員指向的是一張表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
INT表
3、確定需要導入的函數的名字
確定函數名字為ExitThread
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //可能為空,編譯器決定,如果不為空,是函數在導出表中的索引
BYTE Name[1]; //函數名稱,以0結尾
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
5.14.導入表_確定函數地址
PE文件加載前
PE文件加載后
5.15.重定位表
重定位表的位置(第六個表)
導入表位置,數組DataDirectory[5]
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;