原文地址:http://www.codeproject.com/Articles/30140/API-Hooking-with-MS-Detours
在这篇文章里,我将要介绍API拦截技术的相关理论和实现方式。API拦截是一项强大的技术,他让你可以拦截某些函数,重定位到自定义的函数上。在将控制权交给原始API之前,你可以在这个自定义的函数里做任何想做的事。
1.介绍
本文中,我将讨论API拦截主题。API拦截包括拦截程序中调用的函数,和重定向被拦截的的函数到另一个我们定义的函数。通过拦截,函数的参数可以被修改;而且你可以通过修改返回码来误导源程序,比如函数调用本应该返回正确的结果,你却返回了一个错误的,从而使源程序获的错误的返回值。当然你还可以利用拦截做许多其他的事情。所有这些都在实际函数被调用之前进行。在你修改/储存/扩展了原函数/参数后,控制权被传递回原来应调用的函数。本文要求读者对C++有深入的了解。我将使用微软的Detours库(免费下载)实现API拦截。为了能够编译提供的代码例子,你需要运行Detours库自带的Makefile文件,编译出所需的库文件和其他的东西。具体的操作步骤可以在MSDN forum里面或者其他网站上找到。由于篇幅所限,本文中的代码例子没有注释,但是有很多解释说明。附件中的代码有详细的注释。
2.开始拦截之旅:传统的API拦截技术
在介绍Detours库之前,我讨论一下传统使用的API拦截方式,通过用自定义函数的地址代替API函数的地址。这其实只是API拦截的一种方式,其他的方法包括修改导入地址表;使用代理DLL和manifest文件;在内核地址空间加载驱动等等。我将使用的技术是很基本的,被拦截的API每次都需要脱钩,这在多线程的程序里会造成并发性冲突。有一个解决的方法是在其他地方为原来的函数分配一块内存,然后设置一个钩子来阻止不停的重写detour。在本文中,为了使代码和调试变得简单,我没有使用这种方式。当然这也不是最好的方式。由于这篇文章主要是讨论利用detours进行API拦截,所以冲突问题的解决就不是特别重要了。
#include
#define SIZE 6
typedef int (WINAPI *pMessageBoxW)(HWND, LPCWSTR, LPCWSTR, UINT);
int WINAPI MyMessageBoxW(HWND, LPCWSTR, LPCWSTR, UINT);
void BeginRedirect(LPVOID);
pMessageBoxW pOrigMBAddress = NULL;
BYTE oldBytes[SIZE] = {0};
BYTE JMP[SIZE] = {0};
DWORD oldProtect, myProtect = PAGE_EXECUTE_READWRITE;
INT APIENTRY DllMain(HMODULE hDLL, DWORD Reason, LPVOID Reserved)
{
switch(Reason)
{
case DLL_PROCESS_ATTACH:
pOrigMBAddress = (pMessageBoxW)
GetProcAddress(GetModuleHandle("user32.dll"),
"MessageBoxW");
if(pOrigMBAddress != NULL)
BeginRedirect(MyMessageBoxW);
break;
case DLL_PROCESS_DETACH:
memcpy(pOrigMBAddress, oldBytes, SIZE);
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
void BeginRedirect(LPVOID newFunction)
{
BYTE tempJMP[SIZE] = {0xE9, 0x90, 0x90, 0x90, 0x90, 0xC3};
memcpy(JMP, tempJMP, SIZE);
DWORD JMPSize = ((DWORD)newFunction - (DWORD)pOrigMBAddress - 5);
VirtualProtect((LPVOID)pOrigMBAddress, SIZE,
PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(oldBytes, pOrigMBAddress, SIZE);
memcpy(&JMP[1], &JMPSize, 4);
memcpy(pOrigMBAddress, JMP, SIZE);
VirtualProtect((LPVOID)pOrigMBAddress, SIZE, oldProtect, NULL);
}
int WINAPI MyMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uiType)
{
VirtualProtect((LPVOID)pOrigMBAddress, SIZE, myProtect, NULL);
memcpy(pOrigMBAddress, oldBytes, SIZE);
int retValue = MessageBoxW(hWnd, lpText, lpCaption, uiType);
memcpy(pOrigMBAddress, JMP, SIZE);
VirtualProtect((LPVOID)pOrigMBAddress, SIZE, oldProtect, NULL);
return retValue;
}
上面的代码就是标准API拦截的框架。这个DLL里包含的所有内容都会被注入到进程中。举个例子,拦截MessageBoxW这个函数。一旦DLL被注入,它(理论上)会获取MessageBoxW函数在user32.dll里的地址,然后拦截开始。在BeginRedirect函数中,有无条件相对转移指令JMP的操作码(0xE9),并且包含需要跳转的距离。反编译源代码后,指令看起来是这个样子:
JMP
RET
按下面的方法对BeginRedirect进行下修改可以提供更多有用的信息:
sprintf_s(debugBuffer, 128, "pOrigMBAddress: %x", pOrigMBAddress);
OutputDebugString(debugBuffer);
..
memcpy(oldBytes, pOrigMBAddress, SIZE);
sprintf_s(debugBuffer, 128, "Old bytes: %x%x%x%x%x", oldBytes[0], oldBytes[1],
oldBytes[2], oldBytes[3], oldBytes[4], oldBytes[5]);
OutputDebugString(debugBuffer);
..
memcpy(&JMP[1], &JMPSize, 4);
sprintf_s(debugBuffer, 128, "JMP: %x%x%x%x%x", JMP[0], JMP[1],
JMP[2], JMP[3], JMP[4], JMP[5]);
OutputDebugString(debugBuffer);
注入DLL后,可以打开DebugView工具进行查看:
可以从上图看出,在设置API钩子之前,从0x7E466534开始的MessageBoxW的代码流有下面5个字节:8B,FF,55,8B,EC。在使用memcpy(pOrigMBAddresss,JMP,SIZE)之后,代码流将会可以跳转到自定义函数的字节(E9,A7,AC,B9,91)。因此,每次位于0x7E466534地址的MessageBoxW被调用,程序会立即跳转到自定义函数的地址处。
int WINAPI MyMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uiType)
{
OutputDebugString("In MyMessageBoxW");
// VirtualProtect((LPVOID)pOrigMBAddress, SIZE, myProtect, NULL);
// memcpy(pOrigMBAddress, oldBytes, SIZE);
int retValue = MessageBoxW(hWnd, lpText, lpCaption, uiType);
// memcpy(pOrigMBAddress, JMP, SIZE);
// VirtualProtect((LPVOID)pOrigMBAddress, SIZE, oldProtect, NULL);
return retValue;
}
如果你像上面的代码这样的话。当你注入DLL后,进程调用MessageBoxW,会发生下面的事情:
当你仔细看代码的时候发现,在自定义函数的内部调用了MessageBoxW并且返回一个值。但问题是,这里的MessageBoxW又重定向到你自定义的函数MyMessageBoxW中了。因此导致函数无限的递归调用。这就是为什么函数本身不能拦截自身的原因。为了能够调用原来的MessageBoxW函数,覆盖jump的字节需要写回去。然后调用原本的函数并返回结果。调用之后,再次重写跳转代码即可继续拦截。为了证明,我添加了下面这行:
MessageBoxW(NULL, L"This should pop up", L"Hooked MBW", MB_ICONEXCLAMATION);
在将原始字节写回内存后调用这个语句。我这里利用NotePad进行测试,当你搜索的文本不存在时,NotePad会弹出一个对话框。现在被注入DLL后的结果是:
效果如上。理论上说,譬如修改导入表的方法是比较好的,因为钩子可以保存起来,随时可以删除掉。
3.Detours API Hooking
微软的Detours库工作方式和上述的类似,看如下的概述:
“Detours是一个库,用于拦截x86机器上任意的win32二进制函数。拦截代码是在运行时动态使用的。Detours将目标函数开头的几条指令用指向用户提供的函数的跳转指令替代。目标函数的指令被放在一个地方,这个地方的地址放在一个目标指针里。Detour函数既可以替换目标函数,也可以通过在自定义函数内部调用目标函数指针来扩充目标函数。”
尽管上面的步骤非常的复杂并且抽象,但实际上,DetourAttach这个函数会完成所有的事情。
LONG DetourAttach(PVOID* ppPointer, PVOID pDetour);
这是负责拦截目标API的函数。第一个参数是一个指向被拦截函数的指针的指针。第二个参数是指向替换函数的指针。但是,在拦截开始之前,还需要做一些初始化的操作:
一个detour事务需要被初始化;
和这个事务相关的线程需要更新。
通过下面的调用即可实现上面的两步:
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
这两件事情做完后,detour开始attach。在attach之后,需要调用DetourTransactionCommit()来使detour生效。可以通过其返回值来判断是否拦截成功。
3.1例子:日志记录器
为了演示利用detours进行API拦截,我将写一段代码,去拦截Winsock模块里的send(...)和recv(...)两个函数。在这两个函数里,我会在交还控制权之前,将发送信息和接收信息的缓冲区的内容写到日志文件里。特别需要注意的事情是detour函数的调用规约和参数必须和被detour的函数完全一致。send函数的申明如下:
int send(
__in SOCKET s,
__in const char *buf,
__in int len,
__in int flags
);
因此,自定义的函数需要这样:
int WINAPI MySend(SOCKET s, const char* buf, int len, int flags);
这里还需要申明一个指向原函数的指针:
int (WINAPI *pSend)(SOCKET, const char*, int, int) = send;
这里,将函数指针=send是一种比较好的方式。我之后使用的,是先将指针置为NULL,然后使用DetourFindFunction()去定位地址。recv函数同理,因此:
int (WINAPI *pSend)(SOCKET s, const char* buf, int len, int flags) = send;
int WINAPI MySend(SOCKET s, const char* buf, int len, int flags);
int (WINAPI *pRecv)(SOCKET s, char* buf, int len, int flags) = recv;
int WINAPI MyRecv(SOCKET s, char* buf, int len, int flags);
现在为止,将被hook的函数和将被重定向的函数已经定义好了。下面,开始进行拦截:
INT APIENTRY DllMain(HMODULE hDLL, DWORD Reason, LPVOID Reserved)
{
switch(Reason)
{
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls(hDLL);
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)pSend, MySend);
if(DetourTransactionCommit() == NO_ERROR)
OutputDebugString("send() detoured successfully");
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)pRecv, MyRecv);
if(DetourTransactionCommit() == NO_ERROR)
OutputDebugString("recv() detoured successfully");
break;
.
因正如我之前介绍的,先初始化一个detour事务,再更新相应的线程,然后调用DetourAttach。在调用DetourTransactionCommit()之后可以根据相应的返回值进行验证是否成功。这里,被detour的函数需要以指针的指针进行传递,所以类型修饰为&(PVOID&)。当这些操作成功后,便会对send,recv函数进行拦截。为了及时的记录发送和接收的内容,我选择open,write,close来操作日志文件。当然,这并不一定是最好的方法。代码如下:
int WINAPI MySend(SOCKET s, const char* buf, int len, int flags)
{
fopen_s(&pSendLogFile, "C:\\SendLog.txt", "a+");
fprintf(pSendLogFile, "%s\n", buf);
fclose(pSendLogFile);
return pSend(s, buf, len, flags);
}
int WINAPI MyRecv(SOCKET s, char* buf, int len, int flags)
{
fopen_s(&pRecvLogFile, "C:\\RecvLog.txt", "a+");
fprintf(pRecvLogFile, "%s\n", buf);
fclose(pRecvLogFile);
return pRecv(s, buf, len, flags);
}
代码中的pRecvLogFile
和pSendLogFile的类型都是FILE*。我在“Internet Checkers”上测试了这个程序,两个文件都成功的捕获到理论数据。在detour函数中,一定要注意返回语句。与其他修改内存的方法不同,detours允许你将控制权返回给程序,只需简单通过地址调用原函数即可。如果你不需要阻塞被处理的API,则在做完你想做的事情后将控制权返回给原来的函数。否则,你可以返回一些有用的结果。
3.2 一个更复杂的例子:MSN消息框
为了展示API拦截的强大功能,我决定对上述的例子做些功能扩充,允许用户可以往活动的会话窗口发送MSN即时消息。
通过拦截MSN消息框使用的WSARecv函数,可以从数据包里解析邮件内容,并且记录聊天过程中活动的SOCKET的个数。我下面会开一个对话框(MSN:[email protected]),将对话框接收到的数据包里的内容记录下来,类似下面的信息:
MSG [email protected] -%20SmarterChild%20-%20*unicef%20contributing%20to%20charity 137..MIME-Version: 1.0..Content-Type: text/plain; charset=UTF-8..X-MMS-IM-Format: FN=Courier%20New; EF=; CO=800000; CS=0; PF=22....:-D :-) :-)上面的协议很简单,不需要通过逆向工程进行加解密。上面数据包里包含了发送者的邮件信息。一些文本标记,邮件内容。MSN消息协议在这里是完全文档化的。我需要里面的明文信息。在接收到的数据包里,检测某些属性。检测通过后,就可以开始解析邮件内容并且保持活动的会话。
if(strstr(lpBuffers->buf, "MSG ") != 0 &&
(strstr(lpBuffers->buf, "MIME-Version") != 0 &&
strstr(lpBuffers->buf, "X-MMS-IM-Format") != 0))
ParseAndStoreEmail(socket, lpBuffers->buf);
lpBuffers是WSARecv/WSASend的参数,是一个LPWSABUF结构体。被解析后,邮件和活动的SOCKET就被保存在两个相同的数组中,这便于以后的匹配和更新。
vectoremailList;
vectorsessionList;
The parsing and storing function works like this
void ParseAndStoreEmail(SOCKET session, const char* buffer)
{
string email;
int i = 4; //4 to skip "MSG " part
while(buffer[i] != ' ')
{
email += buffer[i];
i++;
}
if(SearchForDuplicates(session, email.c_str()) != -1)
{
emailList.push_back(email);
sessionList.push_back(session);
SendDlgItemMessage(::g_hDlg, IDC_CBUSERS, CB_ADDSTRING, NULL,
(LPARAM)email.c_str());
SendDlgItemMessage(::g_hDlg, IDC_CBUSERS, CB_SETCURSEL, emailList.size()-1, 0);
}
}
由于这个函数只有在接收到数据包的时候被调用,因此为了解析出邮件地址,我选取MSG之后的第一个空格和下一个空间之前的内容。如果这个地址是之前向量中没有的,则该地址和活动的SOCKET会分别保存到相应的向量中。邮件地址之后会通过自定义的消息发送到组合框空间里进行显示:
case IDOK:
{
int index = SendDlgItemMessage(hDlg, IDC_CBUSERS, CB_GETCURSEL, 0, 0);
int textLength = SendDlgItemMessage(hDlg, IDC_CHAT, WM_GETTEXTLENGTH, 0, 0);
if(textLength == 0)
break;
char* emailSelected = new char[128];
char* packet = new char[textLength+1];
GetDlgItemText(hDlg, IDC_CHAT, packet, textLength+1);
SendDlgItemMessage(hDlg, IDC_CBUSERS, CB_GETLBTEXT, index, (LPARAM)emailSelected);
SOCKET sessionToSendTo = GetSessionFromEmail(emailSelected);
BuildPacket(sessionToSendTo, packet);
delete [] emailSelected;
delete [] packet;
}
break;
上述代码是从组合框中获取正确的邮件地址和对应想SOCKET,然后创建并发送一个数据包。BuildPacket
代码如下:
void BuildPacket(SOCKET session, const char* message)
{
char packetSize[8];
ZeroMemory(packetSize, 8);
string packetHeader = "MSG 10 N ";
string packetSettings = "MIME-Version: 1.0\r\nContent-Type: "
"text/plain; charset=UTF-8\r\n";
packetSettings += "X-MMS-IM-Format: FN=MS%20Shell%20Dlg; "
"EF=; CO=0; CS=0; PF=0\r\n\r\n";
string packetMessage = message;
int sizeOfPacket = packetSettings.length() + packetMessage.length();
_itoa_s(sizeOfPacket, packetSize, 8, 10);
packetHeader += packetSize;
packetHeader += "\r\n";
string fullPacket = packetHeader;
fullPacket += packetSettings;
fullPacket += packetMessage;
psend(session, fullPacket.c_str(), fullPacket.length(), 0);
}
将被发送出去的MSN消息的大致结构如下:
MSG [Message #] [Acknowledge Flag] [Size of packet] [Text flags] [Message]上述结构中,Message #对于数据包而言,是什么内容并不重要,因此可以当作数据包的一部分。而数据包大小这个字段非常重要,如果一个错误的大小被发送出去,session将会中断,然后重新建立一个新的SOCKET。对于文本的属性,我只是按正常的设置下即可。最终效果如下:
4.DLL注入
本文中我讨论了很多关于DLL注入的知识。API拦截属于DLL注入的一种,它是通过注入到目标进程的地址空间实现的。DLL注入是让目标进程加载我们自定义的DLL而实现的,有很多办法可以做到。很常见的一种方法是通过调用函数SetWindowsHookEx,给目标进程设置一个钩子。另一个常见的方法是调用CreateRemotethread函数。这个方法是我本文中将要介绍的,它不仅简单,而且有效。在开始拦截之前,需要找到将被注入的目标进程。
4.1 进程遍历
微软提供了一个强大的API进行进程遍历-CreateToolhelp32Snapshot,这个函数可以获取正在运行的进程,线程,堆等的快照。通过使用Process32First和Process32Next函数,可以遍历出每一个运行的进程。知道了这些,写代码就简单了:
#undef UNICODE
#include
#include
#include
#include
using std::vector;
using std::string;
int main(void)
{
vectorprocessNames;
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
HANDLE hTool32 = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
BOOL bProcess = Process32First(hTool32, &pe32);
if(bProcess == TRUE)
{
while((Process32Next(hTool32, &pe32)) == TRUE)
processNames.push_back(pe32.szExeFile);
}
CloseHandle(hTool32);
return 0;
}
PROCESSENTRY32
结构有一个保存进程ID的变量。在循环中,当我们把进程名放入向量中的时候,我们开始注入DLL。
4.2 CreateRemoteThread
while((Process32Next(hTool32, &pe32)) == TRUE)
{
processNames.push_back(pe32.szExeFile);
if(strcmp(pe32.szExeFile, "notepad.exe") == 0)
{
char* DirPath = new char[MAX_PATH];
char* FullPath = new char[MAX_PATH];
GetCurrentDirectory(MAX_PATH, DirPath);
sprintf_s(FullPath, MAX_PATH, "%s\\testdll.dll", DirPath);
HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION |
PROCESS_VM_WRITE, FALSE, pe32.th32ProcessID);
LPVOID LoadLibraryAddr = (LPVOID)GetProcAddress(GetModuleHandle("kernel32.dll"),
"LoadLibraryA");
LPVOID LLParam = (LPVOID)VirtualAllocEx(hProcess, NULL, strlen(FullPath),
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProcess, LLParam, FullPath, strlen(FullPath), NULL);
CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryAddr,
LLParam, NULL, NULL);
CloseHandle(hProcess);
delete [] DirPath;
delete [] FullPath;
}
}
上述代码中,先获取测试DLL“testdll.dll”的完整路径。然后打开需要注入的进程,这里要使用3个标记,
PROCESS_ALL_ACCESS标记是最好的,但它要求将进程的权限修改为SE_DEBUG_ACCESS,限于篇幅,我没有这样做。进程被正确的打开后,通过GetProcAddress获取LoadLibraryA函数的地址。接下来需要在被注入的进程地址空间里,分配一些内存给DLL的路径。这一步做好后,就可以调用WriteProcessMemory将保存DLL的字符串写入到内存中,再然后就是调用CreateRemoteThread,将LoadLibraryA的开始地址和字符串当作参数传递进去。看下图,我们的DLL被目标进程加载起来了。
4.3 DetourCreateProcessWithDll
上面的技术,适应于注入正在运行的进程。如果是在一个进程运行之前注入呢?Detours库提供了一个DetourCreateProcessWithDll函数可以实现这个功能。使用这个函数相当于以CREATE_SUSPENDED
标记调用CreateProcess。这样进程被创建后,主线程会处于挂起状态。因此DLL可以再实际程序运行之前注入进去。一个重要的事情是被注入的DLL必须导出一个函数。如果没有,DetourCreateProcessWithDll函数会失败。下面是一个例子,将testdll.dll注入到notepad.exe里。
#undef UNICODE
#include
#include
#include
int main()
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(STARTUPINFO));
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
si.cb = sizeof(STARTUPINFO);
char* DirPath = new char[MAX_PATH];
char* DLLPath = new char[MAX_PATH]; //testdll.dll
char* DetourPath = new char[MAX_PATH]; //detoured.dll
GetCurrentDirectory(MAX_PATH, DirPath);
sprintf_s(DLLPath, MAX_PATH, "%s\\testdll.dll", DirPath);
sprintf_s(DetourPath, MAX_PATH, "%s\\detoured.dll", DirPath);
DetourCreateProcessWithDll(NULL, "C:\\windows\\notepad.exe", NULL,
NULL, FALSE, CREATE_DEFAULT_ERROR_MODE, NULL, NULL,
&si, &pi, DetourPath, DLLPath, NULL);
delete [] DirPath;
delete [] DLLPath;
delete [] DetourPath;
return 0;
}
DetourCreateProcessWithDll函数就是CreateProcessAPI的扩展版本,它包含了CreateProcess的所有参数,并需要额外的参数。一个是被注入的DLL的路径,另一个是detoured.dll的路径。这个过程就相当于,创建处于挂起状态的目标进程,注入DLL,然后调用ResumeThread使进程运行。
4.4 通过地址拦截
有时候,需要去拦截的函数不是标准的win32 API或者没有已知的导出函数。这就需要知道这个函数的固定地址。知道了这个函数的地址和参数后(通过逆向工程或者文档或者其他的手段),就可以拦截这个函数。下面的代码演示了具体过程:
#undef UNICODE
#include
#include
#include
typedef void (WINAPI *pFunc)(DWORD);
void WINAPI MyFunc(DWORD);
pFunc FuncToDetour = (pFunc)(0x0100347C); //Set it at address to detour in
//the process
INT APIENTRY DllMain(HMODULE hDLL, DWORD Reason, LPVOID Reserved)
{
switch(Reason)
{
case DLL_PROCESS_ATTACH:
{
DisableThreadLibraryCalls(hDLL);
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)FuncToDetour, MyFunc);
DetourTransactionCommit();
}
break;
case DLL_PROCESS_DETACH:
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)FuncToDetour, MyFunc);
DetourTransactionCommit();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
void WINAPI MyFunc(DWORD someParameter)
{
//Some magic can happen here
}
5.常见的错误
6.结束语
本文是讨论API拦截技术。还有很多方法可以实现API拦截,效率不一。读完本文后,能从Detours库的角度很容易的理解API拦截技术。通过使用Detours库,API拦截变得非常简单。因为内部过程诸如修改导入表,改变程序流等都已经被实现。这节省了大量的调试时间,可以允许程序员随意设置或移除钩子,而不必担心内存泄露。
简洁的语