深入理解Windows反调试技术之常见反调试技术二
前言 由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。(本文仅用于交流学习),本文仅作技术研究。 本文涉及到的案例可在附件获取。 NtQueryInformationProcess 概述 NtQueryInformationProcess 是Windows API的一个函数,它提供了获取关于Windows进程信息的方法。 函数原型
前言
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。(本文仅用于交流学习),本文仅作技术研究。
本文涉及到的案例可在附件获取。
NtQueryInformationProcess
概述
NtQueryInformationProcess 是Windows API的一个函数,它提供了获取关于Windows进程信息的方法。
函数原型
__kernel_entry NTSTATUS NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
参数列表:
ProcessHandle:要查询信息的进程的句柄。ProcessInformationClass:进程信息类,指定你希望查询什么类型的信息。这是一个枚举值,可以是如ProcessBasicInformation(基本信息)等。这里列举一些常用的ProcessBasicInformation:查询基本信息,如进程ID,父进程ID等。ProcessDebugPort:检查进程是否正在被调试。ProcessWow64Information:如果进程是运行在WOW64(Windows 32-bit On Windows 64-bit)环境中,则返回一个非零值。ProcessImageFileName:获取进程的图像文件名。ProcessBreakOnTermination:查询或设置Debug Break在进程终止时是否会触发。Pro
ProcessInformation:返回进程信息的缓冲区的指针。这是一个void指针,这意味着它可以指向任何类型的数据。ProcessInformationLength:ProcessInformation缓冲区的大小。必须至少为你请求的数据类型的大小。ReturnLength:如果非NULL,则返回在ProcessInformation中返回的信息的大小。
分析
#include <windows.h>
#include <ntstatus.h>
#include <stdio.h>
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
typedef LONG KPRIORITY;
typedef struct _PROCESS_BASIC_INFORMATION {
NTSTATUS ExitStatus;
PVOID PebBaseAddress;
ULONG_PTR AffinityMask;
KPRIORITY BasePriority;
ULONG_PTR UniqueProcessId;
ULONG_PTR InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION, * PPROCESS_BASIC_INFORMATION;
typedef NTSTATUS(WINAPI* NT_QUERY_INFORMATION_PROCESS)(HANDLE, UINT, PVOID, ULONG, PULONG);
NT_QUERY_INFORMATION_PROCESS NtQueryInfoProcess = NULL;
BOOL IsDebugged() {
PROCESS_BASIC_INFORMATION pbi;
DWORD debugPort = 0;
//ULONG returnLength;
// Check ProcessDebugPort
NtQueryInfoProcess(GetCurrentProcess(), 7/*ProcessDebugPort*/, &debugPort, sizeof(debugPort), NULL);
if (debugPort != 0)
return TRUE; // Debugged
// Check ProcessDebugObjectHandle
HANDLE debugObject = NULL;
NtQueryInfoProcess(GetCurrentProcess(), 30/*ProcessDebugObjectHandle*/, &debugObject, sizeof(debugObject), NULL);
if (debugObject != NULL)
return TRUE; // Debugged
// Check ProcessDebugFlags
DWORD debugFlags = 1;
NtQueryInfoProcess(GetCurrentProcess(), 31/*ProcessDebugFlags*/, &debugFlags, sizeof(debugFlags), NULL);
if (debugFlags !=1) // 修改这里
return TRUE; // Debugged
return FALSE; // Not debugged
}
LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
{
switch (message)
{
case WM_CREATE:
{
NtQueryInfoProcess = (NT_QUERY_INFORMATION_PROCESS)GetProcAddress(GetModuleHandle(TEXT("ntdll")), "NtQueryInformationProcess");
CreateWindow(TEXT("BUTTON"), TEXT("Check Debugger"),
WS_VISIBLE | WS_CHILD,
10, 10, 120, 25,
hwnd, (HMENU)1, NULL, NULL);
}
break;
case WM_COMMAND:
{
if (LOWORD(wparam) == 1) {
if (IsDebugged())
MessageBox(hwnd, TEXT("Debugging"), TEXT("Status"), MB_OK);
else
MessageBox(hwnd, TEXT("No Debugger"), TEXT("Status"), MB_OK);
}
}
break;
case WM_CLOSE:
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, message, wparam, lparam);
}
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR, int nCmdShow)
{
MSG msg;
WNDCLASSW wc = { 0 };
wc.lpszClassName = L"Class";
wc.hInstance = hInst;
wc.lpfnWndProc = WindowProcedure;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
RegisterClassW(&wc);
CreateWindowW(wc.lpszClassName, L"Debugger Check",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
100, 100, 320, 240, NULL, NULL, hInst, NULL);
while (GetMessage(&msg, NULL, 0, 0)) {
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
未调试状态

调试状态
为了防止一些插件,我们这里使用纯净版OD,仅仅汉化和有Command插件和中文搜索插件
为了方便查看,我们直接使用中文搜索插件来定位,查看debugging反向追踪,设置断点

双击然后我们可以跟踪到其在反汇编窗口中的定位,我们可以看到我们使用的三个NtQueryInformationProcess函数,以及两个Messagebox函数

自行设置三个NtQueryInformationProcess断点,F9运行
ProcessDebugPort(0x07)
进程处于调试状态就会为它分配一个调试端口,Debug Port,
- 进程处于调试的时候,变量
debugPort被设置为0xFFFFFFFF, - 非调试状态的时候,
debugPort被设置为0x0
可以看到,处于调试的时候,值变为了 0xFFFFFFFF

ProcessDebugObjectHandle(0x1E)
调试的时候会生成调试对象(DebugObject),
- 若进程处于调试状态的时候,调试对象句柄值就存在,
- 处于非调试状态的时候,调试对象句柄值为NULL.
处于被调试状态,句柄存在则debugObject不为NULL

ProcessDebugFlags(0x1F)
检测Debug Flags(调试标志)的值可以判断进程处于调试状态,通过函数第三个参数就可以获取调试标志的值,
- 若为0,则处于调试状态,
- 若为1,则处于非调试状态。
在OD中调试,则debugFlags为0x0

checkRemoteDebuggerPresent
CheckRemoteDebuggerPresent 也是一个微软提供的反调试函数,该函数可以在本地或者远程进程中使用。函数接收两个参数进程句柄和一个指向布尔值的指针。
如果指定的进程正在被调试,则函数会把指向布尔值的指针设为 TRUE,否则设为FALSE。
BOOL CheckRemoteDebuggerPresent(
[in] HANDLE hProcess,
[in, out] PBOOL pbDebuggerPresent
);
自定义代码
#include <windows.h>
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_CREATE:
{
CreateWindow(TEXT("button"), TEXT("Check Debugger"),
WS_VISIBLE | WS_CHILD,
10, 10, 150, 30,
hwnd, (HMENU)1, NULL, NULL);
break;
}
case WM_COMMAND:
{
BOOL isDebugged;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &isDebugged);
if (!isDebugged)
{
MessageBox(hwnd, TEXT("No Debugger"), TEXT("Debugger Status"), 0);
}
else
{
MessageBox(hwnd, TEXT("Debugging"), TEXT("Debugger Status"), 0);
}
break;
}
case WM_DESTROY:
{
PostQuitMessage(0);
break;
}
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR, int nCmdShow)
{
WNDCLASS wc = { 0 };
wc.lpfnWndProc = WndProc;
wc.hInstance = hInst;
wc.lpszClassName = TEXT("myWindowClass");
if (!RegisterClass(&wc))
return 1;
HWND hwnd = CreateWindow(wc.lpszClassName, TEXT("Debugger Check"), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT, 300, 100, 0, 0, hInst, 0);
if (!hwnd)
return 1;
MSG msg = { 0 };
while (GetMessage(&msg, 0, 0, 0))
DispatchMessage(&msg);
return 0;
}
深入分析
我们调试发现CheckRemoteDebuggerPresent在内部调用了NtQueryInformationProcess,且检测的时ProcessDebugPort(0x07),

NtQuerySystemInformation
原理分析
NtQuerySystemInformation 是 Windows Native API 中的一个函数, 它被用于获取系统底层的详细信息。在 Windows 的公开 API 中, 它对应的函数是 GetSystemInformation。虽然这两个函数的作用相同,但是 NtQuerySystemInformation 提供了更丰富和详尽的系统参数。
__kernel_entry NTSTATUS NtQuerySystemInformation(
[in] SYSTEM_INFORMATION_CLASS SystemInformationClass,
[in, out] PVOID SystemInformation,
[in] ULONG SystemInformationLength,
[out, optional] PULONG ReturnLength
);
参数列表:
SystemInformationClass:系统信息类别,定义了你希望查询的系统信息的种类。SystemInformation:这是输出参数,是一个指向缓冲区的指针,用于存储查询结果。SystemInformationLength:输入的缓冲区的长度。ReturnLength:如果函数成功,这个参数会包含返回的数据长度。如果SystemInformationLength参数提供的长度不够,此处返回的值将大于SystemInformationLength,并且函数将返回一个错误码,表明缓冲区太小。
SYSTEM_INFORMATION_CLASS结构
https://www.vergiliusproject.com/kernels/x86/Windows%2010/2210%2022H2%20(May%202023%20Update)/_SYSTEM_INFORMATION_CLASS
第一个参数SystemInformationClass传入SystemKernelDebuggerInformation(0x23), 第二个参数SystemInformation为结构体SYSTEM_KERNEL_DEBUGGER_INFORMATION的地址, SYSTEM_KERNEL_DEBUGGER_INFORMATION.DebuggerEnabled == 1则表明系统处于调试状态.
绕过与破解
- windows7中在命令行窗口(管理员模式)执行
bcdedit /debug off即可。
SetUnhandledExceptionFilter异常处理
概述
一般异常处理流程,UnhandledExceptionFilter()内部调用了ZwQueryInformationProcess,来判断进程是否正在调试进程。若进程正常运行(非调试运行),则运行系统最后的异常处理器;若进程处于调试中,则将异常派送给调试器
SetUnhandledExceptionFilter函数是Windows API中的一个函数,它允许一个应用程序注册一个顶级未处理的异常过滤器。当程序中发生一个未被处理的异常时,且程序未被调试。该过滤器会被调用。
我们可以主动发起一个异常,然后检查是否被调试器调试,如果被调试,那么SetUnhandledExceptionFilter里面设置的函数就不会被执行,即注册软件不会被注册。这是一个基本的反调试技术。
前置知识
SetUnhandledExceptionFilterSetUnhandledExceptionFilter函数是Windows API中的一个函数,它允许一个应用程序注册一个顶级未处理的异常过滤器。当程序中发生一个未被处理的异常时,且程序未被调试。该过滤器会被调用。程序可以主动抛出一个异常来判断当前程序是否正在被调试:在程序执行过程中会抛出一个异常,如果当前程序没有被调试,那么就会调用该参数指定的异常处理函数
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( [in] LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter );
lpTopLevelExceptionFilter:指向顶级异常筛选器函数的指针,- 每当
UnhandledExceptionFilter函数获得控制权且进程未调试时,将调用该函数。 - 此参数的 NULL 值指定 UnhandledExceptionFilter 中的默认处理。
- 每当
UnhandledExceptionFilter概念UnhandledExceptionFilter函数是一个由Windows提供的顶层未处理异常过滤器。它会在一个异常未被应用程序的异常处理程序(比如try/catch块)捕获时被调用。LONG UnhandledExceptionFilter( [in] _EXCEPTION_POINTERS *ExceptionInfo );
参数
ExceptionInfo是一个指向EXCEPTION_POINTERS结构的指针,该结构包含了导致未处理异常的异常记录和上下文信息。这个函数返回一个
LONG值,根据返回的值不同,系统会做出不同的反应。以下是可能的返回值及其含义:EXCEPTION_CONTINUE_SEARCH:如果函数返回这个值,系统会寻找其他的处理代码。如果找不到,那么就会执行系统默认的异常处理程序。EXCEPTION_CONTINUE_EXECUTION:如果函数返回这个值,系统就会忽略异常,从异常发生处继续执行代码。这个常需要慎重使用,因为如果异常的原因没有得到解决,那么程序可能会进入未定义的状态。EXCEPTION_EXECUTE_HANDLER:如果函数返回这个值,系统会执行异常的相关处理程序。
ZwQueryInformationProcess概念ZwQueryInformationProcess函数是一个Native API函数,属于NTDLL库,用于查询进程的信息。NTSTATUS WINAPI ZwQueryInformationProcess( _In_ HANDLE ProcessHandle, _In_ PROCESSINFOCLASS ProcessInformationClass, _Out_ PVOID ProcessInformation, _In_ ULONG ProcessInformationLength, _Out_opt_ PULONG ReturnLength );
参数列表:
ProcessHandle:要查询的进程的句柄。ProcessInformationClass:要查询的信息类型,是由PROCESSINFOCLASS枚举确定的。ProcessBasicInformation(0):查询基本的进程信息,像是进程ID,父进程ID等。ProcessDebugPort(7):检测进程是否被调试。ProcessWow64Information(26):确定进程是否在Wow64模式下运行(32位应用程序在64位OS下的运行模式)。ProcessImageFileName(27):返回一个指向进程的镜像文件名称的UNICODE_STRING结构的指针。ProcessBreakOnTermination(29):查询或设置进程的BreakOnTermination标志。ProcessSubsystemInformation(75):获取进程的子系统信息,例如该进程是否为Windows进程,还是为Console进程。
ProcessInformation:这是一个指向缓冲区的指针,用于存储查询到的进程信息。ProcessInformationLength:ProcessInformation缓冲区的大小。ReturnLength:这是一个可选参数,如果不为NULL,会写入实际返回的信息大小。
程序跟踪分析
原流程
首先设置断点,SetUnhandledExceptionFilter和UnhandledExceptionFilter
这里可以是使用OD的Command插件来设置。

F9运行,程序在SetUnhandledExceptionFilter中断
我们在反汇编窗口看到,SetUnhandledExceptionFilter的参数值是0x00401108,则该地址就是函数设置的异常处理函数的地址。

在0x00401108设置断点,然后将SetUnhandledExceptionFilter禁用

F9继续运行,程序弹窗出来,我们输入错误的序列号666666点击check。

此时,程序会中断在 UnhandledExceptionFilter我们一步步追踪函数,发现一个关键函数ZwQueryInformationProcess

前面我们已经知道ZwQueryInformationProcess,当传参为7时,用于查询进程的信息。我们可以看到该缓冲区的大小为4个字节,如果该缓冲区返回的是FFFFFFFF的话表示当前程序正在被调试,如果返回的是0的话,表示当前程序没有被调试

我们执行,发现缓冲区存储的是FFFFFFFF,则说明该程序正在被调试。
再比较缓冲区的值是否为0,为零则跳转,否则不跳转。

我们继续向下,若不挑战,则会终止执行程序


调试
前面我们已经梳理了程序的流程,这次我们修改ZwQueryInformationProcess写入到缓冲区的值

然后我们F9,由于我们修改了ZwQueryInformationProcess的检测结果,即程序未被调试
直接跳转到了SetUnhandledExceptionFilter函数设置的异常处理函数位置.
分析发现,这是程序注册函数。

00401108 . 8B75 08 MOV ESI,DWORD PTR SS:[EBP+8]
0040110B . 8B46 04 MOV EAX,DWORD PTR DS:[ESI+4]
0040110E . 05 B8000000 ADD EAX,0B8
00401113 . 8BF0 MOV ESI,EAX
00401115 . 8B00 MOV EAX,DWORD PTR DS:[EAX]
00401117 . 83C0 0E ADD EAX,0E
0040111A . 8906 MOV DWORD PTR DS:[ESI],EAX
0040111C . A1 53314000 MOV EAX,DWORD PTR DS:[403153]
00401121 . 33C9 XOR ECX,ECX
00401123 . 33D2 XOR EDX,EDX
00401125 . 33FF XOR EDI,EDI
00401127 > 0FBEB9 543040>MOVSX EDI,BYTE PTR DS:[ECX+403054] ; 循环遍历输入的Serial,将字符的Hex值复制给EDI
0040112E . 81C2 78563412 ADD EDX,12345678 ; EDX += 0x12345678
00401134 . D1D2 RCL EDX,1 ; EDX,带进位循环左移一位 即x2
00401136 . 13D7 ADC EDX,EDI ; 带进位加法指令 EDX +=EDI + CF
00401138 . 81C2 21436587 ADD EDX,87654321 ; EDX += 0x87654321
0040113E . 41 INC ECX ; ECX++
0040113F . 3BC8 CMP ECX,EAX ; 比较ECX与EAX,即查看是否遍历完输入的FSerial
00401141 .^ 75 E4 JNZ SHORT Sphynx.00401127
00401143 . 81FA 382AB4C3 CMP EDX,C3B42A38 ; 当计算的结果等于0xC3B42A38时,则正确注册
00401149 . 75 32 JNZ SHORT Sphynx.0040117D ; 输入的Serial错误 则跳转至0x40117D
0040114B . 33C9 XOR ECX,ECX
0040114D . 33D2 XOR EDX,EDX
0040114F . BE 07304000 MOV ESI,Sphynx.00403007
00401154 . BF 63314000 MOV EDI,Sphynx.00403163 ; ASCII "Sphynx by d@b 2004"
00401159 . 8A56 12 MOV DL,BYTE PTR DS:[ESI+12]
0040115C > 8A0431 MOV AL,BYTE PTR DS:[ECX+ESI]
0040115F . 32C2 XOR AL,DL
00401161 . 880439 MOV BYTE PTR DS:[ECX+EDI],AL
00401164 . 41 INC ECX
00401165 . 83F9 31 CMP ECX,31
00401168 .^ 75 F2 JNZ SHORT Sphynx.0040115C
0040116A . 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
0040116C . 68 63314000 PUSH Sphynx.00403163 ; |Title = "Sphynx by d@b 2004"
00401171 . 68 76314000 PUSH Sphynx.00403176 ; |Text = "Well Done ! Write a Tutorial !"
00401176 . 6A 00 PUSH 0 ; |hOwner = NULL
00401178 . E8 19000000 CALL <JMP.&user32.MessageBoxA> ; \MessageBoxA
总结与绕过
这里主要是利用UnhandledExceptionFilter的函数会调用ZwQueryInformationProcess函数来检测程序是否被调试器调试,如果被调试器调试,则将异常交给调试器处理,若不再调试器中,则由最后的异常处理函数处理。
但是SetUnhandledExceptionFilter,更改了异常处理函数,使得异常未被处理,且未在调试器中,则异常由SetUnhandledExceptionFilter定义的函数处理.
绕过方法:
- 修改
ZwQueryInformationProcess的检测值,可以使用HideOD插件。 - 修改里面跳转语句,或手动修改(不建议)。
OutputDebugString
将字符串发送到调试器进行显示。
void OutputDebugStringW(
[in, optional] LPCWSTR lpOutputString
);
原理
OllyDbg在处理OutputDebugString函数的输出时,并没有足够的错误检查和边界检查,未能妥善处理过长字符串导致的。
防护与绕过:
通过HideDebuggger插件的OutputDebugString exploit选项我们可以修复OD的这个bug