禁止线程产生调试事件
一、通过禁止线程产生调试事件
当一个进程被调试器调试的时候,该进程下的所有线程都和调试器建立了调试关系,各个线程通过调试对象 DebugObject
作为中转站来向调试器传递调试事件,当我们通过一些方法,禁止某个线程产生调试事件后,调试器就无法接收到该线程的调试事件了。
在被调试进程和调试器交互的过程中,被调试进程主要产生以下七大类调试事件:
在调试过程中,调试器往往会使用断点
和单步
来对进程进行调试,当我们禁止某线程产生调试事件后,一旦该线程中发生断点或单步异常,而调试器又无法接收并处理,那么该线程中的断点或单步异常就会因为没有被处理而进入程序的默认异常处理例程,导致程序直接被终止。
1 通过 ZwSetInformationThread 函数禁止当前线程产生调试事件
当我们使用 ZwSetInformationThread
函数并指定 ThreadHideFromDebugger(17)
标志禁止当前线程产生调试事件后,当我们在调试器中对将要执行的代码进行单步或者下断点后,会导致异常没有被处理而导致程序退出,代码如下:
#include <stdio.h>
#include <Windows.h>
typedef enum _THREADINFOCLASS {
ThreadBasicInformation,
ThreadTimes,
ThreadPriority,
ThreadBasePriority,
ThreadAffinityMask,
ThreadImpersonationToken,
ThreadDescriptorTableEntry,
ThreadEnableAlignmentFaultFixup,
ThreadEventPair,
ThreadQuerySetWin32StartAddress,
ThreadZeroTlsCell,
ThreadPerformanceCount,
ThreadAmILastThread,
ThreadIdealProcessor,
ThreadPriorityBoost,
ThreadSetTlsArrayAddress,
ThreadIsIoPending,
ThreadHideFromDebugger // 17
} THREAD_INFO_CLASS;
typedef NTSTATUS(NTAPI* PFN_ZwSetInformationThread)(
_In_ HANDLE ThreadHandle, // 线程句柄
_In_ THREAD_INFO_CLASS ThreadInformaitonClass, // 需要设置的线程信息类型
_In_ PVOID ThreadInformation, // 线程信息
_In_ ULONG ThreadInformationLength // 线程信息长度
);
// 定义函数指针
PFN_ZwSetInformationThread ZwSetInformationThread;
int main()
{
/* 通过 ZwSetInformationThread 函数禁止线程产生调试事件 */
// 从 ntdll.dll 中获取 ZwSetInformationThread 函数的地址
HMODULE hMod = LoadLibraryA("ntdll.dll");
ZwSetInformationThread = (PFN_ZwSetInformationThread)GetProcAddress(hMod, "ZwSetInformationThread");
// 禁止线程产生调试事件
ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);
printf("禁止线程产生调试事件完成\r\n");
while (1)
{
// 底层调用 NtDelayExecution 函数
Sleep(1000);
printf("过了 1 秒\r\n");
}
system("pause");
}
当我们对 Sleep(1000);
语句下软件断点后,程序直接异常退出了。
但有一点需要注意:一个程序一般有多个线程,我们上面的示例只是在主线程中禁止产生调试事件,其他线程仍然能够正常和调试器进行交互,由于下断点默认是针对所有线程的,因此当主线程中执行到了断点代码,且主线程产生的断点异常没有被正常处理而导致程序异常退出。
为了验证这个说法,我们通过 windbg 附加示例程序:
如上图,附加成功后,windbg 会在被调试进程中创建一个新的线程,并在新线程中执行远程中断,中断后我们输入 g
命令,程序仍然可以正常执行,因此在主线程中禁止调试事件并不会影响其他线程正常和调试器交互,但是当我们在命令行执行 bp NtDelayExecution
(Sleep 函数底层调用该函数)命令对 NtDelayExecution
下断点后,程序会立刻异常退出,因为主线程被禁止产生调试事件,而其执行 Sleep
函数的时候底层调用 NtDelayExecution
函数遇到了断点异常,而该断点异常没有被正常处理,因此导致程序异常退出。
如果我们需要对特定线程(比如 0 号线程)下断点,我们可以使用如下指令:
~0 bp NtDelayExecution // 仅限于用户模式
我们对上面代码进行改造,加入结构化异常处理程序,看看是不是由于断点异常没有被处理而导致的程序退出:
__try
{
while (1)
{
// 底层调用 NtDelayExecution 函数
Sleep(1000);
printf("过了 1 秒\r\n");
}
}
__except (1)
{
printf("结构化处理器接收了调试器没有处理的断点异常\r\n");
}
继续在 Sleep(1000);
语句处下断点并运行:
可以看到,主线程产生的断点异常没有传递给调试器,而是被我们的结构化异常处理例程给接收了,而且是正常退出。因此可以证明前面的代码确实是因为断点异常没有被处理而导致的异常退出。
2 通过 ZwCreateThreadEx 创建禁止产生调试事件的新线程
除了可以禁止当前线程产生调试事件以外,我们也可以在创建新线程的时候指定 THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER(4)
标志来禁止创建的新线程产生调试事件:
#include <stdio.h>
#include <Windows.h>
#ifdef _WIN64
typedef DWORD(WINAPI* PFN_ZwCreateThreadEx)(
_Out_ PHANDLE ThreadHandle, // 用于接收新创建线程的句柄
_In_ ACCESS_MASK DesiredAccess, // 希望新线程句柄拥有的访问权限
_In_opt_ LPVOID ObjectAttributes, // 指向一个 OBJECT_ATTRIBUTES 结构的指针,用于定义对象的属性(可以为 NULL)
_In_ HANDLE ProcessHandle, // 指向线程将要运行的进程的句柄
_In_ LPTHREAD_START_ROUTINE lpStartAddress, // 指向线程的起始函数的指针
_In_opt_ LPVOID lpParameter, // 传递给线程起始函数的参数(可以为 NULL)
_In_ ULONG CreateThreadFlags, // 创建线程时的标志(用于指定线程创建时的特殊选项,如创建后挂起等)
_In_ SIZE_T ZeroBits, // 用于指定线程堆栈的某些属性,较少使用,通常设置为 0
_In_ SIZE_T StackSize, // 指定线程的初始堆栈大小
_In_ SIZE_T MaximumStackSize, // 指定线程堆栈的最大大小,通常为0,表示使用默认的最大堆栈大小
_In_ LPVOID pUnkown // 未明确指定用途,通常保留为 0
);
#else
typedef DWORD(WINAPI* PFN_ZwCreateThreadEx)(
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ LPVOID ObjectAttributes,
_In_ HANDLE ProcessHandle,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter,
_In_ BOOL CreateSuspended,
_In_ DWORD dwStackSize,
_In_ DWORD dw1,
_In_ DWORD dw2,
_In_ LPVOID pUnkown
);
#endif
#define THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER 0x00000004
// 定义函数指针
PFN_ZwCreateThreadEx ZwCreateThreadEx;
DWORD WINAPI func(void* lpParam)
{
__try
{
while (1)
{
// 底层调用 NtDelayExecution 函数
Sleep(1000);
printf("过了 1 秒\r\n");
}
}
__except(1)
{
printf("结构化处理器接收了调试器没有处理的断点异常\r\n");
}
return 0;
}
int main()
{
/* 通过 ZwCreateThreadEx 函数创建具备禁止调试事件属性的进程 */
// 从 ntdll.dll 中获取 ZwCreateThreadEx 函数的地址
HMODULE hMod = LoadLibraryA("ntdll.dll");
ZwCreateThreadEx = (PFN_ZwCreateThreadEx)GetProcAddress(hMod, "ZwCreateThreadEx");
HANDLE hThread = NULL;
#ifdef _WIN64
NTSTATUS status = ZwCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, GetCurrentProcess(),
(LPTHREAD_START_ROUTINE)func, NULL, THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER, 0, 0, 0, NULL);
#else
NTSTATUS status = ZwCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, GetCurrentProcess(),
(LPTHREAD_START_ROUTINE)func, NULL, THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER, 0, 0, 0, NULL);
#endif
if (status != 0) {
printf("使用 ZwCreateThreadEx 函数创建线程失败,错误代码: %X\n", status);
}
else
{
printf("线程创建成功,线程句柄: %p\n", hThread);
}
// 等待子线程返回
WaitForSingleObject(hThread, INFINITE);
system("pause");
}
注:
ZwCreateThreadEx
函数在 32 位和 64 位系统下的定义不一样,但经过测试,32 位系统下也可以指定THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER
属性。