#逆向-反调试篇

🤡5ea1


前段时间一直在研究如何防止静态分析,但是精心策划的花指令和smc总是被做题的人简简单单两下调试就出来了,因为给题目配的反调试总是随便抄两个api下来,导致反调试的绕过变得极其容易。为了守护我的花指令,我决心学习一下反调试技术。

Debuger1:API反调试

最简单朴素的反调试,功能实现全靠windows自带能力

Debuger1-1

调用IsDebuggerPresent()函数

如果当前进程在调试器的上下文中运行,则返回值为非零。
如果当前进程未在调试器的上下文中运行,则返回值为零。

BOOL CheckDebuger()
{
    int Debuger;
    Debuger=IsDebuggerPresent();
    return Debuger;
}

最简单检测是否调试的函数,返回值直接可以用来判断是否被调试

Debuger1-2

CheckRemoteDebuggerPresent函数,使用方法稍微复杂一些,但还是较为直接简单

该函数需要两个参数,第一个参数为检测进程的句柄,第二个参数为存放返回值的位置,此处选择使用GetCurrentProcess()函数来获取当前进程的句柄

BOOL CheckDebuger()
{
    int Debuger;
    CheckRemoteDebuggerPresent(GetCurrentProcess(), &Debuger);
    return Debuger;
}

运行后,返回值会存放在Debuger变量中,若为0,则没有被调试。

Debuger1-3

此次将用到NtQueryInformationProcess函数,这是Ntdll.dll中的一个API,用来提取一个给定进程的信息

它的第一个参数是进程句柄,第二个参数告诉我们它需要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就会设置到第三个参数。
它的第二个参数是一个枚举参数,即可根据该参数的不同,更改查找的数据,并对输出数据格式发生变化,所以要依据参数的不同来选择需要的输出数据类型,该枚举数据的部分类别如下图所示
1
具体调用代码如下

typedef NTSTATUS(WINAPI* NtQueryInformationProcessPtr)(
    HANDLE ProcessHandle,
    PROCESSINFOCLASS ProcessInformationClass,
    PVOID ProcessInformation,
    ULONG ProcessInformationLength,
    PULONG ReturnLength
    );

BOOL CheckDebug_ProcessDebugPort()
{
    DWORD_PTR debugPort = 0;
    HMODULE hModule = LoadLibrary(L"Ntdll.dll");
    if (hModule == NULL) {
        // 获取错误代码并处理加载失败情况,比如返回FALSE等
        DWORD errorCode = GetLastError();
        printf("%d\n", errorCode);
        return FALSE;
    }
    NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
    NTSTATUS status = NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort, &debugPort, sizeof(debugPort), NULL);

    if (!NT_SUCCESS(status)) {
        // 根据不同的错误状态码(status的值)进行相应的错误处理,比如输出错误信息、记录日志等
        printf("NtQueryInformationProcess failed with status code: 0x%X\n", status);
        return FALSE;
    }
    return debugPort != 0;
}

调用此函数应当先对NTSTATUS进行定义,以免出现未定义的标识符错误。

Debuger1-4

GetLastError()函数的作用是获取最后一次的错误信息,所以我们可以通过故意犯错来构造异常信息。CloseHandle()函数的作用是关闭给定句柄对应的进程,所以我们只需要给定一个不存在的句柄,就能稳定触发异常;
CloseWindow也是同样的原理

BOOL CheckDebug()
{
    DWORD ret = CloseHandle((HANDLE)0x1234);
    if (ret != 0 || GetLastError() != ERROR_INVALID_HANDLE)
    {
        return TRUE;
    }
    else
    {
        return FALSE;
    }
}

BOOL CheckDebug()
{
    DWORD ret = CloseWindow((HWND)0x1234);
    if (ret != 0 || GetLastError() != ERROR_INVALID_WINDOW_HANDLE)
    {
        return TRUE;
    }
    else
    {
        return FALSE;
    }
}

当正常运行时,ret处的语句产生的异常信息会被下方的if语句检测到,然后返回False,而在调试时,异常信息会直接终止调试,达到一个反调试的效果。

Debuger2:调试器与栈结构

虽然使用Windows API是探测调试器存在的最简单办法,但手动检查数据结构是恶意代码编写者最常使用的办法。这是因为很多时候通过Windows API实现的反调试技术无效,例如这些API函数被rootkit挂钩,并返回错误信息。因此,恶意代码编写者经常手动执行与这些API功能相同的操作。在手动检测中,PEB结构中的一些标志暴露了调试器存在的信息。

Debuger2-1

在Windows系统中,当进程运行时,位置fs:[30h]将指向PEB的基地址,而PEB块将包含与这个进程相关的所有用户态参数。这些参数也包含了调试器的状态。
为了实现反调试技术,恶意代码通过fs:[30h]这个位置检查BeingDebugged标志,这个标志标识进程是否正在被调试。

BOOL CheckDebug()  
{  
    int result = 0;  
    __asm  
    {  
        mov eax, fs:[30h]  
        mov al, BYTE PTR [eax + 2]   
        mov result, al  
    }  
    return result != 0;  
}

上面这段代码就能借助内联汇编读取PEB中关于调试器的标志位,从而得知进程是否正在被调试

3

可以从这张图中看到,BeingDebugged的标志位处于第三个字节处,故在调用时使用PTR [eax + 2] 地址

Debuger3:注册表检测

下面是调试器在注册表中的一个常用位置。
SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug(32位系统)
SOFTWARE\Wow6432Node\Microsoft\WindowsNT\CurrentVersion\AeDebug(64位系统)
该注册表项指定当应用程序发生错误时,触发哪一个调试器。默认情况下,它被设置为Dr.Watson。如果该这册表的键值被修改为OllyDbg,则恶意代码就可能确定它正在被调试。

BOOL CheckDebug()
{
    BOOL is_64;
    IsWow64Process(GetCurrentProcess(), &is_64);
    HKEY hkey = NULL;
    char key[] = "Debugger";
    char reg_dir_32bit[] = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug";
    char reg_dir_64bit[] = "SOFTWARE\\Wow6432Node\\Microsoft\\WindowsNT\\CurrentVersion\\AeDebug";
    DWORD ret = 0;
    if (is_64)
    {
        ret = RegCreateKeyA(HKEY_LOCAL_MACHINE, reg_dir_64bit, &hkey);
    }
    else
    {
        ret = RegCreateKeyA(HKEY_LOCAL_MACHINE, reg_dir_32bit, &hkey);
    }
    if (ret != ERROR_SUCCESS)
    {
        return FALSE;
    }
    char tmp[256];
    DWORD len = 256;
    DWORD type;
    ret = RegQueryValueExA(hkey, key, NULL, &type, (LPBYTE)tmp, &len);
    std::cout << tmp<<'\n';
    if (strstr(tmp, "OllyIce") != NULL || strstr(tmp, "OllyDBG") != NULL || strstr(tmp, "WinDbg") != NULL || strstr(tmp, "x64dbg") != NULL || strstr(tmp, "Immunity") != NULL || strstr(tmp, "Immunity") != NULL)
    {
        return TRUE;
    }
    else
    {
        return FALSE;
    }
}

不过这个代码在我本机上跑的时候始终检测的是vs2022的调试器,可能在环境上有一定的要求