線程注入、HOOK APIs(附VC6源碼)

  工作關係,想HOOK並修改一些API,使得不支持某些設備的第三方工具可以正常運行,因此花時間寫了這麼個工具。比如ReadFile時,某些設備不支持指定的緩存大小(如512KB),可以HOOK ReadFile,把緩存大小修改爲更小,可能ReadFile就能正常工作,第三方工具也能正常使用。其實,只是想借工作這個契機,學習遠程線程注入和HOOK API。工作上測試的設備和第三方工具運行在64位機上,還沒有時間在64位機上修改並編譯。

  運行DEMO說明:

  首先進入TestExe目錄,打開MFCDialogApplication.exe,8個按鈕分別簡單的調用8個API,可一一點擊查看效果,標題欄顯示進程ID:


  打開DllImport.exe,彈出Console,以下顯示的是我輸入並HOOK完的界面:


  首先輸入需要HOOK的進程ID,這裏輸入對話框進程ID12600。然後提示選擇需HOOK的API,每個API有FLAG值,佔一位,可以用位組合,這裏輸入511,即9個API都需HOOK。然後選擇是HOOK還是UNHOOK,這裏當然輸入1。然後這個程序向對話框程序注入線程,調用DllExport.dll,DllExport.dll中HOOK這9個API。結果全部成功,見上圖。HOOK對話框後,會在對話框進程中彈出一個Console窗口,用以顯示相關信息,見下圖:


  然後在對話框上一一點擊各個按鈕,並隨時查看彈出的Console窗口中內容,單擊完後,見下圖:


  每點擊一個按鈕後,在Console中會顯示相關信息。這些信息是HOOK時打印的,我只設置了打印簡單的信息。還要注意,HOOK MessageBoxA和MessageBoxW時,改變了彈出消息框的標題、文字等。

  然後再打開DllImport.exe,把HOOK的API還原,並從對話框進程中卸載DllExport.dll,輸入順序與HOOK一致,只是第三步時需指定0,見下圖:


  UNHOOK API時我選擇的全部還原。完後,9個API不再被HOOK,正常執行,之前顯示信息的Console關閉,對話框正常運行。所有API UNHOOK後,DllExport.dll被卸載,可改名、刪除等。此時,再點擊對話框按鈕,就不再有任何顯示顯示。注意點擊兩個MessageBox後,消息框的標題與文字。

  編譯說明:

  MFCDialogApplication是生成被注入的對話框工程;DllWorkspace中,DllExport生成需注入的DllExport.dll,DllImport生成執行注入操作的DllImport.exe。

  DllWorkspace工作空間中,兩個項目都是用的安全字符串操作函數,如果出現找不到頭文件,需要更新SDK,我的VC6 Include路徑包括“C:\Program Files\Microsoft SDKs\Windows\v6.0A\Include”和“C:\Program Files\Microsoft Visual Studio 9.0\VC\include”,即可編譯通過。

  DllWorkspace下的兩個項目,ANSI、UNICODE編譯均可,既四種組合的編譯,都可正常注入並HOOK。

  注入及HOOK代碼簡要說明:
  注入主要代碼如下:

// create a remote thread, and start LoadLibrary to load dll that we make, in the dll, we can
// hook apis and can do everything we want
BOOL CreateRemoteThread(HANDLE hProcess, LPTHREAD_START_ROUTINE pfnStartAddr, LPVOID pRemoteMem)
{
	BOOL bRet = FALSE;
	HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pfnStartAddr, pRemoteMem, 0, NULL);
	do 
	{
		if(!hThread)
		{
			PrintError(_T("CreateRemoteThread"), GetLastError(), __MYFILE__, __LINE__);
			break;
		}
		TCOUT << _T("Waiting for the end of the remote thread...") << endl;
		WaitForSingleObject(hThread, INFINITE);
		CloseHandle(hThread);
		hThread = NULL;
		bRet = TRUE;
	} while (FALSE);
	return bRet;
}
  用CreateRemoteThread創建一個遠程進程下的線程,開始執行pfnStartAddr(爲LoadLibrary的地址)地址處的代碼,傳遞的參數爲pRemoteMem(遠程進程下的一段內存空間,保存的是DllExport.dll的路徑),創建的遠程線程由LoadLibrary開始執行,然後加載DllExport.dll,在DllExport.dll中可執行我們的處理。注意:DllExport.dll的路徑是相對於被注入進程的,因此被注入進程需要能找到這個Dll。程序中szDllPath保存路徑。
  dll運行時可以和執行注入的進程交換數據,我使用file-mapping實現,代碼如下:
// write infomation to file-mapping, the infomation includes witch apis need to be (un)hooked,
// and includes the (un)hook results and so on
LPVOID WriteFileMapping(HANDLE hMap, CONTENT_FILE_MAPPING content)
{
	LPVOID pContent = NULL;
	do 
	{
		if(!hMap)
		{
			PrintMsg(_T("WriteFileMapping fail : hMap is null, file : %s, line : %d\n"), 
				__FILE__, __LINE__);
			break;
		}
		pContent = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(content));
		if(!pContent)
		{
			PrintError(_T("MapViewOfFile"), GetLastError(), __MYFILE__, __LINE__);
			break;
		}
		memcpy(pContent, &content, sizeof(content));
	} while (FALSE);
	return pContent;
}
  hMap是名字爲宏NAME_FILE_MAPPING定義的一個file-mapping,然後向其中寫入數據。dll執行時,再打開這個file-mapping,從中讀取數據,再把結果寫回。
  然後是卸載被注入進程中DllExport.dll的代碼:

// if no apis be hooked, we must free the library
BOOL UnLoadModule(DWORD dwProcesssId, LPCTSTR lpModuleName) 
{ 
	BOOL bRet = FALSE;
	HANDLE hModuleSnap = INVALID_HANDLE_VALUE; 
	MODULEENTRY32 me32; 
	HANDLE hProcess = NULL;
	HMODULE	hModule = NULL;
	
	me32.dwSize = sizeof(me32);
	
	hProcess = OpenProcess(PROCESS_ALL_ACCESS, TRUE, dwProcesssId);
	do 
	{
		if(!hProcess)
		{
			PrintError(_T("OpenProcess"), GetLastError(), __MYFILE__, __LINE__);
			break;
		}
		
		hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcesssId);
		if(INVALID_HANDLE_VALUE == hModuleSnap)
		{
			PrintError(_T("CreateToolhelp32Snapshot"), GetLastError(), __MYFILE__, __LINE__);
			break;
		}

		if(!Module32First(hModuleSnap, &me32)) 
		{ 
			PrintError(_T("Module32First"), GetLastError(), __MYFILE__, __LINE__);
			break;
		}
		
		int nRefCount = 0;
		do 
		{ 	
			if(!StrCmpI(me32.szModule, lpModuleName) || !StrCmpI(me32.szExePath, lpModuleName))
			{
				hModule = me32.hModule;
				nRefCount = me32.ProccntUsage;
				break;
			}
		} while(Module32Next(hModuleSnap, &me32));

		LPTHREAD_START_ROUTINE pfnStartAddr = 
				(LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(_T("Kernel32")), "FreeLibrary");
		if (!pfnStartAddr)
		{
			PrintError(_T("GetProcAddress"), GetLastError(), __MYFILE__, __LINE__);
			break;
		}
		for (int i = 0; i < nRefCount; i++)
		{
			HANDLE hThread = ::CreateRemoteThread(hProcess, NULL, 0, pfnStartAddr, hModule, 0, NULL);
			WaitForSingleObject(hThread, INFINITE);
			CLOSE_HANDLE(hThread);
		}
		PrintMsg(_T("FreeLibrary %s in the process %d finished!\n"), lpModuleName, dwProcesssId);
		
		bRet = TRUE;
	} while (FALSE);
	
	CLOSE_HANDLE(hModuleSnap);
	CLOSE_HANDLE(hProcess);

	return bRet; 
}
  遍歷被注入進程加載的模塊,如果找到了DllExport.dll,得到被這個進程引用的次數,然後再循環創建遠程線程使用FreeLibrary來卸載DllExport.dll。注意,DllExport.dll被加載幾次,就必須執行幾次FreeLibrary才能完全卸載。至此,DllExport.dll就可以任意操作了。
  HOOK代碼簡要說明:
  使用的是跳轉法,先保存API的前幾個字節,再把這幾個字節設置爲跳轉到我們自己函數的地方去。我們自己的函數中,進行相應處理後,再執行保存的API前幾個字節的代碼,然後跳轉到API相應的位置執行。代碼如下:
// hook the specify api
// pRecallApiInfo : infomation of the api
BOOL HookSpecifyApi(PRECALL_API_INFO pRecallApiInfo)
{
	BOOL bRet = FALSE;	
	
	do 
	{
		if (!pRecallApiInfo)
		{
			break;
		}
		if (pRecallApiInfo->pOrgfnMem)
		{
			bRet = TRUE;
			break;
		}
		HMODULE hModule = LoadLibrary(pRecallApiInfo->lpDllName);
		if (!hModule)
		{
			PrintError(_T("LoadLibrary"), GetLastError(), __MYFILE__, __LINE__);
			break;
		}
		USES_CONVERSION;
		FARPROC pfnStartAddr = (FARPROC)GetProcAddress(hModule, T2CA(pRecallApiInfo->lpFunctionName));
		pRecallApiInfo->lpApiAddr = pfnStartAddr;
		if (!pfnStartAddr)
		{
			PrintError(_T("GetProcAddress"), GetLastError(), __MYFILE__, __LINE__);
			break ;
		}

		// we must save the first few bytes of the api(at least five, and these few bytes must complete
		// the assembly codes), then make the 5 bytes in front of api to jump to our function, and our
		// function must execute the few bytes saved before, and then jump to the api to execute
		// the rest code in the api
		int nSize = 0; 
		int nDisassemblerLen = 0;
		while(nSize < 5) 
		{ 
			// GetOpCodeSize can get the assembly code size
			nDisassemblerLen = GetOpCodeSize((BYTE*)(pfnStartAddr) + nSize);
			nSize = nDisassemblerLen + nSize; 
		} 
		
		DWORD dwProtect = 0;
		if (!VirtualProtect(pfnStartAddr, nSize, PAGE_EXECUTE_READWRITE, &dwProtect))
		{
			PrintError(_T("VirtualProtect"), GetLastError(), __MYFILE__, __LINE__);
			break ;
		}

		// be sure that we must change pOrgfnMem's protect, because the code in pOrgfnMem 
		// also need to execute 
		pRecallApiInfo->pOrgfnMem = new BYTE[5 + nSize];
		DWORD dwMemProtect = 0;
		if (!VirtualProtect(pRecallApiInfo->pOrgfnMem, 5 + nSize, PAGE_EXECUTE_READWRITE, &dwMemProtect))
		{
			delete [] pRecallApiInfo->pOrgfnMem;
			pRecallApiInfo->pOrgfnMem = NULL;
			PrintError(_T("VirtualProtect"), GetLastError(), __MYFILE__, __LINE__);
			break ;
		}
		pRecallApiInfo->nOrgfnMemSize = 5 + nSize;

		memcpy(pRecallApiInfo->pOrgfnMem, pfnStartAddr, nSize);
		*(BYTE*)(pRecallApiInfo->pOrgfnMem + nSize) = 0xE9;
		*(DWORD*)(pRecallApiInfo->pOrgfnMem + nSize + 1) 
			= (DWORD)pfnStartAddr + nSize - (DWORD)(pRecallApiInfo->pOrgfnMem + 5 + nSize);
		*(BYTE*)(pfnStartAddr) = 0xE9;
		*(DWORD*)((BYTE*)pfnStartAddr + 1) = (DWORD)pRecallApiInfo->lpRecallfn - ((DWORD)pfnStartAddr + 5);
		memset((BYTE*)pfnStartAddr + 5, 0x90, nSize - 5);
		// be sure that we must set the rest to 0x90(assembly code for nop, do nothing, 
		// and occupy one byte), because we should't change the assembly code

		VirtualProtect(pfnStartAddr, nSize, dwProtect, &dwProtect);

		bRet = TRUE;
	} while (FALSE);

	return bRet;
}
  需要注意的是:保存的可能並不是API的前5個字節,因爲前5個字節可能不是一個或幾個完整的彙編指令,比如第5、6個字節合起來纔是一個指令,我們就不能只保存前5個字節,最後執行這5個字節,再跳轉到第6個字節處執行。這樣破壞了指令,必然造成崩潰。這時需要保存前6個字節才行。程序中,我使用了從網上找到的一段代碼GetOpCodeSize,GetOpCodeSize可以得到當前地址處的彙編指令長度。然後保存API前至少5個字節,並且這些字節可以組成完整的彙編指令。實際也可以不用這樣,可以用另一方式,我們函數中先恢復API的前5個字節,然後再調用API,調用完後再改API前5個字節爲跳轉到我們函數的指令。但是,這種方式並不好,如果調用API時,API的前5個字節正常,如果再有進程中其他線程調用API,這時流程完全正常,沒有被HOOK。
  另外,還需要修改保存API前幾個字節內存的屬性,因爲這些內存是需要執行的,因此修改爲可讀、可寫、可執行。代碼修改pRecallApiInfo->pOrgfnMem段內存在屬性。
  最後,如果保存的API前5個以上的字節,比如保存的6個字節,還需要把第6個字節修改爲0x90,編譯指令爲NOP,不執行任何操作。否則,第6個字節可能和後面的幾個字節組合成新的指令,也是不正確的。其實,這裏也可以不修改,因爲我們是直接跳到第7個字節執行的,既使第6、7個字節組合成一個新指令也沒關係,因爲不是從第6個指令開始執行的。但是,這樣處理後,調試方便,打開彙編窗口,一目瞭解。

  還原就簡單了,直接用之前保存的字節恢復即可。
  我們替換API的函數代碼如下:

int WINAPI MyMessageBoxA(IN HWND hWnd, IN LPCSTR lpText, IN LPCSTR lpCaption, IN UINT uType)
{
	int nOrderHookApi = ORDER_MESSAGEBOXA;

	int nRet = 0;
	static int i = 1;
	if (g_arHookAPIs[nOrderHookApi].pOrgfnMem)
	{
		USES_CONVERSION;
		PrintMsgA("%-18s %08d : 0x%08x \"%s\" \"%s\" 0x%08x\n",
			T2CA(g_arHookAPIs[nOrderHookApi].lpFunctionName),
			i++, hWnd, VALID_CHAR(lpText), VALID_CHAR(lpCaption), uType);

		nRet = ((pfnMessageBoxA)(LPVOID)g_arHookAPIs[nOrderHookApi].pOrgfnMem)(
			hWnd, "HelloWorld", "Caption", MB_OKCANCEL);
	}
	return nRet;
}

  最後一定要從保存的API的地址處開始執行。

  代碼中已實現同時HOOK9個API(MessageBoxA、MessageBoxW、DeviceIoControl、CreateFileA、CreateFileW、ReadFile、ReadFileEx、WriteFile、WriteFileEx),稍加修改,即可實現HOOK更多的API。

  編譯環境:Windows XP SP3、VC++ 6.0 SP6

  源碼及DEMO下載地址:線程注入、HOOK APIs(附VC6源碼)


發佈了42 篇原創文章 · 獲贊 20 · 訪問量 24萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章