C++ 风格的函数拦截






4.84/5 (27投票s)
本文介绍一种更安全的 C++ 编程风格来拦截函数。
引言
如果你要修改某个函数,很容易出错。编译器无法帮助你找到这些错误。大多数时候,你会在运行时遇到崩溃,并伴随一个难看的错误框。更糟的是,如果你全局修改函数,可能会对操作系统造成危险。这就是我创建了一个类来帮助我更安全地完成这项工作的原因。我不太确定重写可执行代码如何才能安全,但我认为一些问题可以在编译时避免。
另一方面,你可能希望在执行某个函数时设置一个补丁,在执行其他函数时移除补丁,在退出函数时自动移除补丁,永久设置补丁,立即应用补丁,或者准备稍后设置补丁。
谈论安全地重写可执行代码,甚至使用 OOP 来拦截函数,这有点滑稽。:) 但你会惊讶,这是可行的。C++ 的模板、封装和自动销毁功能确实让这件事容易多了。
Using the Code
你可以通过将 patcher.cpp 和 patcher.h 添加到你的 C++ 项目中来使用此代码。压缩包中不包含示例。
一些背景信息
这类错误可以分为三种(我们称之为3TM)
- 使用错误的调用约定,这与被修补函数的调用约定不同
- 传递错误的函数参数
- 使用错误的返回类型
很容易在编译时避免这些类型的错误。
让我们来看一下 winsock.h 中的 send
/recv
函数
int PASCAL FAR recv (
IN SOCKET s,
OUT char FAR * buf,
IN int len,
IN int flags);
int PASCAL FAR send (
IN SOCKET s,
IN const char FAR * buf,
IN int len,
IN int flags);
以及它们最简单的修补方法
int (PASCAL FAR *lpfn_send )( IN SOCKET s, IN const char FAR * buf,
IN int len, IN int flags);
int PASCAL FAR my_send ( IN SOCKET s, IN const char FAR * buf, IN int len, IN int flags)
{
return lpfn_send (s, buf, len, flags);
}
int (PASCAL FAR *lpfn_recv )( IN SOCKET s, OUT char FAR * buf, IN int len, IN int flags);
int PASCAL FAR my_recv ( IN SOCKET s, OUT char FAR * buf, IN int len, IN int flags)
{
return lpfn_recv (s, buf, len, flags);
}
规则很简单。如果某个函数 send
的类型是 type_of_send
,那么修补函数 my_send
必须使用相同的类型 type_of_send
。蹦床函数 lpfn_send
也必须是 type_of_send
类型。如果另一个函数 recv
的类型是 type_of_recv
,那么 patch
函数 my_recv
必须使用相同的类型 type_of_recv
。trampoline
函数 lpfn_recv
也必须是 type_of_recv
类型。
以及修补本身(这还只是理论上的代码)
patch(send, my_send, lpfn_send);
patch(recv, my_recv, lpfn_recv);
你看,没有强制类型转换,没有警告,没有错误。编写起来容易得多,也根本不会出现 3TM。
该函数的原型是
template<class T> patch(T, T, T&);
这种结构可以帮助你避免 3TM。如果你在传递参数给函数时没有强制类型转换,那么你就无法编译出带有 3TM 的代码。
使用指针而不是引用时,差别不大
patch(send, my_send, &lpfn_send);
patch(recv, my_recv, &lpfn_recv);
该函数原型是
template<class T> patch(T, T, T*);
这一切都是为了修补的安全性。
函数类型
关于函数类型规则的一些说明。当你通过 GetProcAddress
获取函数地址时,你可能会需要它。让我们看一个简单的 C++ 函数声明
int func(int, char*, void*);
函数类型 T
将是
int (*)(int, char*, void*)
你必须声明一个正确类型的指针,并在进行 GetProcAddress
时正确地进行类型转换。
int (*func_ptr)(int, char*, void*);
func_ptr = ( int (*)(int, char*, void*) ) GetProcAddress(hModule, "func_name");
一个更完整的函数声明是
return_type calling_convention function_name (list of types of arguments);
创建函数类型的规则是
return_type (calling_convention *) (list of types of arguments)
MSVC 中 C++ 函数的默认调用约定是 __cdecl
。你可以省略它。但你也可以在声明中包含它。当你处理具有不同调用约定的函数时,必须包含调用约定。上面带有指针声明和类型转换的 LoadLibrary
的示例函数变为
int __cdecl func(int, char*, void*);
int (__cdecl* func_ptr)(int, char*, void*);
func_ptr = ( int (__cdecl*)(int, char*, void*) )
GetProcAddress(hModule, "func_name");
对于 send
/recv
函数的示例,情况相同,如下所示:
//functions declarations in winsock.h
int PASCAL FAR recv (
IN SOCKET s,
OUT char FAR * buf,
IN int len,
IN int flags);
int PASCAL FAR send (
IN SOCKET s,
IN const char FAR * buf,
IN int len,
IN int flags);
///////////////////////////////////////////////////
//pointer and patch declarations in our application
//let's omit IN and OUT
int (PASCAL FAR *lpfn_send )( SOCKET , const char * , int , int) = send;
int PASCAL FAR my_send ( SOCKET s, const char * buf, int len, int flags);
int (PASCAL FAR *lpfn_recv )( SOCKET, char FAR *, int, int) = recv;
int PASCAL FAR my_recv ( SOCKET s, char FAR * buf, int len, int flags);
//////////////////////////////////////////////////
//pointer assigning with GetProcAddress:
HINSTANCE hInstanceWs2 = GetModuleHandleA("ws2_32.dll");
lpfn_send = (int(PASCAL FAR*)(SOCKET,const char*,int,int))
GetProcAddress(hInstanceWs2, "send");
lpfn_recv = (int(PASCAL FAR*)(SOCKET,char*,int,int))
GetProcAddress(hInstanceWs2, "recv");
//I don't advise to do this, but is ok, not a big deal:
lpfn_send = (int(__stdcall*)(SOCKET,const char*,int,int))
GetProcAddress(hInstanceWs2, "send");
lpfn_recv = (int(__stdcall*)(SOCKET,char*,int,int))GetProcAddress(hInstanceWs2, "recv");
在大多数 WinAPI
函数的情况下,你不需要调用 GetModuleHandle
/GetProcAddress
,因为库和函数会加载到某些默认地址。因此,你不需要上面的类型转换。只需在初始化时或稍后分配函数地址即可。
lpfn_send = send;
lpfn_recv = recv;
实际应用
是时候使用实际代码了,请看 CPatcher.zip。我创建了一个名为 CPatch
的简单类。现在,该类的两个构造函数与上面描述的理论函数 patch(...)
非常相似。
class CPatch
{
...
CPatch(){}
CPatch(CPatch&){}
...
public:
...
template<class TFunction>explicit CPatch
(TFunction FuncToHook, TFunction MyHook, TFunction& NewCallAddress,
bool patch_now = true, bool set_forever = false)
...
template<class TFunction>explicit CPatch
(TFunction FuncToHook, TFunction MyHook, TFunction* NewCallAddress,
bool patch_now = true, bool set_forever = false)
...
template<class TFunction>explicit CPatch(TFunction& NewCallAddress,
TFunction MyHook, bool patch_now = true, bool set_forever = false)
...
template<class TFunction>explicit CPatch(TFunction* NewCallAddress,
TFunction MyHook, bool patch_now = true, bool set_forever = false)
...
如何使用
让我们看看我们想要如何使用补丁。
立即在本地应用补丁,并在退出函数时自动移除。C++ 类的析构函数工作得非常好。
#include<winsock2.h>
#include "patcher.h"
int (PASCAL FAR *lpfn_send )( IN SOCKET s,
IN const char FAR * buf, IN int len, IN int flags);
int PASCAL FAR my_send ( IN SOCKET s, IN const char FAR * buf,
IN int len, IN int flags)
{
return lpfn_send (s, buf, len, flags);
}
int (PASCAL FAR *lpfn_recv )( IN SOCKET s, OUT char FAR * buf,
IN int len, IN int flags);
int PASCAL FAR my_recv ( IN SOCKET s, OUT char FAR * buf,
IN int len, IN int flags)
{
return lpfn_recv (s, buf, len, flags);
}
....
{
CPatch patch_for_send(send, my_send, lpfn_send);
CPatch patch_for_recv(recv, my_recv, lpfn_recv);
//now the functions send and recv are patched
.....
}//the patches are removed automatically
你可以尝试制造一些 3TM 来看看编译器不允许你这样做。试着用 PASCAL 以外的约定。或者尝试改变参数的类型。或者错误地尝试将 recv
用于 send
。
patch_for_send(send, my_recv, lpfn_send);
也许你已经注意到了两对构造函数。一对使用引用。另一对使用指针。所以,如果你喜欢使用指针,你可以这样做:
CPatch patch_for_send(send, my_send, &lpfn_send);
CPatch patch_for_recv(recv, my_recv, &lpfn_recv);
在两个构造函数调用内部,调用相同的非模板和受保护函数 HookFunction
。唯一的区别是,在第一种变体中,使用了 &NewCallAddress
,在第二种变体中,使用了 NewCallAddress
。
你可以将蹦床指针初始化为实际函数,即 lpfn_send = send
和 lpfn_recv = recv
。
int (PASCAL FAR *lpfn_send )( IN SOCKET s, IN const char FAR * buf,
IN int len, IN int flags)
= send;
int (PASCAL FAR *lpfn_recv )( IN SOCKET s, OUT char FAR * buf, IN int len, IN int flags)
= recv;
在这种情况下,你可以使用另外两个构造函数,它们更简短。
CPatch patch_for_send(lpfn_send, my_send);
CPatch patch_for_recv(lpfn_recv, my_recv);
现在,你可以随意多次应用和移除 patch
。
//we do not need patches
patch_for_send.remove_patch();
patch_for_recv.remove_patch();
//we need the patches right now! :)
patch_for_send.set_patch();
patch_for_recv.set_patch();
//we do not need patches
patch_for_send.remove_patch();
patch_for_recv.remove_patch();
//we need patches again
patch_for_send.set_patch();
patch_for_recv.set_patch();
}
//if you've forgotten to remove some patches, not a crime.
//Destructor does this automatically for you
如果你想永久移除 patch
,请将 true
传递给默认参数,该参数默认为 false
。
patch_for_send.remove_patch(true); //true = 4ever
现在让我们使用一个准备好的 patch
。它不会立即应用。
CPatch patch_for_send(send, my_send, lpfn_send, false);
CPatch patch_for_recv(recv, my_recv, &lpfn_recv, false);
//so, patch is ready but is not applied. Call set_patch to apply it.
或
CPatch patch_for_send(&lpfn_send, my_send, false);
CPatch patch_for_recv(lpfn_recv, my_recv, false);
如果你想永久设置 patch
,请将最后一个参数设置为 true
。这是默认参数,默认为 false
。
//apply immediately, once forever:
CPatch patch_for_send(&lpfn_send, my_send, false, true);
//apply later, once forever:
CPatch patch_for_recv(lpfn_recv, my_recv, true, true);
现在析构函数不会移除 patch
。但你仍然可以通过 remove_patch
函数来完成。
成员函数 patched()
告诉你补丁是否已设置。如果你调用 set_patch
,则函数返回 true
。如果你移除 patch
,函数返回 false
。函数 ok()
告诉你补丁生成是否可以创建,以及是否可以重写可执行指令。如果 ok()
返回 false
,那么 set_patch
/remove_patch
将无效。
顺便说一下,这种方法不影响 patch
的性能。
所有其他函数都是 protected
或 private
。我认为你不需要直接调用它们。但如果你确实需要,你可以修改代码使它们变成 public
。
拦截类函数
很久以前,我曾想拦截类函数。经过几次修改,我终于通过这个修补器做到了。假设我们有一个我们想要修补的 class
函数。
class aclass
{
public:
void doSomething(int a, int b)
{
wcout<< L"void doSomething("<< a<<L", "<< b<< L")"<< endl;
}
};
因为成员函数是特殊调用的,并且会传递一个隐藏的参数 this
,所以我们需要遵循相同的逻辑。我们将创建一个类,并将补丁放在这里。通过这种简单的方式,我们将避免所有因调用方式不当而产生的问题。
void (aclass::*pfn_doSomething)(int, int) = &aclass::doSomething;
class class_for_patches
{
public:
void my_doSomething (int a, int b)
{
wcout<< L"patch void doSomething("<< a<< L","<< b<< L")"<< flush;
a += b; //do anything
b++;
(reinterpret_cast<aclass*>(this)->*pfn_doSomething)(a, b);
}
};
//.....
int main()
{
//Let's see how it works
CPatch patch_aclass_doSomething
( pfn_doSomething, &bclass::my_doSomething );
aclass x;
x.doSomething(123, 234);
//...
为此,我添加了几个新的构造函数。你可以查看 patcher_defines.h 文件,位于 CPatcher.zip 中。
工作原理
如果你想了解补丁是如何应用/移除的,请查看 CPatch::set_patch
/CPatch::remove_patch
函数的实现。这些函数会替换函数开头的可执行指令,并用一个 jmp
指令跳转到钩子函数。我试图让它们尽可能简单。
大部分的“手术”都在 CPatch::HookFunction(...)
中完成。此函数为蹦床生成可执行指令,计算 jmp
指令的偏移量,并在对象打算立即设置 patch
时调用 set_patch
。它只在构造函数中调用一次。关于 jmp
指令,你还需要知道的是,jmp
的地址是相对于 jmp
指令末尾第一个字节的。简而言之,它相对于 jmp 的地址 + 5
。
函数 okToRewriteTragetInstructionSet
解析被修补函数启动处的指令集。它返回重写所需的最少字节数 N
。这个 N
必须不小于 5 字节,因为 jmp
指令需要 5 字节。蹦床函数包含正好 N
+ 5 字节,这些字节填充了被修补函数启动处的前 N
个字节,以及一个跳转到被修补函数下一条指令的 jmp
。这样你就可以调用蹦床函数而不是原始的被修补函数了。我认为 okToRewriteTragetInstructionSet
可能不完整。我只填充了调试时发现的指令。对于大多数 WinAPI
函数,这应该足够了。但如果不够,你可以在 okToRewriteTragetInstructionSet
中添加新的指令。你只需要从调试器中复制/粘贴即可。
出于安全原因,蹦床函数分配在单独的堆中。我们必须设置一些 PAGE_EXECUTE*
属性。否则,蹦床将无法工作。如果我们通过 VirtualProtect
请求此属性,则属性将设置到整个堆。将默认堆的属性更改为 PAGE_EXECUTE*
可能会导致各种漏洞。
修补其他进程(注入 DLL)
此修补器不仅可以在当前应用程序中使用,你还可以创建一个 DLL 并将其注入到某个进程中。
将 DLL 注入到某个进程中非常简单。
首先,你需要打开该进程。你可以在 Windows 任务管理器中找到进程 ID。
你还可以使用 EnumProcesses
来查找 ProcessID
,打开每个进程,并使用 GetModuleBaseName
检查名称。
如果你使用 CreateProcess
来启动你的进程,那么你已经在返回的 PROCESS_INFORMATION
中获得了进程 ID
。但在这种情况下,你不需要它。你已经有了 PROCESS_INFORMATION
的 hProcess
成员。因为你已经有了 hProcess
,所以现在不需要调用 OpenProcess
。
既然你已经获得了进程 ID(PID
),打开进程,在该进程中分配一个缓冲区,然后使用 CreateRemoteThread
调用 LoadLibrary
到该进程中,这样就完成了。
HANDLE hProcess = OpenProcess(
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
PROCESS_VM_OPERATION |PROCESS_VM_WRITE | PROCESS_VM_READ |
PROCESS_TERMINATE , FALSE, PID);
if(hProcess)
{
#define INJECTED_DLL L"InjectDllTest.dll"
DWORD dwThreadId;
LPVOID pvProcMem = VirtualAllocEx(hProcess, 0, sizeof(INJECTED_DLL),
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
SIZE_T written;
WriteProcessMemory(hProcess, pvProcMem, INJECTED_DLL, sizeof(INJECTED_DLL), &written);
DWORD oldProtectRemoteThreadProc;
HANDLE hThread = CreateRemoteThread(hProcess, 0, 0,
(LPTHREAD_START_ROUTINE)LoadLibraryW,
pvProcMem, 0, &dwThreadId);
DWORD wtso = WAIT_FAILED;
wtso = WaitForSingleObject(hThread, 1000);
//check wtso to be sure what you code is successfully injected
请不要使用 #define
和 sizeof
来传递要注入的 DLL 的名称。这段代码只是为了尽可能简单。
如果你需要为操作系统设置全局补丁,那么你必须重写包含该函数的 DLL 导出表中的函数入口。
待续...