本文詳細的介紹了在Visual Studio(如下簡稱VS)下實現API鉤子的編程方法,閱讀本文須要基礎:有操做系統的基本知識(進程管理,內存管理),會在VS下編寫和調試Win32應用程序和動態連接庫(如下簡稱DLL)。html
API鉤子是一種高級編程技巧,經常用來完成一些特別的功能,好比詞典軟件的屏幕取詞,遊戲修改軟件的數據修改等。固然,此技術更多的是被黑客或是病毒用來攻擊其它程序,截獲須要的數據或改變目標程序的行爲。本文不探討此技術的應用,只講實現。同時但願掌握此技術的人都可以合法的應用它,不要去作危險或違法的事情,害人害己。ios
1、原理
每個程序在操做系統中運行,都必須調用操做系統提供的函數——也就是API(應用程序編程接口)——來實現程序的各類功能。在Windows操做系統下,API就是那幾千個系統函數。在有些程序中並沒直接調用API的代碼,好比下面的程序:git
1
2
3
4
5
6
7
|
#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. 執行假裝函數。整個程序也分爲兩部分,一部分是負責查找並打開宿主進程和注入代碼的應用程序,另外一部分是包含修改代碼和假裝函數的注入體。windows
2、查找指定的進程
查找指定的進程有不少方法,下面簡單的介紹三種:api
1. 找到鼠標所指窗體的進程句柄
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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. 查找指定文件名的進程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
#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數據結構
3、代碼注入
上面提到過LoadLibrary能夠將指定的DLL代碼注入當前進程,若是能讓宿主進程來執行這個函數,並把咱們本身的DLL的文件名傳入,那麼咱們的代碼就能夠在宿主進程中運行了。架構
1
2
3
|
HMODULE
WINAPI LoadLibrary(
__in
LPCTSTR
lpFileName
);
|
再看另外一個函數:CreateRemoteThread,它可讓宿主進程新開一個線程,可是新線程的處理函數(LPTHREAD_START_ROUTINE)必須是宿主進程中的函數地址或系統API。併發
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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申請到的內存,並關閉打開的全部句柄,完成最後的清理工做。
如今注入代碼的步驟就比較清晰了:
- 調用OpenProcess獲取宿主進程句柄;
- 調用GetProcAddress查找LoadLibrary函數在宿主進程中的地址;
- 調用VirtualAllocEx和WriteProcessMemory將DLL文件名字符串寫入宿主進程的內存;
- 調用CreateRemoteThread執行LoadLibrary在宿主進程中運行DLL;
- 調用VirtualFreeEx釋放剛申請的內存;
- 調用WaitForSingleObject等待注入線程結束;
- 調用GetExitCodeThread獲取前面加載的DLL的句柄;
- 調用CreateRemoveThead執行FreeLibrary卸載DLL;
- 調用CloseHandle關閉打開的全部句柄。
代碼注入的全部代碼整理以下。(注意:這個程序須要在win32控制檯模式下編譯生成一個exe文件。在控制檯下運行時須要兩個參數:第1個參數爲宿主進程的映象名稱,能夠在任務管理器中查看;第2個參數爲注入體DLL的完整路徑文件名。程序運行後就會將指定的DLL裝入指定名稱的宿主進程)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
#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;
}
|
4、掛鉤
上面的程序已經能夠將自編代碼注入到宿主進程中了,下面就要進一步討論如何來編寫注入體(動態連接庫)以實現對目標API進行攔截。這一部分的內容比上面要深一些,須要一點彙編基礎知識。
1. 在VS中進行彙編級調試
VS爲用戶提供了很是強大的調試功能,能夠方便的查看注入代碼與宿主代碼的運行狀況。如今須要另建立一個項目做爲宿主進程,MFC簡單對話框程序是一個不錯的選擇。下面就以GetTickCount做爲目標API進行講解。先響應對話框的鼠標左鍵按下事件,並添加GetTickCount代碼:
1
2
3
4
5
|
void
CMyTargetDlg::OnLButtonDown(
UINT
nFlags, CPoint point)
{
GetTickCount();
CDialog::OnLButtonDown(nFlags, point);
}
|
在GetTickCount前設置斷點,運行程序後點左鍵讓程序停在這裏,而後打開反彙編(調試菜單->窗口),會看到下面的反彙編代碼:
上圖中有4行彙編指令,第1列是指令所在的內存地址,第2列是彙編指令,第3列是操做數。在不一樣的機器上編譯結果也不一樣,因此內存地址會不同,但後面的指令和操做數都大同小異。按一下F10(逐過程),運行到0063E615這一行,再按下F11(逐語句)就會進入到GetTickCount的代碼中去,見下圖:
接下來要執行的指令是:
mov edx, 7FFE0000h
|
注意這一句代碼所在的地址是7C80934A,下一句代碼是7C80934F,說明這一行mov指令的長度爲5。如今打開內存查看窗口(調試->窗口->內存),並在地址裏輸入0x7C80934A,顯示以下:
可知這條mov指令對應的機器碼便是:ba 00 00 fe 7f。此時打開寄存器窗口(調試->窗口->寄存器),能夠看到當前各寄存器的值。按下F10執行單步,還能夠看到各寄存器的變化(變化的值用紅色標出),以下圖:
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指令,第一列是這種指令的操做碼格式,第二列是指令格式,最後一列是描述信息。格令格式一列中r*是指位長爲*的寄存 器,r/m*是指位長爲*的內存地址,Imm*是指位長爲*的當即數。上文中的mov指令「mov edx, 7FFE0000h」的操做數有兩個:第一個是32位寄存器(r32)edx,第二個是一個32位當即數(Imm32)7FFE0000h。查上表可知該 mov指令就是用紅框劃出的那一種:「MOV r32, Imm32」,它對應的Opcode(操做碼)是「B8+rd」,機器碼編碼格式爲OI。在1258頁能夠看到各類mov指令的編碼格式,第一列與上表中的第三列對應,後面四列是四個操做數。
表中編碼格式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頁),以下圖:
從上表中紅色線框標出的部分中能夠看出,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指令的機器碼:
因爲自定義的函數位置隨機,且在win32操做系統的保護下,每一個進程的段地址都是固定的,程序能夠經過CS寄存器訪問,但不可以改變。所以咱們有兩種選擇,一是用JMP r/m32指令執行段內絕對跳轉,二是用JMP rel32指令執行段內相對跳轉。先講解如何利用JMP r/m32執行絕對跳轉。機器碼的格式參見IA32SDM的Vol. 2A - 2.1,以下圖:
從上圖可知,機器碼由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,以下圖:
先看一下表頭最左邊一格,第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函數,以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#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:
1
2
3
4
5
6
7
8
|
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函數很好寫:
1
2
3
4
|
void
WINAPI MonFunc(
void
)
{
MessageBox(NULL, _T(
"注入代碼"
), _T(
"示例"
), 0);
}
|
至此,您就能夠按上面的代碼編譯一個注入體DLL了,而後利用本文第三部分的注入程序就能夠將此DLL注入到宿主進程執行。
5、完美欺騙
若是您按上文所述的方法執行出成功的結果,那麼你極可能會發如今對話框肯定後宿主進程崩潰了。緣由有下面幾條:
- 假裝函數沒有正確的保持棧的平衡,致使返回時宿主清棧出錯;
- 假裝函數沒有按API執行方式執行出結果,宿主不能正常的調用系統API致使錯誤;
- 假裝函數不是線程安全的,致使宿主在併發調用時出錯;
- 宿主有安全防禦措施,檢查到攻擊後自動恢復或自我毀滅。
本文只討論前3條緣由的解決方案,不考慮第4條緣由。下面逐條解釋。
1. 保持棧的平衡
大部分API都是有參數的,而參數是由宿主在call指令執行前壓入堆棧。Win32API的調用約定是__stdcall,表示由API負責棧的清理,那麼若是假裝函數在返回時沒有適當的清棧必將致使出錯。所以,假裝函數的參數表必定要與原API相同,才能保證編譯器生成的代碼可以正確返回到宿主代碼。
2. 執行原API的功能
爲了可以執行原API的功能,必須在調用它以前恢復它原來的代碼,不然就會陷入死循環。固然,應該在改寫機器碼時保留原先的機器碼,這樣就能夠利用WriteProcessMemory將其恢復原狀。ReadProcessMemory這個API函數與WriteProcessMemory的功能相反,能夠讀取指定位置的機器碼。還要記得,在原API調用結束後還要修改它的入口點,不然下次就沒法欺騙了。整個假裝函數的結構以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//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是保證線程安全的最佳選擇,將假裝函數用這對函數包起來就能夠解決併發訪問的問題。如今的代碼應該看起來是這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
//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結束鉤子程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
#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),<br>
sizeof
(g_aBackup)/
sizeof
(g_aBackup[0]), NULL);<br>
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;
}
|