Windows系統大量使用dll作為組件復用,應用程序也會通過dll實現功能模塊的拆分。DLL注入技術是向一個正在運行的進程插入自有DLL的過程。
Window下的代碼注入
常見的Windows代碼注入方法如下:
-
注冊表注入
編譯注冊表中的AppInit_DLLs選項,凡是使用GUI的進程,都會讀取AppInit_DLLs內容,加載這些Dll。 -
Windows Hook注入
使用 SetWindowsHookEx、UnHkkkWindowsHookEx 來進行,為目標進程安裝鈎子,在注入dll中監聽目標進程消息。 -
遠程線程注入
使用 CreateRemoteThread 函數在目標進程中創建線程,在該線程中加載注入dll。
-
DLL函數轉發
使用偽造的dll來替換目標dll,兩個dll的導出符號完全相同,在自定義DLL中,先利用函數轉發器將請求轉發到真實dll中,然后進行自己的一些處理。
在本篇文章中,主要介紹 Windows Hook注入 這一種方式。在具體介紹之前,先介紹下Dll的加載順序、加載過程。
DLL加載順序
系統在搜索加載指定DLL之前,按照如下順序做檢查:
- 如果同名DLL已在內存中加載,則直接使用
- 如果DLL在系統DLL列表中,系統直接使用已知DLL的拷貝
- 如果DLL依賴其他DLL,系統會按照名字搜索並加載依賴的DLL,待依賴的DLL加載完畢后再加載自身。
系統已知DLL列表配置位於注冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
選項中,本機上的內容如下:
標准的DLL搜索順序取決於系統安全DLL搜索模式,該模式默認使能。按照如下順序搜索DLL:
- 進程對應的應用程序目錄,可通過 GetModuleFileName 獲得,程序啟動后為固定值。
- Windows系統目錄,一般為 C:\Windows\system32
- Windows目錄,一般為 C:\Windows
- 當前進程目錄,通過 GetCurrentDirectory 獲得,程序可通過 SetCurrentDirectory 進行修改。
- PATH環境變量中的目錄
DLL加載過程
DLL加載分為隱式加載和顯示加載。
隱式加載既為在編譯鏈接選項中增加導入庫,在程序運行目錄存放待加載的dll。雙擊程序啟動時,由系統加載程序根據exe中的導入表加載對應dll到進程空間,若dll有依賴其他dll的,會遞歸加載直到加載完成所有必需的dll,然后進行exe的導入函數地址重定位,使得能夠調用dll的導出函數。
顯示加載指的是由應用程序按需加載,具體為調用 LoadLibrary
、 FreeLibrary
和 GetProcAddress
這三個API函數,加載並獲取dll的導出函數地址。
加載器執行流程
- 為進程創建虛擬地址空間
- 把可執行模塊映射到進程地址空間中
- 檢查可執行模塊的導入段,根據DLL搜索規則,找到所需的DLL並加載。如在此階段,未找到需要的dll,彈出"無法啟動,因為計算機中缺失XXX.dll"
- 檢查dll的導入表(IAT),如果該dll還依賴其他的dll,那么繼續去定位所需的dll並加載.
- 修復導入符號的引用符號。具體做法,遍歷所有模塊的導入段,針對每個導入符號,加載程序在導出段中檢查是否存在匹配的導入符號,若存在,則取導出符號的RVA,加上對應dll的基址,得到導出符號的真實地址,填入對應的導入表中。如在此階段未找到對應的導出符號,彈出"程序入口點XXX無法定位到動態鏈接庫xxx.dll上"
- 可執行模塊運行期間如果調用到某個dll的導出函數,則會跳轉到IAT,得到導出函數的地址,然后進行調用。
使用CreateRemoteThread注入DLL
使用CreateRemoteThread可使得目前進程創建線程,但要加載注入dll,需要在目標線程的虛擬地址空間申請內存,用於保存目標dll的名稱,通過 GetProcAddress 函數在遠程線程中 kernel32.dll 模塊的LoadLibrary函數地址,用於遠程線程的入口函數,主要流程如下:
- 獲取 LoadLibrary 函數的地址
- 調用 VirtualAllocEx 函數在遠程進程中申請一段虛擬內存
- 調用 WriteProcessMemory 將 待加載的dll名稱寫入虛擬內存 ReadProcessMemory 讀寫進程地址內容
- 調用 CreateRemoteThread 創建遠程線程,回調函數為 LoadLibrary,參數為對應字符串地址
- 調用 VirtualFreeEx 釋放遠程虛擬內存
執行完自己的函數后,就要遠程卸載dll,思路與注入類似,函數變為FreeLibrary,傳入參數對對應dll的句柄。
獲取已加載的模塊句柄(通過EnumProcessModule實現)
注入代碼示例:
// 獲得指定進程名稱的進程PID
int GetProcessId(const char* pName)
{
PROCESSENTRY32 pe;
DWORD id = 0;
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
pe.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(hSnapshot, &pe))
return 0;
while (1)
{
pe.dwSize = sizeof(PROCESSENTRY32);
if (Process32Next(hSnapshot, &pe) == FALSE)
break;
if (strcmp(pe.szExeFile, pName) == 0)
{
id = pe.th32ProcessID;
break;
}
}
CloseHandle(hSnapshot);
return id;
}
//利用遠程線程來注入dll
bool RemoteThreadDllInject()
{
////提權代碼,在Windows Vista 及以上的版本需要將進程的權限提升,否則打開進程會失敗
if (!SetDebugPrivilege(TRUE))
{
printf("提升權限失敗\n");
return false;
}
int nPid = GetProcessId(INJECT_EXE_NAME);
// 打開目標進程
HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, nPid);
if (NULL == hRemoteProcess)
{
printf("OpenProcess failed! %d", GetLastError());
return false;
}
// 獲得 LoadLibraryW 在 kernel32.dll 中的地址
typedef HMODULE(WINAPI *pfnLoadLibrary)(LPCWSTR);
// 這里注意要載入寬字節版本還是普通版本的 LoadLibrary
pfnLoadLibrary pfnThreadRtn2 = (pfnLoadLibrary)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "LoadLibraryA");
// 在遠程進程中申請內存空間,用於保存遠程線程的參數
LPVOID lpRemoteMemory = VirtualAllocEx(hRemoteProcess, 0, MAX_PATH, MEM_COMMIT, PAGE_READWRITE);
string strInjectDllName(INJECT_DLL_NAME);
DWORD nWritten = 0;
BOOL bRet = WriteProcessMemory(hRemoteProcess, lpRemoteMemory, strInjectDllName.c_str(), strInjectDllName.length() + 1, &nWritten);
HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pfnThreadRtn2, lpRemoteMemory, 0, NULL);
WaitForSingleObject(hRemoteThread, INFINITE);
VirtualFreeEx(hRemoteProcess, lpRemoteMemory, 0, MEM_RELEASE);
CloseHandle(hRemoteThread);
CloseHandle(hRemoteProcess);
return true;
}
遠程卸載dll示例代碼:
// 獲得指定進程內指定模塊信息
bool GetProcessModule(DWORD dwPid, string strModuleName, LPMODULEENTRY32 lpMe32, DWORD cbMe32)
{
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPid);
if (INVALID_HANDLE_VALUE == hSnapshot)
{
printf("CreateToolhelp32Snapshot Error");
return false;
}
bool bFind = false;
MODULEENTRY32 moduleInfo = {0};
moduleInfo.dwSize = sizeof(MODULEENTRY32);
if (Module32First(hSnapshot, &moduleInfo))
{
do
{
if (strModuleName == string(moduleInfo.szModule))
{
memcpy(lpMe32, &moduleInfo, cbMe32);
bFind = true;
break;
}
} while (!bFind && Module32Next(hSnapshot, &moduleInfo));
}
CloseHandle(hSnapshot);
return bFind;
}
bool RemoteDllUnLoad()
{
//在遠程線程執行結束后,注入的 dll 仍然存在與目標進程中,我們需要再次使用 CreateRemoteThread,執行 FreeLibrary ,將之前注入的 dll 卸載掉
// 實現思路:枚舉進程的模塊,根據模塊名稱找到對應模塊的句柄。
int nPid = GetProcessId(INJECT_EXE_NAME);
////提權代碼,在Windows Vista 及以上的版本需要將進程的權限提升,否則打開進程會失敗
if (!SetDebugPrivilege(TRUE))
{
printf("提升權限失敗\n");
return false;
}
MODULEENTRY32 moduleInfo = {0};
if (!GetProcessModule(nPid, INJECT_DLL_NAME, &moduleInfo, sizeof(moduleInfo)))
{
printf("can't find %s dll in %s", INJECT_DLL_NAME, INJECT_EXE_NAME);
return false;
}
typedef BOOL(*pfnFreeLibrary)(HMODULE);
pfnFreeLibrary pFreeLibrary = (pfnFreeLibrary)GetProcAddress(GetModuleHandle("kernel32.dll"), "FreeLibrary");
HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, nPid);
if (hRemoteProcess == NULL)
{
printf("OpenProcess Error");
return false;
}
HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFreeLibrary, moduleInfo.hModule, 0, NULL);
WaitForSingleObject(hRemoteThread, INFINITE);
CloseHandle(hRemoteThread);
CloseHandle(hRemoteProcess);
return true;
}
注意事項:
- 遠程進程可能以Unicode編碼編譯,也可能是MBSC編碼編譯,因此,要區分 LoadLibraryW 還是 LoadLibraryA 版的函數,FreeLibrary函數無需區分不同版本。
- 在獲得目標進程的句柄時,需要對自身程序進程提權操作。一般需要當前登陸用戶屬於Administrator組成員,才有能力提升至調試權限。Administrator組成員的access token中會含有一些可以執行系統級操作,注意,非Administrator組成員創建的進程 無法提升自身的權限。如果提權失敗,建議通過管理權權限打開VS開發環境。
- 在注入時,需要將待注入的dll放在遠程進程同一目錄下。
- 驗證注入,可通過在dllMain函數的加載和卸載分支中彈框提示,也可以通過 procexp64.exe 程序來查看程序當前加載的dll信息。
工程實踐
如何獲得運行過程中不符合預期的dll
- 通過讀取exe的PE頭,獲得依賴的dll列表,遍歷讀取dll依賴的dll,得到集合1
- 在程序運行過程中,調用 EnumProcessModules 得到當前加載所有dll,得到集合2
- 從集合2中排除掉集合1的內容,得到生成集合3,即為不符合預期的dll。
一般來說,輸入法(搜狗輸入法的皮膚組件PicFace.dll、資源組件Resource.dll等)、監控軟件的dll會自動加載到所有進程的dll中去。
模塊基址重定位
每個模塊(exe和dll)在編譯輸出時,都有一個首選基址,它指示加載器將模塊映射到進程地址空間中的首選位置,一般exe的基址設定為 0x00400000,dll模塊為 0x10000000.當一個exe依賴於多個dll時,第一個dll被正確的加載到 0x10000000上,隨后的dll就不能在加載到0x10000000上,加載程序會對隨后的dll進行基址重定位,把它放到別的地方。基址重定位會增加程序初始化時間,因此,如果將多個模塊載入同一進程空間,可以給不同模塊指定不同的基址。在VS開發環境中,基址配置方式:DLL工程的配置屬性-->鏈接器-->高級-->基址。
在所有dll編譯完成后,使用 Rebase.exe
工具,對需要載入進程地址空間的所有模塊進行基址重定位,將結果寫回到dll文件中。
一旦一個dll模塊的基址已知,那么可直接推算出exe在使用該DLL的導出函數的真實地址。這種預先將exe和所依賴的dll綁定在一起的做法,可以提高應用程序的啟動速度。
VS提供 Bind.exe
程序提供了綁定可執行文件與dll的功能。工作原理為,讀取所有dll的基址和導出符號的RVA,計算后填充到可執行文件的導入表中。
參考文檔:多種DLL注入技術原理介紹