Win32環境下代碼注入與API鈎子的實現


本文詳細的介紹了在Visual Studio(以下簡稱VS)下實現API鈎子的編程方法,閱讀本文需要基礎:有操作系統的基本知識(進程管理,內存管理),會在VS下編寫和調試Win32應用程序和動態鏈接庫(以下簡稱DLL)。

API鈎子是一種高級編程技巧,常常用來完成一些特別的功能,比如詞典軟件的屏幕取詞,游戲修改軟件的數據修改等。當然,此技術更多的是被黑客或是病毒用來攻擊其它程序,截獲需要的數據或改變目標程序的行為。本文不探討此技術的應用,只講實現。同時希望掌握此技術的人都能夠合法的應用它,不要去做危險或違法的事情,害人害己。

一、原理

每一個程序在操作系統中運行,都必須調用操作系統提供的函數——也就是API(應用程序編程接口)——來實現程序的各種功能。在Windows操作系統下,API就是那幾千個系統函數。在有些程序中並沒直接調用API的代碼,比如下面的程序:

#include <iostream>
using namespace std;
int main(void)
{
	cout << "Hello World!" << endl;
	return 0;
}

事實上,cout對象的內部處理函數已經替你調用API。就算你的main函數是空的,里面什么代碼都不寫,只要程序被操作系統啟動,也會調用一些基本的API,比如LoadLibrary。這個函數是用來加載DLL的,也就是在進程運行的過程中,把DLL中的程序指令和數據讀入當前進程並執行啟動代碼,我們后面會用到這個函數。

如果能夠設法用自定義函數替換宿主進程調用的目標API函數,那么就可以截獲宿主進程傳入目標API的參數,並可以改變宿主進程的行為。但要想修改目標API函數必須先查找並打開宿主進程,並讓自定義代碼能在宿主進程中運行。因此掛API鈎子分為四步:1. 查找並打開宿主進程,2. 將注入體裝入宿主進程中運行,3. 用偽裝函數替換目標API,4. 執行偽裝函數。整個程序也分為兩部分,一部分是負責查找並打開宿主進程和注入代碼的應用程序,另一部分是包含修改代碼和偽裝函數的注入體。

二、查找指定的進程

查找指定的進程有很多方法,下面簡單的介紹三種:

1. 找到鼠標所指窗體的進程句柄

DWORD GetProcIDFromCursor(void)
{
	//Get current mouse cursor position
	POINT ptCursor;
	if (!GetCursorPos(&ptCursor))
	{
		cout << "GetCursorPos Error: " << GetLastError() << endl;
		return 0;
	}

	//Get window handle from cursor postion
	HWND hWnd = WindowFromPoint(&ptCursor);
	if (NULL == hWnd)
	{
		cout << "No window exists at the given point!" << endl;
		return 0;
	}

	//Get the process ID belong to the window.
	DWORD dwProcId;
	GetWindowThreadProcessId(hWnd, &dwProcId);

	return dwProcId;
} 

2. 查找指定文件名的進程

#include <Psapi.h>
#pragma comment(lib, "Psapi.lib")
DWORD GetProcIDFromName(LPCTSTR lpName)
{
	DWORD aProcId[1024], dwProcCnt, dwModCnt;
	HMODULE hMod;
	TCHAR szPath[MAX_PATH];

	//枚舉出所有進程ID
	if (!EnumProcesses(aProcId, sizeof(aProcId), &dwProcCnt))
	{
		cout << "EnumProcesses error: " << GetLastError() << endl;
		return 0;
	}

	//遍例所有進程
	for (DWORD i = 0; i < dwProcCnt; ++i)
	{
		//打開進程,如果沒有權限打開則跳過
		HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, aProcId[i]);
		if (NULL != hProc)
		{
			//打開進程的第1個Module,並檢查其名稱是否與目標相符
			if (EnumProcessModules(hProc, &hMod, sizeof(hMod), &dwModCnt))
			{
				GetModuleBaseName(hProc, hMod, szPath, MAX_PATH);
				if (0 == lstrcmpi(szPath, lpName))
				{
					CloseHandle(hProc);
					return aProcId[i];
				}
			}
			CloseHandle(hProc);
		}
	}
	return 0;
}

 3. 查找其它指定信息的進程

通過CreateToolhelp32Snapshot枚舉系統中正在運行的所有進程,並通過相關數據結構得到進程的信息,具體用法可以參見:

http://msdn.microsoft.com/en-us/library/windows/desktop/ms686701.aspx

三、代碼注入

上面提到過LoadLibrary可以將指定的DLL代碼注入當前進程,如果能讓宿主進程來執行這個函數,並把我們自己的DLL的文件名傳入,那么我們的代碼就可以在宿主進程中運行了。

HMODULE WINAPI LoadLibrary(
  __in          LPCTSTR lpFileName
);

再看另一個函數:CreateRemoteThread,它可以讓宿主進程新開一個線程,但是新線程的處理函數(LPTHREAD_START_ROUTINE)必須是宿主進程中的函數地址或系統API。

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
);

//其中LPTHREAD_START_ROUTINE的定義如下
typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(
    LPVOID lpThreadParameter
    );

如果可以用讓宿主進程新開一個線程,執行LoadLibrary函數,而參數是注入體DLL的文件名,就大功告成了。不過要完成這些操作,我們先來分析一下可行性。

我們知道,所有系統API函數的調用方式都是__stdcall,即參數采用從右到左的壓棧方式,自己在退出時清空堆棧。這一類函數的具體調用過程如下:在調用前先由調用者將所有參數以地址或數值的形式從右向左壓入棧中,然后用call指令調用該函數;進入函數后,先從棧中取出這些參數再進行運算,並在函數返回前將之前壓入的棧數據全部彈出以維持棧平衡,最后用eax寄存儲傳遞返回值(地址或數值)給調用者。這也就是說在指令層面上講,API函數的基本調用方式都相同,然而調用者必須在棧中壓入確定數量的參數,若壓入的參數數量不匹配,函數內的取棧和彈棧操作將會使得棧數據錯亂,最終導致程序崩潰。

通過觀察發現LoadLibrary的參數數量剛好與LPTHREAD_START_ROUTINE都只有一個參數,那么如果能夠獲取LoadLibrary函數在宿主進程中的地址,作為lpStartAddress傳入CreateRemoteThread,並將我們的注入體DLL的文件名作為lpParameter傳入,那么就可以讓宿主進程執行注入體代碼了。為了將DLL的文件名傳入宿主進程,我們還需要以下四個API:VirtualAllocEx和VirtualFreeEx可以在宿主進程中分配和釋放一段內存空間;ReadProcessMemory和WriteProcessMemory可以在宿主進程中的指定內存地址讀出或寫入數據。

在注入的代碼執行完畢后,還要完成清理工作。首先是卸載剛剛載入的DLL,需要使用另一個系統API:FreeLibrary。過程與上面的代碼注入一樣,使用CreateRemoteThread,將FreeLibrary的地址作為lpStartAddress參數傳入。注意到FreeLibrary的參數是一個HMODULE,該句柄其實是一個Module的全局ID,一般由LoadLibrary的返回值給出。因此可以調用GetExitCodeThread獲取前面執行的LoadLibrary線程的返回值,再作為CreateRemoteThread的lpParameter參數傳入,這樣就完成了DLL的卸載。還要記得用VirtualFreeEx釋放VirtualAllocEx申請到的內存,並關閉打開的所有句柄,完成最后的清理工作。

現在注入代碼的步驟就比較清晰了:

  1. 調用OpenProcess獲取宿主進程句柄;
  2. 調用GetProcAddress查找LoadLibrary函數在宿主進程中的地址;
  3. 調用VirtualAllocEx和WriteProcessMemory將DLL文件名字符串寫入宿主進程的內存;
  4. 調用CreateRemoteThread執行LoadLibrary在宿主進程中運行DLL;
  5. 調用VirtualFreeEx釋放剛申請的內存;
  6. 調用WaitForSingleObject等待注入線程結束;
  7. 調用GetExitCodeThread獲取前面加載的DLL的句柄;
  8. 調用CreateRemoveThead執行FreeLibrary卸載DLL;
  9. 調用CloseHandle關閉打開的所有句柄。

代碼注入的所有代碼整理如下。(注意:這個程序需要在win32控制台模式下編譯生成一個exe文件。在控制台下運行時需要兩個參數:第1個參數為宿主進程的映象名稱,可以在任務管理器中查看;第2個參數為注入體DLL的完整路徑文件名。程序運行后就會將指定的DLL裝入指定名稱的宿主進程)

#include <tchar.h>
#include <Windows.h>
#include <atlstr.h>
#include <Psapi.h>
#pragma comment(lib, "Psapi.lib") 

#include <iostream>
#include <string>
using namespace std;

DWORD FindProc(LPCSTR lpName)
{
	DWORD aProcId[1024], dwProcCnt, dwModCnt;
	char szPath[MAX_PATH];
	HMODULE hMod;

	//枚舉出所有進程ID
	if (!EnumProcesses(aProcId, sizeof(aProcId), &dwProcCnt))
	{
		//cout << "EnumProcesses error: " << GetLastError() << endl;
		return 0;
	}

	//遍例所有進程
	for (DWORD i = 0; i < dwProcCnt; ++i)
	{
		//打開進程,如果沒有權限打開則跳過
		HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, aProcId[i]);
		if (NULL != hProc)
		{
			//打開進程的第1個Module,並檢查其名稱是否與目標相符
			if (EnumProcessModules(hProc, &hMod, sizeof(hMod), &dwModCnt))
			{
				GetModuleBaseNameA(hProc, hMod, szPath, MAX_PATH);
				if (0 == _stricmp(szPath, lpName))
				{
					CloseHandle(hProc);
					return aProcId[i];
				}
			}
			CloseHandle(hProc);
		}
	}
	return 0;
}

//第一個參數為宿主進程的映象名稱,可以任務管理器中查看
//第二個參數為需要注入的DLL的完整文件名
int main(int argc, char *argv[])
{
	if (argc != 3)
	{
		cout << "Invalid parameters!" << endl;
		return -1;
	}
	//查找目標進程,並打開句柄
	DWORD dwProcID = FindProc(argv[1]);
	if (dwProcID == 0)
	{
		cout << "Target process not found!" << endl;
		return -1;
	}
	HANDLE hTarget = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcID);
	if (NULL == hTarget)
	{
		cout << "Can't Open target process!" << endl;
		return -1;
	}

	//獲取LoadLibraryW和FreeLibrary在宿主進程中的入口點地址
	HMODULE hKernel32 = GetModuleHandle(_T("Kernel32"));
	LPTHREAD_START_ROUTINE pLoadLib = (LPTHREAD_START_ROUTINE)
		GetProcAddress(hKernel32, "LoadLibraryW");
	LPTHREAD_START_ROUTINE pFreeLib = (LPTHREAD_START_ROUTINE)
		GetProcAddress(hKernel32, "FreeLibrary");
	if (NULL == pLoadLib || NULL == pFreeLib)
	{
		cout << "Library procedure not found: " << GetLastError() << endl;
		CloseHandle(hTarget);
		return -1;
	}

	WCHAR szPath[MAX_PATH];
	MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, argv[2], -1,
		szPath, sizeof(szPath) / sizeof(szPath[0]));

	//在宿主進程中為LoadLibraryW的參數分配空間,並將參數值寫入
	LPVOID lpMem = VirtualAllocEx(hTarget, NULL, sizeof(szPath),
		MEM_COMMIT, PAGE_READWRITE);
	if (NULL == lpMem)
	{
		cout << "Can't alloc memory block: " << GetLastError() << endl;
		CloseHandle(hTarget);
		return -1;
	}

	// 參數即為要注入的DLL的文件路徑
	if (!WriteProcessMemory(hTarget, lpMem, (void*)szPath, sizeof(szPath), NULL))
	{
		cout << "Can't write parameter to memory: " << GetLastError() << endl;
		VirtualFreeEx(hTarget, lpMem, sizeof(szPath), MEM_RELEASE);
		CloseHandle(hTarget);
		return -1;
	}

	//創建信號量,DLL代碼可以通過ReleaseSemaphore來通知主程序清理
	HANDLE hSema = CreateSemaphore(NULL, 0, 1, _T("Global\\InjHack"));

	//將DLL注入宿主進程
	HANDLE hThread = CreateRemoteThread(hTarget, NULL, 0, pLoadLib, lpMem, 0, NULL);

	//釋放宿主進程內的參數內存
	VirtualFreeEx(hTarget, lpMem, sizeof(szPath), MEM_RELEASE);

	if (NULL == hThread)
	{
		cout << "Can't create remote thread: " << GetLastError() << endl;
		CloseHandle(hTarget);
		return -1;
	}

	//等待DLL信號量或宿主進程退出
	WaitForSingleObject(hThread, INFINITE);
	HANDLE hObj[2] = {hTarget, hSema};
	if (WAIT_OBJECT_0 == WaitForMultipleObjects(2, hObj, FALSE, INFINITE))
	{
		cout << "Target process exit." << endl;
		CloseHandle(hTarget);
		return 0;
	}
	CloseHandle(hSema);

	//根據線程退出代碼獲取DLL的Module ID
	DWORD dwLibMod;
	if (!GetExitCodeThread(hThread, &dwLibMod))
	{
		cout << "Can't get return code of LoadLibrary: " << GetLastError() << endl;
		CloseHandle(hThread);
		CloseHandle(hTarget);
		return -1;
	}

	//關閉線程句柄
	CloseHandle(hThread);

	//再次注入FreeLibrary代碼以釋放宿主進程加載的注入體DLL
	hThread = CreateRemoteThread(hTarget, NULL, 0, pFreeLib, (void*)dwLibMod, 0, NULL);
	if (NULL == hThread)
	{
		cout << "Can't call FreeLibrary: " << GetLastError() << endl;
		CloseHandle(hTarget);
		return -1;
	}
	WaitForSingleObject(hThread, INFINITE);
	CloseHandle(hThread);

	CloseHandle(hTarget);
	return 0;
}

四、掛鈎

上面的程序已經可以將自編代碼注入到宿主進程中了,下面就要進一步討論如何來編寫注入體(動態鏈接庫)以實現對目標API進行攔截。這一部分的內容比上面要深一些,需要一點匯編基礎知識。

1. 在VS中進行匯編級調試

VS為用戶提供了非常強大的調試功能,可以方便的查看注入代碼與宿主代碼的運行情況。現在需要另創建一個項目作為宿主進程,MFC簡單對話框程序是一個不錯的選擇。下面就以GetTickCount作為目標API進行講解。先響應對話框的鼠標左鍵按下事件,並添加GetTickCount代碼:

void CMyTargetDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
	GetTickCount();
	CDialog::OnLButtonDown(nFlags, point);
}

 在GetTickCount前設置斷點,運行程序后點左鍵讓程序停在這里,然后打開反匯編(調試菜單->窗口),會看到下面的反匯編代碼:

 GetTickCount的斷點

 上圖中有4行匯編指令,第1列是指令所在的內存地址,第2列是匯編指令,第3列是操作數。在不同的機器上編譯結果也不同,所以內存地址會不一樣,但后面的指令和操作數都大同小異。按一下F10(逐過程),運行到0063E615這一行,再按下F11(逐語句)就會進入到GetTickCount的代碼中去,見下圖:

 GetTickCount的反匯編

 接下來要執行的指令是:

mov edx, 7FFE0000h

注意這一句代碼所在的地址是7C80934A,下一句代碼是7C80934F,說明這一行mov指令的長度為5。現在打開內存查看窗口(調試->窗口->內存),並在地址里輸入0x7C80934A,顯示如下:

 0x7C80934A

可知這條mov指令對應的機器碼即是:ba 00 00 fe 7f。此時打開寄存器窗口(調試->窗口->寄存器),可以看到當前各寄存器的值。按下F10執行單步,還可以看到各寄存器的變化(變化的值用紅色標出),如下圖:

 registers

2. 指令的格式

為了繼續要了解x86架構下匯編碼和機器碼的對應關系,需要參考一部非常重要的文獻“Intel® 64 and IA-32 Architectures Software Developer's Manual”(以下簡稱IA32SDM),這是Intel公司免費提供給開發者的,可以在下面的網址找到3卷合訂本:

http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html

在IA32SDM的Vol. 2B - 4.2(總第1257頁)可以找到各種mov指令的說明:

 mov_opcode

上表中每一行表示了一種mov指令,第一列是這種指令的操作碼格式,第二列是指令格式,最后一列是描述信息。格令格式一列中r*是指位長為*的寄存 器,r/m*是指位長為*的內存地址,Imm*是指位長為*的立即數。上文中的mov指令“mov edx, 7FFE0000h”的操作數有兩個:第一個是32位寄存器(r32)edx,第二個是一個32位立即數(Imm32)7FFE0000h。查上表可知該 mov指令就是用紅框划出的那一種:“MOV r32, Imm32”,它對應的Opcode(操作碼)是“B8+rd”,機器碼編碼格式為OI。在1258頁可以看到各種mov指令的編碼格式,第一列與上表中的第三列對應,后面四列是四個操作數。

mov_encode

表中編碼格式OI包含兩個操作數,先是Opcode加寄存器代碼,后面緊跟了一個立即數。而Opcode的編寫格式參見IA32SDM的Vol. 2A - 3.1.1.1(總第606頁),摘錄如下:

The “Opcode” column in the table above shows the object code produced for each form of the instruction. When possible, codes are given as hexadecimal bytes in the same order in which they appear in memory. Definitions of entries other than hexadecimal bytes are as follows:

  • REX.W — Indicates the use of a REX prefix that affects operand size or instruction semantics. The ordering of the REX prefix and other optional/mandatory instruction prefixes are discussed Chapter 2. Note that REX prefixes that promote legacy instructions to 64-bit behavior are not listed explicitly in the opcode column.
  • /digit — A digit between 0 and 7 indicates that the ModR/M byte of the instruction uses only the r/m (register or memory) operand. The reg field contains the digit that provides an extension to the instruction's opcode.(翻譯:這是一個0到7的數字,表示指令的ModR/M字節只使用r/m操作數。ModR/M的reg位就是該數,作為操作碼的一個附加碼)
  • /r — Indicates that the ModR/M byte of the instruction contains a register operand and an r/m operand.
  • cb, cw, cd, cp, co, ct — A 1-byte (cb), 2-byte (cw), 4-byte (cd), 6-byte (cp), 8-byte (co) or 10-byte (ct) value following the opcode. This value is used to specify a code offset and possibly a new value for the code segment register.
  • ib, iw, id, io — A 1-byte (ib), 2-byte (iw), 4-byte (id) or 8-byte (io) immediate operand to the instruction that follows the opcode, ModR/M bytes or scaleindexing bytes. The opcode determines if the operand is a signed value. All words, doublewords and quadwords are given with the low-order byte first.
  • +rb, +rw, +rd, +ro — A register code, from 0 through 7, added to the hexadecimal byte given at the left of the plus sign to form a single opcode byte. See Table 3-1 for the codes(翻譯:這是一個寄存器代碼,范圍由0到7。與+號左邊的16進制數代數相加構成一個完整的操作碼字節。具體代碼參見Table 3-1)The +ro columns in the table are applicable only in 64-bit mode.
  • +i — A number used in floating-point instructions when one of the operands is ST(i) from the FPU register stack. The number i (which can range from 0 to 7) is added to the hexadecimal byte given at the left of the plus sign to form a single opcode byte.

 按照標記為紅色的描述可知Opcode“B8+rd”中的B8是基礎碼值0xB8,rd表示32位寄存器EDX的代號。寄存器的代碼表可參見IA32SDM的Vol. 2A - Table 3-1(總第607頁),如下圖:

 reg_tbl

 從上表中紅色線框標出的部分中可以看出,EDX對應的附加碼為2,因此這條mov指令的Opcode就是0xB8 + 0x02 = 0xBA。跟據編碼格式OI,后面緊跟一個32位的立即數0x7FFE0000,由於Intel的CPU體系是Little Ending,所以字節序為逆序,故在內存查看器中立即數顯示為“00 00 fe 7f”。綜上所述,該mov指令的完整機器碼為:“ba 00 00 fe 7f”,與內存查看器的結果吻合。

2. 准備JMP

上面簡單介紹了在VS進行匯編級調試的基本方法,並以mov指令為范例講解了如何分析機器碼。掌握了這些工具和資料,就可以清晰地了解我們下面要完成的代碼在系統內部執行的細節。從上節可知,GetTickCount這個API執行的第一條指令是mov,如果能把mov的Opcode改為jmp,那就可以跳轉到自定義的函數地址執行任意代碼了。從IA32SDM(Vol. 2A - 3.2)中查出jmp指令的機器碼:

 jmp_opcode

由於自定義的函數位置隨機,且在win32操作系統的保護下,每個進程的段地址都是固定的,程序可以通過CS寄存器訪問,但不能夠改變。因此我們有兩種選擇,一是用JMP r/m32指令執行段內絕對跳轉,二是用JMP rel32指令執行段內相對跳轉。先講解如何利用JMP r/m32執行絕對跳轉。機器碼的格式參見IA32SDM的Vol. 2A - 2.1,如下圖:

 instruction_format

從上圖可知,機器碼由6大部分組成,而JMP r/m32指令對應的機器碼為“FF /4”(其中/4的含義參見上文中Opcode說明里用藍色標記的文字),用到了其中3個部分:1個字節的Opcode(即0xFF)、1個字節的ModR/M和4個字節的Displacement操作數。其中的ModR/M指定了CPU的尋址方式以及Opcode的附加碼,它又分為三段:Mod、Reg/Opcode和R/M,具體構成可參見IA32SDM的Vol. 2A - 2.1.3和后面的Table 2-2,如下圖:

 ModRM

先看一下表頭最左邊一格,第6行“/digit (Opcode)”就是機器碼“FF /4”中的4,所以看紅框標記的那一列(4的二進制為100)就可以了。“Effective Address”指定了尋指方式,為了避免對寄存器進行操作,用1條指令就完成跳轉,我們選擇最簡單的“disp32”這一行,它表示僅用指令機器碼中的第3部分Displacement表示跳轉的目標地址。這樣就確定了使用的Mod位為00,Reg/Opcode位為100,R/M位為101。計算可得ModR/M字節為00 100 101(二進制) = 0x25。

Displacement指向一段4字節的內存,這段內存里存放的是最終的目標地址。因此需要先用VirtualAllocEx申請4個字節的空間,將自定義函數的地址存入,然后再將申請的地址填入Displacement。綜上所述,完整的機器碼應該是FF 25 XX XX XX XX,最后面的4個字節是一個存有目標函數入口地址的內存地址。

用JMP r/m32指令完成跳轉是比較復雜的,不僅需要申請和釋放內存,且整個機器指令有6個字節。更簡單的方法就是利用JMP rel32指令執行相對跳轉,而機器碼只有5個字節。JMP rel32對應的機器碼是E9 cd,其中cd就是相對地址,計算方法為:目標地址 - 當前指令地址 - 5。在准備好JMP指令的機器碼后,就可以將其替換到目標API的入口地址處,欺騙宿主進程執行偽裝函數。

3. 修改入口點

看完上面的介紹,相信您已經迫不及待的想要嘗試如何對目標API掛鈎了。雖然還有很多問題沒有解決,比如怎樣返回,怎樣執行原API功能,怎樣全身而退等等,但這些問題可以先放一放,先來看看能否利用上面的方法成功掛鈎。

首先需要建立一個DLL項目以生成注入體,自定義一個DllMain函數,如下:

#include <windows.h>

BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, LPVOID lpvReserved)
{
	switch (fdwReason)
	{
	case DLL_PROCESS_ATTACH:
		DisableThreadLibraryCalls(hInstDll);
		InstallMonitor();
		break;

	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

 然后編寫掛鈎函數InstallMonitor:

void InstallMonitor(void)
{
	HANDLE hProc = GetCurrentProcess();
	BYTE aOpcode[5] = {0xE9}; //JMP Procudure
	*(DWORD*)(&g_aOpcode[1]) = DWORD(MonFunc) - DWORD(GetTickCount) - 5;
	WriteProcessMemory(hProc, LPVOID(GetTickCount), LPVOID(aOpcode), 5, NULL);
	CloseHandle(hProc);
}

上面的代碼很好理解,aOpcode就是根據前文介紹的方法構造的jmp指令,指定跳轉到自定義的偽裝函數MonFunc,然后用WriteProcessMemory將jmp指令填寫入GetTickCount的代碼處。偽裝函數MonFunc函數很好寫:

void WINAPI MonFunc(void)
{
	MessageBox(NULL, _T("注入代碼"), _T("示例"), 0);
}

 至此,您就可以按上面的代碼編譯一個注入體DLL了,然后利用本文第三部分的注入程序就可以將此DLL注入到宿主進程執行。

五、完美欺騙

如果您按上文所述的方法執行出成功的結果,那么你很可能會發現在對話框確定后宿主進程崩潰了。原因有下面幾條:

  1. 偽裝函數沒有正確的保持棧的平衡,導致返回時宿主清棧出錯;
  2. 偽裝函數沒有按API執行方式執行出結果,宿主不能正常的調用系統API導致錯誤;
  3. 偽裝函數不是線程安全的,導致宿主在並發調用時出錯;
  4. 宿主有安全防護措施,檢查到攻擊后自動恢復或自我毀滅。

本文只討論前3條原因的解決方案,不考慮第4條原因。下面逐條解釋。

1. 保持棧的平衡

大部分API都是有參數的,而參數是由宿主在call指令執行前壓入堆棧。Win32API的調用約定是__stdcall,表示由API負責棧的清理,那么如果偽裝函數在返回時沒有適當的清棧必將導致出錯。因此,偽裝函數的參數表一定要與原API相同,才能保證編譯器生成的代碼能夠正確返回到宿主代碼。

2. 執行原API的功能

為了能夠執行原API的功能,必須在調用它之前恢復它原來的代碼,否則就會陷入死循環。當然,應該在改寫機器碼時保留原先的機器碼,這樣就可以利用WriteProcessMemory將其恢復原狀。ReadProcessMemory這個API函數與WriteProcessMemory的功能相反,可以讀取指定位置的機器碼。還要記得,在原API調用結束后還要修改它的入口點,否則下次就無法欺騙了。整個偽裝函數的結構如下:

//Monitor Function
DWORD WINAPI MonFunc()
{
	//Restore the original API before calling it
	ReleaseBase();

	//Calling the original API
	DWORD dw = GetTickCount();

	//Monitor the original API again
	MonitorBase();

	//You can do anything here

	return dw;
}

3. 線程安全

用EnterCriticalSection和LeaveCriticalSection是保證線程安全的最佳選擇,將偽裝函數用這對函數包起來就可以解決並發訪問的問題。現在的代碼應該看起來是這樣:

//Monitor Function
DWORD WINAPI MonFunc()
{
	//Thread safety
	EnterCriticalSection(&g_cs);

	//Restore the original API before calling it
	ReleaseBase();
	DWORD dw = GetTickCount();
	MonitorBase();

	//You can do anything here

	//Thread safety
	LeaveCriticalSection(&g_cs);
	return dw;
}

4. 完整示例

下面貼出注入體DLL的完整代碼,供您參考。這個DLL對GetTickCount掛了鈎子,您可以在偽裝函數MonFunc中添加任意的自定義代碼,並在退出的時候調用UninstallMonitor結束鈎子程序。

#include <tchar.h>
#include <Windows.h>

//Handle of current process
HANDLE g_hProc;

//Backup of orignal code of target api
BYTE g_aBackup[6];
BYTE g_aOpcode[6];

//Critical section, prevent concurrency of calling the monitor
CRITICAL_SECTION g_cs;

//Base address of target API in DWORD
DWORD g_dwApiFunc = (DWORD)GetTickCount;

//Hook the target API
__inline BOOL MonitorBase(void)
{
	// Modify the heading 6 bytes opcode in target API to jmp instruction,
	// the jmp instruction will lead the EIP to our fake function
  ReadProcessMemory(g_hProc, LPVOID(g_dwApiFunc), LPVOID(g_aBackup),
    sizeof(g_aBackup)/ sizeof(g_aBackup[0]), NULL);
return WriteProcessMemory(g_hProc, LPVOID(g_dwApiFunc), LPVOID(g_aOpcode), sizeof(g_aOpcode) / sizeof(g_aOpcode[0]), NULL); } //Unhook the target API __inline BOOL ReleaseBase(void) { // Restore the heading 6 bytes opcode of target API. return WriteProcessMemory(g_hProc, LPVOID(g_dwApiFunc), LPVOID(g_aBackup), sizeof(g_aOpcode) / sizeof(g_aOpcode[0]), NULL); } //Pre-declare BOOL UninstallMonitor(void); //Monitor Function DWORD WINAPI MonFunc() { //Thread safety EnterCriticalSection(&g_cs); //Restore the original API before calling it ReleaseBase(); DWORD dw = GetTickCount(); MonitorBase(); //You can do anything here, and you can call the UninstallMonitor //when you want to leave. //Thread safety LeaveCriticalSection(&g_cs); return dw; } //Install Monitor BOOL InstallMonitor(void) { //Get handle of current process g_hProc = GetCurrentProcess(); g_aOpcode[0] = 0xE9; //JMP Procudure *(DWORD*)(&g_aOpcode[1]) = (DWORD)MonFunc - g_dwApiFunc - 5; InitializeCriticalSection(&g_cs); //Start monitor return MonitorBase(); } BOOL UninstallMonitor(void) { //Release monitor if (!ReleaseBase()) return FALSE; DeleteCriticalSection(&g_cs); CloseHandle(g_hProc); //Synchronize to main application, release semaphore to free injector HANDLE hSema = OpenSemaphore(EVENT_ALL_ACCESS, FALSE, _T("Global\\InjHack")); if (hSema == NULL) return FALSE; return ReleaseSemaphore(hSema, 1, (LPLONG)g_hProc); } BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, LPVOID lpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: DisableThreadLibraryCalls(hInstDll); InstallMonitor(); break; case DLL_PROCESS_DETACH: break; } return TRUE; }

 


免責聲明!

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



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