惡意代碼分析之注入技術
在很多時候為了能夠對目標進程空間數據進行修改,或者使用目標進程的名稱來執行自己的代碼,實現危害用戶的操作,通常是將一個DLL
文件或者ShellCode
注入到目標進程中去執行。這里分享四種常用的注入技術,其中使用DLL
注入的方法最為普遍。
全局鈎子注入
在Windows中大部份的應用程序都是基於消息機制的,他們都有一個消息過程函數,根據消息完成不同的功能。Windows操作系統提供的鈎子機制就是用來截獲和監視這些消息的。按照鈎子的范圍不同,它們又可以分為局部鈎子和全局鈎子,局部鈎子是針對某個線程的;而全局鈎子則是作用於整個系統的基於消息的應用。全局鈎子需要使用DLL
文件,在DLL
中實現相應的鈎子函數。
- 關鍵函數安裝鈎子程序
SetWindowsHookEx()
WINUSERAPI
HHOOK
WINAPI SetWindowsHookExA(
_In_ int idHook, // 要安裝的鈎子的類型例如鍵盤 鼠標 對話框等
_In_ HOOKPROC lpfn, // 一個指向鈎子程序的指針
_In_opt_ HINSTANCE hmod,// 包含lpfn參數指向的鈎子過程的DLL句柄
_In_ DWORD dwThreadId); // 與鈎子程序相關聯的線程標識符
成功返回DLL
句柄,失敗返回NULL
- 卸載鈎子函數
UnhookWindowsHookEx
BOOL UnhookWindowsHookEx(
HHOOK hhk
);
- 鈎子回調函數
// 表示將當前的鈎子傳遞給鈎子鏈中的下一個鈎子
LRESULT
WINAPI CallNextHookEx(
_In_opt_ HHOOK hhk,
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam);
遠程線程注入
遠程線程注入是指一個進程在另一個進程中創建線程的技術,是一種經典的注入技術
- 函數
OpenProcess()
——打開目標進程
HANDLE
WINAPI OpenProcess(
_In_ DWORD dwDesiredAccess, // 訪問進程對象
_In_ BOOL bInheritHandle, // 若此值為true,則此進程創建的進程將繼承該句柄
_In_ DWORD dwProcessId // 要打開的本地進程的PID
);
// 返回值: 若成功返回句柄,失敗返回NULL
- 函數
VirtualAllocEx()
指定進程的虛擬地址空間內保留、提交或者更改內存的狀態
LPVOID
WINAPI
VirtualAllocEx(
_In_ HANDLE hProcess, // 進程句柄
_In_opt_ LPVOID lpAddress, // 指定要分配頁面所需的起始指針,為NULL自動分配
_In_ SIZE_T dwSize, // 要分配內存的大小
_In_ DWORD flAllocationType, // 內存分配的類型:保留、提交和更改
_In_ DWORD flProtect // 頁面區域的內存保護
);
// 返回值:函數成功返回分配的基址,失敗返回NULL
- 函數
WriteProcessMemory()
——在指定的進程中將數據寫入內存區域
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()
——實現注入的核心函數在另一個進程的虛擬地址中創建運行的線程
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
函數,除非該線程處於可通知狀態,調用的順序為先入先出。
- 函數
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
這個函數的樣子。
我們在hook的時候就是將CALL指令直接改成jmp
指令,跳到我們自己編寫的函數的位置,執行完成之后跳回函數原來指令的下一條指令0171B7B3
,需要注意的是跳轉偏移要多計算5個字節
計算公式: 跳轉偏移 = 目標地址 - jmp
所在的地址 - 5
- 實現方法
- 獲取函數的實際地址
- 修改內存分頁屬性
- 計算跳轉偏移,修改目標地址,還原內存屬性
- 獲取實際地址返回
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
)和加載基址等概念可以自行查閱相關資料。
- 實現方法
//獲取指定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
函數組合,在查看程序基本信息的時候就可以大致做出猜測。下一篇繼續分享常見的啟動和隱藏技術,繼續剖析病毒的實現原理。