在很多情况下,为了测试代码或扩展操作系统的功能,软件开发人员或测试人员必须截取系统函数调用。有一些软件包能够提供该功能,如微软公司的 Detours* 库,或 OK Thinking Software 的 Syringe*。但是从另一个角度而言,开发人员可能希望不需借助第三方软件,自己就能实现该功能。
本文描述了函数截取的几种不同方式,并详细介绍了无需使用商业软件包,也不需受 GNU*(通用公共许可证)许可的约束,就能够实现该功能的一种通用方法。本文所有材料由英特尔公司开发,或根据 MSDN* 样本代码修改而来。
截取系统函数调用的两项基本技术
大部分截取任意函数调用的方法都是准备一个
DLL,用来替代将被截取的目标函数,然后将
DLL 注入至目标进程;在与目标进程连接的基础上,
DLL 将自己与目标函数相连。这种技术之所以适合此任务,是因为在大多数情况下我们无法获得目标应用程序的源代码,而这种技术只需相对简单地编写一个包含代换函数的
DLL,就可将其与软件的其它部分分离开来。
两种截取方法已经过研究和分析。Syringe 通过修改函数输入条目(
thunking 表)运行。而Detours 库则直接修改目标函数(
在目标进程空间内),并无条件地跳转至代换函数。此外,它还提供能够调用原始函数的 trampoline 函数。
Detours 技术之所以采用后一种方法,是因为在许多情况下,Syringe 无法找到 thunk,并且它不能提供 trampoline功能来调用原始函数。在这两种方法下,注入
DLL 的工作方式相同。
截取系统函数调用的全部工作流程如下所示:
 | DLL 注入 — 首先,主软件打开目标进程,并使其加载包含代换函数的 DLL。 |
 | 目标函数修改 — 当 DLL 连接至进程时,它在目标进程空间内修改目标函数,从而直接跳转至 DLL 中的代换函数。Trampoline 函数能够随意调用原始函数。 |
 | 目标函数截取 — 当调用目标函数时,它直接跳转至 DLL 中的代换函数。如果开发人员希望调用原始的功能,则他或她就可以调用 trampoline 函数。 |
DLL 注入
本节内容完全以 MSDN 文章“
定制调试诊断工具和实用程序 — 摆脱 DLL“地狱”

(DLL Hell)*”为基础,该文章还包括可下载的源代码。在本文附录中可获得
Inject.cpp 和
Inject.h 。已对它们进行了定制以便于集成——仅需将其包括在项目中然后调用
InjectLib 即可。使目标进程加载
DLL 的算法按如下步骤工作:
 | 通过调用 OpenProcess 打开目标进程。 |
 | 通过调用 VirtualAllocEx 在目标进程中分配内存。利用 WriteProcessMemory 将要被注入的 DLL 名称写入分配的内存。 |
 | 通过调用 GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW") 来获取 LoadLibrary 的地址; |
 | 调用 CreateRemoteThread,指定 LoadLibrary 的入口点,并将 DLL (第 2 步中) 的名称作为其自变量。目标进程将加载 DLL。 |
 | 利用 VirtualFreeEx 释放分配的内存。已不再需要该内存。 |
Inject.cpp 融合了包括稳固的安全特性等大量其它功能,但上述步骤已足够阐明其核心概念。
目标函数修改
目标函数修改为自我修改代码,尽管在将 jmp 注入进程内存的过程中存在一些缺陷,但它在
MSDN*

上具有完善的文件证明。为避免混淆,本节列出了几乎全部的样本代码。
目标函数修改的两个主要方面为代换函数和 trampoline 函数。下面的代码片断为截取
GetSystemPowerStatus API 的
DLL 示例:
这段代码为连接所做的第一件事就是调用
InterceptAPI。需要使用包含目标函数的模块名称、目标函数的名称以及代换函数的地址。
GetSystemPowerStatus 位于
kernel32.dll 中。其它基本的 Win32* API,如
MessageBox 和
PeekMessage,都能够在
user32.dll 中获得。MSDN 指定每个 API 所属的模块;未来的增强版中,将自动为给定的 API 找到正确的模块。
InterceptAPI 将目标函数的前五个字节覆盖为无条件跳转(
opcode 0xE9),后面为四个字节的带符号整数(向代换函数的位移)。位移从下一个指令开始;因此需要使用
pbReplaced - (pbTargetCode +4)。进行该代码操作时,需要注意以下两点:
 | 将区域覆盖的保护模式改为 VirtualProtect。否则,将发生非法访问错误。 |
 | 必须使用 FlushInstructionCache 来支持指令已存在于高速缓存中的情况。否则,即使内存中的指令已经有所变化,旧代码仍将在高速缓存中运行。 |
现在,当调用
GetSystemPowerStatus 函数时,它跳转至代换函数,然后直接返回调用方,成功截取调用。
Trampoline 函数
在很多情况下,代换函数除使用自身代码外,还需调用原始目标函数,这样就能够扩展 API 的功能,而不是替换整个 API。Trampoline 函数可以提供该功能。Trampoline 函数的原理如下所示:
 | 编写一个具有相同声明的哑元函数(dummy function),将作为 trampoline 使用。确保哑元函数的长度超过 10 个字节。 |
 | 在覆盖目标函数的前五个字节之前,将它们复制到 trampoline 函数的起始处。 |
 | 利用无条件跳转,将trampoline的第六个字节覆盖为目标函数的第六个字节。 |
 | 与之前一样覆盖目标函数。 |
 | 当从代换函数或其它地方调用 trampoline 函数时,它执行复制出的原始代码的前五个字节,然后跳转至实际原始代码的第六个字节。控制返回至 trampoline 的调用方。当完成其它任务时,控制返回至 API 的控制方。 |
可能存在一种复杂的情况,即原始代码的第六个字节可能是先前指令的一部分。在这种情况下,函数会覆盖部分先前指令,然后崩溃。在
GetSystemPowerStatus 的情况中,前五个字节后的新指令开始于第七个字节。因此,对于这种工作机制,需要将六个字节复制到 trampoline,并且代码必须相应地调整这个偏移量。
代码需要复制的字节数取决于 API。查看原始目标代码(
利用调试器或反汇编器)并计算需要复制的字节数是非常必要的。未来的增强版将自动检测正确的偏移量。假设我们已经知道正确的偏移量,下面的代码则显示出可建立 trampoline 函数的可扩展
InterceptAPI 函数:
1
BOOL InterceptAPI(HMODULE hLocalModule,
const
char
*
c_szDllName,
const
char
*
c_szApiName,
2
DWORD dwReplaced, DWORD dwTrampoline,
int
offset)
3
{
4
int
i;
5
DWORD dwOldProtect;
6
DWORD dwAddressToIntercept
=
(DWORD)GetProcAddress(
7
GetModuleHandle((
char
*
)c_szDllName), (
char
*
)c_szApiName);
8
9
BYTE
*
pbTargetCode
=
(BYTE
*
) dwAddressToIntercept;
10
BYTE
*
pbReplaced
=
(BYTE
*
) dwReplaced;
11
BYTE
*
pbTrampoline
=
(BYTE
*
) dwTrampoline;
12
13
//
Change the protection of the trampoline region
14
//
so that we can overwrite the first 5 + offset bytes.
15
VirtualProtect((
void
*
) dwTrampoline,
5
+
offset, PAGE_WRITECOPY,
&
dwOldProtect);
16
for
(i
=
0
;i
<
offset;i
++
)
17
*
pbTrampoline
++
=
*
pbTargetCode
++
;
18
pbTargetCode
=
(BYTE
*
) dwAddressToIntercept;
19
20
//
Insert unconditional jump in the trampoline.
21
*
pbTrampoline
++
=
0xE9
;
//
jump rel32
22
*
((signed
int
*
)(pbTrampoline))
=
(pbTargetCode
+
offset)
-
(pbTrampoline
+
4
);
23
VirtualProtect((
void
*
) dwTrampoline,
5
+
offset, PAGE_EXECUTE,
&
dwOldProtect);
24
25
//
Overwrite the first 5 bytes of the target function
26
VirtualProtect((
void
*
) dwAddressToIntercept,
5
, PAGE_WRITECOPY,
&
dwOldProtect);
27
*
pbTargetCode
++
=
0xE9
;
//
jump rel32
28
*
((signed
int
*
)(pbTargetCode))
=
pbReplaced
-
(pbTargetCode
+
4
);
29
VirtualProtect((
void
*
) dwAddressToIntercept,
5
, PAGE_EXECUTE,
&
dwOldProtect);
30
31
//
Flush the instruction cache to make sure
32
//
the modified code is executed.
33
FlushInstructionCache(GetCurrentProcess(), NULL, NULL);
34
return
TRUE;
35
}
36
结论
本文描述了截取系统函数调用的一种通用方法,同时还提供了 trampoline 函数,从而保留了原始功能。本文仅对方法进行简要描述,并未对完整的软件包作出说明,因此如下一些细节并没有实现:
 | 自动检测包含目标 API 的模块。 |
 | 自动检测 trampoline 函数的偏移量。 |
 | 删除代换函数,并注入 DLL。(到目前为止,清空代换函数的唯一方法是关闭应用程序。) |
然而,对于开发人员而言,无需依赖第三方软件包,执行截取任意系统函数调用的软件,本文中涉及的技术、说明及源代码已经足够。
1
#include
"
stdafx.h
"
2
#include
"
Inject.h
"
3
4
#include
<
tchar.h
>
5
#include
<
malloc.h
>
//
For alloca
6
#include
<
pi.h
>
7
8
#ifdef UNICODE
9
#define
InjectLib InjectLibW
10
#else
11
#define
InjectLib InjectLibA
12
#endif
//
!UNICODE
13
14
BOOL AdjustDacl(HANDLE h, DWORD DesiredAccess)
15
{
16
//
the WORLD Sid is trivial to form programmatically (S-1-1-0)
17
SID world
=
{ SID_REVISION,
1
, SECURITY_WORLD_SID_AUTHORITY,
0
};
18
19
EXPLICIT_ACCESS ea
=
20
{
21
DesiredAccess,
22
SET_ACCESS,
23
NO_INHERITANCE,
24
{
25
0
, NO_MULTIPLE_TRUSTEE,
26
TRUSTEE_IS_SID,
27
TRUSTEE_IS_USER,
28
reinterpret_cast
<
LPTSTR
>
(
&
world)
29
}
30
};
31
ACL
*
pdacl
=
0
;
32
DWORD err
=
SetEntriesInAcl(
1
,
&
ea,
0
,
&
pdacl);
33
if
(err
==
ERROR_SUCCESS)
34
{
35
err
=
SetSecurityInfo(h, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION,
0
,
0
, pdacl,
0
);
36
LocalFree(pdacl);
37
return
(err
==
ERROR_SUCCESS);
38
}
39
else
40
return
(FALSE);
41
}
42
43
//
Useful helper function for enabling a single privilege
44
BOOL EnableTokenPrivilege(HANDLE htok, LPCTSTR szPrivilege, TOKEN_PRIVILEGES
&
tpOld)
45
{
46
TOKEN_PRIVILEGES tp;
47
tp.PrivilegeCount
=
1
;
48
tp.Privileges[
0
].Attributes
=
SE_PRIVILEGE_ENABLED;
49
if
(LookupPrivilegeValue(
0
, szPrivilege,
&
tp.Privileges[
0
].Luid))
50
{
51
//
htok must have been opened with the following permissions:
52
//
TOKEN_QUERY (to get the old priv setting)
53
//
TOKEN_ADJUST_PRIVILEGES (to adjust the priv)
54
DWORD cbOld
=
sizeof
tpOld;
55
if
(AdjustTokenPrivileges(htok, FALSE,
&
tp, cbOld,
&
tpOld,
&
cbOld))
56
//
Note that AdjustTokenPrivileges may succeed, and yet
57
//
some privileges weren't actually adjusted.
58
//
You've got to check GetLastError() to be sure!
59
return
(ERROR_NOT_ALL_ASSIGNED
!=
GetLastError());
60
else
61
return
(FALSE);
62
}
63
else
64
return
(FALSE);
65
}
66
67
68
//
Corresponding restoration helper function
69
BOOL RestoreTokenPrivilege(HANDLE htok,
const
TOKEN_PRIVILEGES
&
tpOld)
70
{
71
return
(AdjustTokenPrivileges(htok, FALSE, const_cast
<
TOKEN_PRIVILEGES
*>
(
&
tpOld),
0
,
0
,
0
));
72
}
73
74
HANDLE GetProcessHandleWithEnoughRights(DWORD PID, DWORD AccessRights)
75
{
76
HANDLE hProcess
=
::OpenProcess(AccessRights, FALSE, PID);
77
if
(hProcess
==
NULL)
78
{
79
HANDLE hpWriteDAC
=
OpenProcess(WRITE_DAC, FALSE, PID);
80
if
(hpWriteDAC
==
NULL)
81
{
82
//
hmm, we don't have permissions to modify the DACL
83
//
time to take ownership
84
HANDLE htok;
85
if
(
!
OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY
|
TOKEN_ADJUST_PRIVILEGES,
&
htok))
86
return
(FALSE);
87
88
TOKEN_PRIVILEGES tpOld;
89
if
(EnableTokenPrivilege(htok, SE_TAKE_OWNERSHIP_NAME, tpOld))
90
{
91
//
SeTakeOwnershipPrivilege allows us to open objects with
92
//
WRITE_OWNER, but that's about it, so we'll update the owner,
93
//
and dup the handle so we can get WRITE_DAC permissions.
94
HANDLE hpWriteOwner
=
OpenProcess(WRITE_OWNER, FALSE, PID);
95
if
(hpWriteOwner
!=
NULL)
96
{
97
BYTE buf[
512
];
//
this should always be big enough
98
DWORD cb
=
sizeof
buf;
99
if
(GetTokenInformation(htok, TokenUser, buf, cb,
&
cb))
100
{
101
DWORD err
=
102
SetSecurityInfo(
103
hpWriteOwner,
104
SE_KERNEL_OBJECT,
105
OWNER_SECURITY_INFORMATION,
106
reinterpret_cast
<
TOKEN_USER
*>
(buf)
->
User.Sid,
107
0
,
0
,
0
108
);
109
if
(err
==
ERROR_SUCCESS)
110
{
111
//
now that we're the owner, we've implicitly got WRITE_DAC
112
//
permissions, so ask the system to reevaluate our request,
113
//
giving us a handle with WRITE_DAC permissions
114
if
(
115
!
DuplicateHandle(
116
GetCurrentProcess(),
117
hpWriteOwner,
118
GetCurrentProcess(),
119
&
hpWriteDAC,
120
WRITE_DAC, FALSE,
0
121
)
122
)
123
hpWriteDAC
=
NULL;
124
}
125
}
126
127
//
don't forget to close handle
128
::CloseHandle(hpWriteOwner);
129
}
130
131
//
not truly necessary in this app,
132
//
but included for completeness
133
RestoreTokenPrivilege(htok, tpOld);
134
}
135
136
//
don't forget to close the token handle
137
::CloseHandle(htok);
138
}
139
140
if
(hpWriteDAC)
141
{
142
//
we've now got a handle that allows us WRITE_DAC permission
143
AdjustDacl(hpWriteDAC, AccessRights);
144
145
//
now that we've granted ourselves permission to access
146
//
the process, ask the system to reevaluate our request,
147
//
giving us a handle with right permissions
148
if
(
149
!
DuplicateHandle(
150
GetCurrentProcess(),
151
hpWriteDAC,
152
GetCurrentProcess(),
153
&
hProcess,
154
AccessRights,
155
FALSE,
156
0
157
)
158
)
159
hProcess
=
NULL;
160
161
CloseHandle(hpWriteDAC);
162
}
163
}
164
165
return
(hProcess);
166
}
167
168
BOOL WINAPI InjectLibW(DWORD dwProcessId, PCWSTR pszLibFile)
169
{
170
BOOL fOk
=
FALSE;
//
Assume that the function fails
171
HANDLE hProcess
=
NULL, hThread
=
NULL;
172
PWSTR pszLibFileRemote
=
NULL;
173
174
//
Get a handle for the target process.
175
hProcess
=
176
GetProcessHandleWithEnoughRights(
177
dwProcessId,
178
PROCESS_QUERY_INFORMATION
|
//
Required by Alpha
179
PROCESS_CREATE_THREAD
|
//
For CreateRemoteThread
180
PROCESS_VM_OPERATION
|
//
For VirtualAllocEx/VirtualFreeEx
181
PROCESS_VM_WRITE
//
For WriteProcessMemory
182
);
183
if
(hProcess
==
NULL)
184
return
(FALSE);
185
186
//
Calculate the number of bytes needed for the DLL's pathname
187
int
cch
=
1
+
lstrlenW(pszLibFile);
188
int
cb
=
cch
*
sizeof
(WCHAR);
189
190
//
Allocate space in the remote process for the pathname
191
pszLibFileRemote
=
192
(PWSTR) VirtualAllocEx(hProcess, NULL, cb, MEM_COMMIT, PAGE_READWRITE);
193
194
if
(pszLibFileRemote
!=
NULL)
195
{
196
//
Copy the DLL's pathname to the remote process's address space
197
if
(WriteProcessMemory(hProcess, pszLibFileRemote,
198
(PVOID) pszLibFile, cb, NULL))
199
{
200
//
Get the real address of LoadLibraryW in Kernel32.dll
201
PTHREAD_START_ROUTINE pfnThreadRtn
=
(PTHREAD_START_ROUTINE)
202
GetProcAddress(GetModuleHandle(TEXT(
"
Kernel32
"
)),
"
LoadLibraryW
"
);
203
if
(pfnThreadRtn
!=
NULL)
204
{
205
//
Create a remote thread that calls LoadLibraryW(DLLPathname)
206
hThread
=
CreateRemoteThread(hProcess, NULL,
0
,
207
pfnThreadRtn, pszLibFileRemote,
0
, NULL);
208
if
(hThread
!=
NULL)
209
{
210
//
Wait for the remote thread to terminate
211
WaitForSingleObject(hThread, INFINITE);
212
213
fOk
=
TRUE;
//
Everything executed successfully
214
215
CloseHandle(hThread);
216
}
217
}
218
}
219
//
Free the remote memory that contained the DLL's pathname
220
VirtualFreeEx(hProcess, pszLibFileRemote,
0
, MEM_RELEASE);
221
}
222
223
CloseHandle(hProcess);
224
225
return
(fOk);
226
}
227
228
229
BOOL WINAPI InjectLibA(DWORD dwProcessId, PCSTR pszLibFile) {
230
231
//
Allocate a (stack) buffer for the Unicode version of the pathname
232
PWSTR pszLibFileW
=
(PWSTR)
233
_alloca((lstrlenA(pszLibFile)
+
1
)
*
sizeof
(WCHAR));
234
235
//
Convert the ANSI pathname to its Unicode equivalent
236
wsprintfW(pszLibFileW, L
"
%S
"
, pszLibFile);
237
238
//
Call the Unicode version of the function to actually do the work.
239
return
(InjectLibW(dwProcessId, pszLibFileW));
240
}