在很多情况下,为了测试代码或扩展操作系统的功能,软件开发人员或测试人员必须截取系统函数调用。

有一些软件包能够提供该功能,如微软公司的 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 函数:

BOOL InterceptAPI(HMODULE hLocalModule, const char* c_szDllName, const char* c_szApiName,
DWORD dwReplaced, DWORD dwTrampoline, int offset)
{
int i;
DWORD dwOldProtect;
DWORD dwAddressToIntercept = (DWORD)GetProcAddress(
GetModuleHandle((char*)c_szDllName), (char*)c_szApiName);

BYTE *pbTargetCode = (BYTE *) dwAddressToIntercept;
BYTE *pbReplaced = (BYTE *) dwReplaced;
BYTE *pbTrampoline = (BYTE *) dwTrampoline;

// Change the protection of the trampoline region
// so that we can overwrite the first 5 + offset bytes.
VirtualProtect((void *) dwTrampoline, 5+offset, PAGE_WRITECOPY, &dwOldProtect);
for (i=0;i<offset;i++)
*pbTrampoline++ = *pbTargetCode++;
pbTargetCode = (BYTE *) dwAddressToIntercept;

// Insert unconditional jump in the trampoline.
*pbTrampoline++ = 0xE9; // jump rel32
*((signed int *)(pbTrampoline)) = (pbTargetCode+offset) - (pbTrampoline + 4);
VirtualProtect((void *) dwTrampoline, 5+offset, PAGE_EXECUTE, &dwOldProtect);

// Overwrite the first 5 bytes of the target function
VirtualProtect((void *) dwAddressToIntercept, 5, PAGE_WRITECOPY, &dwOldProtect);
*pbTargetCode++ = 0xE9; // jump rel32
*((signed int *)(pbTargetCode)) = pbReplaced - (pbTargetCode +4);
VirtualProtect((void *) dwAddressToIntercept, 5, PAGE_EXECUTE, &dwOldProtect);

// Flush the instruction cache to make sure
// the modified code is executed.
FlushInstructionCache(GetCurrentProcess(), NULL, NULL);
return TRUE;
}
结论
本文描述了截取系统函数调用的一种通用方法,同时还提供了 trampoline 函数,从而保留了原始功能。本文仅对方法进行简要描述,并未对完整的软件包作出说明,
因此如下一些细节并没有实现:
自动检测包含目标 API 的模块。

自动检测 trampoline 函数的偏移量。

删除代换函数,并注入 DLL。(到目前为止,清空代换函数的唯一方法是关闭应用程序。)
然而,对于开发人员而言,无需依赖第三方软件包,执行截取任意系统函数调用的软件,本文中涉及的技术、说明及源代码已经足够。

附录

#include "stdafx.h"

#include "Inject.h"



#include <tchar.h>

#include <malloc.h> // For alloca

#include <pi.h>



#ifdef UNICODE

#define InjectLib InjectLibW

#else

#define InjectLib InjectLibA

#endif // !UNICODE



BOOL AdjustDacl(HANDLE h, DWORD DesiredAccess)

{

// the WORLD Sid is trivial to form programmatically (S-1-1-0)

SID world = { SID_REVISION, 1, SECURITY_WORLD_SID_AUTHORITY, 0 };



EXPLICIT_ACCESS ea =

{

DesiredAccess,

SET_ACCESS,

NO_INHERITANCE,

{

0, NO_MULTIPLE_TRUSTEE,

TRUSTEE_IS_SID,

TRUSTEE_IS_USER,

reinterpret_cast<LPTSTR>(&world)

}

};

ACL* pdacl = 0;

DWORD err = SetEntriesInAcl(1, &ea, 0, &pdacl);

if (err == ERROR_SUCCESS)

{

err = SetSecurityInfo(h, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, 0, 0, pdacl, 0);

LocalFree(pdacl);

return(err == ERROR_SUCCESS);

}

else

return(FALSE);

}



// Useful helper function for enabling a single privilege

BOOL EnableTokenPrivilege(HANDLE htok, LPCTSTR szPrivilege, TOKEN_PRIVILEGES& tpOld)

{

TOKEN_PRIVILEGES tp;

tp.PrivilegeCount = 1;

tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

if (LookupPrivilegeValue(0, szPrivilege, &tp.Privileges[0].Luid))

{

// htok must have been opened with the following permissions:

// TOKEN_QUERY (to get the old priv setting)

// TOKEN_ADJUST_PRIVILEGES (to adjust the priv)

DWORD cbOld = sizeof tpOld;

if (AdjustTokenPrivileges(htok, FALSE, &tp, cbOld, &tpOld, &cbOld))

// Note that AdjustTokenPrivileges may succeed, and yet

// some privileges weren't actually adjusted.

// You've got to check GetLastError() to be sure!

return(ERROR_NOT_ALL_ASSIGNED != GetLastError());

else

return(FALSE);

}

else

return(FALSE);

}





// Corresponding restoration helper function

BOOL RestoreTokenPrivilege(HANDLE htok, const TOKEN_PRIVILEGES& tpOld)

{

return(AdjustTokenPrivileges(htok, FALSE, const_cast<TOKEN_PRIVILEGES*>(&tpOld), 0, 0, 0));

}



HANDLE GetProcessHandleWithEnoughRights(DWORD PID, DWORD AccessRights)

{

HANDLE hProcess = ::OpenProcess(AccessRights, FALSE, PID);

if (hProcess == NULL)

{

HANDLE hpWriteDAC = OpenProcess(WRITE_DAC, FALSE, PID);

if (hpWriteDAC == NULL)

{

// hmm, we don't have permissions to modify the DACL...

// time to take ownership...

HANDLE htok;

if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, &htok))

return(FALSE);



TOKEN_PRIVILEGES tpOld;

if (EnableTokenPrivilege(htok, SE_TAKE_OWNERSHIP_NAME, tpOld))

{

// SeTakeOwnershipPrivilege allows us to open objects with

// WRITE_OWNER, but that's about it, so we'll update the owner,

// and dup the handle so we can get WRITE_DAC permissions.

HANDLE hpWriteOwner = OpenProcess(WRITE_OWNER, FALSE, PID);

if (hpWriteOwner != NULL)

{

BYTE buf[512]; // this should always be big enough

DWORD cb = sizeof buf;

if (GetTokenInformation(htok, TokenUser, buf, cb, &cb))

{

DWORD err =

SetSecurityInfo(

hpWriteOwner,

SE_KERNEL_OBJECT,

OWNER_SECURITY_INFORMATION,

reinterpret_cast<TOKEN_USER*>(buf)->User.Sid,

0, 0, 0

);

if (err == ERROR_SUCCESS)

{

// now that we're the owner, we've implicitly got WRITE_DAC

// permissions, so ask the system to reevaluate our request,

// giving us a handle with WRITE_DAC permissions

if (

!DuplicateHandle(

GetCurrentProcess(),

hpWriteOwner,

GetCurrentProcess(),

&hpWriteDAC,

WRITE_DAC, FALSE, 0

)

)

hpWriteDAC = NULL;

}

}



// don't forget to close handle

::CloseHandle(hpWriteOwner);

}



// not truly necessary in this app,

// but included for completeness

RestoreTokenPrivilege(htok, tpOld);

}



// don't forget to close the token handle

::CloseHandle(htok);

}



if (hpWriteDAC)

{

// we've now got a handle that allows us WRITE_DAC permission

AdjustDacl(hpWriteDAC, AccessRights);



// now that we've granted ourselves permission to access

// the process, ask the system to reevaluate our request,

// giving us a handle with right permissions

if (

!DuplicateHandle(

GetCurrentProcess(),

hpWriteDAC,

GetCurrentProcess(),

&hProcess,

AccessRights,

FALSE,

0

)

)

hProcess = NULL;



CloseHandle(hpWriteDAC);

}

}



return(hProcess);

}



BOOL WINAPI InjectLibW(DWORD dwProcessId, PCWSTR pszLibFile)

{

BOOL fOk = FALSE; // Assume that the function fails

HANDLE hProcess = NULL, hThread = NULL;

PWSTR pszLibFileRemote = NULL;



// Get a handle for the target process.

hProcess =

GetProcessHandleWithEnoughRights(

dwProcessId,

PROCESS_QUERY_INFORMATION | // Required by Alpha

PROCESS_CREATE_THREAD | // For CreateRemoteThread

PROCESS_VM_OPERATION | // For VirtualAllocEx/VirtualFreeEx

PROCESS_VM_WRITE // For WriteProcessMemory

);

if (hProcess == NULL)

return(FALSE);



// Calculate the number of bytes needed for the DLL's pathname

int cch = 1 + lstrlenW(pszLibFile);

int cb = cch * sizeof(WCHAR);



// Allocate space in the remote process for the pathname

pszLibFileRemote =

(PWSTR) VirtualAllocEx(hProcess, NULL, cb, MEM_COMMIT, PAGE_READWRITE);



if (pszLibFileRemote != NULL)

{

// Copy the DLL's pathname to the remote process's address space

if (WriteProcessMemory(hProcess, pszLibFileRemote,

(PVOID) pszLibFile, cb, NULL))

{

// Get the real address of LoadLibraryW in Kernel32.dll

PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)

GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");

if (pfnThreadRtn != NULL)

{

// Create a remote thread that calls LoadLibraryW(DLLPathname)

hThread = CreateRemoteThread(hProcess, NULL, 0,

pfnThreadRtn, pszLibFileRemote, 0, NULL);

if (hThread != NULL)

{

// Wait for the remote thread to terminate

WaitForSingleObject(hThread, INFINITE);



fOk = TRUE; // Everything executed successfully



CloseHandle(hThread);

}

}

}

// Free the remote memory that contained the DLL's pathname

VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE);

}



CloseHandle(hProcess);



return(fOk);

}





BOOL WINAPI InjectLibA(DWORD dwProcessId, PCSTR pszLibFile) {



// Allocate a (stack) buffer for the Unicode version of the pathname

PWSTR pszLibFileW = (PWSTR)

_alloca((lstrlenA(pszLibFile) + 1) * sizeof(WCHAR));



// Convert the ANSI pathname to its Unicode equivalent

wsprintfW(pszLibFileW, L"%S", pszLibFile);



// Call the Unicode version of the function to actually do the work.

return(InjectLibW(dwProcessId, pszLibFileW));

}