惡意程式碼常用技術解析-注入篇
阿新 • • 發佈:2020-07-01
# 惡意程式碼分析之注入技術
在很多時候為了能夠對目標程序空間資料進行修改,或者使用目標程序的名稱來執行自己的程式碼,實現危害使用者的操作,通常是將一個`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`函式組合,在檢視程式基本資訊的時候就可以大致做出猜測。下一篇繼續分享常見的啟動和隱藏技術,繼續刨析病毒的實現