-
-
[原创][翻译]反调试:直接与调试器交互
-
2021-7-13 17:58 5265
-
备注
原文地址:https://anti-debug.checkpoint.com/techniques/interactive.html
原文标题:Anti-Debug: Direct debugger interaction
更新日期:2021年7月13日
此文后期:根据自身所学进行内容扩充
因自身技术有限,只能尽自身所能翻译国外技术文章,供大家学习,若有不当或可完善的地方,希望可以指出,用于共同完善这篇文章。
目录
直接与调试器交互
1. 自我调试
2. GenerateConsoleCtrlEvent()
3. BlockInput()
4. NtSetInformationThread()
5. EnumWindows() and SuspendThread()
6. SwitchDesktop()
7. OutputDebugString()
反制措施
直接与调试器交互
下面的技术让正在运行的进程管理一个用户界面,或者与它的父进程交互以发现对一个被调试的进程来说是固有的不一致的地方
1. 自我调试
至少有三个函数可以用来作为调试器附加到一个正在运行的进程上:
kernel32!DebugActiveProcess()
ntdll!DbgUiDebugActiveProcess()
ntdll!NtDebugActiveProcess()
由于一次只能将一个调试器附加到一个进程上,附加到进程上的失败可能表明有另一个调试器存在。
在下面的例子中,我们运行我们进程的第二个示例,它试图将调试器附加到它的父级(进程的第一个实例)。如果kenel32!DebugActiveProcess()没有成功完成,我们就设置由第一个示例创建的命名事件。如果该事件被设置,第一个示例就明白有一个调试器存在。
C/C++代码:
#define EVENT_SELFDBG_EVENT_NAME L"SelfDebugging" bool IsDebugged() { WCHAR wszFilePath[MAX_PATH], wszCmdLine[MAX_PATH]; STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi; HANDLE hDbgEvent; hDbgEvent = CreateEventW(NULL, FALSE, FALSE, EVENT_SELFDBG_EVENT_NAME); if (!hDbgEvent) return false; if (!GetModuleFileNameW(NULL, wszFilePath, _countof(wszFilePath))) return false; swprintf_s(wszCmdLine, L"%s %d", wszFilePath, GetCurrentProcessId()); if (CreateProcessW(NULL, wszCmdLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return WAIT_OBJECT_0 == WaitForSingleObject(hDbgEvent, 0); } return false; } bool EnableDebugPrivilege() { bool bResult = false; HANDLE hToken = NULL; DWORD ec = 0; do { if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) break; TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1; if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid)) break; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; if( !AdjustTokenPrivileges( hToken, FALSE, &tp, sizeof(tp), NULL, NULL)) break; bResult = true; } while (0); if (hToken) CloseHandle(hToken); return bResult; } int main(int argc, char **argv) { if (argc < 2) { if (IsDebugged()) ExitProcess(0); } else { DWORD dwParentPid = atoi(argv[1]); HANDLE hEvent = OpenEventW(EVENT_MODIFY_STATE, FALSE, EVENT_SELFDBG_EVENT_NAME); if (hEvent && EnableDebugPrivilege()) { if (FALSE == DebugActiveProcess(dwParentPid)) SetEvent(hEvent); else DebugActiveProcessStop(dwParentPid); } ExitProcess(0); } // ... return 0; }
2. GenerateConsoleCtrlEvent()
当用户按下Ctrl+C或Ctrl+Break并且控制台窗口处于focus位置时,Windows会检查是否有这个事件的处理程序。所有的控制台进程都有一个默认的处理函数,调用kernel32!ExitProcess()函数。然而,我们可以为这些事件注册一个自定义的处理程序,它忽略了Ctrl+C或Ctrl+Break信号。
然而,如果一个控制台进程正在被调试,而CTRL+C信号没有被禁用,系统会产生一个DBG_CONTROL_C异常。通常这个异常会被调试器拦截,但是如果我们注册一个异常处理程序,我们将能够检查DBG_CONTROL_C是否被引发。如果我们在自己的异常处理程序中拦截了DBG_CONTROL_C异常,它可能表明该进程正在被调试。
C/C++代码:
bool g_bDebugged{ false }; std::atomic<bool> g_bCtlCCatched{ false }; static LONG WINAPI CtrlEventExeptionHandler(PEXCEPTION_POINTERS pExceptionInfo) { if (pExceptionInfo->ExceptionRecord->ExceptionCode == DBG_CONTROL_C) { g_bDebugged = true; g_bCtlCCatched.store(true); } return EXCEPTION_CONTINUE_EXECUTION; } static BOOL WINAPI CtrlHandler(DWORD fdwCtrlType) { switch (fdwCtrlType) { case CTRL_C_EVENT: g_bCtlCCatched.store(true); return TRUE; default: return FALSE; } } bool IsDebugged() { PVOID hVeh = nullptr; BOOL bCtrlHadnlerSet = FALSE; __try { hVeh = AddVectoredExceptionHandler(TRUE, CtrlEventExeptionHandler); if (!hVeh) __leave; bCtrlHadnlerSet = SetConsoleCtrlHandler(CtrlHandler, TRUE); if (!bCtrlHadnlerSet) __leave; GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); while (!g_bCtlCCatched.load()) ; } __finally { if (bCtrlHadnlerSet) SetConsoleCtrlHandler(CtrlHandler, FALSE); if (hVeh) RemoveVectoredExceptionHandler(hVeh); } return g_bDebugged; }
3. BlockInput()
函数user32!BlockInput()可以阻止所有的鼠标和键盘事件,这是禁用调试器的一个相当有效的方法。在Windows Vista和更高版本中,这个调用需要管理员权限。
我们还可以检测是否有钩住user32!BlockInput()和其他反调试调用的工具存在。该函数只允许阻断输入一次。第二次调用将返回FALSE。如果该函数无论输入多少都返回TRUE,可能表明存在一些拦截方案。
C/C++代码:
bool IsHooked () { BOOL bFirstResult = FALSE, bSecondResult = FALSE; __try { bFirstResult = BlockInput(TRUE); bSecondResult = BlockInput(TRUE); } __finally { BlockInput(FALSE); } return bFirstResult && bSecondResult; }
4. NtSetInformationThread()
函数ntdll!NtSetInformationThread()可以用来从调试器中隐藏一个线程。借助未记录的值THREAD_INFORMATION_CLASS::ThreadHideFromDebugger (0x11)的帮助下实现。这是由一个外部进程使用的,但任何线程都可以在自己身上使用它。
线程从调试器中隐藏后,它将继续运行,但调试器不会收到与此线程相关的事件。这个线程可以进行反调试检查,如代码校验、调试标志验证等。
然而,如果在隐藏的线程中有一个断点,或者我们把主线程从调试器中隐藏起来,进程就会崩溃,调试器就会被卡住。
在这个例子中,我们从调试器中隐藏了当前线程。这意味着,如果我们在调试器中跟踪这段代码,或者把断点放在这个线程的任何指令上,一旦ntdll!NtSetInformationThread()被调用,调试就会被卡住。
C/C++代码:
#define NtCurrentThread ((HANDLE)-2) bool AntiDebug() { NTSTATUS status = ntdll::NtSetInformationThread( NtCurrentThread, ntdll::THREAD_INFORMATION_CLASS::ThreadHideFromDebugger, NULL, 0); return status >= 0; }
5. EnumWindows() and SuspendThread()
这个技术的想法是暂停父进程的自有线程。
首先,我们需要验证父进程是否是一个调试器。这可以通过列举屏幕上的所有顶级窗口来实现(使用user32!EnumWindows()或user32!EnumThreadWindows()),搜索进程ID为父进程ID的窗口(使用user32!GetWindowThreadProcessId()),并检查此窗口的标题(通过user32!GetWindowTextW())。如果父进程的窗口标题看起来像调试器的标题,我们可以用kernel32!SuspendThread()或ntdll!NtSuspendThread()暂停拥有的线程。
C/C++代码:
DWORD g_dwDebuggerProcessId = -1; BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) { DWORD dwProcessId = *(PDWORD)lParam; DWORD dwWindowProcessId; GetWindowThreadProcessId(hwnd, &dwWindowProcessId); if (dwProcessId == dwWindowProcessId) { std::wstring wsWindowTitle{ string_heper::ToLower(std::wstring(GetWindowTextLengthW(hwnd) + 1, L'\0')) }; GetWindowTextW(hwnd, &wsWindowTitle[0], wsWindowTitle.size()); if (string_heper::FindSubstringW(wsWindowTitle, L"dbg") || string_heper::FindSubstringW(wsWindowTitle, L"debugger")) { g_dwDebuggerProcessId = dwProcessId; return FALSE; } return FALSE; } return TRUE; } bool IsDebuggerProcess(DWORD dwProcessId) const { EnumWindows(EnumWindowsProc, reinterpret_cast<LPARAM>(&dwProcessId)); return g_dwDebuggerProcessId == dwProcessId; } bool SuspendDebuggerThread() { THREADENTRY32 ThreadEntry = { 0 }; ThreadEntry.dwSize = sizeof(THREADENTRY32); DWORD dwParentProcessId = process_helper::GetParentProcessId(GetCurrentProcessId()); if (-1 == dwParentProcessId) return false; HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwParentProcessId); if(Thread32First(hSnapshot, &ThreadEntry)) { do { if ((ThreadEntry.th32OwnerProcessID == dwParentProcessId) && IsDebuggerProcess(dwParentProcessId)) { HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, ThreadEntry.th32ThreadID); if (hThread) SuspendThread(hThread); break; } } while(Thread32Next(hSnapshot, &ThreadEntry)); } if (hSnapshot) CloseHandle(hSnapshot); return false; }
6. SwitchDesktop()
Windows支持每个会话有多个桌面。可以选择一个不同的活动桌面,其效果是隐藏了之前活动桌面的窗口,而且没有明显的方法可以切换回旧的桌面。
此外,来自被调试进程桌面的鼠标和键盘事件将不再被传递给调试器,因为它们的来源不再被共享。这显然使调试变得不可能。
C/C++代码:
BOOL Switch() { HDESK hNewDesktop = CreateDesktopA( m_pcszNewDesktopName, NULL, NULL, 0, DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_SWITCHDESKTOP, NULL); if (!hNewDesktop) return FALSE; return SwitchDesktop(hNewDesktop); }
7. OutputDebugString()
这种技术已经被废弃了,因为它只适用于早于Vista的Windows版本。然而,这项技术非常有名,不能在此不提。
这个想法很简单。如果调试器不存在,而kernel32!OutputDebugString被调用,那么就会发生错误。
C/C++代码:
bool IsDebugged() { if (IsWindowsVistaOrGreater()) return false; DWORD dwLastError = GetLastError(); OutputDebugString(L"AntiDebug_OutputDebugString"); return GetLastError() != dwLastError; }
反制措施
在调试过程中,最好跳过可疑的函数调用(例如用NOP填充)。
如果你写一个反调试方案,以下所有的函数都可以被拦截。
kernel32!DebugActiveProcess
ntdll!DbgUiDebugActiveProcess
ntdll!NtDebugActiveProcess
kernel32!GenerateConsoleCtrlEvent()
user32!NtUserBlockInput
ntdll!NtSetInformationThread
user32!NtUserBuildHwndList(用于过滤EnumWindows输出)。
kernel32!SuspendThread
user32!SwitchDesktop
kernel32!OutputDebugStringW
拦截函数可以检查输入参数并修改原始函数行为。