Windows反调试技术(下)
阅读原文时间:2021年10月26日阅读:1

OD的DBGHELP模块

检测DBGHELP模块,此模块是用来加载调试符号的,所以一般加载此模块的进程的进程就是调试器。绕过方法也很简单,将DBGHELP.DLL改名。

#include <Windows.h>
#include <TlHelp32.h>
int main(int argc, char * argv[])
{
    HANDLE hSnapProcess;
    HANDLE hSnapModule;
    PROCESSENTRY32  pe32;
    pe32.dwSize = sizeof(PROCESSENTRY32);

    MODULEENTRY32   md32;
    md32.dwSize = sizeof(MODULEENTRY32);
    hSnapProcess = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    if(hSnapProcess != INVALID_HANDLE_VALUE)
    {
        Process32First(hSnapProcess, &pe32);
        do{
            hSnapModule = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pe32.th32ProcessID);
            Module32First(hSnapModule, &md32);
            do{
                if(lstrcmp(md32.szModule, "DBGHELP.DLL") == 0)
                {
                    MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
                    ExitProcess(NULL);
                }
            }while(Module32Next(hSnapModule, &md32));
        }while(Process32Next(hSnapProcess, &pe32));

        MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
    }
    else
        CloseHandle(hSnapProcess);
    return 0;
}

查看窗口

通过GetWindowText( )获取窗口标题文本,绕过方法也很简单就是更改窗口标题名。我们下面是检测OD调试器的示例,类比可以用来检测其他调试器如X64dbg等。

#include <Windows.h>
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam );
int main(int argc, char* argv[])
{
    EnumWindows(EnumWindowsProc, NULL);
    MessageBox(NULL,TEXT("程序正常运行!"), NULL, MB_OK);
}

BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam )
{
    char szWindowText[256] = {0};
    GetWindowText(hwnd, szWindowText, 256);                 //获取的是标题栏的文本

    if(lstrcmp(szWindowText, "OllyDbg") == 0)
    {
        MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
        ExitProcess(NULL);
        return FALSE;
    }
    return TRUE;
}

也可以通过FindWindow来查找窗口。

int main(int argc, char* argv[])
{
    if(NULL != FindWindow(TEXT("OLLYDBG"),TEXT("OllyDbg")))
    {
        MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
        ExitProcess(NULL);
    }
    MessageBox(NULL,TEXT("程序正常运行!"), NULL, MB_OK);
    return 0;

}

创建进程快照来检测是否存在调试器进程

这种方法和查看窗口类似,当然也很容易被绕过。直接将程序名称更改就可以轻松绕过检测。

判断进程是否有SeDebugPrivilege权限

对于一般进程而言,如果用OpenProcess()打开csrss.exe程序则会返回无权限访问。如果以管理员身份登录并且进程被调试器调试的话,调试器会赋予进程SeDebugPrivilege权限,有了此权限程序就可以打开csrss.exe程序了。当然如果采用非管理员身份登录则这种检测将失效,因为非管理员身份下不会赋予进程SeDebugPrivilege权限。

typedef DWORD (NTAPI *pfnCsrGetProcessId)();

int main(int argc, char* argv[])
{
    pfnCsrGetProcessId CsrGetProcessId;
    CsrGetProcessId = (pfnCsrGetProcessId)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("CsrGetProcessId"));
    DWORD a = CsrGetProcessId();
    if(NULL != OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, CsrGetProcessId()))
    {
        MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
        ExitProcess(NULL);
    }
    MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
    return 0;
}

利用OD漏洞攻击调试器

OutputDebugString()漏洞,程序调用OutputDebugString()会产生一个特殊标志的软件异常,如果程序正在被调试那么调试器线程的WaitForDebugEvent函数会将此异常捕获并转化为OUTPUT_DUBGU_STRING_EVENT调试事件,OD在捕获此调试事件后会接着调用Sprintf()将OutPutDebugString中的字符串打印出来。而Springf函数并不会对参数进行检查,如果OutputDebugStringA(TEXT("%s%s%s")),OD中是Springf(目标缓冲区,"调试字符串"),因为现在调试字符串是“%s%s%s”,那么Springf(目标缓冲区,“%s%s%s”,X1,X2,X3),而此X1,X2,X3就会随机从栈中取出数据作为字符换的首地址,所以很容易取到的数据是一个无效指针会产生缓冲区异常。但是目前多数版本的OD已经将此漏洞修复。

#include <Windows.h>
int main(int argc, char* argv[])
{
    MessageBox(NULL, TEXT("程序开始运行!"), NULL, MB_OK);
    OutputDebugStringA(TEXT("%s%s%s"));                      //
    MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
}

判断父进程

通过判断当前进程父进程的PID是否等于explorer.exe或cmd.exe或services.exe的PID来判断其是否是调试器创建的进程。

#include <TlHelp32.h>
int main(int argc, char* argv[])
{
    DWORD   dwPid;
    DWORD   dwParentPid;
    DWORD   dwPidExplorer = 0;
    DWORD   dwPidCmd      = 0;
    DWORD   dwPidServices = 0;
    HANDLE  hSnapProcess;
    DWORD   dwFlag = 0;

    dwPid           = GetCurrentProcessId();
    hSnapProcess    = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32  pe32;
    pe32.dwSize = sizeof(PROCESSENTRY32);

    if(hSnapProcess != INVALID_HANDLE_VALUE)
    {
        Process32First(hSnapProcess, &pe32);
        do{
            if(pe32.th32ProcessID == dwPid)
                dwParentPid = pe32.th32ParentProcessID;
            if(lstrcmp(pe32.szExeFile, "explorer.exe") == 0)
                dwPidExplorer = pe32.th32ProcessID;
            if(lstrcmp(pe32.szExeFile, "cmd.exe") == 0)
                dwPidCmd = pe32.th32ProcessID;
            if(lstrcmp(pe32.szExeFile, "services.exe") == 0)
                dwPidServices = pe32.th32ProcessID;
        }while(Process32Next(hSnapProcess, &pe32));

        if(dwParentPid == dwPidExplorer)
            dwFlag = 1;
        else if(dwParentPid == dwPidCmd)
            dwFlag = 1;
        else if(dwParentPid == dwPidServices)
            dwFlag = 1;

    }
    else
    {
        CloseHandle(hSnapProcess);
        return 0;
    }

    if(dwFlag == 1)
        MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
    else if(dwFlag == 0)
        MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
    return 0;
}

实时调试器检测

当程序崩溃时其一般会弹出弹框来询问是否用事先设置的JIT及时调试器附加程序。一旦设置了JIT调试器就会在注册表的对应位置留下对应调试器的路径。我们可以通过检测注册表对应位置的键值来达到反调试的目的

int main(int argc, char* argv[])
{
    HKEY hKey;                                                      //注册表键的句柄
    BOOL bExe64  = FALSE;
    IsWow64Process(GetCurrentProcess(), &bExe64);                                       //判断系统是32位还是64位,32位和64位其对应需要检测注册表的位置不同
    TCHAR * szRegedit = bExe64 ?
        TEXT("SOFTWARE\\WOW6432Node\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug")
        : TEXT("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug");

    TCHAR  szValue[256];
    DWORD  dwLen = 256;
    //打开注册表键
    RegCreateKey(HKEY_LOCAL_MACHINE, szRegedit, &hKey);
    //查询对应项的值
    RegQueryValueEx(hKey, TEXT("Debugger"), NULL, NULL, (LPBYTE)szValue, &dwLen);
    //关闭注册表键
    RegCloseKey(hKey);

    if(_tcsstr(szValue, TEXT("吾爱破解.exe")))                                      //检测是否为调试器名称
    {
        MessageBox(NULL, TEXT("已检测到调试器!"),NULL, MB_OK);
        ExitProcess(NULL);
    }

    MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
    return 0;
}

时间差

一般对于直接运行的程序而言,连续的几条指令执行所需的时间是很少的,因此指令与指令之间的时间差是很小的。而对于调试中的程序而言,就算我们按着F8不放让程序执行,其两条指令执行后也是会有时间差的。RDTSC指令可以计算出CPU自启动以后的运行周期,那么我们就可以用两条RDTSC指令计算出这两条指令执行所用的时间差。RDTSC指令执行后会将CPU运行周期的高32位放到edx,低32位放到eax中。

int main(int argc, char* argv[])
{
    if(_AntiDebug() != 0)
        MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
    else
        MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
    return 0;
}

DWORD _AntiDebug()
{
    _asm{
        rdtsc
        mov ecx,eax
        mov ebx,edx
        //一些运算
        rdtsc
        cmp edx,ebx
        jne s
        sub eax,ecx
        cmp eax,0x200
        ja  s
        xor eax,eax
        jmp s1
s:
        mov eax,1
s1:
    }
}

TF位检测

因为一般调试器都会在TF为1时处理单步异常让eip指向下一条指令。我们可以利用此特点,主动将TF为置1让调试器误认为是单步运行从而eip指向下一条指令。我们可以在正常的程序流程中设置异常处理程序,在异常处理程序中我们做一些处理,这样如果被调试器就会忽略异常处理程序从而不能够执行正确的程序流程。

int main(int argc, char* argv[])
{
    BOOL isDebugged = TRUE;
    __try
    {
        __asm
        {
            pushfd
            or dword ptr[esp], 0x100
            popfd
            nop
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        isDebugged = FALSE;
    }
    if (isDebugged)
    {
        MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
        ExitProcess(NULL);
    }
}

调试模式检测

在进行双机内核调试时,虚拟机会处于调试状态。我们通过检测虚拟机的状态来判断是否正在进行内核调试

typedef NTSTATUS (NTAPI *pfnNtQuerySystemInformation)(
    IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
    OUT PVOID SystemInformation,
    IN ULONG SystemInformationLength,
    OUT PULONG ReturnLength OPTIONAL
);

struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION
{
    BOOLEAN DebuggerEnabled;
    BOOLEAN DebuggerNotPresent;
}DebuggerInfo = {0};

int main(int argc,char* argv[])
{
    pfnNtQuerySystemInformation NtQuerySystemInformation;
    NtQuerySystemInformation = (pfnNtQuerySystemInformation)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtQuerySystemInformation"));

    NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)0x23, &DebuggerInfo, sizeof(DebuggerInfo), NULL);
    if(DebuggerInfo.DebuggerEnabled != 0)
    {
        MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
        return 0;
    }
    MessageBox(NULL, TEXT("正常运行!"), NULL, MB_OK);
    return 0;
}

TLS线程本地存储

利用TLS回调函数可以在到达main()前被调用非常的隐蔽,我们可以利用这一点在TLS回调函数中进行反调试的操作。

void NTAPI Tls_Call(PVOID DllHandle, DWORD Reason, PVOID Reserved);      //声明TLS回调函数

#pragma comment(linker, "/INCLUDE:__tls_used")                  //告知连接器使用TLS

#pragma data_seg(".CRT$XLS")                          //在共享数据段中存储TLS回调函数的地址
PIMAGE_TLS_CALLBACK pTlsAddress = Tls_Call;
#pragma data_seg()

int main(int argc, char* argv[])
{
    MessageBox(NULL, TEXT("Main()"), NULL, MB_OK);
    return 0;
}
void NTAPI Tls_Call(PVOID DllHandle, DWORD dwReason, PVOID Reserved)
{
    switch (dwReason)
    {
    case DLL_THREAD_ATTACH:                       //Reason会有4种参数
        break;
    case DLL_PROCESS_ATTACH:                          //主线程在调用Main函数前调用TLS回调函数的原因就是DLL_PROCESS_ATTACH
                                                          //可以在此处进行反调试的操作(较隐蔽)
        if(IsDebuggerPresent())
        {
            MessageBox(NULL, TEXT("已检测到调试器!"),NULL, MB_OK);
            ExitProcess(NULL);
        }
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        break;
    }
}

IMAGE_LOAD_CONFIG_DIRECTORY的GlobalFlagsClear

通过检查磁盘或内存中的可执行文件中PIMAGE_LOAD_CONFIG_DIRECTORY结构(程序加载到内存的一些其他配置信息)的GlobalFlagsClear字段。

默认是文件中是没有次结构,可以手动添加。此结构不为0则表示存在调试器。

有问题:无法获得__load_config_used结构的值。

extern "C"
    IMAGE_LOAD_CONFIG_DIRECTORY _load_config_used = {sizeof(IMAGE_LOAD_CONFIG_DIRECTORY)};

软件断点

一般调试器会利用0xCC也就时INT3指令实现软件断点功能,我们可以通过对特定的代码片段进行检验检测是否有指令被下断点,从而达到反调试的目的。

//可以让链接器生成的代码函数调用采用CALL [ ]的形式,否则器默认采用call,jmp dword的形式
#pragma comment(linker, "/INCREMENTAL:NO")
DWORD OldCrc = 0x2159;

#pragma auto_inline(off)                      //防止编译器嵌入函数(关)
void DebugFunc()
{
    DWORD dwNum = 0;
    dwNum++;
    dwNum >> 3;
    dwNum = dwNum - 3;
}
void DebugFuncEnd()
{
}
#pragma auto_inline(on)                          //防止编译器嵌入函数(开)

int main(int argc, char* argv[])
{
    DWORD dwCrc = 0;
    for(DWORD i = (DWORD)DebugFunc; i <= (DWORD)DebugFuncEnd; i++)
        dwCrc = *(BYTE*)i + dwCrc;

    if(dwCrc != OldCrc)
    {
        MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
        ExitProcess(NULL);
    }

    MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
    return 0;
}

硬件断点

通过检测调试寄存器的值来检测是否有硬件断点,达到反调试的目的。

int main(int argc, char* argv[])
{
    CONTEXT stContext;

    stContext.ContextFlags = CONTEXT_ALL;
    GetThreadContext(GetCurrentThread(),&stContext);
    if(stContext.Dr0 | stContext.Dr1 | stContext.Dr2 | stContext.Dr3)
    {
        MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
        ExitProcess(NULL);
    }
    MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
    return 0;
}

SEH和VEH

程序主动产生异常,然后利用SEH或VEH设置异常处理程序。然后在异常处理程序中进行反调试。

SetUnhandledExceptionFilter()

利用SEH的顶级异常处理程序过滤函数UnhandledExceptionFilter()会检测调试器是否存在,如果不存在就执行SetUnhandledExceptionFilter()设置的顶级异常处理过滤干扰函数。如果存在就直接掠过SetUnhandledExceptionFilter()设置的顶级异常处理过滤干扰函数。那么我们就可以SetUnhandledExceptionFilter()设置的顶级异常处理过滤干扰函数,主动产生异常然后将程序一部分流程放到此函数中。如果被调试的话此函数中正常的程序流程将不会执行。

INT 0x2D

OD并不会把int 0x2d认定为是一个异常,也就是其并不能够被传递给异常处理程序(忽略异常也没用),如果在OD中使用F8/F9,程序会直接运行知道遇到断点后暂停。

int main(int argc, char* argv[])
{
    BOOL bReturn;
    bReturn = AntiDebug();

    if(bReturn == 1)
    {
        MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
        exit(0);
    }
    else if(bReturn == 0)
        MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);

    return 0;
}

BOOL AntiDebug()
{
    BOOL bReturn = 0;
    __asm
    {
        push offset handler
        push dword ptr fs:[0]
        mov  dword ptr fs:[0],esp

        int 0x2D
        nop
        mov bReturn,1;
        jmp end

handler:
        mov eax, dword ptr ss : [esp + 0xc]
        mov dword ptr ds : [eax + 0xb8], offset end                 //修改CONTEXT的eip

        xor eax,eax
        retn

end:
        pop dword ptr fs:[0]
        add esp,4

    }
    return bReturn;
}

句柄追踪机制

windows提供内核对象句柄跟踪机制,如果程序被调试则用CloseHandle关闭无效句柄时会产生异常。如果不是从调试器中启动进程,则该CloseHandle返回FALSE

EXCEPTION_DISPOSITION _ExceptionProc(
    PEXCEPTION_RECORD ExceptionRecord,
    PVOID             EstablisherFrame,
    PCONTEXT          ContextRecord,
    PVOID             DispatcherContext)
{
    if (EXCEPTION_INVALID_HANDLE == ExceptionRecord->ExceptionCode)
    {
        MessageBox(NULL, TEXT("已检测到调试器!"), NULL, MB_OK);
        ExitProcess(NULL);
    }
    return ExceptionContinueExecution;
}
int main()
{
    __asm
    {
        push _ExceptionProc
        push dword ptr fs : [0]
        mov  dword ptr fs : [0], esp
    }
    CloseHandle((HANDLE)0xBAAD);
    __asm
    {
        mov  eax, [esp]
        mov  dword ptr fs : [0], eax
        add  esp, 8
    }
    return 0;
}

调试输出异常

从win10开始,调试输出异常需要由调试器处理,以下两种异常需要可以检测调试器是否存在。

DBG_PRINTEXCEPTION_C(0x40010006)和DBG_PRINTEXCEPTION_W(0x4001000A)

int main(int argc, char* argv[])
{
    __try
    {
        RaiseException(0x4001000A, 0, 4, (ULONG_PTR *)"SDFSDF");
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        MessageBox(NULL, TEXT("无调试器!"), NULL, MB_OK);
    }
}

双进程自调试自修改

利用在进程中创建进程,或者通过自调试创建两个进程。其中一个为父进程,另一个为子进程。两个进程都是同一个可执行文件只不过执行流程不一样。然后通过在父进程中修复子进程,子进程修改自身的自修改手段达到反调试的目的。

Hook DbgUiRemoteBreakin防附加

为了使运行中进程能够即时中断到调试器中,操作系统提供了一个函数DbgUiRemoteBreakin,其内部通过调用DbgBreakPoint产生一个中断异常从而被调试器捕获,为了实现及时中断我们需要在运行中的进程中创建远程线程,线程回调函数就是DbgUiRemoteBreakin函数。实际在我们附加进程时,调试器就时这么做的。所以我们通过hookDbgUiRemoteBreakin函数可以达到反附加的目的。

我们下面代码通过将DbgUiRemoteBreakin函数的入口点代码改为jmp ExitProcess函数的入口点,这样一旦有调试器附加进程就会调用ExitProcess结束运行。

int main(int argc, char* argv[])
{
    BYTE    bBuffer[0x10] = {0};
    DWORD   dwBreakAddress;                 //DbgUiRemoteBreakin函数的地址
    DWORD   dwOldProtect;
    DWORD   dwNum;

    dwBreakAddress  = (DWORD)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("DbgUiRemoteBreakin"));
    bBuffer[0] = 0xE9;                                  //jmp指令字节码
    *((DWORD *)(bBuffer + 1)) = (DWORD)ExitProcess - dwBreakAddress;            //ExitProcess函数偏移地址

    VirtualProtect((LPVOID)dwBreakAddress, 0x10, PAGE_EXECUTE_READWRITE, &dwOldProtect);
    WriteProcessMemory(GetCurrentProcess(), (LPVOID)dwBreakAddress, bBuffer, 5, &dwNum);
    VirtualProtect((LPVOID)dwBreakAddress, 0x10, dwOldProtect, &dwOldProtect);

    //此死循环是为了检测
    while(1)
    {

    }
    return 0;
}

参考资料: 看雪学院《加密解密》

张银奎《软件调试》

https://www.apriorit.com/dev-blog/367-anti-reverse-engineering-protection-techniques-to-use-before-releasing-software