首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

PE 可執行文件修改實戰:代碼注入、入口點重定向與區段擴展完整教程

     編輯:關於C++

修改 PE (Portable Executable) 文件是逆向工程、軟體護盾、熱補丁、惡意代碼分析的核心技能。本文系統講解 PE 文件物理結構、關鍵頭部字段,並以「向 .text 區段尾部空洞注入代碼並重定向入口點」為例,提供完整的 C/C++ 實作代碼,涵蓋文件映射、結構解析、空間計算、入口點修改、代碼寫入、跳轉指令構造、校驗和更新全流程。

PE 文件物理結構速覽


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
  ...

關鍵結構體與字段 (winnt.h 核心子集)

  • 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 區段尾部空洞

編譯器產生的 .text 區段文件大小 (SizeOfRawData) 通常大於真實代碼大小 (VirtualSize),差值即為**文件層面的空洞** (Padding)。若 SizeOfRawData - VirtualSize >= 注入代碼長度,可直接利用此空間,無需擴展區段/新增區段,隱蔽性最好。

空間計算公式


CodeCaveOffset = PointerToRawData + VirtualSize;           // 文件偏移
CodeCaveRVA    = VirtualAddress + VirtualSize;             // 記憶體 RVA
// 對齊邊界 (可選,視原區段屬性)
CodeCaveOffset = AlignUp(CodeCaveOffset, FileAlignment);   // 文件對齊
CodeCaveRVA    = AlignUp(CodeCaveRVA, SectionAlignment);   // 記憶體對齊
NewEntryPoint  = CodeCaveRVA;                              // 新入口點 RVA

完整實作代碼 (C/C++ 控制台程式)


#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;
}

關鍵技術細節深度解析

1. 相對跳轉指令編碼 (x86)

  • CALL rel32 (E8 xx xx xx xx):rel32 = 目標地址 - (指令地址 + 5)。
  • JMP rel32 (E9 xx xx xx xx):同理。
  • 地址均為 **載入後虛擬地址 (VA) = ImageBase + RVA**。

2. 獲取目標 API 真實地址

注入代碼需調用 MessageBoxA 等 API。方法:

  1. 解析目標 IAT:遍歷 DataDirectory[1] (導入表),找到 user32.dll 的 MessageBoxA 條目,讀取其 IAT 槽值 (若已綁定) 或通過導入名查找導出表計算。
  2. 動態解析 (Shellcode 內):Shellcode 內嵌哈希查找 kernel32.dll 基址 -> 導出表查找 LoadLibrary/GetProcAddress -> 載入 user32 -> 獲取 MessageBoxA。體積大但通用。
  3. 假設同地址空間 (演示用):注入器進程調用 GetProcAddress 獲取地址填充。僅在 ASLR 關閉且基址不衝突時有效,現代系統不可行。

3. 區段屬性修改

若注入代碼需寫入數據 (如解密後的字串),原 .text 區段通常無寫屬性。需修改 Characteristics |= IMAGE_SCN_MEM_WRITE (0x80000000),或將代碼放入新增可讀寫區段。

4. 校驗和更新

使用 ImageHlp.h 的 CheckSumMappedFile 重新計算並寫回 OptionalHeader.CheckSum。驅動程式、系統文件、啟用簽名驗證的文件強制要求校驗和正確。

5. 64 位 (PE32+) 差異

  • 結構體:IMAGE_NT_HEADERS64、IMAGE_OPTIONAL_HEADER64 (無 BaseOfData,指標 8 字節)。
  • 指令編碼:x64 相對跳轉 E8/E9 仍為 32 位相對偏移 (RIP 相對),但地址 64 位。
  • Shellcode 需重寫為 x64 機器碼 (調用約定 RCX/RDX/R8/R9,堆疊對齊 16 字節)。

進階注入技術對比

技術原理隱蔽性複雜度適用場景
代碼洞注入利用區段 Padding 空間高 (無體積增長)中小型 Shellcode、熱補丁
區段擴展增大 SizeOfRawData/VirtualSize中 (區段變大)中大型 Payload
新增區段增加 Section Header、調整 SizeOfImage低 (明顯新區段)高大型模塊、加殼
導入表劫持 (IAT Hook)修改 IAT 指向注入代碼高 (無代碼修改)中API 監控、熱補丁
TLS Callback 注入利用 TLS 回呼數組高 (入口點前執行)中反調試、早期初始化

法律與道德邊界

PE 修改技術雙刃劍:

  • 合法:軟體護盾加殼、熱補丁系統 (Microsoft Detours)、二進制難讀化、編譯器優化後端、惡意樣本分析沙箱儀器化。
  • 非法:病毒/蠕蟲感染、破解軟體 (移除授權)、惡意代碼注入 (DLL 注入、Process Hollowing)、繞過安全產品、竊取知識產權。

從業者必須遵守《網絡安全法》、《計算機信息系統安全保護條例》及行業規範,將技術用於授權防護、分析、教學場景。

總結

PE 文件修改的核心在於**精準解析結構、準確計算 RVA/文件偏移/對齊、正確構造機器指令相對跳轉、維護 PE 完整性 (校驗和、簽名、區段屬性)**。本文提供的「代碼洞注入 + 入口點重定向」完整代碼展示了最基礎也最實用的修改流程。掌握此流程,再結合區段操作、導入表解析、重定位處理、x64 調用約定,即可應對絕大多數二進制修改工程需求。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved