6.3 應用動態內存補丁

動態內存補丁可以理解爲在程序運行時動態地修改程序的內存,在某些時候某些應用程序會帶殼運行,而此類程序的機器碼只有在內存中被展開時纔可以被修改,而想要修改此類應用程序動態補丁將是一個不錯的選擇,動態補丁的原理是通過CreateProcess函數傳遞CREATE_SUSPENDED將程序運行起來並暫停,此時程序會在內存中被解碼,當程序被解碼後我們則可以通過內存讀寫實現對特定區域的動態補丁。

當讀者需要手動拉起一個進程時則可以使用OpenExeFile函數實現,該函數調用後會拉起一個進程,並默認暫停在程序入口處,返回一個PROCESS_INFORMATION結構信息;

// 打開進程並暫停運行
PROCESS_INFORMATION OpenExeFile(char *szFileName)
{
    STARTUPINFO si = { 0 };
    PROCESS_INFORMATION pi = { 0 };

    si.cb = sizeof(STARTUPINFO);
    si.wShowWindow = SW_SHOW;
    si.dwFlags = STARTF_USESHOWWINDOW;

    // 創建子線程並默認暫停
    BOOL bRet = CreateProcess(szFileName, 0, 0, 0, 0, CREATE_SUSPENDED, 0, 0, &si, &pi);
    if (bRet == FALSE)
    {
        exit(0);
    }
    ResumeThread(pi.hThread);
    return pi;
}

其中CreateProcess函數的一般格式:

BOOL WINAPI CreateProcess(
  LPCWSTR lpApplicationName,
  LPWSTR lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL bInheritHandles,
  DWORD dwCreationFlags,
  LPVOID lpEnvironment,
  LPCWSTR lpCurrentDirectory,
  LPSTARTUPINFOW lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

下面是函數的參數說明:

  • lpApplicationName:指向一個空字符結束的字符串,指定將要執行的可執行文件的名稱。如果lpApplicationNameNULL,那麼應該將可執行文件的名稱包含在lpCommandLine所指向的字符串中。
  • lpCommandLine:指向一個空字符結束的字符串,該字符串包含了要執行的命令行和參數,用於指定要運行的可執行文件和要傳遞給該進程的命令行參數。
  • lpProcessAttributes:指向PROCESS_ATTRIBUTES結構體,用於指定新進程的安全描述符。
  • lpThreadAttributes:指向THREAD_ATTRIBUTES結構體,用於指定新進程的主線程的安全描述符。
  • bInheritHandles:一個布爾值,指定新進程是否繼承了它的父進程的句柄。
  • dwCreationFlags:指定新進程的創建標誌。一般情況下會指定爲 0。
  • lpEnvironment:指向一個環境塊,用於指定新進程的環境塊。如果爲NULL,則新進程將繼承調用進程的環境塊。
  • lpCurrentDirectory:指向一個空字符結束的字符串,該字符串指定新進程的當前工作目錄。如果爲NULL,則新進程將繼承父進程的當前工作目錄。
  • lpStartupInfo:指向STARTUPINFO結構體,該結構體指定了新進程的主窗口外觀。
  • lpProcessInformation:指向PROCESS_INFORMATION結構體,該結構體返回了新進程的信息,例如新進程的進程標識符和主線程標識符等。

CreateProcess 函數返回一個布爾值,表示函數的調用是否成功。如果成功,則返回值爲非零,否則返回值爲零,並通過調用GetLastError函數獲取錯誤代碼。爲了使得新進程與父進程獨立運行,一般需要用到獨立的進程空間和線程,這通常需要在創建新進程之前調用一些Windows系統API函數,如VirtualAlloc、CreateThread等。

接着來看封裝過的三個內存讀寫函數,其中ReadMemory()用於讀取進程內存數據,WriteMemory()用於寫入內存數據,CheckMemory()則用於驗證兩個內存空間內的字節是否匹配。

// 讀取指定的內存地址
BYTE * ReadMemory(PROCESS_INFORMATION pi, DWORD dwVAddress, int Size)
{
    BYTE bCode = 0;
    BYTE *buffer = new BYTE[Size];

    for (int x = 0; x < Size; x++)
    {
        ReadProcessMemory(pi.hProcess, (LPVOID)dwVAddress, (LPVOID)&bCode, sizeof(BYTE), 0);
        buffer[x] = bCode;
        dwVAddress++;
    }
    return buffer;
}

// 寫入內存特徵
BOOL WriteMemory(PROCESS_INFORMATION pi, DWORD dwVAddress, unsigned char *ShellCode, int Size)
{
    BYTE *Buff = new BYTE[Size];

    SuspendThread(pi.hThread);
    memset(Buff, *ShellCode, Size);
    VirtualProtectEx(pi.hProcess, (LPVOID)dwVAddress, Size, 0x40, 0);
    BOOL Ret = WriteProcessMemory(pi.hProcess, (LPVOID)dwVAddress, Buff, Size, 0);
    if (Ret != 0)
    {
        ResumeThread(pi.hThread);
        return TRUE;
    }
    return FALSE;
}

// 比較內存中前幾個字節是否一致
BOOL CheckMemory(PROCESS_INFORMATION pi, DWORD dwVAddress, BYTE OldCode[], int Size)
{
    BYTE *Buff = new BYTE[Size];
    ReadProcessMemory(pi.hProcess, (LPVOID)dwVAddress, Buff, Size, 0);

    if (!memcmp(Buff, OldCode, Size))
    {
        /*
        for (int x = 0; x < Size; x++)
        {
            printf("內存地址: %x --> 對比地址: %x \n", Buff[x], OldCode[x]);
        }
        */
        return TRUE;
    }
    return FALSE;
}

接下來我們將通過使用特徵碼定位技術來實現對特定內存區域的定位並實現特徵替換,首先我們搜索0x85, 0xed, 0x57, 0x74, 0x07這段特徵值,並定位到0x0402507內存區域,如下圖所示;

當定位到內存區域後,我們首先通過ReadMemory讀取前五個字節的內存,並調用CheckMemory函數用於驗證此片內存區域是否時我們需要修改的,如果驗證一致則通過調用WriteMemory函數向該內存中寫出替換一段0x90, 0x90, 0x90, 0x90, 0x90的指令,最後通過調用ResumeThread恢復線程運行,並以此實現動態內存補丁;

int main(int argc, char *argv[])
{
    // 動態加載進程
    PROCESS_INFORMATION pi = OpenExeFile("d://lyshark.exe");

    // 開始搜索特徵碼
    char ScanOpCode[5] = { 0x85, 0xed, 0x57, 0x74, 0x07 };

    // 依次傳入開始地址,結束地址,特徵碼,以及特徵碼長度
    ULONG Address = ScanMemorySignatureCode(pi.dwProcessId, 0x401000, 0x47FFFF, ScanOpCode, 5);
    printf("[*] 找到內存地址 = 0x%x \n", Address);

    // 讀取位於Address地址處的5條機器指令
    BYTE *recv_buffer = ReadMemory(pi, Address, 5);
    for (int x = 0; x < 5; x++)
    {
        printf("%x ", recv_buffer[x]);
    }
    printf("\n");

    // 比較Address內存中前5個字節是否一致
    BYTE cmp_code[] = { 0x85, 0xed, 0x57, 0x74, 0x07 };
    BOOL ret = CheckMemory(pi, Address, cmp_code, 5);
    if (ret == TRUE)
    {
        printf("[*] 內存一致,可以進行打補丁 \n");
    }
    else
    {
        printf("[-] 不一致 \n");
    }

    // 寫入修補文件
    unsigned char set_buffer[] = { 0x90, 0x90, 0x90, 0x90, 0x90 };
    WriteMemory(pi, Address, set_buffer, 5);

    // 運行修補後的程序
    ResumeThread(pi.hThread);
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);

    system("pause");
    return 0;
}

當調用成功後,讀者可自行跳轉到0x0402507處的內存區域,觀察替換效果,當替換成功後,其內存輸出效果如下圖所示;

本文作者: 王瑞
本文鏈接: https://www.lyshark.com/post/fe53d98c.html
版權聲明: 本博客所有文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章