May 19, 2022

反调试总结

PEB结构体

FS段寄存器指向TEB结构体,TEB中偏移为0x30的地方存储PEB结构体。
使用FS:[0x30]获取PEB结构体地址。

TEB结构体:

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
kd> dt _teb
nt!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : Ptr32 Void
+0x020 ClientId : _CLIENT_ID //进程的pid
+0x028 ActiveRpcHandle : Ptr32 Void
+0x02c ThreadLocalStoragePointer : Ptr32 Void
+0x030 ProcessEnvironmentBlock : Ptr32 _PEB //进程PEB
+0x034 LastErrorValue : Uint4B
+0x038 CountOfOwnedCriticalSections : Uint4B
+0x03c CsrClientThread : Ptr32 Void
+0x040 Win32ThreadInfo : Ptr32 Void
+0x044 User32Reserved : [26] Uint4B
+0x0ac UserReserved : [5] Uint4B
+0x0c0 WOW32Reserved : Ptr32 Void
+0x0c4 CurrentLocale : Uint4B
+0x0c8 FpSoftwareStatusRegister : Uint4B
+0x0cc SystemReserved1 : [54] Ptr32 Void
+0x1a4 ExceptionCode : Int4B
+0x1a8 ActivationContextStack : _ACTIVATION_CONTEXT_STACK
+0x1bc SpareBytes1 : [24] UChar
+0x1d4 GdiTebBatch : _GDI_TEB_BATCH
+0x6b4 RealClientId : _CLIENT_ID
+0x6bc GdiCachedProcessHandle : Ptr32 Void
+0x6c0 GdiClientPID : Uint4B
+0x6c4 GdiClientTID : Uint4B
+0x6c8 GdiThreadLocalInfo : Ptr32 Void
+0x6cc Win32ClientInfo : [62] Uint4B
+0x7c4 glDispatchTable : [233] Ptr32 Void
+0xb68 glReserved1 : [29] Uint4B

PEB结构体:

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
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
+0x000 InheritedAddressSpace : UChar
+0x001 ReadImageFileExecOptions : UChar
+0x002 BeingDebugged : UChar
+0x003 SpareBool : UChar
+0x004 Mutant : Ptr32 Void
+0x008 ImageBaseAddress : Ptr32 Void
+0x00c Ldr : Ptr32 _PEB_LDR_DATA
+0x010 ProcessParameters : Ptr32 _RTL_USER_PROCESS_PARAMETERS
+0x014 SubSystemData : Ptr32 Void
+0x018 ProcessHeap : Ptr32 Void
+0x01c FastPebLock : Ptr32 _RTL_CRITICAL_SECTION
+0x020 FastPebLockRoutine : Ptr32 Void
+0x024 FastPebUnlockRoutine : Ptr32 Void
+0x028 EnvironmentUpdateCount : Uint4B
+0x02c KernelCallbackTable : Ptr32 Void
+0x030 SystemReserved : [1] Uint4B
+0x034 AtlThunkSListPtr32 : Uint4B
+0x038 FreeList : Ptr32 _PEB_FREE_BLOCK
+0x03c TlsExpansionCounter : Uint4B
+0x040 TlsBitmap : Ptr32 Void
+0x044 TlsBitmapBits : [2] Uint4B
+0x04c ReadOnlySharedMemoryBase : Ptr32 Void
+0x050 ReadOnlySharedMemoryHeap : Ptr32 Void
+0x054 ReadOnlyStaticServerData : Ptr32 Ptr32 Void
+0x058 AnsiCodePageData : Ptr32 Void
+0x05c OemCodePageData : Ptr32 Void
+0x060 UnicodeCaseTableData : Ptr32 Void
+0x064 NumberOfProcessors : Uint4B
+0x068 NtGlobalFlag : Uint4B
+0x070 CriticalSectionTimeout : _LARGE_INTEGER
+0x078 HeapSegmentReserve : Uint4B
+0x07c HeapSegmentCommit : Uint4B
+0x080 HeapDeCommitTotalFreeThreshold : Uint4B
+0x084 HeapDeCommitFreeBlockThreshold : Uint4B
+0x088 NumberOfHeaps : Uint4B
+0x08c MaximumNumberOfHeaps : Uint4B
+0x090 ProcessHeaps : Ptr32 Ptr32 Void
+0x094 GdiSharedHandleTable : Ptr32 Void
+0x098 ProcessStarterHelper : Ptr32 Void
+0x09c GdiDCAttributeList : Uint4B
+0x0a0 LoaderLock : Ptr32 Void
+0x0a4 OSMajorVersion : Uint4B
+0x0a8 OSMinorVersion : Uint4B
+0x0ac OSBuildNumber : Uint2B
+0x0ae OSCSDVersion : Uint2B
+0x0b0 OSPlatformId : Uint4B
+0x0b4 ImageSubsystem : Uint4B
+0x0b8 ImageSubsystemMajorVersion : Uint4B
+0x0bc ImageSubsystemMinorVersion : Uint4B
+0x0c0 ImageProcessAffinityMask : Uint4B
+0x0c4 GdiHandleBuffer : [34] Uint4B
+0x14c PostProcessInitRoutine : Ptr32 void
+0x150 TlsExpansionBitmap : Ptr32 Void
+0x154 TlsExpansionBitmapBits : [32] Uint4B
+0x1d4 SessionId : Uint4B
+0x1d8 AppCompatFlags : _ULARGE_INTEGER
+0x1e0 AppCompatFlagsUser : _ULARGE_INTEGER
+0x1e8 pShimData : Ptr32 Void
+0x1ec AppCompatInfo : Ptr32 Void
+0x1f0 CSDVersion : _UNICODE_STRING
+0x1f8 ActivationContextData : Ptr32 Void
+0x1fc ProcessAssemblyStorageMap : Ptr32 Void
+0x200 SystemDefaultActivationContextData : Ptr32 Void
+0x204 SystemAssemblyStorageMap : Ptr32 Void
+0x208 MinimumStackCommit : Uint4B

BeingDebugged(+0x2)

程序在运行在调试器中时值为1,否则为0

IsDebuggerPresent()API获取PEB.BeingDebugged值,其代码为:

1
2
3
4
mov eax,dword ptr fs:[0x18]     ;获取TEB结构体地址
mov eax,dword ptr ds:[eax+0x30] ;通过TEB.ProcessEnvironmentBlock(+0x30)获取PEB地址
movzx eax,byte ptr ds:[eax+2] ;访问PEB.BeingDebugged成员
retn

这里插一句,如果你在网上查找关于TEB的资料,会发现所有人都会说fs指向TEB结构体的起始地址。那么获取TEB起始地址为什么不直接用fs的值,而是要用fs:[0x18]呢?
实际上是因为FS寄存器并非直接指向TEB结构体的地址,而是它持有SDT的索引,而该索引持有实际的TEB地址

SDT位于内核内存区域,其地址在GDTR(全局描述符表寄存器)中。

fs:[0x18]=TEB.NtTib.Self=address of TEB=fs:0,即fs:[0x18]处存储实际TEB结构体的地址(这是TEB中的一个成员)。

ProcessHeap(+0x18)

kernel32GetProcessHeap()函数或直接取偏移。

Flags 字段

ForceFlags 字段

非调试状态下,Flags值为2,ForceFlags为0。想要绕过,更改这些值即可。

注意:将运行中的进程附加到调试器时,不会出现以上特征。

NtGlobalFlag(+0x68)

在 32 位机器上, NtGlobalFlag字段位于PEB(进程环境块)0x68的偏移处, 64 位机器则是在偏移0xBC位置。该字段的默认值为0。当调试器正在运行时, 该字段会被设置为0x70。
由调试器创建的进程会设置以下标志位:

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

NtGlobalFlag(0x70)由以上三个值按位或得到。
NtGlobalFlag的3个标志位只有当程序是由调试器创建, 而非由调试器附加上去的进程时, 才会被设置。

绕过

NtQueryInformationProcess()

1
2
3
4
5
6
7
NTSTATUS WINAPI NtQueryInformationProcess(
_In_ HANDLE ProcessHandle,
_In_ PROCESSINFOCLASS ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);

参数ProcessHandle是我们要查询的进程的句柄。
参数ProcessInformationClass是一个枚举类型,通过传入特定的值,系统就会把相关的信息保存在ProcessInformation中。
参数ProcessInformation就是我们用来保存查询结果的缓冲区。
参数ProcessInformationLength是我们缓冲区的长度。
参数ReturnLength是调用函数后最终返回的数据长度。

ProcessDebugPort(0x7)

ProcessInformationClass参数被设置为ProcessDebugPort(0x7)时,调用NtQueryInformationProcess()函数可以获得调试端口。
若非调试,则返回值(dwDebugPort)值为0,否则为0xffffffff。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
BOOL IsDebugger()
{
BOOL isDebugger = FALSE;
DWORD dwDebugPort = 0;
HMODULE hDll = NULL;
pNtQueryInformationProcess NtQueryInformationProcess = NULL;

hDll = LoadLibrary("ntdll。dll");

if (hDll)
{
NtQueryInformationProcess = (pNtQueryInformationProcess)GetProcAddress(hDll, "NtQueryInformationProcess");
if (NtQueryInformationProcess)
{
NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort, &dwDebugPort, sizeof(dwDebugPort), NULL);
if (dwDebugPort != 0) isDebugger = TRUE;
}
FreeLibrary(hDll);
}

return isDebugger;
}

ProcessDebugObjectHandle(0x1e)

调试进程时生成调试对象。
函数第二个参数值为ProcessDebugObjectHandle(0x1e)时,调用函数后通过第三个参数获取调试对象的句柄。进程处于调试状态时,句柄值存在,否则为NULL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
BOOL IsDebugger()
{
BOOL isDebugger = FALSE;
HANDLE hDebugObj = NULL;
HMODULE hDll = NULL;
pNtQueryInformationProcess NtQueryInformationProcess = NULL;

hDll = LoadLibrary("ntdll。dll");

if (hDll)
{
NtQueryInformationProcess = (pNtQueryInformationProcess)GetProcAddress(hDll, "NtQueryInformationProcess");
if (NtQueryInformationProcess)
{
NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugObjectHandle, &hDebugObj, sizeof(hDebugObj), NULL);
if (hDebugObj != NULL) isDebugger = TRUE;
}
FreeLibrary(hDll);
}

return isDebugger;
}

ProcessDebugFlags(0x1f)

函数第二个参数值为ProcessDebugFlags(0x1f)时,调用函数后通过第三个参数获取调试标志的值。为0表示进程处于调试状态,为1表示非调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BOOL IsDebugger()
{
BOOL isDebugger = FALSE;
HMODULE hDll = NULL;
pNtQueryInformationProcess NtQueryInformationProcess = NULL;

hDll = LoadLibrary("ntdll。dll");

if (hDll)
{
NtQueryInformationProcess = (pNtQueryInformationProcess)GetProcAddress(hDll, "NtQueryInformationProcess");
if (NtQueryInformationProcess)
{
NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugFlags, &isDebugger, sizeof(isDebugger), NULL);
isDebugger = !isDebugger;
}
FreeLibrary(hDll);
}

return isDebugger;
}

CheckRemoteDebuggerPresent

kernel32的CheckRemoteDebuggerPresent()函数用于检测指定进程是否正在被调试。
Remote在单词里是指同一个机器中的不同进程。

1
2
3
4
BOOL WINAPI CheckRemoteDebuggerPresent(
_In_ HANDLE hProcess,
_Inout_ PBOOL pbDebuggerPresent
);

如果调试器存在 (通常是检测自己是否正在被调试), 该函数会将pbDebuggerPresent指向的值设为0xffffffff。

绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc, char *argv[])
{
BOOL isDebuggerPresent = FALSE;
if (CheckRemoteDebuggerPresent(GetCurrentProcess(), &isDebuggerPresent ))
{
if (isDebuggerPresent )
{
std::cout << "Stop debugging program!" << std::endl;
exit(-1);
}
}
return 0;
}

我们可以直接修改isDebuggerPresent的值或修改跳转条件来绕过 (注意不是CheckRemoteDebuggerPresent的 izhi, 它的返回值是用于表示函数是否正确执行)。

但如果要针对CheckRemoteDebuggerPresent这个 api 函数进行修改的话,首先要知道CheckRemoteDebuggerPresent内部其实是通过调用NtQueryInformationProcess来完成功能的,所以就需要对NtQueryInformationProcess的返回值进行修改。

ZwSetInformationThread

ZwSetInformationThread 等同于 NtSetInformationThread,通过为线程设置 ThreadHideFromDebugger,可以禁止线程产生调试事件。

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;
}

如果处于调试状态,执行完ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0);,程序就会退出

绕过

注意到该处 ZwSetInformationThread 函数的第 2 个参数为 ThreadHideFromDebugger,其值为 0x11。调试执行到该函数时,若发现第 2 个参数值为 0x11,跳过或者将 0x11 修改为其他值即可

NtSetInformationThread

1
2
3
4
5
6
7
8
NTSTATUS
NTAPI
NtSetInformationThread(
_In_ HANDLE ThreadHandle,
_In_ THREADINFOCLASS ThreadInformationClass,
_In_reads_bytes_(ThreadInformationLength) PVOID ThreadInformation,
_In_ ULONG ThreadInformationLength
);

参数一:当前线程的句柄,可以设置为NtCurrentThread
参数二:可以设置为ThreadHideFromDebugger标志
参数三:NULL
参数四:NULL

调用方法:NtSetInformationThread(handle.get(), ThreadHideFromDebugger, nullptr, 0);

ThreadHideFromDebugger值为0x11。
设置此参数将使这条线程对调试器“隐藏”。
即调试器收不到调试信息,因而就会出现当调试器对被调试进程下断点,线程执行到被下断点代码的时候就会卡死这种现象。

时钟检测

rdtsc

rdtsc指令可以返回系统重启以来的时钟数,并且会将它作为一个64位的值放到EDX:EAX中。

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
BOOL IsDebugger()
{
BOOL IsDebugger = FALSE;

__asm
{
push eax
push ecx

xor eax, eax
xor ecx, ecx

rdtsc
mov ecx, eax
rdtsc
sub eax, ecx
cmp eax, 0xFFF
jb NotDebugger
mov ecx, 1
mov IsDebugger, ecx
NotDebugger:
pop ecx
pop eax
}
}

GetTickCount

GetTickCount函数可以返回最近系统重启时间与当前时间相差的毫秒数。

1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL IsDebugger()
{
BOOL bIsDebugger = FALSE;
DWORD dwFirst = 0, dwSecond = 0;

dwFirst = GetTickCount();

dwSecond = GetTickCount();

if (dwSecond - dwFirst > 0x1A) bIsDebugger = TRUE;

return bIsDebugger;
}

BlockInput

在函数头部加上这个禁止键盘输入的函数,然后在函数尾部恢复键盘输入。
感觉这个好有意思hhhhhhh

其他

遇到新的再补充(x

参考资料:
用户层反调试技术总结
CTF-wiki
调试陷阱ThreadHideFromDebugger的另一种对抗方法
Windows最全反调试知识汇总
https://hitworld.github.io/posts/e7c3b7f1/

关于本文

本文作者 云之君, 许可由 CC BY-NC 4.0.