修改 PE (Portable Executable) 文件是逆向工程、軟體護盾、熱補丁、惡意代碼分析的核心技能。本文系統講解 PE 文件物理結構、關鍵頭部字段,並以「向 .text 區段尾部空洞注入代碼並重定向入口點」為例,提供完整的 C/C++ 實作代碼,涵蓋文件映射、結構解析、空間計算、入口點修改、代碼寫入、跳轉指令構造、校驗和更新全流程。
DOS MZ Header (offset 0)
e_lfanew -> PE Header Offset
DOS Stub
PE Header (IMAGE_NT_HEADERS)
Signature "PE\0\0"
FileHeader (IMAGE_FILE_HEADER)
OptionalHeader (IMAGE_OPTIONAL_HEADER) <- 關鍵: EntryPoint, ImageBase, SectionAlignment, DataDirectory[]
Section Table (IMAGE_SECTION_HEADER[]) <- 每區段 40 bytes
[0] .text (代碼區)
[1] .data (數據區)
[2] .rsrc (資源區)
...
Section Data (對齊 FileAlignment)
.text raw data
.data raw data
...
IMAGE_DOS_HEADER.e_lfanew:PE Header 文件偏移。IMAGE_FILE_HEADER.NumberOfSections:區段數。IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint:**程序入口點 RVA** (修改目標)。IMAGE_OPTIONAL_HEADER.ImageBase:建議載入基址。IMAGE_OPTIONAL_HEADER.SectionAlignment / FileAlignment:記憶體/文件對齊粒度。IMAGE_OPTIONAL_HEADER.SizeOfImage:映像在記憶體總大小。IMAGE_SECTION_HEADER 關鍵字段:
VirtualAddress:區段 RVA。VirtualSize (Misc.VirtualSize):區段在記憶體中真實大小。SizeOfRawData:區段在文件中大小 (FileAlignment 對齊)。PointerToRawData:區段數據文件偏移。Characteristics:屬性 (0x60000020 = 代碼/可執行/可讀)。編譯器產生的 .text 區段文件大小 (SizeOfRawData) 通常大於真實代碼大小 (VirtualSize),差值即為**文件層面的空洞** (Padding)。若 SizeOfRawData - VirtualSize >= 注入代碼長度,可直接利用此空間,無需擴展區段/新增區段,隱蔽性最好。
CodeCaveOffset = PointerToRawData + VirtualSize; // 文件偏移
CodeCaveRVA = VirtualAddress + VirtualSize; // 記憶體 RVA
// 對齊邊界 (可選,視原區段屬性)
CodeCaveOffset = AlignUp(CodeCaveOffset, FileAlignment); // 文件對齊
CodeCaveRVA = AlignUp(CodeCaveRVA, SectionAlignment); // 記憶體對齊
NewEntryPoint = CodeCaveRVA; // 新入口點 RVA
#include
#include
#include // CheckSumMappedFile
#pragma comment(lib, "imagehlp.lib")
// 待注入的 Shellcode (彈 MessageBox 並跳回 OEP)
// 這裡僅作演示,實際需根據目標架構(x86/x64)手工編寫或編譯提取
unsigned char Shellcode[] = {
0x6A, 0x00, // push 0 (MB_OK)
0x6A, 0x00, // push 0 (標題)
0x6A, 0x00, // push 0 (內容)
0x6A, 0x00, // push 0 (hWnd)
0xE8, 0x00, 0x00, 0x00, 0x00, // call MessageBoxA (相對偏移稍後填充)
0xE9, 0x00, 0x00, 0x00, 0x00 // jmp OEP (相對偏移稍後填充)
};
#define SHELLCODE_SIZE sizeof(Shellcode)
#define CALL_OFFSET 9 // call 指令偏移
#define JMP_OFFSET 14 // jmp 指令偏移
int main(int argc, char* argv[]) {
if (argc < 2) { printf("Usage: %s \n", argv[0]); return 1; }
char* filename = argv[1];
HANDLE hFile = CreateFile(filename, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) { perror("OpenFile"); return 1; }
// 1. 文件映射讀取頭部
HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
LPVOID pBase = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
CloseHandle(hMap);
// 2. 解析 PE 結構
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBase;
if (pDos->e_magic != IMAGE_DOS_SIGNATURE) { printf("Not MZ\n"); goto CLEANUP; }
PIMAGE_NT_HEADERS pNT = (PIMAGE_NT_HEADERS)((BYTE*)pBase + pDos->e_lfanew);
if (pNT->Signature != IMAGE_NT_SIGNATURE) { printf("Not PE\n"); goto CLEANUP; }
// 3. 定位 .text 區段 (假設為第一區段)
PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(pNT);
if (strncmp((char*)pSec->Name, ".text", 5) != 0) { printf("First section not .text\n"); goto CLEANUP; }
// 4. 計算空洞
DWORD caveFileOff = pSec->PointerToRawData + pSec->Misc.VirtualSize;
DWORD caveRVA = pSec->VirtualAddress + pSec->Misc.VirtualSize;
DWORD caveSize = pSec->SizeOfRawData - pSec->Misc.VirtualSize;
if (caveSize < SHELLCODE_SIZE) { printf("Cave too small: %u < %u\n", caveSize, SHELLCODE_SIZE); goto CLEANUP; }
// 5. 計算新入口點與相對跳轉偏移
DWORD oldEP = pNT->OptionalHeader.AddressOfEntryPoint;
DWORD newEP = caveRVA;
DWORD imageBase = pNT->OptionalHeader.ImageBase;
// 獲取 MessageBoxA 真實地址 (需目標進程上下文,此處簡化假設同地址空間)
// 實際注入器需解析目標 IAT 或導出表獲取真實地址
HMODULE hUser32 = LoadLibrary("user32.dll");
FARPROC pMsgBox = GetProcAddress(hUser32, "MessageBoxA");
DWORD msgBoxAddr = (DWORD)pMsgBox;
// 6. 填充 Shellcode 相對偏移
// call MessageBoxA: E8 rel32 -> rel32 = Target - (NextInst)
*(DWORD*)(Shellcode + CALL_OFFSET + 1) = msgBoxAddr - (imageBase + newEP + CALL_OFFSET + 5);
// jmp OEP: E9 rel32 -> rel32 = OEP - (NextInst)
*(DWORD*)(Shellcode + JMP_OFFSET + 1) = oldEP - (newEP + JMP_OFFSET + 5);
// 7. 修改文件:寫入 Shellcode + 修改 EntryPoint
UnmapViewOfFile(pBase);
CloseHandle(hFile);
// 重新以寫入模式打開
hFile = CreateFile(filename, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) { perror("Reopen"); return 1; }
// 7.1 寫入 Shellcode 到空洞
SetFilePointer(hFile, caveFileOff, NULL, FILE_BEGIN);
DWORD dwWritten;
WriteFile(hFile, Shellcode, SHELLCODE_SIZE, &dwWritten, NULL);
// 7.2 修改 OptionalHeader.AddressOfEntryPoint
DWORD epFileOff = pDos->e_lfanew + offsetof(IMAGE_NT_HEADERS, OptionalHeader) + offsetof(IMAGE_OPTIONAL_HEADER, AddressOfEntryPoint);
SetFilePointer(hFile, epFileOff, NULL, FILE_BEGIN);
WriteFile(hFile, &newEP, 4, &dwWritten, NULL);
// 7.3 更新 PE 校驗和 (可選,但建議)
// 重新映射計算校驗和
CloseHandle(hFile);
hFile = CreateFile(filename, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
hMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);
pBase = MapViewOfFile(hMap, FILE_MAP_WRITE, 0, 0, 0);
pDos = (PIMAGE_DOS_HEADER)pBase;
pNT = (PIMAGE_NT_HEADERS)((BYTE*)pBase + pDos->e_lfanew);
DWORD HeaderSum, CheckSum;
CheckSumMappedFile(pBase, GetFileSize(hFile, NULL), &HeaderSum, &CheckSum);
pNT->OptionalHeader.CheckSum = CheckSum;
FlushViewOfFile(pBase, 0);
printf("Injection success! New EP: 0x%X\n", newEP);
CLEANUP:
if (pBase) UnmapViewOfFile(pBase);
if (hMap) CloseHandle(hMap);
if (hFile != INVALID_HANDLE_VALUE) CloseHandle(hFile);
return 0;
}
CALL rel32 (E8 xx xx xx xx):rel32 = 目標地址 - (指令地址 + 5)。JMP rel32 (E9 xx xx xx xx):同理。注入代碼需調用 MessageBoxA 等 API。方法:
DataDirectory[1] (導入表),找到 user32.dll 的 MessageBoxA 條目,讀取其 IAT 槽值 (若已綁定) 或通過導入名查找導出表計算。kernel32.dll 基址 -> 導出表查找 LoadLibrary/GetProcAddress -> 載入 user32 -> 獲取 MessageBoxA。體積大但通用。GetProcAddress 獲取地址填充。僅在 ASLR 關閉且基址不衝突時有效,現代系統不可行。若注入代碼需寫入數據 (如解密後的字串),原 .text 區段通常無寫屬性。需修改 Characteristics |= IMAGE_SCN_MEM_WRITE (0x80000000),或將代碼放入新增可讀寫區段。
使用 ImageHlp.h 的 CheckSumMappedFile 重新計算並寫回 OptionalHeader.CheckSum。驅動程式、系統文件、啟用簽名驗證的文件強制要求校驗和正確。
IMAGE_NT_HEADERS64、IMAGE_OPTIONAL_HEADER64 (無 BaseOfData,指標 8 字節)。E8/E9 仍為 32 位相對偏移 (RIP 相對),但地址 64 位。| 技術 | 原理 | 隱蔽性 | 複雜度 | 適用場景 |
|---|---|---|---|---|
| 代碼洞注入 | 利用區段 Padding 空間 | 高 (無體積增長) | 中 | 小型 Shellcode、熱補丁 |
| 區段擴展 | 增大 SizeOfRawData/VirtualSize | 中 (區段變大) | 中 | 大型 Payload |
| 新增區段 | 增加 Section Header、調整 SizeOfImage | 低 (明顯新區段) | 高 | 大型模塊、加殼 |
| 導入表劫持 (IAT Hook) | 修改 IAT 指向注入代碼 | 高 (無代碼修改) | 中 | API 監控、熱補丁 |
| TLS Callback 注入 | 利用 TLS 回呼數組 | 高 (入口點前執行) | 中 | 反調試、早期初始化 |
PE 修改技術雙刃劍:
從業者必須遵守《網絡安全法》、《計算機信息系統安全保護條例》及行業規範,將技術用於授權防護、分析、教學場景。
PE 文件修改的核心在於**精準解析結構、準確計算 RVA/文件偏移/對齊、正確構造機器指令相對跳轉、維護 PE 完整性 (校驗和、簽名、區段屬性)**。本文提供的「代碼洞注入 + 入口點重定向」完整代碼展示了最基礎也最實用的修改流程。掌握此流程,再結合區段操作、導入表解析、重定位處理、x64 調用約定,即可應對絕大多數二進制修改工程需求。