視訊資源詳見網盤搜尋 或 線上的B站滴水逆向三期
其課件也能在CSDN或百度搜尋到,以下部分為課件内容摘要,部分為自己的了解
最後附上詳細注釋的自寫代碼
PE(Portable Executable)檔案,是windows上的可移植的可執行檔案,常見的 .exe .dll .sys 等都是PE檔案。那麼PE檔案和計算機網絡的各種標頭一樣,都有自己獨特的格式。
這裡有張PE格式圖的部分截圖,完整版連結:https://pan.baidu.com/s/1ToG2wc_qOi0BfrLTeN0HPw
提取碼:PEst
不要被這麼多字段吓到,隻有小部分是常用的
首先PE檔案開頭就是DOS頭 DOS_HEADER,DOS頭隻需要看第一個字段magic和最後一個lfanew,magic字段為一個WORD即兩位元組,若為MZ則屬于PE檔案,lfanew是指向NT頭NT_HEADER的相對偏移,即檔案起始位置(magic字段所在位置)+ lfanew 即為NT頭的位置
可以用winhex等十六進制檢視器檢視會有更直覺的印象。lfanew字段到NT頭之間會有一段說明字元串或者垃圾資料稱為DOS stub
接下來NT頭分為一個字段和兩大部分,Signature字段,一大部分是FILE_HEADER(即COFF頭或稱檔案頭/标準PE頭),另一大部分是OPTIONAL_HEADER 可選頭。注意FILE_HEADER一共有0x14的長度,可以看成一個大的結構體,結構體内容就是圖中_IMAGE_FILE_HEADER的部分。OPTIONAL_HEADER 可選頭同理。
過了NT頭,就是節表SECTION_HEADER 了。節表類似于結構體數組的形式,圖所示會有多個同樣結構的SECTION_HEADER重複。
以下就是PE頭中的常用字段,這些字段現隻需了解,具體的意義可能得等後續的文章講解中逐漸都會用到後才能真正了解,後面用到了回頭看也不遲
1、DOS頭: | |
WORD e_magic * | "MZ标記" 用于判斷是否為可執行檔案. |
DWORD e_lfanew; * | PE頭相對于檔案的偏移,用于定位PE檔案 |
2、标準PE頭(COFF頭): | |
WORD Machine; * | 程式運作的CPU型号:0x0 任何處理器/0x14C 386及後續處理器 |
WORD NumberOfSections; * | 檔案中存在的節的總數,如果要新增節或者合并節 就要修改這個值. |
DWORD TimeDateStamp; * | 時間戳:檔案的建立時間(和作業系統的建立時間無關),編譯器填寫的. |
DWORD PointerToSymbolTable; | |
DWORD NumberOfSymbols; | |
WORD SizeOfOptionalHeader; * | 可選PE頭的大小,32位PE檔案預設E0h 64位PE檔案預設為F0h 大小可以自定義. |
WORD Characteristics; * | 每個位有不同的含義,可執行檔案值為10F 即0 1 2 3 8位置1 |
3、可選PE頭: | |
WORD Magic; * | 說明檔案類型:10B 32位下的PE檔案 20B 64位下的PE檔案 |
BYTE MajorLinkerVersion; | |
BYTE MinorLinkerVersion; | |
DWORD SizeOfCode;* | 所有代碼節的和,必須是FileAlignment的整數倍 編譯器填的 沒用 |
DWORD SizeOfInitializedData;* | 已初始化資料大小的和,必須是FileAlignment的整數倍 編譯器填的 沒用 |
DWORD SizeOfUninitializedData;* | 未初始化資料大小的和,必須是FileAlignment的整數倍 編譯器填的 沒用 |
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;* | 記憶體中整個PE檔案的映射的尺寸,可以比實際的值大,但必須是SectionAlignment的整數倍 |
DWORD SizeOfHeaders;* | 所有頭+節表按照檔案對齊後的大小,否則加載會出錯 |
DWORD CheckSum;* | 校驗和,一些系統檔案有要求.用來判斷檔案是否被修改. |
WORD Subsystem; | |
WORD DllCharacteristics; | |
DWORD SizeOfStackReserve;* | 初始化時保留的堆棧大小 |
DWORD SizeOfStackCommit;* | 初始化時實際送出的大小 |
DWORD SizeOfHeapReserve;* | 初始化時保留的堆大小 |
DWORD SizeOfHeapCommit;* | 初始化時實踐送出的大小 |
DWORD LoaderFlags; | |
DWORD NumberOfRvaAndSizes;* | 目錄項數目 |
DOS PE OPTION | |
DOS + PE标記 + 标準PE頭 + 可選PE |
接下來就是寫代碼讀取PE檔案的部分字段了,其他更多字段可以自行添加.
看懂代碼或者自己寫一遍代碼才是完全了解了PE頭結構和這些字段分布
使用window.h 都有相比對的結構體,直接.或者->都能找到需要的字段,在VS中會識别出比這裡更多的高亮,比如DWORD 、 PIMAGE_DOS_HEADER 之類的windows.h 才有的宏定義
代碼中用到的exe 可自行找一個小點的獨立的exe,比如網上搜些逆向用的crackme exe程式檢測自寫代碼用
#include "stdio.h"
#include "windows.h"
inline LPVOID ReadPEFile(LPSTR lpszFile) //LPSTR 即 CHAR *
{
FILE *pFile = NULL;
DWORD fileSize = 0;
LPVOID pFileBuffer = NULL; //LPVOID 即 void *
//打開檔案
pFile = fopen(lpszFile, "rb");
if (!pFile)
{
printf(" 無法打開 EXE 檔案! ");
return NULL;
}
//讀取檔案大小
fseek(pFile, 0, SEEK_END);
fileSize = ftell(pFile);
fseek(pFile, 0, SEEK_SET);
//配置設定緩沖區
pFileBuffer = malloc(fileSize); //pFileBuffer指向一段 配置設定的exe檔案這麼大的記憶體
if (!pFileBuffer)
{
printf(" 配置設定空間失敗! ");
fclose(pFile);
return NULL;
}
//将檔案資料讀取到緩沖區
size_t n = fread(pFileBuffer, fileSize, 1, pFile); //size_t 即 unsigned int
if (!n)
{
printf(" 讀取資料失敗! ");
free(pFileBuffer);
fclose(pFile);
return NULL;
}
//關閉檔案
fclose(pFile);
return pFileBuffer; //最後傳回指向 已填充exe檔案内容 的新開辟空間 的指針
}
VOID h312()
{
char FilePath[] = "CRACKME.EXE";
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pFileHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
pFileBuffer = ReadPEFile(FilePath); //pFileBuffer即指向已裝載到記憶體中的exe首部
if (!pFileBuffer)
{
printf("檔案讀取失敗\n");
return;
}
//判斷是否是有效的MZ标志
if (*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE) //pFileBuffer強轉 WORD* 後 取指針指向的内容
{
printf("不是有效的MZ标志\n");
free(pFileBuffer);
return;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer; // 強轉 DOS_HEADER 結構體指針
//列印DOS頭
printf("********************DOS頭********************\n");
printf("MZ标志:%x\n", pDosHeader->e_magic);
printf("PE偏移:%x\n", pDosHeader->e_lfanew);
//判斷是否是有效的PE标志
if (*((PDWORD)((DWORD)pFileBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE) //基址pFileBuffer + lfanew 為 NTHeader首址
{
printf("不是有效的PE标志\n");
free(pFileBuffer);
return;
}
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
//列印NT頭
printf("*****************NT HEADERS*****************\n");
printf("NT:%x\n", pNTHeader->Signature);
pFileHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4); //NT頭位址 + 4 為 FileHeader 首址
printf("****************FILE HEADERS****************\n");
printf("PE:%x\n", pFileHeader->Machine);
printf("節的數量:%x\n", pFileHeader->NumberOfSections);
printf("SizeOfOptionalHeader:%x\n", pFileHeader->SizeOfOptionalHeader);
//可選PE頭
pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);//SIZEOF_FILE_HEADER為固定值且不存在于PE檔案字段中
printf("**************OPTIOINAL HEADERS*************\n");
printf("Magic:%x\n", pOptionalHeader->Magic);
//釋放記憶體
free(pFileBuffer);
}
運作程式列印内容如下:
********************DOS頭********************
MZ标志:5a4d
PE偏移:100
*****************NT HEADERS*****************
NT:4550
****************FILE HEADERS****************
PE:14c
節的數量:6
SizeOfOptionalHeader:e0
**************OPTIOINAL HEADERS*************
Magic:10b
可使用 PETool 或 LoadPE 等小軟體檢視相應PE檔案内容。對照列印内容檢測自寫的程式是否正确。