API 挂钩揭秘 第二部分 - 有用技巧
在能够检测到死锁之前的一些实用技巧。
引言
这是构建线程死锁检测器的第二部分。请参阅第一篇文章以了解发生了什么:API Hooking 的实现(第一部分)。下一部分(包含一个可用的死锁检测器)也在这里:线程死锁检测器
用于线程死锁检测的 API Hooking 函数
为了检测死锁,应该拦截哪些内容
- 线程函数(
Create[Remote]Thread
、[Suspend/Resume]Thread
、ExitThread
、TerminateThread
、OpenThread
)。 - 同步函数(
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 中的隐藏函数 NtQueryInformationThread
(NTDLL.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),但我相信现在这已经不是问题了。
这是客户端部分。我将在下一部分(第三部分)提供服务器和客户端的源代码。届时,我们将看到如何获取进程中同步函数调用的完整日志,以及如何将调用栈值映射到函数名称。