DLL注入實踐


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之前,按照如下順序做檢查:

  1. 如果同名DLL已在內存中加載,則直接使用
  2. 如果DLL在系統DLL列表中,系統直接使用已知DLL的拷貝
  3. 如果DLL依賴其他DLL,系統會按照名字搜索並加載依賴的DLL,待依賴的DLL加載完畢后再加載自身。

系統已知DLL列表配置位於注冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs選項中,本機上的內容如下:

系統DLL列表.jpg

標准的DLL搜索順序取決於系統安全DLL搜索模式,該模式默認使能。按照如下順序搜索DLL:

  1. 進程對應的應用程序目錄,可通過 GetModuleFileName 獲得,程序啟動后為固定值。
  2. Windows系統目錄,一般為 C:\Windows\system32
  3. Windows目錄,一般為 C:\Windows
  4. 當前進程目錄,通過 GetCurrentDirectory 獲得,程序可通過 SetCurrentDirectory 進行修改。
  5. PATH環境變量中的目錄

DLL加載過程

DLL加載分為隱式加載和顯示加載。

隱式加載既為在編譯鏈接選項中增加導入庫,在程序運行目錄存放待加載的dll。雙擊程序啟動時,由系統加載程序根據exe中的導入表加載對應dll到進程空間,若dll有依賴其他dll的,會遞歸加載直到加載完成所有必需的dll,然后進行exe的導入函數地址重定位,使得能夠調用dll的導出函數。

顯示加載指的是由應用程序按需加載,具體為調用 LoadLibraryFreeLibraryGetProcAddress這三個API函數,加載並獲取dll的導出函數地址。

加載器執行流程

  1. 為進程創建虛擬地址空間
  2. 把可執行模塊映射到進程地址空間中
  3. 檢查可執行模塊的導入段,根據DLL搜索規則,找到所需的DLL並加載。如在此階段,未找到需要的dll,彈出"無法啟動,因為計算機中缺失XXX.dll"
  4. 檢查dll的導入表(IAT),如果該dll還依賴其他的dll,那么繼續去定位所需的dll並加載.
  5. 修復導入符號的引用符號。具體做法,遍歷所有模塊的導入段,針對每個導入符號,加載程序在導出段中檢查是否存在匹配的導入符號,若存在,則取導出符號的RVA,加上對應dll的基址,得到導出符號的真實地址,填入對應的導入表中。如在此階段未找到對應的導出符號,彈出"程序入口點XXX無法定位到動態鏈接庫xxx.dll上"
  6. 可執行模塊運行期間如果調用到某個dll的導出函數,則會跳轉到IAT,得到導出函數的地址,然后進行調用。

使用CreateRemoteThread注入DLL

使用CreateRemoteThread可使得目前進程創建線程,但要加載注入dll,需要在目標線程的虛擬地址空間申請內存,用於保存目標dll的名稱,通過 GetProcAddress 函數在遠程線程中 kernel32.dll 模塊的LoadLibrary函數地址,用於遠程線程的入口函數,主要流程如下:

  1. 獲取 LoadLibrary 函數的地址
  2. 調用 VirtualAllocEx 函數在遠程進程中申請一段虛擬內存
  3. 調用 WriteProcessMemory 將 待加載的dll名稱寫入虛擬內存 ReadProcessMemory 讀寫進程地址內容
  4. 調用 CreateRemoteThread 創建遠程線程,回調函數為 LoadLibrary,參數為對應字符串地址
  5. 調用 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

  1. 通過讀取exe的PE頭,獲得依賴的dll列表,遍歷讀取dll依賴的dll,得到集合1
  2. 在程序運行過程中,調用 EnumProcessModules 得到當前加載所有dll,得到集合2
  3. 從集合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注入技術原理介紹


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM