DebugPort 字段
一、通过检测 DebugPort 字段
当调试器与进程建立调试关系后,调试器会通过调试子系统来设置被调试进程的内核结构体 EPROCESS
中的 DebugPort
字段,该字段保存的是调试端口,调试器和被调试进程通过这个端口来进行通信。
因此,我们可以通过检测该调试端口,来判断目标进程是否处于被调试状态。首先,我们来观察一下这个 DebugPort
字段所处的位置,来直观的感受一下。
我们打开双机调试,然后在虚拟机中打开 notepad.exe
程序,通过下面命令查询其 EPROCESS
结构地址:
!process 0 0 notepad.exe
结果如下:
然后通过下面命令进行观察其 EPROCESS
结构:
dt nt!_EPROCESS ffffe107c29ef1c0
结果如下:
我们可以看到,在 0x578 偏移处看到了 DebugPort
字段,当然这个偏移不是固定的,不同版本的系统会有变化。
接下来,我们通过一些方法来检测 DebugPort
字段。
1 通过 CheckRemoteDebuggerPresent 函数来检测
这个函数不仅可以检测自己进程是否被调试,同时也可以检测系统中的其他线程是否处于被调试状态。
我们来研究研究这个函数的底层实现,在 x64dbg 中任意打开一个进程,跳转到这个函数的代码:
可以看到,CheckRemoteDebuggerPresent
函数其实是调用了 NtQueryInformationProcess
来对 DebugPort
字段进行的检测。
我们现在用这个函数来检测调试器:
/* 通过 CheckRemoteDebuggerPresent 函数检测 DebugPort 字段 */
BOOL isDebug = FALSE;
if (CheckRemoteDebuggerPresent(GetCurrentProcess(), &isDebug))
{
if (isDebug)
{
printf("正在被调试\r\n");
}
else
{
printf("没有被调试\r\n");
}
}
结果如下:
2 通过 NtQueryInformationProcess 函数来检测
我们从刚刚的分析中可以知道,CheckRemoteDebuggerPresent
函数底层其实是通过调用 NtQueryInformationProcess
来检测 DebugPort
字段的,那么我们肯定也可以通过这个更底层的函数来进行检测。而且,由于这个函数是微软未公开的一个函数,相比 CheckRemoteDebuggerPresent
函数来说,反调试中被干掉的难度显著增加。
这个未公开函数的原型如下:
// NtQueryInformationProcess 函数原型
NTSTATUS NtQueryInformationProcess(
IN HANDLE ProcessHandle, // 需查询的进程句柄
IN PROCESSINFOCLASS ProcessInformationClass, // 需查询的进程信息枚举类型
OUT PVOID ProcessInformation, // 输出缓冲区
IN ULONG ProcessInformationLength, // 输出缓冲区大小
OUT PULONG ReturnLength OPTIONAL // 实际返回大小
);
其中,第二个参数是一个枚举类型,而我们需要传入的参数就是 ProcessDebugPort(7)
,其实就是对应的十进制的 7,我们可以用 x64dbg 打开第一个检测程序,然后在 CheckRemoteDebuggerPresent
函数处理下一个断点,单步运行到其底层函数 NtQueryInformationProcess
处停下:
从图中可以看到,RDX
寄存器的值为 7,也就是传入 NtQueryInformationProcess
函数的第二个参数,再次应证了我们的想法。
接下来我们给出检测代码:
#include <stdio.h>
#include <Windows.h>
typedef enum _PROCESSINFOCLASS {
ProcessBasicInformation,
ProcessQuotaLimits,
ProcessIoCounters,
ProcessVmCounters,
ProcessTimes,
ProcessBasePriority,
ProcessRaisePriority,
ProcessDebugPort,
ProcessExceptionPort,
ProcessAccessToken,
ProcessLdtInformation,
ProcessLdtSize,
ProcessDefaultHardErrorMode,
ProcessIoPortHandlers, // Note: this is kernel mode only
ProcessPooledUsageAndLimits,
ProcessWorkingSetWatch,
ProcessUserModeIOPL,
ProcessEnableAlignmentFaultFixup,
ProcessPriorityClass,
ProcessWx86Information,
ProcessHandleCount,
ProcessAffinityMask,
ProcessPriorityBoost,
ProcessDeviceMap,
ProcessSessionInformation,
ProcessForegroundInformation,
ProcessWow64Information,
ProcessImageFileName,
ProcessLUIDDeviceMapsEnabled,
ProcessBreakOnTermination,
ProcessDebugObjectHandle,
ProcessDebugFlags,
ProcessHandleTracing,
ProcessIoPriority,
ProcessExecuteFlags,
ProcessResourceManagement,
ProcessCookie,
ProcessImageInformation,
MaxProcessInfoClass // MaxProcessInfoClass should always be the last enum
} PROCESSINFOCLASS;
typedef NTSTATUS(NTAPI* PFN_NtQueryInformationProcess)(
HANDLE ProcessHandle, // 需查询的进程句柄
DWORD ProcessInformationClass, // 需查询的进程信息枚举类型
PVOID ProcessInformation, // 输出缓冲区
ULONG ProcessInformationLength, // 输出缓冲区大小
PULONG ReturnLength // 实际返回大小
);
typedef struct _PROCESS_BASIC_INFORMATION {
NTSTATUS ExitStatus;
PVOID PebBaseAddress; // PPEB PebBaseAddress;
ULONG_PTR AffinityMask;
LONG BasePriority; // KPRIORITY BasePriority;
ULONG_PTR UniqueProcessId;
ULONG_PTR InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION;
typedef PROCESS_BASIC_INFORMATION* PPROCESS_BASIC_INFORMATION;
// 定义函数指针
PFN_NtQueryInformationProcess NtQueryInformationProcess;
int main
{
/* 通过 NtQueryInformationProcess 函数检测 DebugPort 字段 */
// 从 ntdll.dll 中获取 NtQueryInformationProcess 函数的地址
HMODULE hMod = LoadLibraryA("ntdll.dll");
NtQueryInformationProcess = (PFN_NtQueryInformationProcess)GetProcAddress(hMod, "NtQueryInformationProcess");
// 查询是否存在 DebugPort
PVOID DebugPort = NULL;
NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort, &DebugPort, sizeof(PVOID), NULL); // ProcessDebugPort(7)
if (DebugPort != NULL)
{
printf("正在被调试,传回的 DebugPort 为 %p\r\n", DebugPort);
}
else
{
printf("没有被调试\r\n");
}
}
运行结果如下:
我们能够看到当用调试器启动或附加进程的时候,被调试进程是能够检测到被调试的,但是通过 NtQueryInformationProcess
函数返回的 DebugPort
字段却是 FFFFFFFFFFFFFFFF
,这是因为 DebugPort
是一个重要的内核字段,需要对用户层隐藏,因此如果进程没有被调试,则返回 NULL,如果正在被调试则返回 FFFFFFFFFFFFFFFF
。