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

API 挂钩揭秘 第二部分 - 有用技巧

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.20/5 (8投票s)

2005年2月1日

CPOL

5分钟阅读

viewsIcon

70665

downloadIcon

1

在能够检测到死锁之前的一些实用技巧。

引言

这是构建线程死锁检测器的第二部分。请参阅第一篇文章以了解发生了什么:API Hooking 的实现(第一部分)。下一部分(包含一个可用的死锁检测器)也在这里:线程死锁检测器

用于线程死锁检测的 API Hooking 函数

为了检测死锁,应该拦截哪些内容

  • 线程函数(Create[Remote]Thread[Suspend/Resume]ThreadExitThreadTerminateThreadOpenThread)。
  • 同步函数(WaitFor[Single/Multiple]Object[Ex]SignalObjectAndWait[Set/Reset/Pulse]Event)。
  • 同步对象创建(Create/Open[Mutex/Semaphore/Event]DuplicateHandle)。
  • 同步对象删除(CloseHandle)。

为了拦截代码,我只是在之前的代码中添加了所需的函数。请参阅上一篇文章,了解如何添加要 Hook 的函数。主要思想是声明一个 Hook 结构,如下所示:

typedef struct
{
    char        szDLLName[MAX_PATH]; // The DLL name
    char        szFuncName[MAX_PATH];// The function name
    void *        pNewFunc;          // The new function pointer
    void *        pPrevFunc;         // The previous function pointer
    Flags        flags;              // The flags (hooked or not, etc...)
} HookStruct;

在 Hook 所有模块中的函数时,之前的(真实的)函数指针保存在 pPrevFunc 中,而新的函数指针(设置为我们的 Hook 函数)会替换模块的 IAT

然后,在我们的函数中,我们可以通过将 pPrevFunc 指针转换为正确的函数指针签名来调用之前的函数。我为此定义了一个有用的宏(源代码将在第三部分提供)。

#define CallFunction(X)    
((Signature_##X)GetPreviousFunctionAddress(Index_##X))
// with signature defined like this
#define Signature_CreateThread                   HANDLE (FAR PASCAL *)\
(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, \
LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD \
dwCreationFlags, LPDWORD lpThreadId)
// And index like this
#define Index_CreateThread             3

好的,现在我们可以调用正确的函数了,让我们开始真正的工作。

通信过程

那么,我们如何通知服务器“Hooked”的应用程序当前正在执行受监控的操作呢?简单的答案是创建一个结构,通过 WM_COPYDATA 消息将其发送到服务器,其中包含所需的信息。所需的信息取决于应用程序,在我们的例子中,它包括:

  • 当前线程句柄(谁在调用此函数)。
  • 当前线程 ID(仅凭句柄不足,请参见下文)。
  • 被操作的对象 ID/句柄(我们在操作什么)。
  • 附加参数(如果有)。
  • 当前命令(我们正在调用哪个函数)。
  • 对象名称(如果有)(仅用于调试)。
  • 调用栈(用于查找调用来源)。
  • 当前时间戳(用于检查死锁)。

然后定义结构如下:

typedef struct 
{
    void *            lAddress;   // The address pointer in the stack
    unsigned int    lFlags;       // Some flags
} StackPointer; 

typedef struct 
{ 
    HANDLE            hObjectID;  // The waiting object ID
    HANDLE            hThread;    // The current thread ID
    DWORD            dwThreadID;  // The current thread ID
    unsigned int    lState;       // The current state, count, etc...
    HANDLE            hFlag;      // The stack here
    Commands        Command;      // The current command
    char            sName[256];   // The object name (if any)
    LARGE_INTEGER   llTimestamp;  // The message timestamp
    StackPointer    xPointers[10];// The stack pointers 
                                  // (only 10 pointers are saved)
} CommunicationObject;
// with Commands being an enum like 
// CmdCreateThread = 0, CmdExitThread = 1, etc...

Hook 函数将如下所示:

// Declare the signature for waiting functions
THREADSPY_API HANDLE WINAPI MyCreateThread(LPSECURITY_ATTRIBUTES 
lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, 
LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId)
{
    DWORD dwID;
    HANDLE hHandle =  CallFunction(CreateThread)(lpThreadAttributes, 
dwStackSize, lpStartAddress, lpParameter, dwCreationFlags, &dwID);
    if (hHandle != NULL)
    {
        CommunicationObject xObj;
        memset(&xObj, 0, sizeof(xObj));
                // Get the true current thread handle
        xObj.hThread   = GetTrueCurrentThread();
                // Get the manipulated handle (it is a thread id here)
        xObj.hObjectID = hHandle;
                // And other data
        xObj.lState    = dwID;
                // Save the caller thread ID
        xObj.dwThreadID = GetCurrentThreadId();
        if (lpThreadId != NULL) 
        {
            *lpThreadId = dwID;
        }
                // Save the current thread
        xObj.Command = CmdCreateThread;
                // Then send the structure (but fill it 
                // with timestamp and call stack before)
        Communicate(&xObj, sizeof(xObj));
                // Send another command if the thread is suspended
        if (dwCreationFlags & CREATE_SUSPENDED)
        {
            xObj.lState = 1;
            xObj.Command = CmdSuspendThread;
            CommunicateWithoutTime(&xObj, sizeof(xObj));
        }

        // We don't need the handle anymore
        if (xObj.hThread != NULL) CallFunction(CloseHandle)(xObj.hThread);
    }

    return hHandle;
}

棘手的部分

好的,现在我们只需要填充结构。即使看起来很简单,但由于 Win32 API 的一些限制,它并不简单。例如,如果您使用 GetCurrentThread 函数,Windows API 将返回一个特殊的句柄 CURRENT_THREAD_HANDLE。这个信息在这里当然没有用。这是 Windows 处理 HANDLE 的方式。对于同一个内核对象,可以在不同的内存空间中拥有多个句柄。因此,仅凭 HANDLE 无法唯一地标识一个线程,我们需要其 ID。虽然在一个程序中将线程句柄和 ID 存储在结构中很容易,但调试程序是否具有这样的映射并不明显。这就是为什么我们需要找出如何根据线程句柄获取线程 ID。例如,当一个线程调用 TerminateThread 来杀死另一个线程时,它只使用被杀死线程的句柄,而不是其 ID。服务器将永远无法匹配哪个线程被杀死(或者这将需要一种匹配算法等)。Windows Server 2003 提供了一个名为 GetThreadId 的函数来获取线程 ID,但由于它仅在 Win2K3 中可用,因此没有用。

解决此问题的方法是使用 NTDLL.DLL 中的隐藏函数 NtQueryInformationThreadNTDLL.DLL 被映射到每个进程内存中),就像 Visual Studio 调试器所做的那样。此函数可以返回一个包含线程 ID 的 CLIENT_ID 结构。有关更多信息,请参阅未文档化的 NT 内部信息

现在我们已经可以识别正在操作的对象了,我们需要获取堆栈跟踪。这可以通过使用 Win32 API 中的 StackWalk 函数来完成。通常,此函数用于暂停被调试线程,检索其上下文,然后恢复线程。由于我们不想暂停线程(因为这会改变调试过程中的调度顺序),因此我们需要自己填充上下文结构。技巧是在使用 StackWalk 之前读取 EIP 寄存器,这可以通过一些简单的 ASM 命令轻松完成,例如:

    CONTEXT c;

    _EnterCriticalSection(&mhSection);
    // This is a ugly code to get the stack trace
    __asm
    {
        call GetEIP
        GetEIP:
        pop eax
        mov c.Eip, eax
        mov c.Ebp, ebp
    };
        _LeaveCriticalSection(&mhSection);

最后一个需要的技巧是检测线程何时停止。由于注入的 DLL 无法创建线程而不干扰程序,因此我们提供了一个名为 CheckRunningThread 的函数,该函数具有与线程启动例程相同的签名,它接受客户端内存空间中的线程句柄,并返回一个 DWORD 作为线程状态。通过使用相同的方法注入 DLL(感谢 CreateRemoteThread),服务器可以停止正在调试的进程,创建从 CheckRunningThread 函数开始的远程线程,并在恢复被调试进程之前读取线程状态。这样,服务器就可以检查线程何时完成,并检测到从未释放的对象。

总结

本文回答了一些(在 MSDN 中被认为是)不可能的事情,例如:

  • 从线程句柄获取线程 ID(在 Google 上搜索一下,您会发现这是一个真正的问题)。
  • 获取正在运行的线程的堆栈跟踪(同样,这也不是常规做法)。
  • 获取真实的线程句柄而不是默认的通用值(使用 DuplicateHandle)。
  • 监视远程进程中的线程何时完成。
  • 就任何操作与服务器进行通信。
  • Hook 任何 Win32 API 函数。

缺点是它需要 WinNT 内核(如 XP、2K 和 2K3),但我相信现在这已经不是问题了。

这是客户端部分。我将在下一部分(第三部分)提供服务器和客户端的源代码。届时,我们将看到如何获取进程中同步函数调用的完整日志,以及如何将调用栈值映射到函数名称。

© . All rights reserved.