梦幻的彼岸 发表于 2021-7-20 15:26:41

[翻译]反调试:杂项


备注
原文地址:https://anti-debug.checkpoint.com/techniques/misc.html
原文标题:Anti-Debug: Misc
更新日期:2021年7月20日
此文后期:根据自身所学进行内容扩充
因自身技术有限,只能尽自身所能翻译国外技术文章,供大家学习,若有不当或可完善的地方,希望可以指出,用于共同完善这篇文章。

https://bbs.huorong.cn/static/image/hrline/1.gif


目录

[*]杂项
[*]1. FindWindow()
[*]2. 父进程检查
[*]2.1. NtQueryInformationProcess()
[*]2.2. CreateToolhelp32Snapshot()
[*]3. 选择器
[*]4. DbgPrint()
[*]5. DbgSetDebugFilterState()
[*]6. NtYieldExecution() / SwitchToThread()
[*]反制措施

杂项
1. FindWindow()
这种技术包括系统中窗口类的简单枚举,并将它们与调试器的已知窗口类进行比较。
可以使用以下函数:

[*]user32!FindWindowW()
[*]user32!FindWindowA()
[*]user32!FindWindowExW()
[*]user32!FindWindowExA()

C/C++ 代码:
const std::vector<std::string> vWindowClasses = {
    "OLLYDBG",
    "WinDbgFrameClass", // WinDbg
    "ID",               // Immunity Debugger
    "Zeta Debugger",
    "Rock Debugger",
    "ObsidianGUI",
};

bool IsDebugged()
{
    for (auto &sWndClass : vWindowClasses)
    {
      if (NULL != FindWindowA(sWndClass.c_str(), NULL))
            return true;
    }
    return false;
}

2. 父进程检查
通常情况下,一个用户模式的进程是通过双击一个文件图标来执行的。如果该进程以这种方式执行,其父进程将是shell进程("explorer.exe")。

下面两种方法的主要思路是比较父进程的PID和 "explorer.exe "的PID。

2.1. NtQueryInformationProcess()
这个方法包括使用user32!GetShellWindow()获得shell进程窗口句柄,通过调用user32!GetWindowThreadProcessId()获得其进程ID。

然后,通过调用ntdll!NtQueryInformationProcess()与ProcessBasicInformation类,可以从PROCESS_BASIC_INFORMATION结构中获得父进程ID。
C/C++ 代码:
bool IsDebugged()
{
    HWND hExplorerWnd = GetShellWindow();
    if (!hExplorerWnd)
      return false;

    DWORD dwExplorerProcessId;
    GetWindowThreadProcessId(hExplorerWnd, &dwExplorerProcessId);

    ntdll::PROCESS_BASIC_INFORMATION ProcessInfo;
    NTSTATUS status = ntdll::NtQueryInformationProcess(
      GetCurrentProcess(),
      ntdll::PROCESS_INFORMATION_CLASS::ProcessBasicInformation,
      &ProcessInfo,
      sizeof(ProcessInfo),
      NULL);
    if (!NT_SUCCESS(status))
      return false;

    return (DWORD)ProcessInfo.InheritedFromUniqueProcessId != dwExplorerProcessId;
}

2.2. CreateToolhelp32Snapshot()
父进程ID和父进程名称可以通过kernel32!CreateToolhelp32Snapshot()和kernel32!Process32Next()函数获得。
C/C++ 代码:
DWORD GetParentProcessId(DWORD dwCurrentProcessId)
{
    DWORD dwParentProcessId = -1;
    PROCESSENTRY32W ProcessEntry = { 0 };
    ProcessEntry.dwSize = sizeof(PROCESSENTRY32W);

    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if(Process32FirstW(hSnapshot, &ProcessEntry))
    {
      do
      {
            if (ProcessEntry.th32ProcessID == dwCurrentProcessId)
            {
                dwParentProcessId = ProcessEntry.th32ParentProcessID;
                break;
            }
      } while(Process32NextW(hSnapshot, &ProcessEntry));
    }

    CloseHandle(hSnapshot);
    return dwParentProcessId;
}

bool IsDebugged()
{
    bool bDebugged = false;
    DWORD dwParentProcessId = GetParentProcessId(GetCurrentProcessId());

    PROCESSENTRY32 ProcessEntry = { 0 };
    ProcessEntry.dwSize = sizeof(PROCESSENTRY32W);

    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if(Process32First(hSnapshot, &ProcessEntry))
    {
      do
      {
            if ((ProcessEntry.th32ProcessID == dwParentProcessId) &&
                (strcmp(ProcessEntry.szExeFile, "explorer.exe")))
            {
                bDebugged = true;
                break;
            }
      } while(Process32Next(hSnapshot, &ProcessEntry));
    }

    CloseHandle(hSnapshot);
    return bDebugged;
}


3. 选择器
选择器的值可能看起来很稳定,但实际上在某些情况下是不稳定的,而且还取决于Windows的版本。例如,一个选择器的值可以在一个线程中被设置,但它可能不会保持这个值很久。某些事件可能会导致选择器的值被改回其默认值。一个这样的事件是一个异常。在调试器的上下文中,单步异常仍然是一个异常,它可能导致一些意外的行为。
x86汇编:
    xoreax, eax
    push fs
    popds
l1: xchg , cl
    xchg , cl
在64位版本的Windows中,单步执行此代码将导致l1处的访问冲突异常,因为即使在到达l1之前,DS选择器也将恢复到默认值。在32位版本的Windows上,DS选择器不会恢复其值,除非发生非调试异常。如果使用SS选择器,行为中的特定版本差异会进一步扩大。在64位版本的Windows上,SS选择器将恢复到默认值,就像DS选择器一样。然而,在32位版本的Windows上,即使发生异常,SS选择器值也不会被恢复。

x86-64汇编:
    xoreax, eax
    push offset l2
    push d fs:
    movfs:, esp
    push fs
    popss
    xchg , cl
    xchg , cl
l1: int3 ;force exception to occur
l2: ;looks like it would be reached
    ;if an exception occurs
    ...
那么当 "int 3 "指令在l1处到达并发生断点异常时,l2处的异常处理程序就不会像预期的那样被调用。相反,进程被简单地终止了。

这种技术的一个变种是通过简单地检查赋值是否成功来检测单步事件。
push 3
popgs
movax, gs
cmpal, 3
jnebeing_debugged
FS和GS选择器是特殊情况。对于某些值,它们会受到单步事件的影响,即使是在32位版本的Windows上。但是,对于FS选择器(从技术上讲,是GS选择器),如果它被设置为从0到3的值,那么在32位Windows版本上它将不能恢复到它的默认值。相反,它将被设置为零(GS选择器以同样的方式受到影响,但GS选择器的默认值为零)。在64位版本的Windows上,它(它们)将恢复到它(它们)的默认值。

此代码也容易受到线程切换事件引起的竞态条件的影响。当线程切换事件发生时,它的行为就像一个异常,并将导致选择器值被更改,对于FS选择器来说,这意味着它将被设置为零。
种技术的一种变体可以通过有意地等待线程切换事件的发生来解决这个问题。
    push 3
    popgs
l1: movax, gs
    cmpal, 3
    je   l1
但是,这段代码容易受到它首先试图检测到的问题的影响,因为它没有检查原始的赋值是否成功。当然,可以将这两个代码片段组合起来以产生所需的效果,方法是等待线程切换事件发生,然后在应该存在的时间窗口内执行赋值,直到下一个事件发生。
C/C++ 代码:
bool IsTraced()
{
    __asm
    {
      push 3
      popgs

    __asm SeclectorsLbl:
      movax, gs
      cmpal, 3
      je   SeclectorsLbl

      push 3
      popgs
      movax, gs
      cmpal, 3
      jneSelectors_Debugged
    }

    return false;

Selectors_Debugged:
    return true;
}

4. DbgPrint()
调试函数如ntdll!DbgPrint()、kernel32!OutputDebugStringW()导致DBG_PRINTEXCEPTION_C (0x40010006)异常。如果程序使用附加的调试器执行,则调试器将处理此异常。但是如果没有调试器,并且注册了异常处理程序,异常处理程序将捕获该异常。
C/C++ 代码:
bool IsDebugged()
{
    __try
    {
      RaiseException(DBG_PRINTEXCEPTION_C, 0, 0, 0);
    }
    __except(GetExceptionCode() == DBG_PRINTEXCEPTION_C)
    {
      return false;
    }

    return true;
}

5. DbgSetDebugFilterState()
函数ntdll!DbgSetDebugFilterState()和ntdll!NtSetDebugFilterState()只设置一个标志寄存器,如果内核模式的调试器存在,将被检查。因此,如果一个内核调试器被连接到系统上,这些函数将成功。然而,这些函数也可能因为一些用户模式的调试器引起的副作用而成功。这些功能需要管理员的权限。
C/C++ 代码:
bool IsDebugged()
{
    return NT_SUCCESS(ntdll::NtSetDebugFilterState(0, 0, TRUE));
}

6. NtYieldExecution() / SwitchToThread()
这种方法其实并不可靠,因为它只显示当前进程中是否有一个高优先级的线程。然而,它可以作为一种反跟踪技术。

当一个应用程序在调试器中被追踪,并且执行了一个单步,上下文不能被切换到其他线程。这意味着ntdll!NtYieldExecution()返回STATUS_NO_YIELD_PERFORMED(0x40000024),这导致kernel32! SwitchToThread()返回0。

使用这种技术的策略是,如果kernel32!SwitchToThread()返回0,或者ntdll!NtYieldExecution()返回STATUS_NO_YIELD_PERFORMED,则有一个循环修改一些计数器。这可能是一个解密字符串的循环,或者其他一些应该在调试器中手动分析的循环。如果计数器在离开循环后有预期值(预期即如果所有kernel32!SwitchToThread()返回0的值),我们认为调试器是存在的。

在下面的例子中,我们定义了一个一字节的计数器(初始化为0),如果kernel32!SwitchToThread返回0,它将向左移动一位。如果它移动了8次,那么计数器的值将变成0,调试器被认为是存在的。
C/C++ 代码:
bool IsDebugged()
{
    BYTE ucCounter = 1;
    for (int i = 0; i < 8; i++)
    {
      Sleep(0x0F);
      ucCounter <<= (1 - SwitchToThread());
    }

    return ucCounter == 0;
}


反制措施
调试期间:用NOP来填补反调试pr反跟踪的检查。

对于反调试绕过工具的开发:

[*]对于FindWindow():拦截user32!NtUserFindWowEx()函数。在拦截中,调用原始的user32!NtUserFindWowEx()函数。如果它是从被调试的进程中调用的,并且父进程看起来很可疑,那么从拦截中返回unsuccessfully(不成功)。
[*]用于父进程检查:拦截 ntdll! NtQuerySystemInformation()函数。如果SystemInformationClass是以下值之一:
SystemProcessInformation
SystemSessionProcessInformation
SystemExtendedProcessInformation
和进程名称看起来很可疑,那么拦截必须修改进程名称。
[*]对于选择器:没有反制措施
[*]对于DbgPrint:你必须为特定的调试器实现一个插件,并改变DBG_PRINTEXCEPTION_C异常到来后触发的事件处理器的行为
[*]对于DbgSetDebugFilterState():拦截ntdll!NtSetDebugFilterState()函数。如果进程是以调试权限运行的,则从拦截中返回unsuccessfully(不成功)。
[*]对于SwitchToThread:拦截ntdll!NtYieldExecution()函数并从拦截中返回一个unsuccessfully(不成功)的状态。

飘零未忍 发表于 2021-8-30 11:18:10

感谢翻译 mark



wer1030 发表于 2021-12-1 10:41:48

学习了,感谢大神
页: [1]
查看完整版本: [翻译]反调试:杂项