1. 程式人生 > >惡意程式碼常用技術解析-注入篇

惡意程式碼常用技術解析-注入篇

# 惡意程式碼分析之注入技術 ​ 在很多時候為了能夠對目標程序空間資料進行修改,或者使用目標程序的名稱來執行自己的程式碼,實現危害使用者的操作,通常是將一個`DLL`檔案或者`ShellCode`注入到目標程序中去執行。這裡分享四種常用的注入技術,其中使用`DLL`注入的方法最為普遍。 ## 全域性鉤子注入 ​ 在Windows中大部份的應用程式都是基於訊息機制的,他們都有一個訊息過程函式,根據訊息完成不同的功能。Windows作業系統提供的鉤子機制就是用來截獲和監視這些訊息的。按照鉤子的範圍不同,它們又可以分為區域性鉤子和全域性鉤子,區域性鉤子是針對某個執行緒的;而全域性鉤子則是作用於整個系統的基於訊息的應用。全域性鉤子需要使用`DLL`檔案,在`DLL`中實現相應的鉤子函式。 * 關鍵函式安裝鉤子程式`SetWindowsHookEx()` ```cpp WINUSERAPI HHOOK WINAPI SetWindowsHookExA( _In_ int idHook, // 要安裝的鉤子的型別例如鍵盤 滑鼠 對話方塊等 _In_ HOOKPROC lpfn, // 一個指向鉤子程式的指標 _In_opt_ HINSTANCE hmod,// 包含lpfn引數指向的鉤子過程的DLL控制代碼 _In_ DWORD dwThreadId); // 與鉤子程式相關聯的執行緒識別符號 ``` 成功返回`DLL`控制代碼,失敗返回`NULL` * 解除安裝鉤子函式`UnhookWindowsHookEx` ```cpp BOOL UnhookWindowsHookEx( HHOOK hhk ); ``` * 鉤子回撥函式 ```cpp // 表示將當前的鉤子傳遞給鉤子鏈中的下一個鉤子 LRESULT WINAPI CallNextHookEx( _In_opt_ HHOOK hhk, _In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam); ``` ## 遠端執行緒注入 遠端執行緒注入是指一個程序在另一個程序中建立執行緒的技術,是一種經典的注入技術 * 函式`OpenProcess()`——開啟目標程序 ```cpp HANDLE WINAPI OpenProcess( _In_ DWORD dwDesiredAccess, // 訪問程序物件 _In_ BOOL bInheritHandle, // 若此值為true,則此程序建立的程序將繼承該控制代碼 _In_ DWORD dwProcessId // 要開啟的本地程序的PID ); // 返回值: 若成功返回控制代碼,失敗返回NULL ``` * 函式`VirtualAllocEx()` 指定程序的虛擬地址空間內保留、提交或者更改記憶體的狀態 ```cpp LPVOID WINAPI VirtualAllocEx( _In_ HANDLE hProcess, // 程序控制代碼 _In_opt_ LPVOID lpAddress, // 指定要分配頁面所需的起始指標,為NULL自動分配 _In_ SIZE_T dwSize, // 要分配記憶體的大小 _In_ DWORD flAllocationType, // 記憶體分配的型別:保留、提交和更改 _In_ DWORD flProtect // 頁面區域的記憶體保護 ); // 返回值:函式成功返回分配的基址,失敗返回NULL ``` * 函式`WriteProcessMemory()`——在指定的程序中將資料寫入記憶體區域 ```cpp BOOL WINAPI WriteProcessMemory( _In_ HANDLE hProcess, // 要修改的程序控制代碼 _In_ LPVOID lpBaseAddress, // 指向指定程序中寫入資料的基地址指標 _In_reads_bytes_(nSize) LPCVOID lpBuffer, // 指向緩衝區的指標 _In_ SIZE_T nSize, // 要寫入指定程序的位元組數 _Out_opt_ SIZE_T* lpNumberOfBytesWritten // 指向變數的指標,該變數接收傳輸到指定程序的位元組數 ); // 返回值: 函式成功 != 0;失敗返回0 // 注意:寫入區域的記憶體要可訪問,否則操作失敗 ``` * 函式`CreateRemoteThread()`——實現注入的核心函式在另一個程序的虛擬地址中建立執行的執行緒 ```cpp HANDLE WINAPI CreateRemoteThread( _In_ HANDLE hProcess, // 要建立執行緒的程序的控制代碼 _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, // 指向安全描述符的指標 _In_ SIZE_T dwStackSize, // 堆疊的初始大小,若為0則新執行緒使用可執行檔案的預設大小 _In_ LPTHREAD_START_ROUTINE lpStartAddress, // 指向由執行緒執行型別為LPTHREAD_START_ROUTINE的應用程式定義的函式指標,並表示遠端程序 中執行緒的起始地址,該函式必須存在於遠端程序中 _In_opt_ LPVOID lpParameter, // 要傳遞給執行緒函式的變數的指標 _In_ DWORD dwCreationFlags, // 控制執行緒建立的標誌 _Out_opt_ LPDWORD lpThreadId // 接收執行緒標誌符變數的指標 ); // 返回值: 成功 新執行緒的控制代碼,失敗:返回NULL ``` ​ 從以上這些函式的作用我們實現的原理就很清晰了,先在指定程序申請一段地址然後將準備好的`shellcode`或者一個`DLL`檔案寫入到這塊記憶體空間中。 ​ 注意:對於一些系統服務這樣通常會注入失敗,由於系統存在SESSION 0隔離的安全機制,需呼叫一個更加底層的`ZwCreateThreadEx()`來實現。 ## `APC`佇列注入 ​ **`APC`**(`Asynchronus Procedure Call`)為非同步過程呼叫,是指函式在特定執行緒中被非同步執行。在Windows系統中,`APC`是一種併發機制,用於非同步IO或者定時器。 每一個執行緒都有自己的`APC`佇列,使用`QueueUserAPC`函式把一個`APC`函式壓入`APC`佇列中。當處於使用者模式的`APC`壓入執行緒`APC`佇列後,該執行緒並不直接呼叫`APC`函式,除非該執行緒處於可通知狀態,呼叫的順序為先入先出。 * 函式 ```cpp WINBASEAPI DWORD WINAPI QueueUserAPC( _In_ PAPCFUNC pfnAPC, // 指向APC函式的指標 _In_ HANDLE hThread, // 執行緒控制代碼 _In_ ULONG_PTR dwData // 由pfnAPC引數指向的APC函式的單個值 ); // 返回值 成功非0; 失敗返回0 ``` ​ `APC`的注入原理是利用當執行緒被喚醒時`APC`中的註冊函式會執行的機制,並以此去執行`DLL`載入程式碼,進而完成`DLL`注入。為了增加成功率,可以向目標程序中的所有執行緒都插入`APC`。 ## 自定義HOOK * 自定義HOOK大致可以分為兩類 * `inlineHOOK` * `IATHOOK` * `inlineHook`是一種通過修改機器碼的方式來實現HOOK的技術 **原理:**對於一個正常的程式如下圖,通過`CALL`指令來呼叫函式。關於`CALL`指令相當於`push` 當前函式地址和`jmp`要執行的指令位置,即 `push 0171B7B3` `jmp 0171B430`,這是我們正常執行`00.0171B430`這個函式的樣子。 ![](https://img2020.cnblogs.com/blog/1766430/202006/1766430-20200630225724016-1035359478.png) 我們在hook的時候就是將CALL指令直接改成`jmp`指令,跳到我們自己編寫的函式的位置,執行完成之後跳回函式原來指令的下一條指令`0171B7B3`,需要注意的是跳轉偏移要多計算5個位元組 **計算公式:** 跳轉偏移 = 目標地址 - `jmp`所在的地址 - **5** * 實現方法 1. 獲取函式的實際地址 2. 修改記憶體分頁屬性 3. 計算跳轉偏移,修改目標地址,還原記憶體屬性 4. 獲取實際地址返回 ```cpp void OnHook() { //獲取函式實際地址 HMODULE Module = GetModuleHandleA("kernel32.dll"); LPVOID func = GetProcAddress(Module, "OpenProcess"); //儲存5個位元組 memcpy(g_oldCode, func, 5); //修改記憶體分頁屬性,由於程式碼段是不可寫的,所有必須先將它的屬性變成可寫 DWORD dwProtect; VirtualProtect(func, 5, PAGE_EXECUTE_READWRITE, &dwProtect); //計算跳轉偏移 *(DWORD*)&g_newCode[1] = (DWORD)MyOpenProcess - (DWORD)func - 5; //修改目標地址 memcpy(func, g_newCode, 5); //還原記憶體分頁屬性 VirtualProtect(func, 5, dwProtect, &dwProtect); }; ``` * 使用者層的`IATHook`是通過替換`IAT`表中函式的原始地址從而實現的Hook ​ 與普通的`InlineHook`不一樣,`IATHook`需要充分理解PE檔案的結構才能完成,關於相對虛擬地址(`RVA`)、檔案偏移地址(`FOA`)和載入基址等概念可以自行查閱相關資料。 * 實現方法 ```cpp //獲取指定dll匯出地址表的中函式地址 DWORD * GetIatAddress(const char * dllName, const char* funName) { // 1. 獲取載入基址並轉換成DOS頭 auto DosHeader = (PIMAGE_DOS_HEADER)GetModuleHandle(NULL); // 2. 通過 DOS 頭的後一個欄位 e_lfanew 找到 NT 頭的偏移 auto NtHeader = (PIMAGE_NT_HEADERS)(DosHeader->e_lfanew + (DWORD)DosHeader); // 3. 在資料目錄表下標為[1]的地方找到匯入表的RVA DWORD ImpRVA = NtHeader->OptionalHeader.DataDirectory[1].VirtualAddress; // 4. 獲取到匯入表結構體,因為程式已經運行了,所以不需要轉FOA auto ImpTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)DosHeader + ImpRVA); // 遍歷匯入表,以一組全0的結構結尾 while (ImpTable->Name) { // 獲取當前匯入表結構描述的結構體的名稱 CHAR* Name = (CHAR*)(ImpTable->Name + (DWORD)DosHeader); // 忽略大小寫進行比較,檢視是否是需要的匯入表結構 if (!_stricmp(Name, dllName)) { // 找到對應的 INT 表以及 IAT 表 DWORD* IntTable = (DWORD*)((DWORD)DosHeader + ImpTable->OriginalFirstThunk); DWORD* IatTable = (DWORD*)((DWORD)DosHeader + ImpTable->FirstThunk); // 遍歷所有的函式名稱,包括有/沒有名稱 for (int i = 0; IntTable[i] != 0; ++i) { // 比對函式是否存在函式名稱表中 if ((IntTable[i] & 0x80000000) == 0) { // 獲取到匯入名稱結構 auto Name = (PIMAGE_IMPORT_BY_NAME)((DWORD)DosHeader + IntTable[i]); // 比對函式的名稱 if (!strcmp(funName, Name->Name)) { // 返回函式在IAT中儲存的地址 return &IatTable[i]; } } } } ImpTable++; } return 0; } ``` ## 總結 ​ 鉤子技術總結起來就是通過各種手段來修改**程式碼**或者**地址**從而讓程式來執行我們自己編寫的程式碼,在分析惡意程式時關注一下這些敏感的`API`函式組合,在檢視程式基本資訊的時候就可以大致做出猜測。下一篇繼續分享常見的啟動和隱藏技術,繼續刨析病毒的實現