-
-
[原创]根据al-khaser项目简要的对反调试技术进行整理
-
发表于:
2022-11-25 15:18
17960
-
[原创]根据al-khaser项目简要的对反调试技术进行整理
前言
本整理基于 LordNoteworthy/al-khaser: Public malware techniques used in the wild: Virtual Machine, Emulation, Debuggers, Sandbox detection. (github.com)
作者已经于第贰期 REVERSE 分享会中详细介绍了本文提到的种种技术。现在编辑一遍在看雪上共享给大家。
由于技术复杂,并且Windows存在大量未公开的内核参数、函数等,这些技术可能会过时。
反调试技术整理
PEB表、IsDebuggerPresent
检查FS:[0x30](32位)GS:[0x60](64位),等同于IsDebuggerPresent()
1 | BOOL IsDebuggerPresent();
|
参见IsDebuggerPresent - CTF Wiki (ctf-wiki.org)
CheckRemoteDebuggerPresent()/NtQueryInformationProcess
CheckRemoteDebuggerPresent()调用NtQueryInformationProcess,
1 2 3 4 5 6 7 | __kernel_entry NTSTATUS NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
|
调用此API,传入参数ProcessInformationClass = 7将返回一个句柄指向调试器
参见CheckRemoteDebuggerPresent - CTF Wiki (ctf-wiki.org)、NtQueryInformationProcess - CTF Wiki (ctf-wiki.org)
异常捕获和处理
Windows异常处理流程简述
硬件异常:
CPU转储当前现场
CPU根据IDT查找异常处理例程(KiTrapXX)
参见CPU和软件模拟异常的执行流程_鬼手56的博客-CSDN博客
使用IDA打开ntkrnlpa.idb文件,在其中查找_IDT可以看到对应的处理表
异常处理例程处理异常,完成异常信息封装
调用CommonDispatchException,完善EXCEPTION_RECORD结构
1 2 3 4 5 6 7 8 | typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
|
将结构传递给KiDispatchException
1 2 3 4 5 6 7 | VOID KiDispatchException(
[in] PEXCEPTION_RECORD ExceptionRecord,
[in] PKEXCEPTION_FRAME ExceptionFrame,
[in] PKTRAP_FRAME TrapFrame,
[in] KPROCESSOR_MODE PreviousMode,
[in] BOOLEAN FirstChance
);
|
软件异常:
- 发生
throw关键字
- 转入
_CxxThrowException1 2 3 4 | extern "C" void __stdcall _CxxThrowException(
void* pExceptionObject
_ThrowInfo* pThrowInfo
);
|
- 通过
KERNEL32.DLL!RaiseException填充EXCEPTION_RECORD结构体1 2 3 4 5 6 | void RaiseException(
[in] DWORD dwExceptionCode,
[in] DWORD dwExceptionFlags,
[in] DWORD nNumberOfArguments,
[in] const ULONG_PTR *lpArguments
);
|
- 转入
NTDLL.DLL!RtlRaiseException
- 转入内核
NtRaiseException
- 转入内核
KiRaiseException,Exception Code最高位置零
- 传递到分发函数
KiDispatchException
内核异常
- 尝试传递给内核调试器
- 失败,利用
RtlDispatchException传递至SEH1 2 3 4 | BOOLEAN RtlDispatchException(
[in] PEXCEPTION_RECORD ExceptionRecord,
[in] PCONTEXT ContextRecord
);
|
- 传递给内核调试器
- 终止Windows运行(BSoD)
用户异常
- 传递给内核调试器
- 失败或者不存在内核调试器:填充上下文
CONTEXT,从KiExceptionDispatch转入KeUserExceptionDispather1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | typedef struct _CONTEXT {
DWORD64 P1Home;
DWORD64 P2Home;
DWORD64 P3Home;
DWORD64 P4Home;
DWORD64 P5Home;
DWORD64 P6Home;
DWORD ContextFlags;
DWORD MxCsr;
WORD SegCs;
WORD SegDs;
WORD SegEs;
WORD SegFs;
WORD SegGs;
WORD SegSs;
DWORD EFlags;
DWORD64 Dr0;
DWORD64 Dr1;
DWORD64 Dr2;
DWORD64 Dr3;
DWORD64 Dr6;
DWORD64 Dr7;
DWORD64 Rax;
DWORD64 Rcx;
DWORD64 Rdx;
DWORD64 Rbx;
DWORD64 Rsp;
DWORD64 Rbp;
DWORD64 Rsi;
DWORD64 Rdi;
DWORD64 R8;
DWORD64 R9;
DWORD64 R10;
DWORD64 R11;
DWORD64 R12;
DWORD64 R13;
DWORD64 R14;
DWORD64 R15;
DWORD64 Rip;
union {
XMM_SAVE_AREA32 FltSave;
NEON128 Q[16];
ULONGLONG D[32];
struct {
M128A Header[2];
M128A Legacy[8];
M128A Xmm0;
M128A Xmm1;
M128A Xmm2;
M128A Xmm3;
M128A Xmm4;
M128A Xmm5;
M128A Xmm6;
M128A Xmm7;
M128A Xmm8;
M128A Xmm9;
M128A Xmm10;
M128A Xmm11;
M128A Xmm12;
M128A Xmm13;
M128A Xmm14;
M128A Xmm15;
} DUMMYSTRUCTNAME;
DWORD S[32];
} DUMMYUNIONNAME;
M128A VectorRegister[26];
DWORD64 VectorControl;
DWORD64 DebugControl;
DWORD64 LastBranchToRip;
DWORD64 LastBranchFromRip;
DWORD64 LastExceptionToRip;
DWORD64 LastExceptionFromRip;
} CONTEXT, *PCONTEXT;
|
内核从KiUserExceptionDispatcher获得控制,调用RtlDispatchException查找并调用异常处理函数:VEH→SEH,从fs:[0]开始。
1 2 3 4 5 | VOID KiUserExceptionDispatcher
(
[in] PEXCEPTION_RECORD ExceptionRecord,
[in] PCONTEXT ContextRecord
);
|
参考:VEH和SEH_鬼手56的博客-CSDN博客_veh和seh
- 失败或者未处理:进入内核再次处理
- 如果传递给内核调试器失败或者未处理,终止进程
参见异常处理流程 - ciyze0101 - 博客园 (cnblogs.com)
CloseHandle()
如果一个进程在调试器下运行,并且一个无效的句柄被传递给ntdll!NtClose()或kernel32!CloseHandle()函数,那么将引发EXCEPTION_INVALID_HANDLE:0xC0000008异常。这个异常可以被一个异常处理程序缓存起来。如果控制被传递给异常处理程序,表明有一个调试器存在。
植入中断(包括int 3和int 2dh)
int 2dh可以用于检测包括RING0,RING3在内的所有调试器。通过提前植入一个断点,并附带植入一个VEH异常解决例程,可以用于判断调试器的存在与否。
另外,附加调试器的程序在运行完int 2dh 后,会跳过此指令之后的一个字节.
参见Interrupt 3 - CTF Wiki (ctf-wiki.org)
利用STATUS_GUARD_PAGE_VIOLATION异常
分配被保护的内存区,利用属性PAGE_GUARD标记分配的内存区。向其中填入ret指令。当存在OD调试器解释时将返回到上一个入栈地址,而直接执行将产生STATUS_GUARD_PAGE_VIOLATION错误(数据执行保护)
(Windows XP/2000)使用宏OutputDebugString
1 2 3 | void OutputDebugStringA(
[in, optional] LPCSTR lpOutputString
);
|
此宏在没有调试器附加时,执行将产生一个LastError。检查此错误的值在执行前后是否发生改变,可以知道是否有调试器寄生。
引发EXCEPTION_EXECUTE_HANDLER错误
触发方法:关闭一个不存在的句柄
1 | CloseHandle(0x99999999ULL);
|
使用EFLAGS写入异常标志,然后监视异常
直接将EFLAGS与0x100或运算,利用VEH检查调试器的存在
利用UnhandledExceptionFilter()在除去try…catch块的同时接管错误
1 2 3 4 5 6 7 8 | /*Global*/ BOOL bIsBeinDbg = TRUE;
//...
//示例
LPTOP_LEVEL_EXCEPTION_FILTER Top = SetUnhandledExceptionFilter(myUnhandledExcepFilter);
//在myUnhandledExcepFilter里更改全局变量bIsBeinDbg的值为FALSE
RaiseException(EXCEPTION_FLT_DIVIDE_BY_ZERO, 0, 0, NULL);//在此处产生任意错误
SetUnhandledExceptionFilter(Top);
return bIsBeinDbg;
|
内存扫描与监视
硬件断点寄存器检查
- 提示:对于API
ZeroMemory,部分编译器会将此宏直接优化掉。因此建议使用SecureZeroMemory
- 利用
GetThreadContext获取程序运行上下文,检查DrN寄存器的值是否为0利用MEM_WRITE_WATCH监察申请的内存块的访问、写入情况。尤其适合对反调试部分的字节码进行动态写入之后检查是否被修改。1 2 3 4 | BOOL GetThreadContext(
[in] HANDLE hThread,
[in, out] LPCONTEXT lpContext
);
|
利用MEM_WRITE_WATCH监察申请的内存块的访问、写入情况。尤其适合对反调试部分的字节码进行动态写入之后检查是否被修改。
在分配内存时指定
1 2 3 4 5 6 | LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);
|
使用VirtualAlloc时指定参数[in] flAllocationType为MEM_WRITE_WATCH = 0x00200000
需要检查时,使用GetWriteWatch函数,其中参数[in] lpBaseAddress填入上文申请的内存地址
参见GetWriteWatch function (memoryapi.h) - Win32 apps | Microsoft Docs
1 2 3 4 5 6 7 8 | UINT GetWriteWatch(
[in] DWORD dwFlags,
[in] PVOID lpBaseAddress,
[in] SIZE_T dwRegionSize,
[out] PVOID *lpAddresses,
[in, out] ULONG_PTR *lpdwCount,
[out] LPDWORD lpdwGranularity
);
|
LFH:低碎片堆
LFH 不是单独的堆。 而是应用程序可以为其堆启用的策略。 启用 LFH 后,系统会在某些预先确定的大小中分配内存。 当应用程序从启用了 LFH 的堆请求内存分配时,系统会分配足够大以包含所请求大小的最小内存块。 无论是否启用 LFH,系统都不会将 LFH 用于大于 16 KB 的分配。
低碎片堆 | Microsoft Docs
- 当多次申请、释放堆内存之后,可能出现堆碎片。堆碎片的产生可能导致无法一次性分配大量连续的内存,尽管这个大小的内存在数值上是空闲的。
参见/windows 堆分析 - 合天网安实验室/《软件调试》
在调试模式下,不存在此堆。因此可以通过探测此堆的地址,间接判断调试器存在与否。
探测方法很简单,只需要查看nt!_HEAP.FrontEndHeap
对于从Windows10开始的操作系统,此值的偏移经常变动,因此不便于通过硬编码的方式查找
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | BOOL LowFragmentationHeap(VOID)
{
PINT_PTR FrontEndHeap = NULL;
HANDLE hHeap = GetProcessHeap();
if (IsWindowsVista() || IsWindows7()) {
FrontEndHeap = (PINT_PTR)((CHAR*)hHeap + 0x178);
FrontEndHeap = (PINT_PTR)((CHAR*)hHeap + 0xd4);
}
if (IsWindows8or8PointOne()) {
FrontEndHeap = (PINT_PTR)((CHAR*)hHeap + 0x170);
FrontEndHeap = (PINT_PTR)((CHAR*)hHeap + 0xd0);
}
// In Windows 10. the offset changes very often.
// Ignoring it from now.
if (FrontEndHeap && *FrontEndHeap == NULL) {
return TRUE;
}
return FALSE;
}
|
反HOOK
- 检查某DLL内部的函数的地址(利用
GetProcAddress),并于DLL的地址比较。如果被HOOK,那么函数的地址就将位于对应DLL的地址空间之外。
判断地址空间利用lpBaseOfDllPE属性和SizeOfImage属性
- 要检查函数是否被HOOK,可以传入错误的参数,例如传入错误句柄或者错误大小,观察函数对错误参数的处理
NtGlobalFlag
检查FLG_HEAP_ENABLE_TAIL_CHECK/FLG_HEAP_ENABLE_FREE_CHECK/FLG_HEAP_VALIDATE_PARAMETERS的值。
在 32 位机器上, NtGlobalFlag字段位于PEB(进程环境块)0x68的偏移处, 64 位机器则是在偏移0xBC位置. 该字段的默认值为 0. 当调试器正在运行时, 该字段会被设置为一个特定的值. 尽管该值并不能十分可信地表明某个调试器真的有在运行, 但该字段常出于该目的而被使用.
简单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | __declspec(naked) BOOL DetectDebuggerUsingNtGlobalFlag32()
{
__asm
{
push ebp;
mov ebp, esp;
pushad;
mov eax, fs:[30h]; //从此处获取PEB表
mov al, [eax + 68h];
and al, 70h;
cmp al, 70h;
je being_debugged;
popad;
mov eax, 0;
jmp being_debugged + 6
being_debugged:
popad;
mov eax, 1;
leave;
retn;
}
}
|
参见NtGlobalFlag - CTF Wiki (ctf-wiki.org)
NtQueryInformationProcess
1 2 3 4 5 6 7 | __kernel_entry NTSTATUS NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
|
NtQueryInformationProcess function (winternl.h) - Win32 apps | Microsoft Docs
函数ntdll!NtQueryInformationProcess()可以从一个进程中检索不同种类的信息。它接受一个ProcessInformationClass参数,该参数指定了ProcessInformation参数的输出类型。
1 2 3 4 5 6 7 8 9 | typedef enum _PROCESSINFOCLASS {
ProcessBasicInformation = 0,
ProcessDebugPort = 7,
ProcessWow64Information = 26,
ProcessImageFileName = 27,
ProcessBreakOnTermination = 29,
ProcessDebugObjectHandle = 30, //undocumented, 0x1e
ProcessDebugFlags = 31 //undocumented, 0x1f
} PROCESSINFOCLASS;
|
传递ProcessDebugPort时,如果进程正在被调试,API会检索到一个等于0xFFFFFFFF(十进制-1)的DWORD值。
传递ProcessDebugFlags,将返回一个EPROCESS内核结构
Windows 内核不透明结构 - Windows drivers | Microsoft Docs
参见EPROCESS 结构体属性介绍_hambaga的博客-CSDN博客
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | //这是一个简略的`EPROCESS`结构,请访问参见来查看完整的结构
//undocumented
typedef struct _EPROCESS {
//...
// 调试端口
PVOID DebugPort;
// 异常端口
PVOID ExceptionPort;
//...
// 指向3环PEB(进程环境块),包含了进程地址空间中的堆和系统模块等信息
PPEB Peb;
//...
union {
// 包含了进程的标志位,反映了进程当前状态和配置,上面那一大堆宏就是了
ULONG Flags;
// 字段只能由 PS_SET_BITS 和其他互锁宏设置。 最好通过位定义来读取字段,因此很容易找到引用
struct {
ULONG CreateReported : 1;
ULONG NoDebugInherit : 1; // 调试器
//...
}
}
} EPROCESS, *PEPROCESS;
|
传递ProcessDebugObjectHandle,获取调试对象句柄
通过NtQuerySystemInformation尝试获取调试器句柄
1 2 3 4 5 6 | __kernel_entry NTSTATUS NtQuerySystemInformation(
[in] SYSTEM_INFORMATION_CLASS SystemInformationClass,
[in, out] PVOID SystemInformation,
[in] ULONG SystemInformationLength,
[out, optional] PULONG ReturnLength
);
|
参数SYSTEM_INFORMATION_CLASS结构如下
1 2 3 4 5 6 7 | typedef enum _SYSTEM_INFORMATION_CLASS {
SystemBasicInformation = 0,
//...
SystemKernelDebuggerInformation = 35, //undocumented, 0x23
//...
SystemPolicyInformation = 134,
} SYSTEM_INFORMATION_CLASS;
|
ntdll!NtQuerySystemInformation()函数接受要查询的信息类别作为参数,然而该参数大多数类都没有被记录下来,包括SystemKernelDebuggerInformation(0x23)类,它从Windows NT开始就存在了。
SystemKernelDebuggerInformation返回两个标志寄存器的值:al中的KdDebuggerEnabled,和ah中的KdDebuggerNotPresent。因此,如果内核调试器存在,ah中的返回值为零。
参见反调试:调试标志寄存器-|bbs.pediy.com
查询调试对象
参见调试篇——调试对象与调试事件
DbgUiConnectToDbg函数会创建调试对象,并把它放到TEB的DbgSsReserved[1]成员,也就是该偏移0xF24位置。利用NtQueryObject检查所有调试对象,可以广泛的禁止系统调试,但是容易误伤。
1 2 3 4 5 6 7 | __kernel_entry NTSYSCALLAPI NTSTATUS NtQueryObject(
[in, optional] HANDLE Handle,
[in] OBJECT_INFORMATION_CLASS ObjectInformationClass,
[out, optional] PVOID ObjectInformation,
[in] ULONG ObjectInformationLength,
[out, optional] PULONG ReturnLength
);
|
利用NtSetInformationThread将线程从调试器中隐藏。
1 2 3 4 5 6 | __kernel_entry NTSYSCALLAPI NTSTATUS NtSetInformationThread(
[in] HANDLE ThreadHandle,
[in] THREADINFOCLASS ThreadInformationClass,
[in] PVOID ThreadInformation,
[in] ULONG ThreadInformationLength
);
|
关于ThreadInformationClass的枚举量,请查阅THREADINFOCLASS (geoffchappell.com)
1 2 3 4 5 6 7 | typedef enum _THREADINFOCLASS {
ThreadBasicInformation = 0,
//...
ThreadHideFromDebugger = 17,
//...
MaxThreadInfoClass = 51,
} THREADINFOCLASS;
|
1 2 | //示例
NtSetInformationThread(handle.get(), ThreadHideFromDebugger, nullptr, 0);
|
参见ZwSetInformationThread - CTF Wiki (ctf-wiki.org)
反制[原创]调试陷阱ThreadHideFromDebugger的另一种对抗方法-软件逆向-看雪论坛-安全社区|安全招聘|bbs.pediy.com
NtYieldExecution
这个函数可以让任何就绪的线程暂停执行,等待下一个线程调度。线程放弃剩余时间,让给其他线程执行。如果没有其他准备好的线程,该函数返回false,否则返回true。当前线程如果被调试,那么调试器线程若处于单步状态,随时等待继续运行,则被调试线程执行NtYieldExecution时,调试器线程会恢复执行。此时NtYieldExecution返回true,该线程则认为自身被调试了。
此方法并不准确,因为检测到的行为可能是由上层应用程序触发。因此会设置一个计数器。
父进程检查
如果启动进程不是cmd.exe、explorer.exe,则返回警告
HeapFlags和HeapForceFlags
检查几个位,注意这些位的值的大小。如果HeapFlags的值大于 2 说明程序处于调试状态;如果 HeapForceFlags的值大于 0 则说明处于调试状态。
- Flags 字段:
- 在 32 位 Windows NT, Windows 2000 和 Windows XP 中,
Flags位于堆的0x0C偏移处. 在 32 位 Windows Vista 及更新的系统中, 它位于0x40偏移处.
- 在 64 位 Windows XP 中,
Flags字段位于堆的0x14偏移处, 而在 64 位 Windows Vista 及更新的系统中, 它则是位于0x70偏移处.
- ForceFlags 字段:
- 在 32 位 Windows NT, Windows 2000 和 Windows XP 中,
ForceFlags位于堆的0x10偏移处. 在 32 位 Windows Vista 及更新的系统中, 它位于0x44偏移处.
- 在 64 位 Windows XP 中,
ForceFlags字段位于堆的0x18偏移处, 而在 64 位 Windows Vista 及更新的系统中, 它则是位于0x74偏移处.
参见Heap Flags - CTF Wiki (ctf-wiki.org)
检查作业容器Job Object
Windows 提供一个作业对象,它允许我们将进程组合在一起并创建一个"沙箱"来限制进程能做什么.可以将作业想象成一个进程容器.但是,只包含一个进程的作业同样有用,因为这样可以对进程施加平时不能施加的限制.
检查同在一个作业对象中的其他进程。
SeDebugPrivileges
默认情况下进程是没有SeDebugPrivilege权限的,但是当进程通过调试器启动时,由于调试器本身启动了SeDebugPrivilege权限,所以我们可以检测进程的SeDebugPrivilege权限来间接判断是否存在调试器,而对SeDebugPrivilege权限的判断可以用能否打开csrss.exe进程来判断。
KUSER_SHARED_DATA
参见KUSER_SHARED_DATA (ntddk.h) - Windows drivers | Microsoft Docs
1 2 3 4 5 6 7 8 | //这是一个简略的`EPROCESS`结构,请访问参见来查看完整的结构
typedef struct _KUSER_SHARED_DATA {
ULONG TickCountLowDeprecated;
//...
BOOLEAN KdDebuggerEnabled; //位置: 0x2D4
//...
ULONG64 UserPointerAuthMask;
} KUSER_SHARED_DATA, *PKUSER_SHARED_DATA;
|
用户空间和内核空间其实有一块共享区域KUSER_SHARED_DATA,大小为 4 KB。它们的内存地址虽然不一样,但是它们都是有同一块物理内存映射出来的,其中存在内核调试检查位KdDebuggerEnabled可以获取内核调试状态。
此内存块的地址为0xFFDF0000(x86)、0xFFFFF78000000000(x64),对应的要检查的位在0x2d4处
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | BOOL SharedUserData_KernelDebugger()
{
const ULONG_PTR UserSharedData = 0x7FFE0000;
const UCHAR KdDebuggerEnabledByte = *(UCHAR*)(UserSharedData + 0x2D4); // 0x2D4 = the offset of the field
// Extract the flags.
// The meaning of these is the same as in NtQuerySystemInformation(SystemKernelDebuggerInformation).
// Normally if a debugger is attached, KdDebuggerEnabled is true, KdDebuggerNotPresent is false and the byte is 0x3.
const BOOLEAN KdDebuggerEnabled = (KdDebuggerEnabledByte & 0x1) == 0x1;
const BOOLEAN KdDebuggerNotPresent = (KdDebuggerEnabledByte & 0x2) == 0;
if (KdDebuggerEnabled || !KdDebuggerNotPresent)
return TRUE;
return FALSE;
}
|
扫描关键位置字节码,排除0xCC
利用TLS进行反调试
TLS在执行主函数前执行
WUDF驱动框架动态链接库
从"C:\Windows\System32\WUDFPlatform.dll"中导出几个函数,其中存在一个与IsDebuggerPresent相类似的函数WudfIsAnyDebuggerPresent等
参考资料
al-khaser学习笔记(一)Anti Debug - CrisCzy - 博客园 (cnblogs.com)
我的博客原文
[培训]传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2022-11-25 15:38
被Hedione编辑
,原因: 小修小补