反调试笔记

反调试技术

调试技术可以直接探查出程序运行的机制,程序作者为了隐藏程序底层机制而通过反调试技术来反制。

对于一般的程序需要防止核心代码被调试逆向,软件或专利技术被破解;而病毒或恶意代码会隐藏自己的恶意行为防止被跟踪。

所以对于反调试技术我们是有了解的必要的。

Windows 前置知识

以下存在未公开结构体 (地址为 32 位,具体请参考微软官方文档)

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
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
74
typedef struct _TEB
{
NT_TIB Tib; /* 00h */
PVOID EnvironmentPointer; /* 1Ch */
CLIENT_ID Cid; /* 20h */
PVOID ActiveRpcHandle; /* 28h */
PVOID ThreadLocalStoragePointer; /* 2Ch */
struct _PEB *ProcessEnvironmentBlock; /* 30h */
ULONG LastErrorValue; /* 34h */
ULONG CountOfOwnedCriticalSections; /* 38h */
PVOID CsrClientThread; /* 3Ch */
struct _W32THREAD* Win32ThreadInfo; /* 40h */
ULONG User32Reserved[0x1A]; /* 44h */
ULONG UserReserved[5]; /* ACh */
PVOID WOW32Reserved; /* C0h */
LCID CurrentLocale; /* C4h */
ULONG FpSoftwareStatusRegister; /* C8h */
PVOID SystemReserved1[0x36]; /* CCh */
LONG ExceptionCode; /* 1A4h */
struct _ACTIVATION_CONTEXT_STACK *ActivationContextStackPointer; /* 1A8h */
UCHAR SpareBytes1[0x28]; /* 1ACh */
GDI_TEB_BATCH GdiTebBatch; /* 1D4h */
CLIENT_ID RealClientId; /* 6B4h */
PVOID GdiCachedProcessHandle; /* 6BCh */
ULONG GdiClientPID; /* 6C0h */
ULONG GdiClientTID; /* 6C4h */
PVOID GdiThreadLocalInfo; /* 6C8h */
ULONG Win32ClientInfo[62]; /* 6CCh */
PVOID glDispatchTable[0xE9]; /* 7C4h */
ULONG glReserved1[0x1D]; /* B68h */
PVOID glReserved2; /* BDCh */
PVOID glSectionInfo; /* BE0h */
PVOID glSection; /* BE4h */
PVOID glTable; /* BE8h */
PVOID glCurrentRC; /* BECh */
PVOID glContext; /* BF0h */
NTSTATUS LastStatusValue; /* BF4h */
UNICODE_STRING StaticUnicodeString; /* BF8h */
WCHAR StaticUnicodeBuffer[0x105]; /* C00h */
PVOID DeallocationStack; /* E0Ch */
PVOID TlsSlots[0x40]; /* E10h */
LIST_ENTRY TlsLinks; /* F10h */
PVOID Vdm; /* F18h */
PVOID ReservedForNtRpc; /* F1Ch */
PVOID DbgSsReserved[0x2]; /* F20h */
ULONG HardErrorDisabled; /* F28h */
PVOID Instrumentation[14]; /* F2Ch */
PVOID SubProcessTag; /* F64h */
PVOID EtwTraceData; /* F68h */
PVOID WinSockData; /* F6Ch */
ULONG GdiBatchCount; /* F70h */
BOOLEAN InDbgPrint; /* F74h */
BOOLEAN FreeStackOnTermination; /* F75h */
BOOLEAN HasFiberData; /* F76h */
UCHAR IdealProcessor; /* F77h */
ULONG GuaranteedStackBytes; /* F78h */
PVOID ReservedForPerf; /* F7Ch */
PVOID ReservedForOle; /* F80h */
ULONG WaitingOnLoaderLock; /* F84h */
ULONG SparePointer1; /* F88h */
ULONG SoftPatchPtr1; /* F8Ch */
ULONG SoftPatchPtr2; /* F90h */
PVOID *TlsExpansionSlots; /* F94h */
ULONG ImpersionationLocale; /* F98h */
ULONG IsImpersonating; /* F9Ch */
PVOID NlsCache; /* FA0h */
PVOID pShimData; /* FA4h */
ULONG HeapVirualAffinity; /* FA8h */
PVOID CurrentTransactionHandle; /* FACh */
PTEB_ACTIVE_FRAME ActiveFrame; /* FB0h */
PVOID FlsData; /* FB4h */
UCHAR SafeThunkCall; /* FB8h */
UCHAR BooleanSpare[3]; /* FB9h */
} TEB, *PTEB;

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
typedef struct _PEB
{
UCHAR InheritedAddressSpace; /* 00h */
UCHAR ReadImageFileExecOptions; /* 01h */
UCHAR BeingDebugged; /* 02h */
UCHAR Spare; /* 03h */
PVOID Mutant; /* 04h */
PVOID ImageBaseAddress; /* 08h */
PPEB_LDR_DATA Ldr; /* 0Ch */
PRTL_USER_PROCESS_PARAMETERS ProcessParameters; /* 10h */
PVOID SubSystemData; /* 14h */
PVOID ProcessHeap; /* 18h */
PVOID FastPebLock; /* 1Ch */
PPEBLOCKROUTINE FastPebLockRoutine; /* 20h */
PPEBLOCKROUTINE FastPebUnlockRoutine; /* 24h */
ULONG EnvironmentUpdateCount; /* 28h */
PVOID* KernelCallbackTable; /* 2Ch */
PVOID EventLogSection; /* 30h */
PVOID EventLog; /* 34h */
PPEB_FREE_BLOCK FreeList; /* 38h */
ULONG TlsExpansionCounter; /* 3Ch */
PVOID TlsBitmap; /* 40h */
ULONG TlsBitmapBits[0x2]; /* 44h */
PVOID ReadOnlySharedMemoryBase; /* 4Ch */
PVOID ReadOnlySharedMemoryHeap; /* 50h */
PVOID* ReadOnlyStaticServerData; /* 54h */
PVOID AnsiCodePageData; /* 58h */
PVOID OemCodePageData; /* 5Ch */
PVOID UnicodeCaseTableData; /* 60h */
ULONG NumberOfProcessors; /* 64h */
ULONG NtGlobalFlag; /* 68h */
UCHAR Spare2[0x4]; /* 6Ch */
LARGE_INTEGER CriticalSectionTimeout; /* 70h */
ULONG HeapSegmentReserve; /* 78h */
ULONG HeapSegmentCommit; /* 7Ch */
ULONG HeapDeCommitTotalFreeThreshold; /* 80h */
ULONG HeapDeCommitFreeBlockThreshold; /* 84h */
ULONG NumberOfHeaps; /* 88h */
ULONG MaximumNumberOfHeaps; /* 8Ch */
PVOID** ProcessHeaps; /* 90h */
PVOID GdiSharedHandleTable; /* 94h */
PVOID ProcessStarterHelper; /* 98h */
PVOID GdiDCAttributeList; /* 9Ch */
PVOID LoaderLock; /* A0h */
ULONG OSMajorVersion; /* A4h */
ULONG OSMinorVersion; /* A8h */
ULONG OSBuildNumber; /* ACh */
ULONG OSPlatformId; /* B0h */
ULONG ImageSubSystem; /* B4h */
ULONG ImageSubSystemMajorVersion; /* B8h */
ULONG ImageSubSystemMinorVersion; /* C0h */
ULONG GdiHandleBuffer[0x22]; /* C4h */
PVOID ProcessWindowStation; /* ??? */
} PEB, *PPEB;

TLS 线程局部存储

线程局部存储用来将数据与一个正在执行的指定线程关联起来。

它主要是为了避免多个线程同时访存同一全局变量或者静态变量时所导致的冲突,尤其是多个线程同时需要修改这一变量时。为了解决这个问题,我们可以通过TLS机制,为每一个使用该全局变量的线程都提供一个变量值的副本,每一个线程均可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。而从全局变量的角度上来看,就好像一个全局变量被克隆成了多份副本,而每一份副本都可以被一个线程独立地改变。

WinAPI 反调试

在头文件 windows.h 中存在函数 IsDebuggerPresent()

当函数返回 true 代表检测到调试器,false 则代表未检测到调试器

相同功能的函数还有 CheckRemoteDebuggerPresent()

同时还可以通过 FindWindowEnumWindows或是GetForegroundWindow粗暴查询主流调试器窗口名

又或者通过 CreateToolhelp32SnapshotProcess32FirstProcess32Next查询所有的进程来排查主流调试器进程是否存在

甚至可以通过判断父进程是否为explorer.exe来检查是否被调试,因为一般来说双击点开的程序的父进程都是explorer.exe

利用 PEB 标志位

IsDebuggerPresent() 实际上也是取 PEB 结构体中的 BeingDebugged 标志位

当调试器开始调试应用程序的时候,操作系统会将目标应用程序的 PEB 结构体中的 BeingDebugged 标志位置为 1

FS[] 寄存器指向 TEB 结构体

1
2
3
mov eax, fs:[30h]		; 取 PEB 首地址
mov eax, [eax+2] ; 取 PEB 首地址偏移 2 的位置
and eax, 0xff ; 只需要低 2 位的 PEB->BeingDebugged

或者 (NtGlobalFlag 默认为 0 )

1
2
mov eax, fs:[30h]		; 取 PEB 首地址
mov eax, [eax+68h] ; 取 PEB 首地址偏移 68h 的位置,即 PEB->NtGlobalFlag

在 C++ 中可写为 (仅为示例)

1
2
3
4
5
6
7
8
9
10
11
bool isDebugged()
{
unsigned int flag = false;
__asm {
mov eax, fs:[30h]
mov eax, [eax+2]
and eax, 0xff
mov flag, eax
}
return flag;
}

利用堆标志位

不同系统、不同版本可能不太一样,具体请查询微软官方文档。

在Windows XP系统中,ForceFlags属性位于堆头部偏移量0x10处;在Windows 7系统中,对于32位的应用程序来说ForceFlags属性位于堆头部偏移量 0x44 处。

利用 PEB->ProcessHeap->Flags

1
2
3
mov eax, fs:[30h]		; 取 PEB 首地址
mov eax, [eax + 18h] ; 取 PEB 首地址偏移 18h 的位置,即 PEB->ProcessHeap
mov eax, [eax + 0ch] ; 取 PEB->ProcessHeap 首地址偏移 0ch 的位置,即 PEB->ProcessHeap->Flags (默认为 2)

利用 PEB->ProcessHeap->ForceFlags

1
2
3
mov eax, fs:[30h]		; 取 PEB 首地址
mov eax, [eax + 18h] ; 取 PEB 首地址偏移 18h 的位置,即 PEB->ProcessHeap
mov eax, [eax + 10h] ; 取 PEB->ProcessHeap 首地址偏移 0ch 的位置,即 PEB->ProcessHeap->ForceFlags

使用 ntdll 函数

无论是 kernel32.dll 还是 user32.dll,始终会通过 ntdll.dll 中的函数与驱动层取得联系

其中 ntdll.dll 存在一个未导出函数 NtQueryInformationProcess() 可供查询调试端口

定义如下:

1
extern "C" DWORD NTAPI NtQueryInformationProcess(HANDLE hProcess, ULONG InfoClass, PVOID Buffer, ULONG length, PULONG returnLen);

当我们需要使用时,以下 MSVC 代码可供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extern "C" DWORD NTAPI NtQueryInformationProcess(HANDLE hProcess, ULONG InfoClass, PVOID Buffer, ULONG length, PULONG returnLen);
#pragma comment(lib, "ntdll.lib")

bool isDebugged()
{
HANDLE hDebugPort = NULL; // 调试端口
// (HANDLE)-1 代表当前进程,其他均为查询是否被调试的固定参数
// 如果正在被调试将返回 -1,否则返回 0
if (NtQueryInformationProcess((HANDLE)-1, 7, &hDebugPort, sizeof(HANDLE), NULL))
{
return hDebugPort;
}
return false;
}

CheckRemoteDebuggerPresent() 实际上内部也调用了此函数

时间检测

利用时间差判断是否代码停留时间过长,可判断是否在被动态调试,适合放在关键代码处运行,从某种意义上可以反制一定的调试手段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool isDebugged()
{
DWORD start, end;
__asm
{
rdtsc
mov start, eax
rdtsc
mov end, eax
}
if (end - start > 21)
{
// 时间较长则视为在被调试
return true;
}
return false;
}

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void keyFunction()
{
DWORD start, end;
start = GetTickCount();

/**
* 部分代码
*/

end = GetTickCount();
if (end - start > 100)
{
// 如果运行时间大于理想时间,则说明在调试中
}
else
{
/**
* 其他代码
*/
}
}

硬件断点检测

获取线程上下文,检测硬件断点,查看是否占用

以下 MSVC 代码可供参考:

1
2
3
4
5
6
7
8
9
10
11
12
bool isDebugged()
{
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext((HANDLE)-1, &ctx);

if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3)
{
return true;
}
return false;
}

异常检测

但当调试器捕获异常时,大多数调试器并不会立即将异常传递给被调试程序处理或是直接不传递。我们便可以将这种机制用来反调试。

但以下代码不一定运行成功——时代在发展,科技在进步。

一个字节的 Interrupt 3 中断

在所有会被调试器处理的异常中,interrupt 3 中断算是一个,它会生成一个单字节的断点。

1
2
3
4
5
6
7
8
9
10
11
12
bool isDebugged_Int3()
{
__try
{
__asm int 3
}
__except (1)
{
return false;
}
return true;
}

两个字节的 Interrupt 3 中断

使用 MSVC 内联汇编器的 _emit 伪指令可以生成一个两字节的interrupt 3指令。在测试的所有调试器中,只有 OnllyDbg 调试器识别这个异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool isDebugged_Int3_2Bytes()
{
__try
{
__asm
{
__emit 0xCD
__emit 0x03
}
}
__except (1)
{
return false;
}
return true;
}

Interrupt 0x2D 中断

如果执行了interrupt 0x2D,Windows将抛出一个断点异常

1
2
3
4
5
6
7
8
9
10
11
12
bool isDebugged_Int2d()
{
__try
{
__asm int 0x2d
}
__except (1)
{
return false;
}
return true;
}

GetLastError 检测

当出现异常时,winapi 往往会通过 GetLastError() 函数获取错误原因,而我们也可以通过 SetLastError() 来报错

其中 OutputDebugString() 函数用于在调试器中显示字符串,同时可以用来探测调试器是否存在。

我们可以通过将错误码重置,当使用 OutputDebugString() 函数时,若不存在调试器,则会发生错误,错误码被重新设置;若存在调试器,则之后的错误码则会跟我们重置的错误码相同,以此便可以判断调试器存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool isDebugged()
{
DWORD dwCode = 10086;
SetLastError(dwCode);
OutputDebugString(L"hacker!");
if (GetLastError() == dwCode)
{
return true;
}
else
{
return false;
}
}

利用 TLS 回调

以下 MSVC 代码可供参考:

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
// 自定义 TLS 回调函数
void NTAPI tls_callback(PVOID h, DWORD reason, PVOID pv)
{
if (IsDebuggerPresent())
{
MessageBoxA(NULL, "hacker!", 0, 0);
}
}

// 判断是 x86 还是 x64 系统
#ifdef _M_IX86
#pragma comment(linker, "/INCLUDE:__tls_used")
#pragma comment(linker, "/INCLUDE:__tls_callback")
#else
#pragma comment(linker, "/INCLUDE:_tls_used")
#pragma comment(linker, "/INCLUDE:_tls_callback")
#endif

// 创建一个 TLS 区段
EXTERN_C
#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
#else
#pragma data_seg (".CRT$XLB")
#endif
PIMAGE_TLS_CALLBACK _tls_callback[] = { tls_callback, 0 };
// 结束区段
EXTERN_C
#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
#else
#pragma data_seg (".CRT$XLB")
#endif

我们可以在 TLS 回调函数中,进行我们的反调试手段,如加壳、加密等

内存校验

将内存值进行 hash 得到哈希值 (如 CRC 算法等),当内存区段数据被调试,或者是被补丁后,内存值经过同一哈希函数得到的哈希值会发生改变,可以此来判断是否被调试。

注册表查询检测

以下为调试器在注册表中的一个常用位置。 32位系统: SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug 64位系统: SOFTWARE\Wow6432Node\Microsoft\WindowsNT\CurrentVersion\AeDebug 该注册表项指定当应用程序发生错误时,触发哪一个调试器。默认情况下,它被设置为Dr.Watson。如果该这册表的键值被修改为OllyDbg 或是其他调试器名,则代码就可以确定它正在被调试。

对数据加密

在程序开始前,不将正确的数据显露出来

当用以上技术并未检测到程序正在调试时,再将相应的数据进行解密

可结合 TLS 回调函数等技术

以下代码仅供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
char secret[256] = { 0x54, 0x65, 0x6a, 0x7f, 0x65, 0x4f, 0x21, 0x63, 0x4f, 0x78, 0x24, 0x7e, 0x74, 0x63, 0x20, 0x7d, 0x75 };

void init()
{
if (IsDebuggerPresent())
{
memcpy(secret, "hacker!", 8);
}
else
{
for (size_t i = 0; i < 17; ++i)
{
secret[i] ^= 0x10;
}
}
}

int main()
{
init();
puts(secret);

return 0;
}

自修改代码

将想要加密的 .text 代码区段进行加密

注意,需要使用 PE Editor 等软件将软件的 .text 代码区段改为可写

当程序运行时再进行解密,可以在一定程度上反动态调试

可结合 TLS 回调函数等技术

以下代码仅供参考:

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
void functionA()
{
;
}

void functionB()
{
;
}

void init()
{
if (IsDebuggerPresent())
{

}
else
{
LPBYTE start = (LPBYTE)functionA;
LPBYTE end = (LPBYTE)functionB;
while (start != end)
{
*start ^= 0x66;
}
}
}

int main()
{
init();
functionA();
functionB();

return 0;
}

攻击调试器漏洞

与所有软件一样,调试器也存在漏洞,有时恶意代码编写者为了防止被调试,会攻击这些漏洞。