10.3 除錯事件轉存程式記憶體

lyshark發表於2023-10-05

我們繼續延申除錯事件的話題,實現程式轉存功能,程式轉儲功能是指透過除錯API使獲得了目標程式控制權的程式,將目標程式的記憶體中的資料完整地轉存到本地磁碟上,對於加殼軟體,通常會透過加密、壓縮等手段來保護其程式碼和資料,使其不易被分析。在這種情況下,透過程式轉儲功能,可以將加殼程式的記憶體映象完整地儲存到本地,以便進行後續的分析。

在實現程式轉儲功能時,主要使用除錯API和記憶體讀寫函式。具體實現方法包括:以除錯方式啟動目標程式,將其暫停在執行前的位置;讓目標程式進入執行狀態;使用ReadProcessMemory函式讀取目標程式記憶體,並將結果儲存到緩衝區;將緩衝區中的資料寫入檔案;關閉目標程式的除錯狀態。

首先老樣子先來看OnException回撥事件,當程式被斷下時首先透過執行緒函式恢復該執行緒的狀態,在程式被正確解碼並執行起來時直接將該程式的EIP入口地址傳遞給MemDump();記憶體轉存函式,實現轉存功能;

void OnException(DEBUG_EVENT *pDebug, BYTE *bCode)
{
    CONTEXT context;
    DWORD dwNum;
    BYTE bTmp;

    // 開啟當前程式與執行緒
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pDebug->dwProcessId);
    printf("[+] 當前開啟程式控制程式碼: %d 程式PID: %d \n", hProcess, pDebug->dwProcessId);
    HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, pDebug->dwThreadId);
    printf("[+] 當前開啟執行緒控制程式碼: %d 執行緒PPID: %d \n", hThread, pDebug->dwThreadId);
    // 暫停當前執行緒
    SuspendThread(hThread);

    // 讀取出異常產生的首地址
    ReadProcessMemory(hProcess, pDebug->u.Exception.ExceptionRecord.ExceptionAddress, &bTmp, sizeof(BYTE), &dwNum);
    printf("[+] 當前異常產生地址為: 0x%08X \n", pDebug->u.Exception.ExceptionRecord.ExceptionAddress);

    // 設定當前執行緒上下文,獲取執行緒上下文
    context.ContextFlags = CONTEXT_FULL;
    GetThreadContext(hThread, &context);

    printf("[-] 恢復斷點前: EAX = 0x%08X  EIP = 0x%08X \n", context.Eax, context.Eip);
    // 將剛才的CC斷點取消,也就是回寫原始的指令集
    WriteProcessMemory(hProcess, pDebug->u.Exception.ExceptionRecord.ExceptionAddress, bCode, sizeof(BYTE), &dwNum);

    // 當前EIP減一併設定執行緒上下文
    context.Eip--;
    SetThreadContext(hThread, &context);
    printf("[+] 恢復斷點後: EAX = 0x%08X  EIP = 0x%08X \n", context.Eax, context.Eip);
    printf("[+] 獲取到動態入口點: 0x%08x \n", pDebug->u.CreateProcessInfo.lpBaseOfImage);
    // 轉儲記憶體映象
    MemDump(pDebug, context.Eip, (char *)"dump.exe");
    // 恢復執行緒
    ResumeThread(hThread);
    CloseHandle(hThread);
    CloseHandle(hProcess);
}

MemDump函式中,首先透過呼叫CreateFile函式開啟me32.szExePath路徑也就是轉存之前的檔案,透過使用VirtualAlloc分配記憶體空間,分配大小是PE頭中檔案實際大小,接著OpenProcess開啟正在執行的程式,並使用ReadProcessMemory讀取檔案的資料,此處讀取的實在記憶體中的映象資料,當讀取後手動修正,檔案的入口地址,及檔案的對齊方式,接著定位PE節區資料,找到節區首地址,並迴圈將當前節區資料賦值到新檔案快取中,最後當一切準備就緒,透過使用WriteFile函式將轉存後的檔案寫出到磁碟中;

void MemDump(DEBUG_EVENT *pDe, DWORD dwEntryPoint, char *DumpFileName)
{
    // 得到當前需要操作的程式PID
    DWORD dwPid = pDe->dwProcessId;
    MODULEENTRY32 me32;

    // 對系統程式拍攝快照
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPid);

    me32.dwSize = sizeof(MODULEENTRY32);
    // 得到第一個模組控制程式碼,第一個模組控制程式碼也就是程式的本體
    BOOL bRet = Module32First(hSnap, &me32);
    printf("[+] 當前轉儲原程式路徑: %s \n", me32.szExePath);

    // 開啟原始檔,也就是dump之前的檔案
    HANDLE hFile = CreateFile(me32.szExePath, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    if (hFile == INVALID_HANDLE_VALUE)
        exit(0);

    // 判斷PE檔案的有效性
    IMAGE_DOS_HEADER imgDos = { 0 };
    IMAGE_NT_HEADERS imgNt = { 0 };

    DWORD dwReadNum = 0;

    // 讀入當前記憶體程式的DOS頭結構
    ReadFile(hFile, &imgDos, sizeof(IMAGE_DOS_HEADER), &dwReadNum, NULL);
    // 判斷是否是一個合格的DOS頭
    if (imgDos.e_magic != IMAGE_DOS_SIGNATURE)
        return;
    // 設定檔案指標到NT頭上
    SetFilePointer(hFile, imgDos.e_lfanew, 0, FILE_BEGIN);
    ReadFile(hFile, &imgNt, sizeof(IMAGE_NT_HEADERS), &dwReadNum, NULL);
    // 判斷是否是合格的NT頭
    if (imgNt.Signature != IMAGE_NT_SIGNATURE)
        return;

    // 得到EXE檔案的大小
    DWORD BaseSize = me32.modBaseSize;
    printf("[+] 當前記憶體檔案大小: %d --> NT結構原始大小: %d 一致性檢測: True \n", BaseSize, imgNt.OptionalHeader.SizeOfImage);

    // 如果PE頭中的大小大於實際記憶體大小,則以PE頭中大小為模板
    if (imgNt.OptionalHeader.SizeOfImage > BaseSize)
    {
        BaseSize = imgNt.OptionalHeader.SizeOfImage;
    }

    // 分配記憶體空間,分配大小是PE頭中檔案實際大小,並開啟程式
    LPVOID pBase = VirtualAlloc(NULL, BaseSize, MEM_COMMIT, PAGE_READWRITE);
    printf("[+] 正在分配轉儲空間 控制程式碼: %d \n", pBase);

    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);

    // 讀取檔案的資料,此處讀取的實在記憶體中的映象資料
    bRet = ReadProcessMemory(hProcess, me32.modBaseAddr, pBase, me32.modBaseSize, NULL);

    // 判斷PDOS頭的有效性
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBase;
    if (pDos->e_magic != IMAGE_DOS_SIGNATURE)
        return;

    // 計算出NT頭資料
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + (PBYTE)pBase);
    if (pNt->Signature != IMAGE_NT_SIGNATURE)
        return;

    // 設定檔案的入口地址
    pNt->OptionalHeader.AddressOfEntryPoint = dwEntryPoint - pNt->OptionalHeader.ImageBase;
    printf("[*] 正在設定Dump檔案相對RVA入口地址: 0x%08X \n", pNt->OptionalHeader.AddressOfEntryPoint);

    // 設定檔案的對齊方式
    pNt->OptionalHeader.FileAlignment = 0x1000;
    printf("[*] 正在設定Dump檔案的對齊值: %d \n", pNt->OptionalHeader.FileAlignment);

    // 找到節區首地址,並迴圈將當前節區資料賦值到新檔案快取中
    PIMAGE_SECTION_HEADER pSec = (PIMAGE_SECTION_HEADER)((PBYTE)&pNt->OptionalHeader + pNt->FileHeader.SizeOfOptionalHeader);
    for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
    {
        pSec->PointerToRawData = pSec->VirtualAddress;
        printf("[+] 正在將虛擬地址: 0x%08X --> 設定到檔案地址: 0x%08X \n", pSec->VirtualAddress, pSec->PointerToRawData);
        pSec->SizeOfRawData = pSec->Misc.VirtualSize;
        printf("[+] 正在將虛擬大小: %d --> 設定到檔案大小: %d \n", pSec->Misc.VirtualSize, pSec->SizeOfRawData);
        pSec++;
    }
    CloseHandle(hFile);

    // 開啟轉儲後的檔案.
    hFile = CreateFile(DumpFileName, GENERIC_WRITE, FILE_SHARE_READ, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
    if (hFile == INVALID_HANDLE_VALUE)
        exit(0);
    printf("[*] 轉儲 %s 檔案到本地 \n", DumpFileName);

    DWORD dwWriteNum = 0;

    // 將讀取的資料寫入到檔案
    bRet = WriteFile(hFile, pBase, me32.modBaseSize, &dwWriteNum, NULL);
    if (dwWriteNum != me32.modBaseSize || FALSE == bRet)
        printf("寫入錯誤 !");
    // 關閉於釋放資源
    CloseHandle(hFile);
    VirtualFree(pBase, me32.modBaseSize, MEM_RELEASE);
    CloseHandle(hProcess);
    CloseHandle(hSnap);
}

讀者可自行執行這段程式,當程式執行後即可將指定的一個檔案記憶體資料完整的轉存到磁碟中,輸出效果如下圖所示;

本文作者: 王瑞
本文連結: https://www.lyshark.com/post/5e2f7b11.html
版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協議。轉載請註明出處!

相關文章