win应用层反调试

0x01 PEB相关标志位反调试

简单的获取当前进程的PEB结构体地址的方法:

1
2
3
4
5
#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
#endif // _WIN64

1.1 IsDebuggerPresent

底层原理就是返回PEB结构中BeingDebugged位的值,当有调试器附加的时候BeingDebugged位被置为1。

1.2 NtGlobalFlag

附加一个调试器并不改变NtGlobalFlag的值。但是如果进程是由调试器创建的,则将设置以下标志:

1
2
3
4
5
6
FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)

NtGlobalFlag值:
0000 0000 0111 0000

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define FLG_HEAP_ENABLE_TAIL_CHECK   0x10
#define FLG_HEAP_ENABLE_FREE_CHECK 0x20
#define FLG_HEAP_VALIDATE_PARAMETERS 0x40
#define NT_GLOBAL_FLAG_DEBUGGED (FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS)

#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0x68);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0xBC);
#endif // _WIN64

if (dwNtGlobalFlag & NT_GLOBAL_FLAG_DEBUGGED)
do something...

NtGlobalFlag的汇编代码如下,如果返回值为0x70则程序处于调试状态

1
2
3
4
5
mov eax, fs:[30h] ;Process Environment Block
mov al, [eax+68h] ;NtGlobalFlag
and al, 70h
cmp al, 70h
je being_debugged

相应函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool CheckNtGlobalFlag()
{
BOOL IsDebug = FALSE;

DWORD NtGlobalFlag = 0;
__asm
{
mov eax, fs:[0x30]
mov eax, [eax + 0x68]
mov NtGlobalFlag, eax
}

if (NtGlobalFlag == 0x70)
{
IsDebug = TRUE;
}

return IsDebug;
}

1.3 Heap Flag

在PEB的ProcessHeap位指向_HEAP结构体,该结构体中有俩个字段会受到调试器的影响,具体如何影响,取决于Windows的版本,主要是修改原始的内容,这两个字段是Flags和ForceFlags。

image-20230804165127921
image-20230804165135540

Flags and ForceFlags的值通常分别设置为HEAP_GROWABLE0

在 64 位 Windows XP 和 Windows Vista 及更高版本上,如果存在调试器,则 Flags 字段将设置为以下标志的组合:

  • HEAP_GROWABLE (2)
  • HEAP_TAIL_CHECKING_ENABLED (0x20)
  • HEAP_FREE_CHECKING_ENABLED (0x40)
  • HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)

当存在调试器时,ForceFlags 字段设置为以下标志的组合:

  • HEAP_TAIL_CHECKING_ENABLED (0x20)
  • HEAP_FREE_CHECKING_ENABLED (0x40)
  • HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)

例子: Win10 x86环境

1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL CheckHeapFlagsDebug()
{
PPEB pPeb = (PPEB)__readfsdword(0x30);
PVOID pHeapBase = (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x18));
DWORD dwHeapFlagsOffset = 0x40;
DWORD dwHeapForceFlagsOffset = 0x44;

PDWORD pdwHeapFlags = (PDWORD)((PBYTE)pHeapBase + dwHeapFlagsOffset);
PDWORD pdwHeapForceFlags = (PDWORD)((PBYTE)pHeapBase + dwHeapForceFlagsOffset);

//HEAP_GROWABLE (2)
return (*pdwHeapFlags & ~HEAP_GROWABLE) || (*pdwHeapForceFlags != 0);
}

1.4 堆保护

当进程被调试器调试时该进程堆会被一些特殊的标志填充,这些特殊标记分别是0xABABABAB , 0xFEEEFEEE。

在调试模式下, NtGlobalFlag的HEAP_TAIL_CHECKING_ENABLED 标志将被默认设置,堆内存分配会在末尾追加 0xABABABAB标志进行安全检查,如果NtGlobalFlag设置了HEAP_FREE_CHECKING_ENABLED标志,那么当需要额外的字节来填充堆块尾部时, 就会使用0xFEEEFEEE(或一部分) 来填充。

微软PROCESS_HEAP_ENTRY结构文档

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CheckHeapMagic()
{
PROCESS_HEAP_ENTRY HeapEntry = { 0 };
do
{
if (!HeapWalk(GetProcessHeap(), &HeapEntry))
return false;
} while (HeapEntry.wFlags != PROCESS_HEAP_ENTRY_BUSY);

PVOID pOverlapped = (PBYTE)HeapEntry.lpData + HeapEntry.cbData;
return ((DWORD)(*(PDWORD)pOverlapped) == 0xABABABAB);
}

0x02 ThreadHideFromDebugger线程属性反调试

2.1 ZwSetInformationThread反调试

ThreadHideFromDebugger是线程的一个属性值:

​ 当线程具备该特性时则该线程对“调试器”隐藏(一般是主线程),能够使得主线程无法继续接受该线程的调试事件。

如果线程设置了ThreadHideFromDebugger那么当断点等调试事件触发时调试器表现为卡死。

线程启动后可以通过ZwSetInformationThread()函数来设置,该函数动态加载自ntdll.dll文件,以下是函数声明:

1
2
3
4
5
6
NTSYSAPI NTSTATUS ZwSetInformationThread(
[in] HANDLE ThreadHandle,
[in] THREADINFOCLASS ThreadInformationClass,
[in] PVOID ThreadInformation,
[in] ULONG ThreadInformationLength
);

官方文档

该函数用于设置线程的优先级,ThreadInfomationClass的值可以搜索

原理:

049aa7290cc1f7b23678b2ad8d799d2
04739db9b7cc0b73e687e531ae8b345

将HideFromDebugger的值置1,从而实现反调试。

别人的反反调试:

fdfad3653cd4d850494e556cee18aa1

把对应位置置0.

调用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <Windows.h>
#include <stdio.h>

typedef DWORD(WINAPI* ZW_SET_INFORMATION_THREAD) (HANDLE, DWORD, PVOID, ULONG);
#define ThreadHideFromDebugger 0x11
VOID DisableDebugEvent(VOID)
{
HINSTANCE hModule;
ZW_SET_INFORMATION_THREAD ZwSetInformationThread;
hModule = GetModuleHandleA("Ntdll");
ZwSetInformationThread = (ZW_SET_INFORMATION_THREAD)GetProcAddress(hModule, "ZwSetInformationThread");
ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0);
}

int main()
{
printf("Begin\n");
DisableDebugEvent();
printf("End\n");
return 0;
}

2.2 NtCreateThreadEx反调试

Windows Vista新引入了NtCreateThreadEx函数,以下是函数声明:

在32位下和64位下函数原型不一致

1
2
3
4
5
6
7
8
9
10
11
12
13
NTSTATUS NTAPI NtCreateThreadEx (
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE ProcessHandle,
_In_ PVOID StartRoutine,
_In_opt_ PVOID Argument,
_In_ ULONG CreateFlags,
_In_opt_ ULONG_PTR ZeroBits,
_In_opt_ SIZE_T StackSize,
_In_opt_ SIZE_T MaximumStackSize,
_In_opt_ PVOID AttributeList
);

其中CreateFlags可以使用如下标志:

1
2
3
4
5
6
#define THREAD_CREATE_FLAGS_CREATE_SUSPENDED 0x00000001
#define THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH 0x00000002
#define THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER 0x00000004
#define THREAD_CREATE_FLAGS_HAS_SECURITY_DESCRIPTOR 0x00000010
#define THREAD_CREATE_FLAGS_ACCESS_CHECK_IN_TARGET 0x00000020
#define THREAD_CREATE_FLAGS_INITIAL_THREAD 0x00000080

​ 如果新线程设置了THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER标志,那么在创建时就可以向调试器隐藏该线程信息,这与NtSetInformationThread函数设置的ThreadHideFromDebugger相同。负责安全任务的代码可以在设置THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER标志的线程中执行。


0x03 NtQueryInformationProcess函数反调试

NtQueryInformationProcess函数是操作系统中十分有用的一个关键函数,可以用来查找进程的很多相关信息。以下是函数声明:

1
2
3
4
5
6
7
NTSTATUS NTAPI NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);

[官方文档][https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess]

第一个参数表明待查询的目标进程句柄,而第二个参数则是标明查询的信息种类。

PROCESSINFOCALASS是一个枚举类型,能查询近百种信息,其中以下四种信息是最常见可用于检测调试器的存在。

1
2
3
4
ProcessDebugPort            // 0x7
ProcessDebugFlags // 0x1F
ProcessDebugObjectHandle // 0x1E
ProcessBasicInformation // 0x0

3.1 CheckRemoteDebuggerPresent函数

ProcessDebugPort端口是Windows调试子系统依赖的一个数据结构,可以通过检测调试端口的方式来检测进程是否被调试。

在CheckRemoteDebuggerPresent函数内部就是调用NtQueryInformationProcess函数查询ProcessDebugPort信息,判断目标进程是否在调试状态,没有调试器的时候值为0。

3.2 ProcessDebugObjectHandle

ProcessDebugObjectHandle的内容为调试对象的句柄,没有调试器的时候值为0,以下为示例代码:

1
2
3
4
5
6
7
8
DWORD bDebugger = -1;
NTSTATUS status = NtQueryInformationProcess(
GetCurrentProcess(), // 进程句柄
0x1E, // 要检索的进程信息类型,ProcessDebugObjectHandle
&bDebugger, // 接收进程信息的缓冲区指针
sizeof(DWORD), // 缓冲区大小
NULL // 实际返回进程信息的大小
);

3.3 ProcessDebugFlags反调试

进程调试标志位,当程序处于调试状态的时候ProcessDebugFlags = 0,以下为示例代码:

1
2
3
4
5
6
7
8
DWORD bDebugger = 0;
NTSTATUS status = NtQueryInformationProcess(
GetCurrentProcess(), // 进程句柄
0x1F, // 要检索的进程信息类型,ProcessDebugFlags
&bDebugger, // 接收进程信息的缓冲区指针
sizeof(DWORD), // 缓冲区大小
NULL // 实际返回进程信息的大小
);

3.4 ProcessBasicInformation反调试

当使用ProcessBasicInformation标志调用NtQueryInformationProcess函数时,将返回PROCESS_BASIC_INFORMATION结构,以下是在官方定义的基础上进行完整化的结构信息:

1
2
3
4
5
6
7
8
9
typedef struct _PROCESS_BASIC_INFORMATION 
{
DWORD ExitStatus; // 接收进程终止状态
DWORD PebBaseAddress; // 接收进程环境块地址(PEB)
DWORD AffinityMask; // 接收进程关联掩码
DWORD BasePriority; // 接收进程的优先级类
ULONG UniqueProcessId; // 接收进程ID
ULONG InheritedFromUniqueProcessId; // 接收父进程ID
} PROCESS_BASIC_INFORMATION;

通过这个标志我们就可以获得父进程ID(Reserved3),后续在通过OpenProcess获取父进程句柄,调用GetProcessImageFileName获得父进程名进行比较即可判断是否被调试。


0x04 基于时间的反调试

在现代计算机中一段代码的执行通常不会消耗很多时间,但是如果程序处于调试状态,则他的执行时间就不可控了。这种检测本质就是通过获取时间的函数比较时间差进程检测。


0x05 基于异常处理的反调试

Windows的异常处理流程大致为:

1
2
3
4
5
6
1. 交给调试器(进程必须被调试)
2. 执行VEH
3. 执行SEH
4. TopLevelEH(进程被调试时不会被执行)
5. 交给调试器(上面的异常处理都说处理不了,就再次交给调试器)
6. 调用异常端口通知csrss.exe

未完待续

https://www.anquanke.com/post/id/179709

https://www.anquanke.com/post/id/179710