65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ 风格的函数拦截

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (27投票s)

2009年3月18日

CPOL

8分钟阅读

viewsIcon

96614

downloadIcon

687

本文介绍一种更安全的 C++ 编程风格来拦截函数。

引言

如果你要修改某个函数,很容易出错。编译器无法帮助你找到这些错误。大多数时候,你会在运行时遇到崩溃,并伴随一个难看的错误框。更糟的是,如果你全局修改函数,可能会对操作系统造成危险。这就是我创建了一个类来帮助我更安全地完成这项工作的原因。我不太确定重写可执行代码如何才能安全,但我认为一些问题可以在编译时避免。

另一方面,你可能希望在执行某个函数时设置一个补丁,在执行其他函数时移除补丁,在退出函数时自动移除补丁,永久设置补丁,立即应用补丁,或者准备稍后设置补丁。

谈论安全地重写可执行代码,甚至使用 OOP 来拦截函数,这有点滑稽。:) 但你会惊讶,这是可行的。C++ 的模板、封装和自动销毁功能确实让这件事容易多了。

Using the Code

你可以通过将 patcher.cpppatcher.h 添加到你的 C++ 项目中来使用此代码。压缩包中不包含示例。

一些背景信息

这类错误可以分为三种(我们称之为3TM

  1. 使用错误的调用约定,这与被修补函数的调用约定不同
  2. 传递错误的函数参数
  3. 使用错误的返回类型

很容易在编译时避免这些类型的错误。

让我们来看一下 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_recvtrampoline 函数 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 = sendlpfn_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 的性能。

所有其他函数都是 protectedprivate。我认为你不需要直接调用它们。但如果你确实需要,你可以修改代码使它们变成 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_INFORMATIONhProcess 成员。因为你已经有了 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

请不要使用 #definesizeof 来传递要注入的 DLL 的名称。这段代码只是为了尽可能简单。

如果你需要为操作系统设置全局补丁,那么你必须重写包含该函数的 DLL 导出表中的函数入口。

待续...

© . All rights reserved.