在筆者上一篇文章《驅動開發:核心解析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
模組的節表資訊,如下圖;