本期作者/牛杰
前言
是一种防止逆向的方案。逆向人员如果遇到复杂的代码混淆,有时会使用调试器动态分析代码逻辑简化分析流程。例如恶意软件通常会被安全研究人员、反病毒厂商和其他安全专业人员分析和调试,以了解其行为和功能,并开发相应的安全措施来保护系统,这时,恶意软件开发人员就会使用反调试技术阻碍逆向人员的分析,以达到增加自己恶意代码的存活时间。此外,安全人员也需要了解反调试技术,当遇到反调试代码时,可以使用相对应的反反调试。在反调试技术上中,我们介绍了9种常见的反调试方法,本篇继续介绍6种方式。
反调试
1.NtSetInformationThread
NtSetInformationThread 是 Windows 操作系统提供的一个函数,用于设置线程的信息,该函数通常用来设置线程的优先级,此外通过设置不同的 ThreadInformationClass 参数,可以实现隐藏线程、禁止调试、设置调试状态等操作,从而增加程序的安全性和防御性,该函数原型与枚举信息如下。
__kernel_entry NTSYSCALLAPI NTSTATUS NtSetInformationThread(
[in] HANDLE ThreadHandle,
[in] THREADINFOCLASS ThreadInformationClass,
[in] PVOID ThreadInformation,
[in] ULONG ThreadInformationLength
);
typedef enum _THREADINFOCLASS {
ThreadBasicInformation,
ThreadTimes,
ThreadPriority,
ThreadBasePriority,
ThreadAffinityMask,
ThreadImpersonationToken,
ThreadDescriptorTableEntry,
ThreadEnableAlignmentFaultFixup,
ThreadEventPair_Reusable,
ThreadQuerySetWin32StartAddress,
ThreadZeroTlsCell,
ThreadPerformanceCount,
ThreadAmILastThread,
ThreadIdealProcessor,
ThreadPriorityBoost,
ThreadSetTlsArrayAddress,
ThreadIsIoPending,
ThreadHideFromDebugger,
ThreadBreakOnTermination,
MaxThreadInfoClass
} THREADINFOCLASS;
NtSetInformationThread通过ThreadInformationClass中ThreadHideFromDebugger来反调试 是 NtSetInformationThread 函数的一个参数,用于将当前线程隐藏起来,使得调试器无法对其进行调试。这个参数的作用是防止调试器附加到被隐藏的线程上进行调试操作。
当线程被隐藏后,调试器无法访问和控制该线程的执行。这可以用作一种反调试技术,防止恶意用户或恶意软件使用调试器来分析、修改或干扰程序的执行。
通过隐藏线程,程序可以增加自身的安全性,防止被调试器进行逆向工程、代码分析、内存调试等操作。这对于一些需要保护知识产权、防止恶意调试的应用程序或安全软件来说特别有用。
NtSetInformationThread反调试demo如下。
#include <iostream>
#include <Windows.h>
typedef enum _THREADINFOCLASS {
ThreadBasicInformation,
ThreadTimes,
ThreadPriority,
ThreadBasePriority,
ThreadAffinityMask,
ThreadImpersonationToken,
ThreadDescriptorTableEntry,
ThreadEnableAlignmentFaultFixup,
ThreadEventPair_Reusable,
ThreadQuerySetWin32StartAddress,
ThreadZeroTlsCell,
ThreadPerformanceCount,
ThreadAmILastThread,
ThreadIdealProcessor,
ThreadPriorityBoost,
ThreadSetTlsArrayAddress,
ThreadIsIoPending,
ThreadHideFromDebugger,
ThreadBreakOnTermination,
MaxThreadInfoClass
} THREADINFOCLASS;
typedef NTSTATUS(WINAPI* pNtSetInformationThread)(HANDLE, THREADINFOCLASS, PVOID, ULONG);
void HideFromDebugger() {
HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll"));
pNtSetInformationThread NtSetInformationThread = (pNtSetInformationThread)GetProcAddress(hNtDll, "NtSetInformationThread");
NTSTATUS status = NtSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);
}
void ThreadHid() {
HideFromDebugger();
int x = 1;
while (1) {
printf("%d",x);
Sleep(1000);
x++;
};
}
int main()
{
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)ThreadHid, 0, 0, 0);
getchar();
return 0;
}
使用调试器执调试该程序,可以看到程序正常执行。
然后在线程中下断点,程序会自动关闭。
需要注意的是,虽然使用 ThreadHideFromDebugger 可以增加程序的安全性,但它并不是绝对的安全措施,因为仍然存在其他方法可以绕过或检测到线程隐藏。因此,在设计安全应用程序时,应综合考虑多种防护措施,并定期进行安全评估和更新。
2.SeDebugPrivilege
默认情况下,进程没有调试权限(SeDebugPrivilege),除非自己主动开启,但是调试器启动程序并调试时,会从调试器继承该权限。使用该方式需要先调用CsrGetProcessId获取csrss.exe的pid,该函数在ntdll。获取pid后,通过OpenProcess读取句柄csrss.exe,如果能获取则说明被调试,代码如下。
HMODULE hMod = LoadLibrary(TEXT("ntdll.dll"));
typedef int(*CSRGETPROCESSID)();
CSRGETPROCESSID CsrGetProcessId = (CSRGETPROCESSID)GetProcAddress(hMod, "CsrGetProcessId");
DWORD pid = CsrGetProcessId();
HANDLE handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (handle) {
return true;
}
else {
return false;
}
3.时间检测
通过计算时间差,如果时间间隔过长,判断当前进程被反调试,常用API有API有:QueryPerformanceCounter、GetTickCount、GetSystemTime、GetLocalTime,这些API使用方法相似,我们使用GetTickCount举例。
DWORD starttime = GetTickCount();
DWORD endtime = GetTickCount();
if (endtime - starttime > 500) {
return true;
}
else {
return false;
}
4.父进程
通过检测自身父进程来判定是否被调试,原理非常简单,我们的系统在运行程序的时候,绝大多数应用程序都是由explorer.exe这个父进程派生而来的子进程,也就是说如果没有被调试其得到的父进程就是explorer.exe的进程PID,而如果被调试则该进程的父进程PID就会变成调试器的PID值,通过对父进程的检测即可实现检测是否被调试的功能。
#include <Windows.h>
#include <stdio.h>
#include <tlhelp32.h>
int IsDebug()
{
DWORD ExplorerId = 0;
PROCESSENTRY32 pe32 = { 0 };
DWORD ProcessId = GetCurrentProcessId();
GetWindowThreadProcessId(FindWindow(L"Progman", NULL), &ExplorerId);
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hProcessSnap != INVALID_HANDLE_VALUE)
{
pe32.dwSize = sizeof(PROCESSENTRY32);
Process32First(hProcessSnap, &pe32);
do
{
if (ProcessId == pe32.th32ProcessID)
{
// 判断父进程是否是 Explorer.exe
if (pe32.th32ParentProcessID != ExplorerId)
{
return TRUE;
}
}
} while (Process32Next(hProcessSnap, &pe32));
}
return FALSE;
}
int main(int argc, char* argv[])
{
if (IsDebug())
{
printf("正在被调试 n");
}
system("pause");
return 0;
}
5.NtYieldExecution
NtYieldExecution让当前线程主动放弃其剩余的时间片,并执行下一个等待的线程。如果没有线程被安排执行或在使用调试器单步调试时线程无法切换,NtYieldExecution函数返回为STATUS_NO_YIELD_PERFORMED (0x40000024)。
#define STATUS_NO_YIELD_PERFORMED 0x40000024
typedef NTSTATUS(WINAPI* pNtYieldExecution)();
bool isDebug()
{
HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll"));
pNtYieldExecution NtYieldExecution = (pNtYieldExecution)GetProcAddress(hNtDll, "NtSetInformationThread");
INT iDebugged = 0;
for (int i = 0; i < 0x20; i++)
{
Sleep(0xf);
if (NtYieldExecution() != STATUS_NO_YIELD_PERFORMED)
iDebugged++;
}
if (iDebugged <= 3)
return false;
else
return true;
system("pause");
}
但是这种方法其实并不可靠,因为它只显示当前进程中是否有一个高优先级的线程。然而,它可以作为一种反跟踪技术。
6.DbgUiRemoteBreakin
DbgUiRemoteBreakin打补丁可以反OD附加调试,当我们用OD附加调试时,CreateRemoteThread函数在目标程序中创建了一个远程线程,然后在远程线程中调用DbgUiRemoteBreakin函数,DbgUiRemoteBreakin内部调用了DbgBreakPoint函数,DbgBreakPoint函数内部下了一个int 3断点,触发异常让操作系统运行异常处理程序,然后操作系统把控制权交管给调试器,因此可以创建TLS回调的方式hook DbgUiRemoteBreakin,内部直接调用ExitProcess()退出程序,测试代码如下。
# include<stdio.h>
# include<windows.h>
# include<iostream>
int main(int argc, char* argv[])
{
BYTE bBuffer[0x10] = { 0 };
DWORD dwBreakAddress;
DWORD dwOldProtect;
DWORD dwNum;
dwBreakAddress = (DWORD)GetProcAddress(LoadLibrary(L"ntdll.dll"), "DbgUiRemoteBreakin");
bBuffer[0] = 0xE9;
*((DWORD*)(bBuffer + 1)) = (DWORD)ExitProcess - dwBreakAddress;
VirtualProtect((LPVOID)dwBreakAddress, 0x10, PAGE_EXECUTE_READWRITE, &dwOldProtect);
WriteProcessMemory(GetCurrentProcess(), (LPVOID)dwBreakAddress, bBuffer, 5, &dwNum);
VirtualProtect((LPVOID)dwBreakAddress, 0x10, dwOldProtect, &dwOldProtect);
while (1)
{
static int x = 0;
printf("%drn",x);
Sleep(1000);
x++;
}
return 0;
}
当OD附加,程序终止。
总结
本篇继续介绍了其他反调试的方法,在自己的代码中使用反调试技术,可以增加逆向人员的分析难度,或是通过了解这些技术的原理,在分析恶意代码时进行反反调试,在后续的文章中,将会介绍更多的反调试方法。
原文始发于微信公众号(蛇矛实验室):反调试技术-下