DLL注入和API Hook

第一篇 dll注入

一、什麼是dll注入

  在Windows操作系統中,運行的每一個進程都生活在自己的程序空間中,每一個進程都認爲自己擁有整個機器的控制權,每個進程都認爲自己擁有計算機的整個內存空間,這些假象都是操作系統創造的。理論上而言,運行在操作系統上的每一個進程之間都是互不干擾的,即每個進程都會擁有獨立的地址空間。比如說進程B修改了地址爲0x400000的數據,那麼進程C的地址爲0x400000處的數據並未隨着B的修改而發生改變,並且進程C可能並不擁有地址爲0x400000的內存(操作系統可能沒有爲進程C映射這塊內存)。因此,如果某進程有一個缺陷覆蓋了隨機地址處的內存(這可能導致程序運行出現問題),那麼這個缺陷不會影響到其他進程所使用的內存。
  正是由於進程的地址空間是獨立的,因此我們很難編寫能夠與其它進程通信或控制其它進程的應用程序。
  所謂的dll注入即是讓程序A強行加載你給定的a.dll並執行你給定的a.dll裏面的代碼。注意,你所給定的a.dll原先並不會被程序A加載,但是當你向程序A注入了a.dll後,程序A將會執行a.dll裏的代碼,這個時候,你的a.dll就進入了程序A的地址空間,你就可以爲所欲爲了。

二、什麼時候需要dll注入

  應用程序一般會在以下情況使用dll注入技術來完成某些功能:
    1.爲目標進程添加新的“實用”功能;
    2.需要一些手段來輔助調試被注入dll的進程;
    3.爲目標進程安裝鉤子程序(API Hook);

三、dll注入的方法

  一般情況下有如下dll注入方法:    
    1.修改註冊表來注入dll;
    2.使用CreateRemoteThread函數對運行中的進程注入dll;
    3.使用SetWindowsHookEx函數對應用程序掛鉤(HOOK)迫使程序加載dll;
    4.替換應用程序一定會使用的dll;
    5.把dll作爲調試器來注入;
    6.用CreateProcess對子進程注入dll
    7.修改被注入進程的exe的導入地址表。
  接下來將詳細介紹如何使用這幾種方式完成dll注入。

四、注入方法詳解

(一)、修改註冊表

  如果使用過Windows,那麼對註冊表應該不會陌生。整個系統的配置都保存在註冊表中,我們可以通過修改其中的設置來改變系統的行爲。
  首先打開註冊表並定位到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows項,如下圖所示,他顯示了該註冊表項中的條目。


  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,並在不需要時及時卸載。

(二)、使用CreateRemoteThread函數對運行中的進程注入dll

  這種方法具有最高的靈活性,同時它要求掌握的知識也很多。從根本上說,dll注入技術要求目標進程中的一個線程調用LoadLibrary函數來載入我們想要注入的dll,由於我們不能輕易的控制別人進程中的線程,因此這種方法要求我們在目標進程中創建一個線程並在線程中執行LoadLibrary函數加載我們要注入的dll。幸運的是Windows爲我們提供了CreateRemoteThread函數,它使得在另一個進程中創建一個線程變得非常容易。CreateRemoteThread函數的原型如下:

HANDLE WINAPI CreateRemoteThread(
  _In_  HANDLE                 hProcess,
  _In_  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  _In_  SIZE_T                 dwStackSize,
  _In_  LPTHREAD_START_ROUTINE lpStartAddress,
  _In_  LPVOID                 lpParameter,
  _In_  DWORD                  dwCreationFlags,
  _Out_ LPDWORD                lpThreadId
);

  該函數與CreateThread僅僅只多出第一個參數hProcess,hProcess表示創建的新線程屬於哪一個進程。
  參數lpStartAddress表示線程函數的起始地址,注意這個地址在目標進程的地址空間中。
  現在問題來了,我們如何調用讓創建的線程執行LoadLibrary函數來加載我們要注入的dll呢?答案很簡單:只需要創建的線程的線程函數地址是LoadLibrary函數的起始地址即可。我們都知道,每一個線程創建時應該指定一個參數只有4個字節,返回值也只是4個字節的函數即可(從彙編的角度看確實如此,只要保證調用前後棧平衡即可),而LoadLibrary函數就滿足這些條件。LoadLibrary函數的原型如下:

HMODULE WINAPI LoadLibrary(
  _In_ LPCTSTR lpFileName
);

  可以發現LoadLibrary函數完全滿足上述條件,LoadLibrary的參數是dll路徑的起始地址,這個參數也就是CreateRemoteThread函數的lpParameter參數。但是參數指向的地址應該是目標進程的地址,並且該地址處應保存被加載dll的路徑字符串。但是一開始我們並不知道目標進程是否存在這樣一個地址並且這個地址恰好保存了我們的dll的完整路徑。解決這一問題的最保險的辦法是使用VirtualAllocEx函數在目標進程中開闢一塊內存存放我們的dll的路徑。VirtualAllocEx函數的原型如下:

LPVOID WINAPI VirtualAllocEx(
  _In_     HANDLE hProcess,
  _In_opt_ LPVOID lpAddress,
  _In_     SIZE_T dwSize,
  _In_     DWORD  flAllocationType,
  _In_     DWORD  flProtect
);

  VirtualAllocEx函數允許我們在目標進程中開闢一塊指定大小(以字節爲單位)的內存,並返回這塊內存的起始地址。之後就可以用WriteProcessMemory函數將dll文件路徑的數據複製到目標進程中。WriteProcessMemory函數的原型如下:

BOOL WINAPI WriteProcessMemory(
  _In_  HANDLE  hProcess,
  _In_  LPVOID  lpBaseAddress,
  _In_  LPCVOID lpBuffer,
  _In_  SIZE_T  nSize,
  _Out_ SIZE_T  *lpNumberOfBytesWritten
);

  在開始注入前,還需要確認一件事,就是目標進程使用的字符編碼方式。因爲我們所調用的LoadLibrary函數在底層實際調用有兩種可能:
  如果目標程序使用的是ANSI編碼方式,LoadLibrary實際調用的是LoadLibraryA,其參數字符串應當是ANSI編碼;
  如果目標程序使用的是Unicode編碼方式,LoadLibrary實際調用的是LoadLibraryW,其參數字符串應當是Unicode編碼。
  這使得注入過程變得很麻煩,爲了減少複雜性,不妨直接使用LoadLibraryA或LoadLibraryW而不是用LoadLibrary函數來避免這一麻煩。另外,即使使用的是LoadLibraryA,LoadLibraryA也會將傳入的ANSI編碼的字符串參數轉換成Unicode編碼後再調用LoadLibraryW。綜上,不妨一致使用LoadLibraryW函數,並且字符串用Unicode編碼即可。
  最後,我們可能會爲獲得目標進程中LoadLibraryW函數的起始地址而頭疼,但其實這個問題也很簡單,因爲目標進程中函數LoadLibraryW的起始地址和我們的進程中的LoadLibraryW函數的起始地址是一樣的。因此我們只需要用GetProcAddress即可獲得LoadLibraryW函數的起始地址。
  經過以上漫長的分析,我們對CreateRemoteThread注入方法的原理有了較爲清晰的理解,接下來我們就需要總結一下我們必須採取的步驟:
    (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)步返回的模塊句柄)。
  如果不在上述步驟基礎上執行操作,卸載dll時你需要這麼做:
    (1).獲得被注入的dll在目標進程的模塊句柄。
    (2).重複上述步驟的第(6)、(7)兩步。
  接下來給出編寫的參考代碼,該程序以控制檯應用程序方式運行,並在Windows 10上測試通過。

#include "windows.h"
#include "stdio.h"
#include "tlhelp32.h"
#include "io.h"
#include "tchar.h"

//判斷某模塊(dll)是否在相應的進程中
//dwPID			進程的PID
//szDllPath		查詢的dll的完整路徑
BOOL CheckDllInProcess(DWORD dwPID, LPCTSTR szDllPath)
{
	BOOL                    bMore = FALSE;
	HANDLE                  hSnapshot = INVALID_HANDLE_VALUE;
	MODULEENTRY32           me = { sizeof(me), };

	if (INVALID_HANDLE_VALUE ==
		(hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID)))//獲得進程的快照
	{
		_tprintf(L"CheckDllInProcess() : CreateToolhelp32Snapshot(%d) failed!!! [%d]\n",
			dwPID, GetLastError());
		return FALSE;
	}
	bMore = Module32First(hSnapshot, &me);//遍歷進程內得的所有模塊
	for (; bMore; bMore = Module32Next(hSnapshot, &me))
	{
		if (!_tcsicmp(me.szModule, szDllPath) || !_tcsicmp(me.szExePath, szDllPath))//模塊名或含路徑的名相符
		{
			CloseHandle(hSnapshot);
			return TRUE;
		}
	}
	CloseHandle(hSnapshot);
	return FALSE;
}

//向指定的進程注入相應的模塊
//dwPID			目標進程的PID
//szDllPath		被注入的dll的完整路徑
BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath)
{
	HANDLE                  hProcess = NULL;//保存目標進程的句柄
	LPVOID                  pRemoteBuf = NULL;//目標進程開闢的內存的起始地址
	DWORD                   dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);//開闢的內存的大小
	LPTHREAD_START_ROUTINE  pThreadProc = NULL;//loadLibreayW函數的起始地址
	HMODULE                 hMod = NULL;//kernel32.dll模塊的句柄
	BOOL                    bRet = FALSE;
	if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))//打開目標進程,獲得句柄
	{
		_tprintf(L"InjectDll() : OpenProcess(%d) failed!!! [%d]\n",
			dwPID, GetLastError());
		goto INJECTDLL_EXIT;
	}
	pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize,
		MEM_COMMIT, PAGE_READWRITE);//在目標進程空間開闢一塊內存
	if (pRemoteBuf == NULL)
	{
		_tprintf(L"InjectDll() : VirtualAllocEx() failed!!! [%d]\n",
			GetLastError());
		goto INJECTDLL_EXIT;
	}
	if (!WriteProcessMemory(hProcess, pRemoteBuf,
		(LPVOID)szDllPath, dwBufSize, NULL))//向開闢的內存複製dll的路徑
	{
		_tprintf(L"InjectDll() : WriteProcessMemory() failed!!! [%d]\n",
			GetLastError());
		goto INJECTDLL_EXIT;
	}
	hMod = GetModuleHandle(L"kernel32.dll");//獲得本進程kernel32.dll的模塊句柄
	if (hMod == NULL)
	{
		_tprintf(L"InjectDll() : GetModuleHandle(\"kernel32.dll\") failed!!! [%d]\n",
			GetLastError());
		goto INJECTDLL_EXIT;
	}
	pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");//獲得LoadLibraryW函數的起始地址
	if (pThreadProc == NULL)
	{
		_tprintf(L"InjectDll() : GetProcAddress(\"LoadLibraryW\") failed!!! [%d]\n",
			GetLastError());
		goto INJECTDLL_EXIT;
	}
	if (!CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL))//執行遠程線程
	{
		_tprintf(L"InjectDll() : MyCreateRemoteThread() failed!!!\n");
		goto INJECTDLL_EXIT;
	}
INJECTDLL_EXIT:
	bRet = CheckDllInProcess(dwPID, szDllPath);//確認結果
	if (pRemoteBuf)
		VirtualFreeEx(hProcess, pRemoteBuf, 0, MEM_RELEASE);
	if (hProcess)
		CloseHandle(hProcess);
	return bRet;
}

//讓指定的進程卸載相應的模塊
//dwPID			目標進程的PID
//szDllPath		被注入的dll的完整路徑,注意:路徑不要用“/”來代替“\\”
BOOL EjectDll(DWORD dwPID, LPCTSTR szDllPath)
{
	BOOL                    bMore = FALSE, bFound = FALSE, bRet = FALSE;
	HANDLE                  hSnapshot = INVALID_HANDLE_VALUE;
	HANDLE                  hProcess = NULL;
	MODULEENTRY32           me = { sizeof(me), };
	LPTHREAD_START_ROUTINE  pThreadProc = NULL;
	HMODULE                 hMod = NULL;
	TCHAR                   szProcName[MAX_PATH] = { 0, };
	if (INVALID_HANDLE_VALUE == (hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID)))
	{
		_tprintf(L"EjectDll() : CreateToolhelp32Snapshot(%d) failed!!! [%d]\n",
			dwPID, GetLastError());
		goto EJECTDLL_EXIT;
	}
	bMore = Module32First(hSnapshot, &me);
	for (; bMore; bMore = Module32Next(hSnapshot, &me))//查找模塊句柄
	{
		if (!_tcsicmp(me.szModule, szDllPath) ||
			!_tcsicmp(me.szExePath, szDllPath))
		{
			bFound = TRUE;
			break;
		}
	}
	if (!bFound)
	{
		_tprintf(L"EjectDll() : There is not %s module in process(%d) memory!!!\n",
			szDllPath, dwPID);
		goto EJECTDLL_EXIT;
	}
	if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
	{
		_tprintf(L"EjectDll() : OpenProcess(%d) failed!!! [%d]\n",
			dwPID, GetLastError());
		goto EJECTDLL_EXIT;
	}
	hMod = GetModuleHandle(L"kernel32.dll");
	if (hMod == NULL)
	{
		_tprintf(L"EjectDll() : GetModuleHandle(\"kernel32.dll\") failed!!! [%d]\n",
			GetLastError());
		goto EJECTDLL_EXIT;
	}
	pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "FreeLibrary");
	if (pThreadProc == NULL)
	{
		_tprintf(L"EjectDll() : GetProcAddress(\"FreeLibrary\") failed!!! [%d]\n",
			GetLastError());
		goto EJECTDLL_EXIT;
	}
	if (!CreateRemoteThread(hProcess, NULL, 0, pThreadProc, me.modBaseAddr, 0, NULL))
	{
		_tprintf(L"EjectDll() : MyCreateRemoteThread() failed!!!\n");
		goto EJECTDLL_EXIT;
	}
	bRet = TRUE;
EJECTDLL_EXIT:
	if (hProcess)
		CloseHandle(hProcess);
	if (hSnapshot != INVALID_HANDLE_VALUE)
		CloseHandle(hSnapshot);
	return bRet;
}

int main()
{
	//InjectDll(6836, L"C:\\a.dll");
	EjectDll(6836, L"C:\\a.dll");
	return 0;
}

(三)、使用SetWindowsHookEx函數對應用程序掛鉤(HOOK)迫使程序加載dll

  消息鉤子:Windows操作系統爲用戶提供了GUI(Graphic User Interface,圖形用戶界面),它以事件驅動方式工作。在操作系統中藉助鍵盤、鼠標、選擇菜單、按鈕、移動鼠標、改變窗口大小與位置等都是事件。發生這樣的事件時,操作系統會把事先定義好的消息發送給相應的應用程序,應用程序分析收到的信息後會執行相應的動作。也就是說,在敲擊鍵盤時,消息會從操作系統移動到應用程序。所謂的消息鉤子就是在此期間偷看這些信息。以鍵盤輸入事件爲例,消息的流向如下:
  1.發生鍵盤輸入時,WM_KEYDOWN消息被添加到操作系統的消息隊列中;
  2.操作系統判斷這個消息產生於哪個應用程序,並將這個消息從消息隊列中取出,添加到相應的應用程序的消息隊列中;
  3.應用程序從自己的消息隊列中取出WM_KEYDOWN消息並調用相應的處理程序。
  當我們的鉤子程序啓用後,操作系統在將消息發送給用用程序前會先發送給每一個註冊了相應鉤子類型的鉤子函數。鉤子函數可以對這一消息做出想要的處理(修改、攔截等等)。多個消息鉤子將按照安裝鉤子的先後順序被調用,這些消息鉤子在一起組成了"鉤鏈"。消息在鉤鏈之間傳遞時任一鉤子函數攔截了消息,接下來的鉤子函數(包括應用程序)將都不再收到該消息。
  像這樣的消息鉤子功能是Windows提供的最基本的功能,MS Visual Studio中提供的SPY++就是利用了這一功能來實現的,SPY++是一個十分強大的消息鉤取程序,它能夠查看操作系統中來往的所有消息。
  消息鉤子是使用SetWindowsHookEx來實現的。函數的原型如下:

HHOOK WINAPI SetWindowsHookEx(
  _In_ int       idHook,
  _In_ HOOKPROC  lpfn,
  _In_ HINSTANCE hMod,
  _In_ DWORD     dwThreadId
);

  idHook參數是消息鉤子的類型,可以選擇的類型在MSDN中可以查看到相應的宏定義。比如我們想對所有的鍵盤消息做掛鉤,其取值將是WH_KEYBOARD,WH_KEYBOARD這個宏的值是2。
  lpfn參數是鉤子函數的起始地址,注意:不同的消息鉤子類型的鉤子函數原型是不一樣的,因爲不同類型的消息需要的參數是不同的,具體的鉤子函數原型需要查看MSDN來獲得。注意:鉤子函數可以在結束前任意位置調用CallNextHookEx函數來執行鉤鏈的其他鉤子函數。當然,如果不調用這個函數,鉤鏈上的後續鉤子函數將不會被執行。
  hMod參數是鉤子函數所在的模塊的模塊句柄。
  dwThreadId參數用來指示要對哪一個進程/線程安裝消息鉤子。如果這個參數爲0,安裝的消息鉤子稱爲“全局鉤子”,此時將對所有的進程(當前的進程以及以後要運行的所有進程)下這個消息鉤子。注意:有的類型的鉤子只能是全局鉤子。
  注意:鉤子函數應當放在一個dll中,並且在你的進程中LoadLibrary這個dll。然後再調用SetWindowsHookEx函數對相應類型的消息安裝鉤子。
  當SetWindowsHookEx函數調用成功後,當某個進程生成這一類型的消息時,操作系統會判斷這個進程是否被安裝了鉤子,如果安裝了鉤子,操作系統會將相關的dll文件強行注入到這個進程中並將該dll的鎖計數器遞增1。然後再調用安裝的鉤子函數。整個注入過程非常方便,用戶幾乎不需要做什麼。
  當用戶不需要再進行消息鉤取時只需調用UnhookWindowsHookEx即可解除安裝的消息鉤子,函數的原型如下:

BOOL WINAPI UnhookWindowsHookEx(
  _In_ HHOOK hhk
);

  hhk參數是之前調用SetWindowsHookEx函數返回的HHOOK變量。這個函數調用成功後會使被注入過dll的鎖計數器遞減1,當鎖計數器減到0時系統會卸載被注入的dll。
  這種類型的dll注入的優點是注入簡單,缺點是隻能對windows消息進行Hook並注入dll,而且注入dll可能不是立即被注入,因爲這需要相應類型的事件發生。其次是它不能進行其他API的Hook,如果想對其它的函數進行Hook,你需要再在被注入的dll中添加用於API Hook的代碼。
  接下來將給出這一dll注入方案的示例程序的代碼,代碼包含兩部分,一部分是dll的源文件,另一部分是控制檯程序的源代碼。該程序的功能是屏蔽所有notepad.exe(Windows附帶的記事本程序)的按鍵消息,該程序在Windows xp下測試通過。

#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#pragma warning(disable : 4996)
HHOOK ghHook = NULL;
HINSTANCE ghInstance = NULL;
LRESULT CALLBACK KeyboardProc(
	_In_ int    code,
	_In_ WPARAM wParam,
	_In_ LPARAM lParam
)
{
	TCHAR szPath[MAX_PATH] = {0,};
	TCHAR sProcessName[MAX_PATH] = {0,};
	if (code == 0 && !(lParam & 0x80000000))//如果是釋放按鍵
	{
		GetModuleFileName(NULL, szPath, MAX_PATH);
		_wsplitpath(szPath, NULL, NULL, sProcessName, NULL);
		if (0==_wcsicmp(sProcessName, L"notepad"))//如果進程名是notepad
		{
			return 1;//刪除消息,不再往下傳遞
		}
	}
	return CallNextHookEx(ghHook, code, wParam, lParam);//繼續傳遞消息
}

BOOL APIENTRY DllMain(
	HMODULE hModule,
	DWORD  ul_reason_for_call,
	LPVOID lpReserved
)
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		ghInstance = hModule;//獲得本實例的模塊句柄
		break;
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

extern "C"
{
	__declspec(dllexport) void HookStart()
	{
		ghHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, ghInstance, 0);
	}
	__declspec(dllexport) void HookStop()
	{
		if (ghHook)
		{
			UnhookWindowsHookEx(ghHook);
			ghHook = NULL;
		}
	}
}
#include <stdio.h>
#include <conio.h>
#include <tchar.h>
#include <windows.h>
typedef  void(*PFNHOOKSTART)();
typedef  void(*PFNHOOKSTOP)();
int main()
{
	HMODULE Hmod = LoadLibraryA("hookdll.dll");
	PFNHOOKSTART pHookStart = (PFNHOOKSTART)GetProcAddress(Hmod, "HookStart");
	PFNHOOKSTOP pHookStop = (PFNHOOKSTOP)GetProcAddress(Hmod, "HookStop");
	pHookStart();
	printf("print 'q' to quite!\n");
	while (_getch() != 'q');
	pHookStop();
	FreeLibrary(Hmod);
	return 0;
}

(四)、替換應用程序一定會使用的dll

  這種方法通常被編寫惡意代碼的人員用來編寫木馬,因此又被稱爲使用木馬dll來注入dll。通常我們應當首先確認目標進程一定會載入的dll,然後替換掉它。舉個例子:比如我們知道目標進程一定會載入Xyz.dll,那麼我們可以創建自己的dll並與它起同樣的名字。當然,我們必須將原先被替換掉的Xyz.dll改成別的名字,比如改成Xyz_1.dll。
  注意:在我們編寫的Xyz.dll(將被注入的dll)內部,我們要導出原來的Xyz.dll所導出的所有符號。這一點很容易實現,可以用dll的函數轉發器實現(轉發到Xyz_1.dll的相同函數),這樣一來我們只需要對需要HOOK(掛鉤)的函數編寫掛鉤代碼即可,這一過程我們僅僅是多了一些重複工作。看起來這個方法是完美的,並且很多木馬程序經常這麼幹,但是它存在一個很嚴重的問題:如果被替換的dll後來由於程序升級導致替換的dll添加了新的導出函數,而被注入的dll並未及時添加這些新增導出函數的轉發器(或者Hook程序),這將導致使用了新的導出函數的程序不能正常運行。另外,請不要隨意的替換系統的dll,因爲在dll注入一般應當只注入到目標進程即可,而注入到別的進程之後將帶來很大的安全隱患。

(五)、把dll作爲調試器來注入

  使用過OD(OllyDbg)的人員可能會爲OD的強大功能感到驚歎。因爲OD可以調試一個程序並任意的修改被調試的程序。OD的工作原理是向目標進程使用了調試功能。調試器可以在被調試進程中執行很多特殊操作,操作系統載入一個被調試程序的時候,會在被調試的主線程尚未開始執行任何代碼前,自動通知調試器(用來調試被調試進程的進程),這時調試器可以將一些代碼注入到被調試進程的地址空間中,保存被調試進程的CONTEXT結構,修改EIP指向我們注入的代碼的起始位置執行這些代碼。最後再讓被調試的進程恢復原來的CONTEXT,繼續執行。整個過程對被調試的進程而言好像沒發生任何事情。
  這種注入方式需要對調試功能有所研究,並且能夠對進程的CONTEXT進行操作,最後還需要對不同的CPU平臺進行量身操作。此外,我們可能還需要手工編寫一些彙編指令來讓被調試的程序執行。這對編寫人員的能力要求較高。最後,這種方法在調試器終止後,Windows會自動終止被調試的程序。不過調試器可以通過調用DebugSetProcessKillOnExit函數並傳入FALSE,來改變Windows的默認行爲。然後調試器就可以調用DebugActiveProcessStop函數來終止調試了。
  爲什麼要在主線程尚未開始執行任何代碼前執行代碼注入呢?因爲這個時候注入最安全,其實你可以在任何時候對被調試的程序下斷點並進行以上注入操作,但是爲了保證被調試程序的穩定運行你可能需要做更多的工作。

(六)、用CreateProcess對子進程注入dll

  這個方法與把dll作爲調試器來注入方法有許多相似之處,同樣也具有較大的難度。這裏要求目標進程是注入者進程的子進程。當使用CreateProcess函數來創建一個子進程時,可以選擇創建後立即掛起該進程。這樣,創建的子進程並不會開始執行且EIP指向ntdll.dll的RtlUserThreadStart函數的開始位置(在win10上EIP=0X76F9BA60),此時的子進程處於掛起狀態。因此,我們可以有目的地修改EIP的值讓其從另一個位置繼續執行,但隨意的修改EIP的值往往使創建的子程序崩潰。爲了讓創建的子進程載入dll必須調用LoadLibrary函數。在使用CreatRemoteProcess方法中也介紹了一點:必須在目標進程(這裏指子進程)中寫入載入的dll的完整路徑。因此我們在修改EIP指向我們的代碼之前需要將一部分代碼注入到目標進程中。其中被注入的代碼至少應包括如下操作:將dll路徑首地址壓棧;調用LoadLibrary函數;跳轉回原先EIP位置,讓程序繼續執行,好像什麼都沒發生過。
  但是,爲了程序的穩定運行,這樣做還不夠。注入的代碼應該在執行後能恢復執行前的所有狀態。因此爲了注入dll需要向目標進程注入較爲安全的代碼應該包含如下操作:
    1.保存所有寄存器的值;
    2.將dll路徑首地址壓棧;
    3.調用LoadLibrary函數;
    4.恢復所有寄存器的值;
    5.跳轉到原先EIP位置,讓程序繼續執行,好像什麼都沒發生。
  該方法有如下優點在程序未開始執行前執行了dll注入,一般比較難以被發現。幾乎可以對所有的程序進行注入。
  該方法同樣具有缺點:首先需要嚴謹的設計注入的代碼,並根據不同的cpu平臺進行設計。其次就是目標進程要是注入着創建的子進程。
  接下來將給出一段示例代碼,該程序以控制檯方式運行。並在Windows 10和Windows xp上測試通過。(這段代碼參考自看雪論壇的IamHuskar,這裏表示感謝!)

#include <windows.h>
#include <stdio.h>
#pragma warning(disable : 4996)  

//在子進程創建掛起時注入dll
//hProcess		被創建時掛起的進程句柄
//hThread		進程中被掛起的線程句柄
//szDllPath		被注入的dll的完整路徑
BOOL StartHook(HANDLE hProcess, HANDLE hThread, TCHAR *szDllPath)
{
	BYTE ShellCode[30 + MAX_PATH * sizeof(TCHAR)] =
	{
		0x60,				//pushad
		0x9c,				//pushfd
		0x68,0xaa,0xbb,0xcc,0xdd,	//push xxxxxxxx(xxxxxxxx的偏移爲3)
		0xff,0x15,0xdd,0xcc,0xbb,0xaa,	//call [addr]([addr]的偏移爲9)
		0x9d,				//popfd
		0x61,				//popad
		0xff,0x25,0xaa,0xbb,0xcc,0xdd,	//jmp [eip]([eip]的偏移爲17)
		0xaa,0xaa,0xaa,0xaa,		//保存loadlibraryW函數的地址(偏移爲21)
		0xaa,0xaa,0xaa,0xaa,		//保存創建進程時被掛起的線程EIP(偏移爲25)
		0,				//保存dll路徑字符串(偏移爲29)
	};
	CONTEXT ctx;
	ctx.ContextFlags = CONTEXT_ALL;
	if (!GetThreadContext(hThread, &ctx))
	{
		printf("GetThreadContext() ErrorCode:[0x%08x]\n", GetLastError());
		return FALSE;
	}
	//在目標進程內存空間調撥一塊可執行的內存
	LPVOID LpAddr = VirtualAllocEx(hProcess, NULL, 30 + MAX_PATH * sizeof(TCHAR), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	if (LpAddr == NULL)
	{
		printf("VirtualAllocEx() ErrorCode:[0x%08x]\n", GetLastError());
		return FALSE;
	}
	//獲得LoadLibraryW函數的地址
	DWORD LoadDllAAddr = (DWORD)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
	if (LoadDllAAddr == NULL)
	{
		printf("GetProcAddress() ErrorCode:[0x%08x]\n", GetLastError());
		return FALSE;
	}
	printf("原始EIP=0x%08x\n", ctx.Eip);
	//寫入dllpath
	memcpy((char*)(ShellCode + 29), szDllPath, MAX_PATH);
	//寫入push xxxxxxxx
	*(DWORD*)(ShellCode + 3) = (DWORD)LpAddr + 29;
	//寫入loadlibraryA地址
	*(DWORD*)(ShellCode + 21) = LoadDllAAddr;
	//寫入call [addr]的[addr]
	*(DWORD*)(ShellCode + 9) = (DWORD)LpAddr + 21;
	//寫入原始eip
	*(DWORD*)(ShellCode + 25) = ctx.Eip;
	//寫入jmp [eip]的[eip]
	*(DWORD*)(ShellCode + 17) = (DWORD)LpAddr + 25;
	//把shellcode寫入目標進程
	if (!WriteProcessMemory(hProcess, LpAddr, ShellCode, 30 + MAX_PATH * sizeof(TCHAR), NULL))
	{
		printf("WriteProcessMemory() ErrorCode:[0x%08x]\n", GetLastError());
		return FALSE;
	}
	//修改目標進程的EIP,執行被注入的代碼
	ctx.Eip = (DWORD)LpAddr;
	if (!SetThreadContext(hThread, &ctx))
	{
		printf("SetThreadContext() ErrorCode:[0x%08x]\n", GetLastError());
		return FALSE;
	}
	printf("修改後EIP=0x%08x\n", ctx.Eip);
	return TRUE;
};

int main()
{
	STARTUPINFO sti;
	PROCESS_INFORMATION proci;
	memset(&sti, 0, sizeof(STARTUPINFO));
	memset(&proci, 0, sizeof(PROCESS_INFORMATION));
	sti.cb = sizeof(STARTUPINFO);
	wchar_t ExeName[MAX_PATH] = L"C:\\aimprocess.exe";//子進程的名字及啓動參數
	wchar_t DllName[MAX_PATH] = L"C:\\hookdll2.dll";//被注入的dll的完整路徑
	if (CreateProcess(NULL, ExeName, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &sti, &proci) ==NULL)
	{
		printf("CreateProcess() ErrorCode:[0x%08x]\n", GetLastError());
		getchar();
		return 0;
	}
	if (!StartHook(proci.hProcess, proci.hThread, DllName))
	{
		TerminateProcess(proci.hProcess, 0);
		printf("Terminated Process\n");
		getchar();
		return 0;
	}
	ResumeThread(proci.hThread);
	CloseHandle(proci.hProcess);
	CloseHandle(proci.hThread);
	return 0;
}

  現在對以上代碼做分析,程序首先調用CreateProcess函數來創建一個掛起的進程。創建成功後,prosic結構體保存了子進程的進程句柄和主線程的線程句柄。接下來調用StartHook函數進行代碼注入。
  現在我們來詳細地分析StartHook函數,首先它創建了一段ShellCode,ShellCode的內容將被會複製到目標進程的空間中。但是當前的ShellCode還不能正常工作。因爲它的很多數據要依靠放入目標進程的地址來決定。ShellCode實際上是一段彙編代碼後面附帶了執行這段代碼所需的變量或數據。所有的彙編代碼已在註釋當中進行標註。ShellCode數組的長度由彙編代碼長度和變量的長度的總和。
  接下來的工作是修復ShellCode中部分彙編指令引用的地址,這些地址要以目標進程寫入的地址作爲基礎偏移量。那麼我們首先應該用VirtualAllocEx在目標進程的空間中調撥一塊可執行的物理內存用來保存ShellCode代碼。當然,LoadLibraryW函數的地址還是要從本進程中獲得。當對ShellCode數據修改完畢後,就可以將ShellCode通過WriteProcessMemory函數將ShellCode複製到目標進程中。接下來需要修改目標進程的EIP指針來使主線程從ShellCode的開始處。最後,恢復目標進程,讓其繼續運行即可。
  通過以上分析,對上述代碼的執行步驟做如下總結:
    1.創建一個掛起的子進程作爲目標進程;
    2.準備一份預先設計好的ShellCode(應具有上面所述的基本功能);
    3.用VirtualAllocEx在目標進程中調撥一塊可執行的內存;
    4.以分配的內存爲基準修復ShellCode的彙編代碼引用的地址和數據;
    5.用WriteProcessMemory函數將修復完畢的ShellCode複製目標進程在第3步分配的內存中;
    6.修改目標進程的主線程的EIP指向第3步分配的內存的首地址;
    7.恢復目標進程的主線程。
  此方法的難點是設計好ShellCode代碼,這需要編寫者具有較高的彙編和分析設計能力。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章