在筆者上一篇文章`《驅動開發:內核解析PE結構導出表》`介紹瞭如何解析記憶體導出表結構,本章將繼續延申實現解析PE結構的PE頭,PE節表等數據,總體而言內核中解析PE結構與應用層沒什麼不同,在上一篇文章中`LyShark`封裝實現了`KernelMapFile()`記憶體映射函數,在之後的章節中這個函數... ...
在筆者上一篇文章《驅動開發:內核解析PE結構導出表》
介紹瞭如何解析記憶體導出表結構,本章將繼續延申實現解析PE結構的PE頭,PE節表等數據,總體而言內核中解析PE結構與應用層沒什麼不同,在上一篇文章中LyShark
封裝實現了KernelMapFile()
記憶體映射函數,在之後的章節中這個函數會被多次用到,為了減少代碼冗餘,後期文章只列出重要部分,讀者可以自行去前面的文章中尋找特定的片段。
Windows NT 系統中可執行文件使用微軟設計的新的文件格式,也就是至今還在使用的PE格式,PE文件的基本結構如下圖所示:
在PE文件中,代碼,已初始化的數據,資源和重定位信息等數據被按照屬性分類放到不同的Section(節區/或簡稱為節)
中,而每個節區的屬性和位置等信息用一個IMAGE_SECTION_HEADER
結構來描述,所有的IMAGE_SECTION_HEADER
結構組成了一個節表(Section Table)
,節表數據在PE文件中被放在所有節數據的前面.
上面PE結構圖中可知PE文件的開頭部分包括了一個標準的DOS可執行文件結構,這看上去有些奇怪,但是這對於可執行程式的向下相容性來說卻是不可缺少的,當然現在已經基本不會出現純DOS程式了,現在來說這個IMAGE_DOS_HEADER
結構純粹是歷史遺留問題。
DOS頭結構解析: PE文件中的DOS部分由MZ格式的文件頭和可執行代碼部分組成,可執行代碼被稱為DOS塊(DOS stub)
,MZ格式的文件頭由IMAGE_DOS_HEADER
結構定義,在C語言頭文件winnt.h
中有對這個DOS結構詳細定義,如下所示:
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // DOS的頭部
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; // 指向了PE文件的開頭(重要)
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
在DOS文件頭中,第一個欄位e_magic
被定義為MZ
,標志著DOS文件的開頭部分,最後一個欄位e_lfanew
則指明瞭PE文件的開頭位置,現在來說除了第一個欄位和最後一個欄位有些用處,其他欄位幾乎已經廢棄了,這裡附上讀取DOS頭的代碼。
void DisplayDOSHeadInfo(HANDLE ImageBase)
{
PIMAGE_DOS_HEADER pDosHead = NULL;
pDosHead = (PIMAGE_DOS_HEADER)ImageBase;
printf("DOS頭: %x\n", pDosHead->e_magic);
printf("文件地址: %x\n", pDosHead->e_lfarlc);
printf("PE結構偏移: %x\n", pDosHead->e_lfanew);
}
PE頭結構解析: 從DOS文件頭的e_lfanew
欄位向下偏移003CH
的位置,就是真正的PE文件頭的位置,該文件頭是由IMAGE_NT_HEADERS
結構定義的,定義結構如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // PE文件標識字元
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
如上PE文件頭的第一個DWORD是一個標誌,預設情況下它被定義為00004550h也就是P,E
兩個字元另外加上兩個零,而大部分的文件屬性由標誌後面的IMAGE_FILE_HEADER
和IMAGE_OPTIONAL_HEADER32
結構來定義,我們繼續跟進IMAGE_FILE_HEADER
這個結構:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 運行平臺
WORD NumberOfSections; // 文件的節數目
DWORD TimeDateStamp; // 文件創建日期和時間
DWORD PointerToSymbolTable; // 指向符號表(用於調試)
DWORD NumberOfSymbols; // 符號表中的符號數量
WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HANDLER32結構的長度
WORD Characteristics; // 文件的屬性 exe=010fh dll=210eh
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
繼續跟進 IMAGE_OPTIONAL_HEADER32
結構,該結構體中的數據就豐富了,重要的結構說明經備註好了:
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion; // 連接器版本
BYTE MinorLinkerVersion;
DWORD SizeOfCode; // 所有包含代碼節的總大小
DWORD SizeOfInitializedData; // 所有已初始化數據的節總大小
DWORD SizeOfUninitializedData; // 所有未初始化數據的節總大小
DWORD AddressOfEntryPoint; // 程式執行入口RVA
DWORD BaseOfCode; // 代碼節的起始RVA
DWORD BaseOfData; // 數據節的起始RVA
DWORD ImageBase; // 程式鏡像基地址
DWORD SectionAlignment; // 記憶體中節的對其粒度
DWORD FileAlignment; // 文件中節的對其粒度
WORD MajorOperatingSystemVersion; // 操作系統主版本號
WORD MinorOperatingSystemVersion; // 操作系統副版本號
WORD MajorImageVersion; // 可運行於操作系統的最小版本號
WORD MinorImageVersion;
WORD MajorSubsystemVersion; // 可運行於操作系統的最小子版本號
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; // 記憶體中整個PE映像尺寸
DWORD SizeOfHeaders; // 所有頭加節表的大小
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
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;
IMAGE_DATA_DIRECTORY數據目錄列表,它由16個相同的IMAGE_DATA_DIRECTORY結構組成,這16個數據目錄結構定義很簡單僅僅指出了某種數據的位置和長度,定義如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 數據起始RVA
DWORD Size; // 數據塊的長度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
上方的結構就是PE文件的重要結構,接下來將通過編程讀取出PE文件的開頭相關數據,讀取這些結構也非常簡單代碼如下所示。
// 署名權
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: [email protected]
NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
DbgPrint("hello lyshark.com \n");
NTSTATUS status = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hSection = NULL;
PVOID pBaseAddress = NULL;
UNICODE_STRING FileName = { 0 };
// 初始化字元串
RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");
// 記憶體映射文件
status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
if (!NT_SUCCESS(status))
{
return 0;
}
// 獲取PE頭數據集
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_FILE_HEADER pFileHeader = &pNtHeaders->FileHeader;
DbgPrint("運行平臺: %x\n", pFileHeader->Machine);
DbgPrint("節區數目: %x\n", pFileHeader->NumberOfSections);
DbgPrint("時間標記: %x\n", pFileHeader->TimeDateStamp);
DbgPrint("可選頭大小 %x\n", pFileHeader->SizeOfOptionalHeader);
DbgPrint("文件特性: %x\n", pFileHeader->Characteristics);
DbgPrint("入口點: %p\n", pNtHeaders->OptionalHeader.AddressOfEntryPoint);
DbgPrint("鏡像基址: %p\n", pNtHeaders->OptionalHeader.ImageBase);
DbgPrint("鏡像大小: %p\n", pNtHeaders->OptionalHeader.SizeOfImage);
DbgPrint("代碼基址: %p\n", pNtHeaders->OptionalHeader.BaseOfCode);
DbgPrint("區塊對齊: %p\n", pNtHeaders->OptionalHeader.SectionAlignment);
DbgPrint("文件塊對齊: %p\n", pNtHeaders->OptionalHeader.FileAlignment);
DbgPrint("子系統: %x\n", pNtHeaders->OptionalHeader.Subsystem);
DbgPrint("區段數目: %d\n", pNtHeaders->FileHeader.NumberOfSections);
DbgPrint("時間日期標誌: %x\n", pNtHeaders->FileHeader.TimeDateStamp);
DbgPrint("首部大小: %x\n", pNtHeaders->OptionalHeader.SizeOfHeaders);
DbgPrint("特征值: %x\n", pNtHeaders->FileHeader.Characteristics);
DbgPrint("校驗和: %x\n", pNtHeaders->OptionalHeader.CheckSum);
DbgPrint("可選頭部大小: %x\n", pNtHeaders->FileHeader.SizeOfOptionalHeader);
DbgPrint("RVA 數及大小: %x\n", pNtHeaders->OptionalHeader.NumberOfRvaAndSizes);
ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
ZwClose(hSection);
ZwClose(hFile);
Driver->DriverUnload = UnDriver;
return STATUS_SUCCESS;
}
運行如上這段代碼,即可解析出ntdll.dll
模塊的核心內容,如下圖所示;
接著來實現解析節表,PE文件中的所有節的屬性定義都被定義在節表中,節表由一系列的IMAGE_SECTION_HEADER
結構排列而成,每個結構郵過來描述一個節,節表總被存放在緊接在PE文件頭的地方,也即是從PE文件頭開始偏移為00f8h
的位置處,如下是節表頭部的定義。
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize; // 節區尺寸
} Misc;
DWORD VirtualAddress; // 節區RVA
DWORD SizeOfRawData; // 在文件中對齊後的尺寸
DWORD PointerToRawData; // 在文件中的偏移
DWORD PointerToRelocations; // 在OBJ文件中使用
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; // 節區屬性欄位
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
解析節表也很容易實現,首先通過pFileHeader->NumberOfSections
獲取到節數量,然後迴圈解析直到所有節輸出完成,這段代碼實現如下所示。
// 署名權
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: [email protected]
NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
DbgPrint("hello lyshark.com \n");
NTSTATUS status = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hSection = NULL;
PVOID pBaseAddress = NULL;
UNICODE_STRING FileName = { 0 };
// 初始化字元串
RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");
// 記憶體映射文件
status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
if (!NT_SUCCESS(status))
{
return 0;
}
// 獲取PE頭數據集
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);
PIMAGE_FILE_HEADER pFileHeader = &pNtHeaders->FileHeader;
DWORD NumberOfSectinsCount = 0;
// 獲取區塊數量
NumberOfSectinsCount = pFileHeader->NumberOfSections;
DWORD64 *difA = NULL; // 虛擬地址開頭
DWORD64 *difS = NULL; // 相對偏移(用於遍歷)
difA = ExAllocatePool(NonPagedPool, NumberOfSectinsCount*sizeof(DWORD64));
difS = ExAllocatePool(NonPagedPool, NumberOfSectinsCount*sizeof(DWORD64));
DbgPrint("節區名稱 相對偏移\t虛擬大小\tRaw數據指針\tRaw數據大小\t節區屬性\n");
for (DWORD temp = 0; temp<NumberOfSectinsCount; temp++, pSection++)
{
DbgPrint("%10s\t 0x%x \t 0x%x \t 0x%x \t 0x%x \t 0x%x \n",
pSection->Name, pSection->VirtualAddress, pSection->Misc.VirtualSize,
pSection->PointerToRawData, pSection->SizeOfRawData, pSection->Characteristics);
difA[temp] = pSection->VirtualAddress;
difS[temp] = pSection->VirtualAddress - pSection->PointerToRawData;
}
ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
ZwClose(hSection);
ZwClose(hFile);
Driver->DriverUnload = UnDriver;
return STATUS_SUCCESS;
}
運行驅動程式,即可輸出ntdll.dll
模塊的節表信息,如下圖;