原理:
DLL注入技術,一般來講是向一個正在運作的程序插入/注入代碼的過程。我們注入的代碼以動态連結庫(DLL)的形式存在。
dll注入即是讓程式A強行加載程式B給定的a.dll,并執行程式B給定的a.dll裡面的代碼。注意,程式B所給定的a.dll原先并不會被程式A主動加載,但是當程式B通過某種手段讓程式A“加載”a.dll後,程式A将會執行a.dll裡的代碼,此時,a.dll就進入了程式A的位址空間,而a.dll子產品的程式邏輯由程式B的開發者設計,是以程式B的開發者可以對程式A為所欲為。
實作:
加載Dll的API就是LoadLibrary,它的參數是儲存要加載的DLL的路徑的位址。是以DLL注入的核心就是把要注入的DLL的路徑寫到目标程序中,然後在目标程序中調用LoadLibrary函數,并且指定參數為儲存了DLL路徑的位址。
LoadLibrary函數
HMODULE WINAPI LoadLibrary(__in LPCTSTR lpFileName);
這個函數同樣也隻需要一個參數,這個參數是一個位址,而這個位址中儲存的是我們要加載的DLL的名稱的字元串
跟exe有個main或者WinMain入口函數一樣,DLL也有一個入口函數,就是DllMain。當使用LoadLibrary函數加載DLL時,系統會調用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:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
會有如下檔案,當DLL的狀态發生變化的時候,就會調用DllMain函數。而傳遞的ul_reason_for_call這個參數代表了4種不同的狀态變化的情況,我們就可以根據這四種不同的狀态根據需要來寫出相應的代碼,就會讓注入的DLL執行我們需要的功能
ul_reason_for_call的值 | 代表的狀态 |
DLL_PROCESS_ATTACH | Dll剛剛映射到程序空間中 |
DLL_THREAD_ATTACH | 程序中有新線程建立 |
DLL_THREAD_DETACH | 程序中有新線程銷毀 |
DLL_PROCESS_DETACH | Dll從程序空間中接觸映射 |
代碼架構:
#include <cstdio>
#include <Windows.h>
#include <TlHelp32.h>
#define PROCESS_NAME "Taskmgr.exe" //要注入的程序名,這個是任務管理器的程序名
#define DLL_NAME "InjectDll.dll" //要注入的DLL的名稱
BOOL InjectDll(DWORD dwPid, CHAR szDllName[]); //注入DLL
DWORD GetPID(PCHAR pProName); //根據程序名擷取PID
VOID ShowError(PCHAR msg); //列印錯誤資訊
BOOL EnbalePrivileges(HANDLE hProcess, char *pszPrivilegesName); //提升程序權限
int main()
{
CHAR szDllPath[MAX_PATH] = { 0 }; //儲存要注入的DLL的路徑
DWORD dwPID = 0; //儲存要注入的程序的PID
// 提升目前程序令牌權限
if (!EnbalePrivileges(GetCurrentProcess(), SE_DEBUG_NAME))
{
printf("權限提升失敗\n");
}
dwPID = GetPID(PROCESS_NAME);
if (dwPID == 0)
{
printf("沒有找到要注入的程序\n");
goto exit;
}
GetCurrentDirectory(MAX_PATH, szDllPath); //擷取程式的目錄
strcat(szDllPath, "\\");
strcat(szDllPath, DLL_NAME); //與DLL名字拼接得到DLL的完整路徑
printf("要注入的程序名:%s PID:%d\n", PROCESS_NAME, dwPID);
printf("要注入的DLL的完整路徑%s\n", szDllPath);
if (InjectDll(dwPID, szDllPath))
{
printf("Dll注入成功\n");
}
exit:
system("pause");
return 0;
}
BOOL InjectDll(DWORD dwPid, CHAR szDllName[])
{
BOOL bRet = TRUE;
return bRet;
}
DWORD GetPID(PCHAR pProName)
{
PROCESSENTRY32 pe32 = { 0 };
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
BOOL bRet = FALSE;
DWORD dwPID = 0;
if (hSnap == INVALID_HANDLE_VALUE)
{
printf("CreateToolhelp32Snapshot process %d\n", GetLastError());
goto exit;
}
pe32.dwSize = sizeof(pe32);
bRet = Process32First(hSnap, &pe32);
while (bRet)
{
if (lstrcmp(pe32.szExeFile, pProName) == 0)
{
dwPID = pe32.th32ProcessID;
break;
}
bRet = Process32Next(hSnap, &pe32);
}
CloseHandle(hSnap);
exit:
return dwPID;
}
VOID ShowError(PCHAR msg)
{
printf("%s Error %d\n", msg, GetLastError());
}
BOOL EnbalePrivileges(HANDLE hProcess, char *pszPrivilegesName)
{
HANDLE hToken = NULL;
LUID luidValue = { 0 };
TOKEN_PRIVILEGES tokenPrivileges = { 0 };
BOOL bRet = FALSE;
DWORD dwRet = 0;
// 打開程序令牌并擷取具有 TOKEN_ADJUST_PRIVILEGES 權限的程序令牌句柄
if (!OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES, &hToken))
{
ShowError("OpenProcessToken");
goto exit;
}
// 擷取本地系統的 pszPrivilegesName 特權的LUID值
if (!LookupPrivilegeValue(NULL, pszPrivilegesName, &luidValue))
{
ShowError("LookupPrivilegeValue");
goto exit;
}
// 設定提升權限資訊
tokenPrivileges.PrivilegeCount = 1;
tokenPrivileges.Privileges[0].Luid = luidValue;
tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
// 提升程序令牌通路權限
if (!AdjustTokenPrivileges(hToken, FALSE, &tokenPrivileges, 0, NULL, NULL))
{
ShowError("AdjustTokenPrivileges");
goto exit;
}
else
{
// 根據錯誤碼判斷是否特權都設定成功
dwRet = ::GetLastError();
if (ERROR_SUCCESS == dwRet)
{
bRet = TRUE;
goto exit;
}
else if (ERROR_NOT_ALL_ASSIGNED == dwRet)
{
ShowError("ERROR_NOT_ALL_ASSIGNED");
goto exit;
}
}
exit:
return bRet;
}
遠端線程注入:
遠端線程注入簡單的說,就是調用CreateRemoteThread(),在其他的程序中建立一條線程,執行LoadLibrary(),将特定的DLL加載到程序中間.
由于我們不能輕易的控制别人程序中的線程,是以這種方法要求我們在目标程序中建立一個線程并線上程中執行LoadLibrary函數加載我們要注入的dll。幸運的是Windows為我們提供了CreateRemoteThread函數,它使得在另一個程序中建立一個線程變得非常容易。
HANDLE WINAPI CreateRemoteThread(
_In_ HANDLE hProcess, /要建立線程的程序句柄
_In_ LPSECURITY_ATTRIBUTES lpThreadAttributes,/新線程的安全描述符
_In_ SIZE_T dwStackSize, /堆棧起始大小,為0表示預設大小
_In_ LPTHREAD_START_ROUTINE lpStartAddress,/表示要運作線程的起始位址
_In_ LPVOID lpParameter,/儲存要傳遞給線程參數的位址
_In_ DWORD dwCreationFlags,/控制線程建立的标志,為0表示建立後立即執行
_Out_ LPDWORD lpThreadId/指向接收線程辨別符變量的指針。為NULL表示不傳回線程辨別符
);
- hProcess用來指定在哪個程序中建立新線程
- lpStartAddress用來指定将程序中的哪個位址開始作為新線程運作的起始位址
- lpParameter儲存的也是一個位址,這個位址中儲存的就是新線程要用到的參數
隻要我們可以擷取新程序中的LoadLibrary函數的位址以及包含有要加載的DLL的字元串的位址就可以通過CreateRemoteThread函數來成功開起一個線程執行LoadLibrary函數來加載我們的DLL。
那麼現在的問題就是如何獲得LoadLibrary函數的位址以及儲存有要加載的DLL路徑的字元串的位址。
對于LoadLibrary函數,由于它是在常用的系統DLL,也就是KERNEL32.dll中,是以這個DLL是可以按照它的ImageBase成功裝載到每個程序的空間中。這樣的話Kernel32.dll在每個程序中的起始位址是一樣的,那麼LoadLibrary函數的位址也就會一樣。那麼我們就可以在本程序中查找LoadLibrary函數的位址,并且完全可以相信,在要注入DLL的程序中LoadLibrary的位址也是這個。
對于LoadLibrary函數,由于它是在常用的系統DLL,也就是KERNEL32.dll中,是以這個DLL是可以按照它的ImageBase成功裝載到每個程序的空間中。這樣的話Kernel32.dll在每個程序中的起始位址是一樣的,那麼LoadLibrary函數的位址也就會一樣。那麼我們就可以在本程序中查找LoadLibrary函數的位址,并且完全可以相信,在要注入DLL的程序中LoadLibrary的位址也是這個。
至于DLL名稱的字元串,我們可以通過在程序中申請一塊可以将DLL完整路徑寫入的記憶體,并在這個記憶體中将DLL的完整路徑寫入,将寫入到注入程序DLL完整路徑的記憶體位址作為參數就可以實作程序的注入
注意:在開始注入前,還需要确認一件事,就是目标程序使用的字元編碼方式。因為我們所調用的LoadLibrary函數在底層實際調用有兩種可能:
如果目标程式使用的是ANSI編碼方式,LoadLibrary實際調用的是LoadLibraryA,其參數字元串應當是ANSI編碼;
如果目标程式使用的是Unicode編碼方式,LoadLibrary實際調用的是LoadLibraryW,其參數字元串應當是Unicode編碼。
這使得注入過程變得很麻煩,為了減少複雜性,不妨直接使用LoadLibraryA或LoadLibraryW而不是用LoadLibrary函數來避免這一麻煩。另外,即使使用的是LoadLibraryA,LoadLibraryA也會将傳入的ANSI編碼的字元串參數轉換成Unicode編碼後再調用LoadLibraryW。綜上,不妨一緻使用LoadLibraryW函數,并且字元串用Unicode編碼即可。
最後,我們可能會為獲得目标程序中LoadLibraryW函數的起始位址而頭疼,但其實這個問題也很簡單,因為目标程序中函數LoadLibraryW的起始位址和我們的程序中的LoadLibraryW函數的起始位址是一樣的。是以我們隻需要用GetProcAddress即可獲得LoadLibraryW函數的起始位址。
步驟:
(1).用VirtualAllocEx函數在目标程序的位址空間中配置設定一塊足夠大的記憶體用于儲存被注入的dll的路徑。
(2).用WriteProcessMemory函數把本程序中儲存dll路徑的記憶體中的資料拷貝到第(1)步得到的目标程序的記憶體中。
(3).用GetProcAddress函數獲得LoadLibraryW函數的起始位址。LoadLibraryW函數位于Kernel32.dll中。
(4).用CreateRemoteThread函數讓目标程序執行LoadLibraryW來加載被注入的dll。函數結束将傳回載入dll後的子產品句柄。
(5).用VirtualFreeEx釋放第(1)步開辟的記憶體。
在需要解除安裝dll時我們可以在上述第(5)步的基礎上繼續執行以下步驟:
(6).用GetProcAddress函數獲得FreeLibrary函數的起始位址。FreeLibrary函數位于Kernel32.dll中。
(7).用CreateRemoteThread函數讓目标程序執行FreeLibrary來解除安裝被注入的dll。(其參數是第(4)步傳回的子產品句柄)。
BOOL InjectDll(DWORD dwPid, CHAR szDllName[])
{
BOOL bRet = TRUE;
HANDLE hProcess = NULL, hRemoteThread = NULL;
HMODULE hKernel32 = NULL;
DWORD dwSize = 0;
LPVOID pDllPathAddr = NULL;
PVOID pLoadLibraryAddr = NULL;
// 打開注入程序,擷取程序句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (NULL == hProcess)
{
ShowError("OpenProcess");
bRet = FALSE;
goto exit;
}
// 在注入程序中申請可以容納DLL完成路徑名的記憶體空間
dwSize = 1 + strlen(szDllName);
pDllPathAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
if (!pDllPathAddr)
{
ShowError("VirtualAllocEx");
bRet = FALSE;
goto exit;
}
// 把DLL完整路徑名寫入程序中
if (!WriteProcessMemory(hProcess, pDllPathAddr, szDllName, dwSize, NULL))
{
ShowError("WriteProcessMemory");
bRet = FALSE;
goto exit;
}
hKernel32 = LoadLibrary("kernel32.dll");
if (hKernel32 == NULL)
{
ShowError("LoadLibrary");
bRet = FALSE;
goto exit;
}
// 擷取LoadLibraryA函數位址
pLoadLibraryAddr = GetProcAddress(hKernel32, "LoadLibraryA");
if (pLoadLibraryAddr == NULL)
{
ShowError("GetProcAddress ");
bRet = FALSE;
goto exit;
}
//建立遠端線程進行DLL注入
hRemoteThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)pLoadLibraryAddr,
pDllPathAddr, 0, NULL);
if (hRemoteThread == NULL)
{
ShowError("CreateRemoteThread");
bRet = FALSE;
goto exit;
}
exit:
if (hKernel32) FreeLibrary(hKernel32);
if (hProcess) CloseHandle(hProcess);
if (hRemoteThread) CloseHandle(hRemoteThread);
return bRet;
}
補充:
上面的方法雖然可以友善的注入DLL。但是在WIN7,WIN10系統上,會由于SESSION 0隔離機制進而導緻隻能成功注入普通的使用者程序,如果注入系統程序就會導緻失敗。而經過逆向分析發現,使用Kernel32.dll中的CreateRemoteThread進行注入的時候,程式會走到ntdll.dll中的ZwCreateThreadEx函數進行執行。這是一個未導出的函數,是以需要手動擷取函數位址來進行調用,相比于CreateRemoteThread更加底層。這個函數在64位和32位系統中的函數聲明也不相同,在64位中的聲明如下
typedef DWORD(WINAPI *pFnZwCreateThreadEx)(
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);
它會導緻線程建立的時候就被挂起,随後檢視要運作的程序所在的會話層之後再決定是否要恢複線程的運作。是以要破解這種情況隻需要将第7個參數設為0就可以,相應代碼如下
typedef DWORD(WINAPI *pFnZwCreateThreadEx)(PHANDLE, ACCESS_MASK, LPVOID,
HANDLE, LPTHREAD_START_ROUTINE,
LPVOID, BOOL, DWORD, DWORD, DWORD, LPVOID);
BOOL InjectDll(DWORD dwPid, CHAR szDllName[])
{
BOOL bRet = TRUE;
HANDLE hProcess = NULL, hRemoteThread = NULL;
HMODULE hKernel32 = NULL, hNtDll = NULL;
DWORD dwSize = 0;
LPVOID pDllPathAddr = NULL;
PVOID pLoadLibraryAddr = NULL;
pFnZwCreateThreadEx ZwCreateThreadEx = NULL;
// 打開注入程序,擷取程序句柄
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (NULL == hProcess)
{
ShowError("OpenProcess");
bRet = FALSE;
goto exit;
}
// 在注入程序中申請可以容納DLL完成路徑名的記憶體空間
dwSize = 1 + strlen(szDllName);
pDllPathAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
if (!pDllPathAddr)
{
ShowError("VirtualAllocEx");
bRet = FALSE;
goto exit;
}
// 把DLL完成路徑名寫入程序中
if (!WriteProcessMemory(hProcess, pDllPathAddr, szDllName, dwSize, NULL))
{
ShowError("WriteProcessMemory");
bRet = FALSE;
goto exit;
}
hKernel32 = LoadLibrary("kernel32.dll");
if (hKernel32 == NULL)
{
ShowError("LoadLibrary kernel32");
bRet = FALSE;
goto exit;
}
// 擷取LoadLibraryA函數位址
pLoadLibraryAddr = GetProcAddress(hKernel32, "LoadLibraryA");
if (pLoadLibraryAddr == NULL)
{
ShowError("GetProcAddress LoadLibraryA");
bRet = FALSE;
goto exit;
}
hNtDll = LoadLibrary("ntdll.dll");
if (hNtDll == NULL)
{
ShowError("LoadLibrary ntdll");
bRet = FALSE;
goto exit;
}
ZwCreateThreadEx = (pFnZwCreateThreadEx)GetProcAddress(hNtDll, "ZwCreateThreadEx");
if (!ZwCreateThreadEx)
{
ShowError("GetProcAddress ZwCreateThreadEx");
bRet = FALSE;
goto exit;
}
ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL,
hProcess, (LPTHREAD_START_ROUTINE)pLoadLibraryAddr,
pDllPathAddr, 0, 0, 0, 0, NULL);
if (hRemoteThread == NULL)
{
ShowError("ZwCreateThreadEx");
bRet = FALSE;
goto exit;
}
exit:
if (hKernel32) FreeLibrary(hKernel32);
if (hNtDll) FreeLibrary(hNtDll);
if (hProcess) CloseHandle(hProcess);
if (hRemoteThread) CloseHandle(hRemoteThread);
return bRet;
}
APC注入:
APC 是一個簡稱,具體名字叫做異步過程調用,我們看下MSDN中的解釋,異步過程調用,屬于是同步對象中的函數,是以去同步對象中檢視.
異步函數調用的原理:
異步過程調用是一種能在特定線程環境中異步執行的系統機制。
異步執行:
A.所有異步程式的執行,都會在同步程式之後在執行
B.異步程式自己之間的執行順序,如果時間是相同的,那麼就是按代碼的先後順序執 行,否則是時間短的會先執行。
1,從一行代碼開始執行程式
2,同步程式正常執行
3,如果發現是異步程式,暫時不執行,存儲在異步池中,等待執行
4,将程式中所有的同步程式執行完畢後
5,開啟異步池,執行異步程式,當設定的時間到達,就會執行對應的異步升序
先到設定時間的異步程式先執行, 如果設定的時間相同,看異步程式的順序,來執行
回調函數:
函數也可以作為函數的參數來傳遞
你到一個商店買東西,剛好你要的東西沒有貨,于是你在店員那裡留下了你的電話,過了幾天店裡有貨了,店員就打了你的電話,然後你接到電話後就到店裡去取了貨。
在這個例子裡,你的電話号碼就叫回調函數,你把電話留給店員就叫登記回調函數,店裡後來有貨了叫做 觸發回調事件,店員給你打電話叫做 調用回調函數,你到店裡去取貨叫做 響應回調事件。
程式的APC
往線程APC隊列添加APC,系統會産生一個軟中斷。線上程下一次被排程的時候,就會執行APC函數,APC有兩種形式,由系統産生的APC稱為核心模式APC,由應用程式産生的APC被稱為使用者模式APC
那麼使用APC場合的注入就有了,
1.必須是多線程環境下
2.注入的程式必須會調用上面的那些同步對象.
那麼我們可以注入APC,注意下條件,也不是所有都能注入的.
注入方法的原理:
1.當對面程式執行到某一個上面的等待函數的時候,系統會産生一個中斷
2.當線程喚醒的時候,這個線程會優先去Apc隊列中調用回調函數
3.我們利用QueueUserApc,往這個隊列中插入一個回調
4.插入回調的時候,把插入的回調位址改為LoadLibrary,插入的參數我們使用VirtualAllocEx申請記憶體,并且寫入進去
使用方法:
1.利用快照枚舉所有的線程
2.寫入遠端記憶體,寫入的是Dll的路徑
3.插入我們的DLL即可
要往APC隊列中增加APC函數,需要通過QueueUserAPC函數來實作,這個函數在文檔中的定義如下
DWORD WINAPI QueueUserAPC(
__in PAPCFUNC pfnAPC, /當滿足條件時,要執行的APC函數的位址
__in HANDLE hThread, /指定增加APC函數的線程句柄
__in ULONG_PTR dwData); /要執行的APC函數參數位址
可以看到pfnAPC和dwData這兩個參數和CreateRemoteThread中的lpStartAddress和lpParameter的作用是一樣的。不過這裡是對線程進行操作,一個程序有多個線程。是以為了確定程式正确運作,是以需要周遊所有線程,檢視是否是要注入的程序的線程,依次獲得句柄插入APC函數。具體代碼如下
BOOL InjectDll(DWORD dwPid, CHAR szDllName[])
{
BOOL bRet = TRUE;
HANDLE hProcess = NULL, hThread = NULL, hSnap = NULL;
HMODULE hKernel32 = NULL;
DWORD dwSize = 0;
PVOID pDllPathAddr = NULL;
PVOID pLoadLibraryAddr = NULL;
THREADENTRY32 te32 = { 0 };
// 打開注入程序,擷取程序句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (NULL == hProcess)
{
ShowError("OpenProcess");
bRet = FALSE;
goto exit;
}
// 在注入程序中申請可以容納DLL完成路徑名的記憶體空間
dwSize = 1 + strlen(szDllName);
pDllPathAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
if (!pDllPathAddr)
{
ShowError("VirtualAllocEx");
bRet = FALSE;
goto exit;
}
// 把DLL完成路徑名寫入程序中
if (!WriteProcessMemory(hProcess, pDllPathAddr, szDllName, dwSize, NULL))
{
ShowError("WriteProcessMemory");
bRet = FALSE;
goto exit;
}
hKernel32 = LoadLibrary("kernel32.dll");
if (hKernel32 == NULL)
{
ShowError("LoadLibrary");
bRet = FALSE;
goto exit;
}
// 擷取LoadLibraryA函數位址
pLoadLibraryAddr = GetProcAddress(hKernel32, "LoadLibraryA");
if (pLoadLibraryAddr == NULL)
{
ShowError("GetProcAddress");
bRet = FALSE;
goto exit;
}
//獲得線程快照
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (!hSnap)
{
ShowError("CreateToolhelp32Snapshot");
bRet = FALSE;
goto exit;
}
//周遊線程
te32.dwSize = sizeof(te32);
if (Thread32First(hSnap, &te32))
{
do
{
//這個線程的程序ID是不是要注入的程序的PID
if (te32.th32OwnerProcessID == dwPid)
{
hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);
if (hThread)
{
QueueUserAPC((PAPCFUNC)pLoadLibraryAddr, hThread, (ULONG_PTR)pDllPathAddr);
CloseHandle(hThread);
hThread = NULL;
}
else
{
ShowError("OpenThread");
bRet = FALSE;
goto exit;
}
}
} while (Thread32Next(hSnap, &te32));
}
exit:
if (hKernel32) FreeLibrary(hKernel32);
if (hProcess) CloseHandle(hProcess);
if (hThread) CloseHandle(hThread);
return bRet;
}
系統資料庫注入:
什麼是系統資料庫
系統資料庫是windows作業系統、硬體裝置以及客戶應用程式得以正常運作和儲存設定的核心“資料庫”,也可以說是一個非常巨大的樹狀分層結構的資料庫系統。
注入原理:
在cmd輸入regedit打開系統資料庫。定位HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows項
是一種比較簡單的注入,主要依賴于倆個表項,AppInit_Dlls和LoadAppInit_DLLs。AppInit_Dlls寫入dll完整路徑,LoadAppInit_DLLs寫為1,重新開機後,指定DLL會注入到所有運作程序。
AppInit_DLLs鍵的值可以是一個dll的檔案名或一組dll的檔案名(通過逗号或空格來分隔),由于空格是用來分隔檔案名的,是以dll檔案名不能含有空格。第一個dll的檔案名可以包含路徑,但其他的dll包含的路徑将被忽略。
LoadAppInit_DLLs鍵的值表示AppInit_DLLs鍵是否有效,為了讓AppInit_DLLs鍵的值有效,需要将LoadAppInit_DLLs的值設定為1。
這兩個鍵值設定後,當應用程式啟動并加載User32.dll時,會獲得上述系統資料庫鍵的值,并調用LoadLibrary來調用這些字元串中指定的每一個dll。這時每個被載入的dll可以完成相應的初始化工作。但是需要注意的是,由于被注入的dll是在程序生命期的早期被載入的,是以這些dll在調用函數時應慎重。調用Kernel32.dll中的函數應該沒有問題,因為Kernel32.dll是在User32.dll載入前已被加載。但是調用其他的dll中的函數時應當注意,因為程序可能還未載入相應的dll,嚴重時可能會導緻藍屏。
這種方法很簡單,隻需要在系統資料庫中修改兩個鍵的值即可,但是有如下缺點:
1.隻有調用了User32.dll的程序才會發生這種dll注入。也就是說某些CUI程式(控制台應用程式)可能無法完成dll注入,比如将dll注入到編譯器或連結器中是不可行的。
2.該方法會使得所有的調用了User32.dll的程式都被注入指定的dll,如果你僅僅想對某些程式注入dll,這樣很多程序将成為無辜的被注入着,并且其他程式你可能并不了解,盲目的注入會使得其他程式發生崩潰的可能性增大。
3.這種注入會使得在應用程式的整個生命周期内被注入的dll都不會被解除安裝。注入dll的原則是值在需要的時間才注入我們的dll,并在不需要時及時解除安裝。
注入流程
第一步:打開系統資料庫鍵值如下:
HKEY_LOCAL_MACHINE\SoftWare\MicroSoft\Windows NT\CurrentVersion\Windows
第二步:修改AppInit_Dlls
在該鍵值中添加自己的DLL的全路徑加dll名,多個DLL以逗号或者空格分開(是以在檔案名中應該盡量不要存在空格),該鍵值隻有第一個dll檔案名可以包含路徑,後面的都不能包含,是以我們最好将dll放在系統路徑 下,這樣就可以不用包含路徑也能被加載了。
第三步:修改LoadAppInit_DLLs
在該系統資料庫項中添加鍵值 LoadAppInit_DLLs ,類型為 DWORD,并将其值置為 1 .
代碼實作
BOOL InjectDll(DWORD dwPid, CHAR szDllName[])
{
BOOL bRet = TRUE;
HKEY hKey = NULL;
CHAR szAppKeyName[] = { "AppInit_DLLs" };
CHAR szLoadAppKeyName[] = { "LoadAppInit_DLLs" };
DWORD dwLoadAppInit = 1; //設定LoadAppInit_DLLs的值
//打開相應系統資料庫鍵
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows",
0, KEY_ALL_ACCESS, &hKey) != ERROR_SUCCESS)
{
ShowError("RegOpenKeyEx");
bRet = FALSE;
goto exit;
}
//設定AppInit_DLLs為相應的DLL路徑
if (RegSetValueEx(hKey, szAppKeyName, 0, REG_SZ, (PBYTE)szDllName, strlen(szDllName) + 1) != ERROR_SUCCESS)
{
ShowError("RegSetValueEx");
bRet = FALSE;
goto exit;
}
//将LoadAppInit_DLLs的值設為1
if (RegSetValueEx(hKey, szLoadAppKeyName, 0, REG_DWORD, (PBYTE)&dwLoadAppInit, sizeof(dwLoadAppInit)) != ERROR_SUCCESS)
{
ShowError("RegSetValueEx");
bRet = FALSE;
goto exit;
}
exit:
return bRet;
}
全局鈎子注入:
Windows系統中的大多數應用都是基于消息機制的,也就是說它們都有一個消息過程函數,可以根據收到的不同消息來執行不同的代碼。基于這種消息機制,Windows維護了一個OS message queue以及為每個程式維護着一個application message queue。當發生各種事件的時候,比如敲擊鍵盤,點選滑鼠等等,作業系統會從OS message queue将消息取出給到相應的程式的application message queue。
而OS message queue和application message queue的中間有一個稱為鈎鍊的結果如下
在這個鈎鍊中儲存的就是設定的各種鈎子函數,而這些鈎子函數會比應用程式還早接收到消息并對消息進行處理。是以程式員可以通過在鈎子中設定鈎子函數,而要設定鈎子函數就需要使用SetWindowHookEx來将鈎子函數安裝到鈎鍊中,函數在文檔中的定義如下
HHOOK SetWindowsHookEx(int idHook, HOOKPROC lpfn, HINSTANCE hMod, DWORD dwThreadId);
參數 | 含義 |
idHook | 要安裝的鈎子類型,為了挂全局鈎子,這裡選擇WH_GETMESSAGE。表示的是安裝一個挂鈎過程,它監視發送到消息隊列的消息 |
lpfn | 表示的是鈎子的回調函數。如果dwThreadId為0,則lpfn指向的鈎子過程必須指向DLL中的鈎子過程 |
hMod | 包含由lpfn參數執行的鈎子過程的DLL句柄 |
dwThreadId | 與鈎子過程關聯的線程辨別符,如果為0則表示與所有線程相關聯。 |
如果函數成功,則傳回鈎子過程的句柄,否則為NULL。
根據上面的介紹可以得知,想要建立一個全局鈎子,就必須在DLL檔案中建立。這是因為程序的位址空間是獨立的,發生對應事件的程序不能調用其他程序位址空間的鈎子函數。如果鈎子函數的實作代碼在DLL中,則在對應事件發生時,系統會把這個DLL加載到發生事件的程序位址空間中,使它可以調用鈎子函數進行處理。
是以隻要在系統中安裝了全局鈎子,那麼隻要程序接收到可以發出鈎子的消息,全局鈎子的DLL就會被系統自動或者強行加載到程序空間中,這就可以實作DLL注入。
而這裡之是以設定為WH_GETMESSAGE,是因為這種類型的鈎子會監視消息隊列,又因為Windows系統是基于消息驅動的,是以所有的程序都會有自己的一個消息隊列,都會加載WH_GETMESSAGE類型的全局鈎子。
當idHook設定為WH_GETMESSAGE的時候,回調函數lpfn的定義如下
LRESULT CALLBACK GetMsgProc(int code,
WPARAM wParam,
LPARAM lParam);
參數 | 含義 |
code | 指定鈎子過程是否必須處理該消息。如果代碼是HC_ACTION,則鈎子過程必須處理該消息。如果代碼小于零,則鈎子過程必須将消息傳遞給CallNextHookEx函數而無需進一步處理,并且應該傳回CallNextHookEx傳回的值 |
wParam | 指定消息是否已從隊列中删除。此參數可以是以下值之一。 PM_NOREMOVE:指定消息尚未從隊列中删除 PM_REMOVE:指定消息已從隊列中删除 |
lParam | 指向包含消息詳細資訊的MSG結構體的指針 |
當使用者不需要再進行消息鈎取時隻需調用UnhookWindowsHookEx即可解除安裝的消息鈎子,函數的原型如下:
BOOL WINAPI UnhookWindowsHookEx(
_In_ HHOOK hhk
);
hhk參數是之前調用SetWindowsHookEx函數傳回的HHOOK變量。這個函數調用成功後會使被注入過dll的鎖計數器遞減1,當鎖計數器減到0時系統會解除安裝被注入的dll。
過程:
1.調用SetWindowsHookEx設定鈎子.
2.在設定過程中.需要一個回調.是以我們填入一個回調.(個人tips:這個地方可以設定自寫函數,自寫功能)
3.回調函數中調用CallNextHookEx函數. 如果不調用.那麼相當于我們設定了不反悔.程式可能出現問題.當然是按需傳回.(個人tips:如果不調用,這裡可能就打亂程式原有的後續代碼邏輯)
4.取消HOOK設定.(個人tips:降低性能,是以需要取消)
注意:鈎子函數應當放在一個dll中,并且在你的程序中LoadLibrary這個dll。然後再調用SetWindowsHookEx函數對相應類型的消息安裝鈎子。
由于設定全局鈎子的代碼需要在DLL檔案中完成,是以首先需要建立一個dll檔案。
phc.h
#include "framework.h"
extern "C" _declspec(dllexport) int SetGlobalHook();
extern "C" _declspec(dllexport) LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam);
extern "C" _declspec(dllexport) BOOL UnsetGlobalHook();
dllmain.cpp
// 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; }
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 SetGlobalHook() {
g_hHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllModule, 0);
if (NULL == g_hHook) { return FALSE; } return TRUE; } // 解除安裝鈎子
BOOL UnsetGlobalHook() { if (g_hHook) { UnhookWindowsHookEx(g_hHook); }
return TRUE; }
SetGlobalHook(): 設定全局鈎子,WH_GETMESSAGE為監視發送到消息隊列的消息的鈎子,第二個參數則為鈎子的回調函數。
GetMsgProc(): 鈎子的回調函數,CallNextHookEx表示将目前鈎子傳遞給下一個鈎子,若傳回值為0,表示中斷鈎子傳遞,對鈎子進行攔截。
UnsetGlobalHook(): 解除安裝鈎子 共
享記憶體: 由于全局鈎子是以DLL形式加載到程序中,程序都是獨立的,要将程序句柄傳遞給其他程序,可以使用共享記憶體突破程序獨立性,使用"/SECTION:mydata,RWS"設定為可讀可寫可共享的資料段。
建立c++空項目 編譯下面代碼,将hookDll.dll放在生成的exe下,運作
#include <windows.h>
#include <stdio.h>
typedef BOOL(*PEN_HOOKSTART)();
typedef BOOL(*PEN_HOOKSTOP)();
int main() { //加載dll
HMODULE hDll = LoadLibrary(L"./hookDll.dll");
if (NULL == hDll) {
printf("LoadLibrary Error[%d]\n", ::GetLastError()); return 1; }
BOOL isHook = FALSE; //導出函數位址
PEN_HOOKSTART SetGlobalHook = (PEN_HOOKSTART)GetProcAddress(hDll, "SetGlobalHook");
if (NULL == SetGlobalHook) { printf("SetGlobalHook:GetProcAddress Error[%d]\n", GetLastError());
return 2; }
PEN_HOOKSTOP UnsetGlobalHook = (PEN_HOOKSTOP)GetProcAddress(hDll, "UnsetGlobalHook");
if (NULL == UnsetGlobalHook) {
printf("UnsetGlobalHook:GetProcAddress Error[%d]\n", GetLastError());
return 3; }
isHook=SetGlobalHook();
if (isHook) { printf("Hook is ok!\n"); }
else { printf("Hook is error[%d]\n", GetLastError()); }
system("pause");
UnsetGlobalHook();
FreeLibrary(hDll);
return 0; }
使用Process Explorer檢視dll:
被注入成功
參考:
https://bbs.pediy.com/thread-269910.htm#msg_header_h1_2
https://www.cnblogs.com/wf751620780/p/10730013.html#autoid-4-5-0