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

使用 MS Detours 进行 API 挂钩

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (68投票s)

2008年10月14日

Ms-PL

16分钟阅读

viewsIcon

546072

downloadIcon

25372

在本文中,我将讨论 API 挂钩的理论和实现。API 挂钩是一种强大的技术,允许任何人劫持一个函数并将其重定向到一个自定义函数。在将控制权交还给原始 API 之前,可以在这些函数中执行任何操作。

目录

  1. 引言
  2. 入门:传统 API 挂钩
  3. Detours API 挂钩
  4. DLL 注入
  5. 常见错误
  6. 结论

1. 引言

在本文中,我将讨论 API 挂钩的主题。API 挂钩包括拦截程序中的函数调用并将其重定向到另一个函数。通过这样做,可以修改参数,如果你选择在实际上应该成功的情况下返回错误代码,就可以欺骗原始程序,等等。所有这些都是在调用实际函数之前完成的,最后,在修改/存储/扩展原始函数/参数后,将控制权交还给原始函数,直到再次调用它为止。本文需要深入了解 C++ 才能完全理解。我将使用 Microsoft Detours 库,该库可免费下载。为了成功编译本文提供的代码示例,你需要运行 Detours 库附带的 Makefile,并让它构建库文件和所有其他内容。有关如何执行此操作的说明可以在 MSDN 论坛或互联网上的其他地方找到。为了节省空间,本文发布的代码示例没有注释,但提供了引导它们或跟进它们的解释。示例下载中的代码已完全注释。

2. 入门:传统 API 挂钩

在介绍 Detours API 之前,我将通过将函数的地址覆盖为一个自定义函数来讨论传统的 API 挂钩方法。这只是 API 挂钩的各种方法之一——其他方法包括修改导入地址表(稍后提供链接)、使用代理 DLL 和清单文件、通过在内核地址空间加载驱动程序进行挂钩等等。我将使用的这种技术相当粗糙,因为每次都需要取消挂钩被挂钩的 API,这可能会导致多线程程序中的并发冲突。有一种解决方法是为原始函数分配其他内存,并在挂钩内部设置一个挂钩,以避免不断重写 Detour。为了代码和调试的简单起见,我选择只保留挂钩/取消挂钩方法。诚然,如我之前提到的,这不是最好的方法,但本文是关于使用 MS Detours 进行 API 挂钩,所以这并不太重要。

#include <windows.h>

#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 被注入,它将(希望)从 *user32.dll* 中获取 `MessageBoXW` 函数的地址,然后开始挂钩。在 `BeginRedirect` 函数中,无条件相对跳转 (`JMP`) 操作码 (0xE9) 指令将包含跳转到的距离。就汇编代码而言,它看起来像这样

JMP <distance>
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 查看

funapihook/Overwrite.jpg

我们看到,在我们设置 API 挂钩之前,从 0x7E466534 开始的 `MessageBoxW` 代码流的最后五个字节是:8B、FF、55、8B、EC。在 `memcpy(pOrigMBAddress, JMP, SIZE);` 之后,代码流将是跳转到我们的函数的字节 (E9、A7、AC、B9、91)。因此,每次调用地址 0x7E466534 (`MessageBoxW`) 时,应用程序将立即跳转到自定义函数的地址。这也正是这种特定的 API 挂钩技术遇到的问题所在。

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` 调用,将会发生以下情况

funapihook/Loop.jpg

当你思考并看到正在发生的事情时,你会发现你在自定义函数中调用 `MessageBoxW` 并返回其值。但是,问题在于 `MessageBoxW` 正在将自身重定向到 `MyMessageBoxW`,从而导致 `MyMessageBoxW` 基本上在无限循环中调用自身。这就是为什么函数会取消挂钩自身。为了实际用作真实的 `MessageBoxW` 函数,需要将跳转的覆盖字节写回。然后,需要调用原始函数并保存返回值。之后,将跳转写回并返回值将会起作用。作为演示,我在将原始字节复制回内存的行之后添加了这行

MessageBoxW(NULL, L"This should pop up", L"Hooked MBW", MB_ICONEXCLAMATION);

我然后尝试用记事本进行测试,记事本在搜索找不到的文本时会弹出一个 `MessageBoxW`。结果是?

funapihook/Hooked.jpg

关于这种方法就到这里。理想情况下,像 导入地址表 (IAT) 挂钩 这样的高级方法更好,因为挂钩可以保留在那里,并在我们希望的任何时候移除。

3. Detours API 挂钩

Microsoft Detours 库的工作方式基本相同。从概述

"Detours 是一个库,用于拦截 x86 机器上的任意 Win32 二进制函数。拦截代码在运行时动态应用。Detours 将目标函数的最初几个指令替换为对用户提供的 Detour 函数的无条件跳转。目标函数的指令被放置在一个 trampoline 中。trampoline 的地址被放置在一个目标指针中。Detour 函数可以替换目标函数,或通过目标指针调用目标函数作为子程序来扩展其语义。"

所以,这有点像上面演示的方式,尽管更复杂、更优雅。驱动这一切的函数是 `DetourAttach(…) `函数。

LONG DetourAttach(
    PVOID * ppPointer,
    PVOID pDetour
    );

这是负责挂钩目标 API 的函数。第一个参数是指向要进行 detour 的函数指针的指针。第二个参数是指向将充当 detour 的函数的指针。但是,在进行 detour 之前,需要做几件事情

  • 需要开始一个 detour 事务。
  • 需要用事务更新一个线程。

这很容易通过以下调用来完成

  • DetourTransactionBegin()
  • DetourUpdateThread(GetCurrentThread())

在完成这两件事之后,就可以附加 detour 了。附加完成后,调用 `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 *pSend)(SOCKET, const char*, int, int) = send;

将指针设置为等于函数是可以的;另一种我稍后使用的方法是将初始指针设置为 `NULL`,然后使用 `DetourFindFunction(…) `来定位地址。对 `send(…) `和 `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);

现在,要挂钩的函数以及它们将被重定向到的函数都已定义。使用 `WINAPI` 是因为这些函数在 `__stdcall` 调用约定下导出。现在,让我们开始吧

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(…) `执行实际的 detour。使用 `DetourTransactionCommit() `函数进行最终化和错误检查,如果 detour 成功,则返回 `NO_ERROR`,如果失败,则返回指定的错误代码。在 `DetourAttach(…) `函数中,要进行 detour 的函数需要作为指针的指针传递,因此将其类型转换为 `&(PVOID&)` 即可。基本上就是这样——它现在可以通过 MS Detours 进行 detour 了。现在,来看看拦截 `send(…) `和 `recv(…) `的函数。为了实时记录发送和接收的数据包,我选择在收到或发送任何内容时打开、写入和关闭日志文件。我不确定这是否是最好的方法,但无论如何。捕获和写入的函数定义如下

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*`。我选择在 Windows XP 自带的“Internet Checkers”游戏中对此进行测试——两个文件都成功捕获了数据。在 detour 函数中,注意 return 语句很重要。与涉及修补内存的其他方法不同,Detours 允许你简单地通过地址调用函数来将控制权交还给程序。一旦函数被 detour,就不需要进行修改,唯一需要做的就是将控制权交还给原始函数(或者如果你阻止 API 被处理,则返回一个有效的值)。

3.2 更复杂的示例:MSN Messenger

为了展示 API 挂钩可以做什么的更复杂示例,我决定将上面的示例扩展为一个允许用户向**活动会话**发送 MSN 即时消息的工具。

funapihook/client.jpg

通过挂钩 MSN Messenger 使用的 `WSARecv(…) `,可以解析数据包中的电子邮件,还可以存储会话中活动 `SOCKET` 的数量。我决定与一个聊天机器人 (MSN: smarterchild@hotmail.com) 进行有趣的对话,并记录一些数据包,看看聊天接收到的内容

MSG smarterchild@hotmail.com 
-%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 Messenger 协议在此处有完整的文档;但是,只需要纯文本消息。我所做的是检查仅存在于接收到的消息数据包中的特定属性。当找到所有这些属性时,就可以安全地解析电子邮件并存储活动会话。

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` 会话可以存储在两个并行向量中,这样可以方便地匹配和更新。

vector<string>emailList;
vector<socket>sessionList;
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 Messenger 数据包的整体分解是

MSG [Message #] [Acknowledge Flag] [Size of packet] [Text flags] [Message]

我发现,至少对于出站数据包来说,*消息 #* 对接收消息的最终用户来说并不重要。它是 01 还是 99 都没有关系,所以我只是将数据包的这部分硬编码了。数据包大小很重要,如果发送了错误的数据包大小,会话将终止,必须在新的 `SOCKET` 下重新建立。我还将文本属性硬编码为标准的 MS Shell 对话文本,没有粗体、下划线、颜色等。以下是一个示例运行

funapihook/MSNConvo.jpg

你可能会注意到发送的消息未出现在聊天窗口中。这是因为向窗口添加文本与套接字无关,它只是在按下“Enter”键(或单击“发送”)时附加的。消息正在通过活动 `SOCKET` 发送,聊天机器人对问题的响应证实它已成功收到消息。在结束本节之前,我想提的一件重要事情是,此应用程序是专门为此文章编写的。它不打算替代 MSN Messenger 窗口,并且确实存在一些缺点,例如不处理聊天会话中的超时。我的目标仅仅是能够通过非传统方式发送数据包,而不是取代 MSN Messenger 界面,或编写一个功能齐全的集成客户端。

4. DLL 注入

在这篇文章中,我已经谈了很多关于 DLL 注入的内容。包含 API 挂钩的 DLL 必须注入到进程的地址空间才能工作。DLL 注入涉及某种方式加载一个进程通常不加载的 DLL。有很多方法可以做到这一点。一种非常常见的方法是使用 `SetWindowsHookEx(…) `和虚拟挂钩过程。另一种常见方法是使用 `CreateRemoteThread(…) ` API。甚至可能将 CodeCave stub 硬编码为加载 DLL(我在 Solitaire 文章中这样做了)。我将在本文中采用并解释的方法是 `CreateRemoteThread` 方法。它非常简单,而且相对高效。但在开始之前,实际找到要注入的进程很重要。

4.1 进程枚举

Windows API 提供了一个很棒的函数来实现这一点——`CreateToolhelp32Snapshot(…) `。此函数拍摄正在运行的进程、线程、堆等的快照。通过使用 `Process32First(…) `和 `Process32Next(…) `函数,可以枚举每个正在运行的进程。有了这些,编写代码就没有问题了。

#undef UNICODE
#include <vector>
#include <string>
#include <windows.h>
#include <Tlhelp32.h>
using std::vector;
using std::string;

int main(void)
{
    vector<string>processNames;
    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;
}

funapihook/processes.jpg

调用 `Process32First` 后,我没有存储返回值,因为它总是“[System Process]”,所以确实没有必要。`Process32Next` 成功时返回 `TRUE`,因此只需将其放入循环中并将收到的进程名称推入向量就足够了。循环完成后,所有进程都应存储在 `processNames` 中。这很好,但是 DLL 注入从何而来?嗯,`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*”附加到它。这确保可执行文件能找到 DLL。然后,我们用执行 DLL 注入所需的三个标志打开进程。理想情况下,`PROCESS_ALL_ACCESS` 标志是最好的,但这需要将进程权限更改为 `SE_DEBUG_NAME`,出于代码考虑,我没有费心实现。更改为 `SE_DEBUG_NAME` 的代码可以在此处找到。继续,在以适当的标志打开进程后,使用 `GetProcAddress(…) `找到 `LoadLibraryA` 函数的地址。然后,我们在要注入的进程的地址空间中为 DLL 路径分配一些内存。完成此操作后,我们可以使用 `WriteProcessMemory(…) `将字符串写入内存。之后,只需使用 `CreateRemoteThread` 并将起始地址设置为 `LoadLibraryA`,并将我们的字符串作为参数传递即可。结果是?

funapihook/dllloaded.jpg

4.3 DetourCreateProcessWithDll

上述技术适用于将 DLL 注入到已运行的进程中。但是,如何在进程执行时注入 DLL 呢?Detours 库提供了一个函数来实现这一点——`DetourCreateProcessWithDll(…) `。使用该函数基本上等同于调用 `CreateProcess` 并设置 `CREATE_SUSPENDED` 创建标志。这会以挂起的创建线程状态创建进程,因此可以在实际应用程序出现之前注入 DLL。一个重要的注意事项是,要注入的 DLL **必须**在序号 1 上导出一个函数。否则,`DetourCreateProcessWithDll(…) `函数将失败。快速实现,它以 `testdll.dll`(同样,必须在序号 1 上导出函数)启动 `notepad.exe`

#undef UNICODE
#include <cstdio>
#include <windows.h>
#include <detours\detours.h>

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` 函数基本上是 `CreateProcess` 的扩展版本。它接受 `CreateProcess` 的所有参数,以及一些额外的参数,这些参数包含要注入的 DLL 的路径以及 `detoured.dll` 的路径,后者会自动与使用 Detours API 的每个 DLL 一起注入。仅使用 `CreateProcess` 进行此操作将仅涉及将 `CREATE_SUSPENDED` 设置为创建标志,注入 DLL,然后使用存储在 `PROCESS_INFORMATION` 结构中的线程句柄调用 `ResumeThread(…) `。

4.4 按地址进行 Detour

有时可能需要 detour 一个不是标准的 Win32 API 或已知导出函数的函数。这时就需要通过硬编码函数地址来进行 detour。了解函数的地址和它接受的参数(通过反向工程、查找文档或类似方式),就可以进行函数挂钩。下面的代码演示了如何进行

#undef UNICODE
#include <cstdio>
#include <windows.h>
#include <detours\detours.h>

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
}

要 detour 的函数直接指向函数的硬编码地址,然后就可以进行 detour 了。这个特定的示例有一个接受 `DWORD` 作为参数的 `void` 函数。

5. 常见错误

在我为本文编写所有这些应用程序时遇到的最常见错误是 DLL 未能正确加载到进程中。非常重要的是要注意要注入的进程正在运行的目录。根据注入的方法及其实现,要注入的 DLL 可能必须与正在运行的进程位于同一目录中。对于我的实现来说,这不应该是一个问题,因为我查找当前目录并将 DLL 名称附加到它。但是,加载器和 DLL 必须在同一路径中。毕竟,由于我的方法是获取加载器的当前目录并将 DLL 名称附加到它,如果 DLL 在不同的目录中,那么它根本就行不通。我认为这可能是人们在编译此代码并自行尝试后可能遇到的最常见错误。文件 `detoured.dll` 也必须与要注入的 DLL 位于同一目录中。

6. 结论

关于本文中的 API 挂钩就到这里了。实现 API 挂钩的方法肯定还有很多,有些更有效,有些则不然。但是,希望在阅读本文之后,从 Detours 的角度更容易理解 API 挂钩。通过使用 MS Detours,API 挂钩大大简化了,因为修改地址表条目、更改程序流等的细节已经实现了。这节省了大量的调试时间,并允许程序员根据需要设置和移除挂钩,并且(如果正确完成)不用担心破坏内存、解析 PE 头等。希望你作为读者,能从本文中学到一些关于 API 挂钩的技术和理论,以及将这些技术应用于日常应用程序的实现。

© . All rights reserved.