逆向初級-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個位元組。
<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;