1. 异常的分发一共有多少轮?

  • 一共有两轮。 KiDispatchExcption 函数的最后一个参数 FirstChance 表示当前是第几次进行异常的的分发,另一个函数 RaiseException 最后一个参数也表示当前是第几次进行分发

2. 通过什么可以区分当前所处的是 R0 还是 R3

  • 在 windows 下,代码被分为了 R3 和 R0 权限,CS 段寄存器的最低两位就表示当前所处的是 3环(用户) 还是 0环(内核),可以通过 mov eax, cs + test eax, 1

3. 异常的产生方式有多少种

  • CPU 满足特定的条件之后,内部主动产生的异常,类似 int 3(IDT)
  • 用户通过 RaiseException 构建 ExceptionRecord 主动抛出异常(KiDispatchException)

4. 编译器会为用户自定义的 try except 添加怎样的异常处理函数

  • 在同一个函数内,无论用户编写了多少个 SEH,编译器只会安装一个 except_handler4

5. 当用户模式下产生异常时,SEH 函数会在什么时候被调用

  • int3 -> idt[3] -> _KiTrap03 -> CommonDispatchException -> KiDispatchExceptijon
    -> KeUserExceptionDispatcher(3) -> RtlDispatchException(3) -> RtlpExecuteHandlerForException(3) -> except_handler4 -> except_handler4_common

-> 用户通过 _try _except 安装的异常处理函数

6. 在 R0 中异常是如何被传递给三环调试器的

  • DbgkForwardException -> DbgkpSendApiMessage -> 三环调试器

7. R0 和 R3 的RtlDispathException 有什么区别

  • KiDispatchException[0] -> RtlDispathException[0] -> SEH
  • KiUserExceptionDispatcher[3] -> RtlDispathException[3] -> VEH SEH UEH (VCH)

反调试技术概览

静态反调试

PEB相关

BeginDebug

bool CheckBeingDebugged()
{
    __asm
    {
        ; 通过 FS : [0x30] 获取到 PEB 结构体的地址
        mov eax, dword ptr fs : [0x30]

        ; 通过 PEB 偏移为 0x02 的地方获取到 BeingDebugged
        movzx eax, byte ptr [eax + 0x02]
    }
}

int main()
{
    if (CheckBeingDebugged())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}
函数读取了当前进程中PEB中的BeginDebugged的标志,PEB(ProcessEnvironmentBlock)存在TEB(ThreadEnvironmentBlock)中,我们查看TEB结构体


我们可以看到在TEB结构体中偏移0x30的位置存放着PEB的结构体,我们再查看PEB结构体

在PEB中偏移0x2的位置保存着我们需要得到的标志。
加下来我们进行测试,首先在VS模式下运行查看结果:

执行生成的文件:

IsDebuggerPresent
同样的IsDebuggerPresent也是在检查PEB的BeingDebugged字段

int main()
{
    // IsDebuggerPresent 就是在检查 PEB.BeingDebugged 字段
    if (IsDebuggerPresent())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}

NtGlobalFlag
我们之前已经看到PEB的结构,我们在向下翻就可以看到NtGlobalFlag

这个字段的值如果是0x70就说明是正在被调试,正常情况下不是0x70,所以我们通过这个字段可以判断现在是不是正在被调试

bool CheckNtGlobalFlag()
{
    int NtGlobalFlag = 0;

    __asm
    {
        ; 通过 TEB 偏移为 0x30 找到 PEB 结构
        mov eax, dword ptr fs : [0x30]

        ; 通过 PEB 偏移为 0x68 的地方找到 NtGlobalFlag
        mov eax, dword ptr[eax + 0x68]

        ; 将结果保存到变量,目的是方便比较
        mov NtGlobalFlag, eax
    }

    return NtGlobalFlag == 0x70 ? true : false;
}

int main()
{
    if (CheckNtGlobalFlag())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}

ProcessHeap
ProcessHeap字段是在PEB结构体中偏移为0x18的位置

在windowsXp中,ForceFlags的字段位于_HEAP结构体0x10的位置

在Win7以后位于0x44的位置

在上面两个标志位中,在没有被调试的情况下,Flags是2,ForceFlags是0,我们使用这两个字段作为是否正在被调试的依据。
实现代码:

// _HEAP 结构不是一个公开的结构体,不同版本的 nt 内核可能
//  对这个结构体有不同的实现,所以兼容性不够强
bool CheckProcessHeap()
{
    int Flags = 0, ForceFlags = 0;

    __asm
    {
        ; 通过 fs : [0x30] 可以找到 PEB 的地址
        mov eax, dword ptr fs:[0x30]

        ; 通过 PEB 偏移为 0x18 的位置找到 ProcessHeap(_HEAP)
        mov eax, dword ptr [eax + 0x18]

        ; 通过 _HEAP 偏移为 0x40 和 0x44 的字段找到两个标志
        mov ecx, dword ptr [eax + 0x40]
        mov Flags, ecx
        mov ecx, dword ptr [eax + 0x44]
        mov ForceFlags, ecx
    }

    printf("%08X %08X\n", Flags, ForceFlags);

    return Flags != 2 || ForceFlags != 0;
}

int main()
{
    if (CheckProcessHeap())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}

NtQueryInformationProcess相关

这个函数是Ntdll.dll中的一个API,用来提取一个给定进程的信息
NtQueryInformationProcess()是一个可以同时在R0和R3运行的函数,它的主要作用是查看进程相关的各种信息。
根据想要查看的信息类别中不同,我们给其第二个参数ProcessInformationClass传入的值也就不同,根据ProcessInformationClass的类别可知,次函数可以查看大概60多种进程相关的信息。
对于所有的使用函数进行反调试的情况,都可以使用ApiHook来进行反调试,但是要注意NtQueryInformationProcess功能非常多,在函数内应该过滤和调试相关的枚举值进行操作,不应该影响到其他的查询信息。
ProcessDebugPort可以获取目标进程的调试端口,如果目标进程未处于调试状态,此端口为0,否则0xFFFFFFFF。
ProcessDebugPort(0x7)如果进程正在被调试,则返回调试端口,否则返回0。

bool CheckProcessDebugPort()
{
    NtQueryInformationProcess(
        GetCurrentProcess(),    //目标进程句柄,伪句柄,每一个进程或线程的伪句柄就是他本身的句柄
        ProcessDebugPort,       //查询信息类型
        &nDebugPort,            //输出查询类型
        sizeof(nDebugPort),     //查询类型大小
        NULL
    );
    return nDebugPort == 0xFFFFFFFF?true:false
}

int main()
{
    if (CheckProcessDebugPort())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;

}

ProcessDebugObjectHandle可以获取目标进程的调试对象句柄,如果未处于调试状态则获取的值为NULL。

// 如果当前的程序被调试了,那么保存的就是非零值
bool CheckProcessDebugObjectHandle()
{
    HANDLE hProcessDebugObjectHandle = 0;

    NtQueryInformationProcess(
        GetCurrentProcess(),               // 目标进程句柄
        (PROCESSINFOCLASS)0x1E,            // 查询信息类型
        &hProcessDebugObjectHandle,        // 输出查询信息
        sizeof(hProcessDebugObjectHandle), // 查询类型大小
        NULL);                             // 实际返回大小

    return hProcessDebugObjectHandle ? true : false;
}


int main()
{
    if (CheckProcessDebugObjectHandle())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}

ProcessDebugFlag可以获取目标进程的调试标记,入股哦初雨调试状态其值为1,否则为0

bool CheckProcessDebugFlag()
{
    BOOL bProcessDebugFlag = 0;

    NtQueryInformationProcess(
        GetCurrentProcess(),       // 目标进程句柄
        (PROCESSINFOCLASS)0x1F,    // 查询信息类型
        &bProcessDebugFlag,        // 输出查询信息
        sizeof(bProcessDebugFlag), // 查询类型大小
        NULL);                     // 实际返回大小

    return bProcessDebugFlag ? false : true;
}


int main()
{
    if (CheckProcessDebugFlag())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}

判断父进程是不是explorer.exe,但是如果进程被调试则这个进程的父进程则是调试器进程。也就是说如果父进程不是explorer.exe则可认为程序正在被调试。

bool CheckParentProcess()
{
    struct PROCESS_BASIC_INFORMATION {
        ULONG ExitStatus;                   // 进程返回码
        PPEB  PebBaseAddress;               // PEB地址
        ULONG AffinityMask;                 // CPU亲和性掩码
        LONG  BasePriority;                 // 基本优先级
        ULONG UniqueProcessId;              // 本进程PID
        ULONG InheritedFromUniqueProcessId; // 父进程PID
    }stcProcInfo;
    
    // 查询到进程相关的基本信息,需要提供一个结构体进行接收
    NtQueryInformationProcess(GetCurrentProcess(), ProcessBasicInformation,
        &stcProcInfo, sizeof(stcProcInfo), NULL);
        
    DWORD ExplorerPID = 0;
    DWORD CurrentPID = stcProcInfo.InheritedFromUniqueProcessId;
    // 以资源管理器的类名查询到资源管理所在的进程PID
    GetWindowThreadProcessId(FindWindow(L"Progman", NULL), &ExplorerPID);

    // 如果相同就说明没有被调试
    return ExplorerPID == CurrentPID ? false : true;
}
    

int main()
{
    if (CheckParentProcess())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}

检查系统是否处于调试状态
我们可以使用NtQuerySystemInformation()函数获取当前系统是否开启调试模式

bool CheckSystemKernelDebuggerInformation() 
{
    // 保存了和系统调试相关的属性
    struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION 
    {
        BOOLEAN KernelDebuggerEnabled;
        BOOLEAN KernelDebuggerNotPresent;
    } DebuggerInfo = { 0 };

    // 查询当前系统的调试情况
    NtQuerySystemInformation(
        (SYSTEM_INFORMATION_CLASS)0x23,       // 查询信息类型
        &DebuggerInfo,                        // 输出查询信息
        sizeof(DebuggerInfo),                 // 查询类型大小
        NULL);                                // 实际返回大小
    return DebuggerInfo.KernelDebuggerEnabled;
}

int main()
{
    if (CheckSystemKernelDebuggerInformation())
        printf("当前处于[被]调试状态\n");
    else
        printf("当前处于[非]调试状态\n");

    system("pause");
    return 0;
}

ThreadHideFromDebugger

ThreadHideFromDebugger,对调试其隐藏当前的线程,原理上就是让DbgkSendApiMessage函数不详建立了调试关系的调试器发送调试信息。

typedef enum THREAD_INFO_CLASS {
    ThreadHideFromDebugger = 17
};
// 一个未公开的函数,需要手动的使用 GetProcAddress 获取
typedef NTSTATUS(NTAPI* ZW_SET_INFORMATION_THREAD)(
    IN  HANDLE ThreadHandle,
    IN  THREAD_INFO_CLASS ThreadInformaitonClass,
    IN  PVOID ThreadInformation,
    IN  ULONG ThreadInformationLength);
    
void ZSIT_DetachDebug()
{
    ZW_SET_INFORMATION_THREAD Func = (ZW_SET_INFORMATION_THREAD)
        // 加载了 ntdll 模块,因为函数保存在这个模块中
        GetProcAddress(LoadLibrary(L"ntdll.dll"), 
        // 函数的名称
        "ZwSetInformationThread");

    // 对调试器隐藏当前的线程,原理上就是让 DbgkpSendApiMessage 函数
    //  不向建立了调试关系的调试器发送调试信息。
    Func(GetCurrentThread(), ThreadHideFromDebugger, NULL, NULL);
}

int main()
{
    ZSIT_DetachDebug();
    printf("runnning...\n");
    system("pause");
    return 0;
}

通过检测窗口来判断是存在调试器

#include <iostream>
#include <windows.h>


int main()
{
    if (FindWindow(L"OllyDbg", NULL))
        printf("存在调试器\n");
    else
        printf("没检测到调试器\n");

    return 0;
}
最后修改:2020 年 08 月 26 日
如果觉得我的文章对你有用,请随意赞赏