前言

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。(本文仅用于交流学习),本文仅作技术研究。

本文涉及到的案例可在附件获取。

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里面设置的函数就不会被执行,即注册软件不会被注册。这是一个基本的反调试技术。

前置知识

  • SetUnhandledExceptionFilter

    SetUnhandledExceptionFilter函数是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