note20210514

反跟踪技术

BeingDebugged

BeingDebugged标志位

Win32 API IsDebuggedPresent可以检测当前进程是否处于调试状态:

1
2
3
BOOL APIENTRY IsDebuggerPresent(VOID){
return NtCurrentPeb()->BeingDebugged;
}

该函数通过读取PEB的BeingDebugged标志位来检测当前进程是否处于调试态,BeingDebugged标志位在PEB的0x002偏移处。

而PEB在TEB的偏移0x30处,又因为TEB被寄存器fs:[18]指向,所以PEB的地址获取方式如下:

1
2
mov eax,fs:[18h]
mov eax,[eax+0x30h]

又因为fs:[18h]其实指向自身,所以上面两句可以用一句mov eax,fs:[0x30]替代,结合BeingDebugged偏移,可以得到BeingDebugged获取方式:

1
2
mov eax,fs:[0x30h]
mov eax,[eax+0x02]

具体实现如下:

1
2
3
4
5
6
7
8
9
10
BOOL is_beingdebugged() {
BYTE flag;
__asm {
push fs:[0x30]
pop eax
movzx al,byte ptr [eax+2]
mov flag,al
}
return flag;
}

NtGlobalFlag标志位

Windows在设置了BeingDebugged标志位后还会更改NtGlobalFlag标志位,因为没设置BeingDebugged之前该标志位的值为0x70,该标志位相对于PEB结构偏移为0x68。根据该标志位得到另一种检测进程是否处于调试态的方法:

1
2
3
4
5
6
7
8
9
10
11
BOOL is_beingdebugged_1() {
BOOL flag;
__asm {
push fs : [0x30]
pop eax
movzx eax, byte ptr[eax + 0x68]
and eax,0x70
mov flag, eax
}
return flag;
}

HeapMagic

调试态下创建的chunk会包含一些Magic标记,包括0xABABABAB0xBAADF00D0xFEEEFEEE,所以可以创建一个chunk然后查找这些Magic标记来判断当前进程是否处于调试态。

代码如下:

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
LPVOID GetHeap(SIZE_T nSize) {
return HeapAlloc(GetProcessHeap(), NULL, nSize);
}

BOOL is_beingdebugged_3() {

//HeapMagic
LPVOID HeapPtr;
PDWORD ScanPtr;
ULONG nMagic = 0;

HeapPtr = GetHeap(0x100);
ScanPtr = (PDWORD)HeapPtr;

try {
for (;;) {
switch (*ScanPtr++) {
case 0xABABABAB:
case 0xBAADF00D:
case 0xFEEEFEEE:
nMagic++;
cout << nMagic << endl;
break;
default:
break;
}
}

}
catch (...) {
return (nMagic > 10) ? TRUE : FALSE;
}

}

chunk创建完毕可以看到Magic标志:

除了上述的这几个标志,还有一些标志也因为BeingDebugged的设置而被“污染”,如进程堆的FlagsForceFlags等,还有一些PEB里的结构。

源头消灭BeingDebugged

方法是在编写调试器时,在第二次LOAD_DLL_DEBUG_EVENT时,将BeingDebugged设置为TRUE,然后停在系统断点处,继而消除BeingDebugged

回归Native

CheckRemoteDebuggerPresent

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
void ckremotedebugger_test() {

HANDLE hProcess = GetCurrentProcess();
BOOL bDebuggerPresent = FALSE;

BOOL check_debugger = CheckRemoteDebuggerPresent(hProcess, &bDebuggerPresent);

if (bDebuggerPresent)
cout << "debugged" << endl;
else
cout << "not debugged" << endl;

}

使用该函数进行反调试,攻击者将BeingDebugged标志位设置为0仍然会检测到进程处于调试态:

该函数关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
76722C84    6A 00           push 0x0
76722C86 6A 04 push 0x4
76722C88 8D45 FC lea eax,dword ptr ss:[ebp-0x4]
76722C8B 50 push eax
76722C8C 6A 07 push 0x7
76722C8E FF75 08 push dword ptr ss:[ebp+0x8]
76722C91 FF15 F0C27476 call dword ptr ds:[<&ntdll.NtQueryInformationProcess>] ; ntdll.ZwQueryInformationProcess
76722C97 85C0 test eax,eax
76722C99 79 09 jns short KernelBa.76722CA4
76722C9B 8BC8 mov ecx,eax
76722C9D E8 AED6F5FF call KernelBa.76680350
76722CA2 EB 17 jmp short KernelBa.76722CBB
76722CA4 33C0 xor eax,eax
76722CA6 3945 FC cmp dword ptr ss:[ebp-0x4],eax
76722CA9 0f95c0 setne al
76722CAC 8906 mov dword ptr ds:[esi],eax
76722CAE 33C0 xor eax,eax
76722CB0 40 inc eax
76722CB1 EB 0A jmp short KernelBa.76722CBD
76722CB3 6A 57 push 0x57
76722CB5 FF15 C8C07476 call dword ptr ds:[<&ntdll.RtlSetLastWin32Error>] ; ntdll.RtlSetLastWin32Error

关键函数:NtQueryInformationProcess(dword ptr ss:[ebp+0x8],0x7,0x4,0x0),根据该函数的返回值1或者0来判断是否被调试,其中0x7代表查询ProcessDebugPort,该参数可以通过使用NtSetInformationProcess函数来设置为0,从而使调试器无法与被调试进程通信以达到反调试的目的,但是这个参数只能在为0情况下被设置。

ThreadHideFromDebugger

测试代码:

1
2
3
4
5
6
7
8
9
10
11
typedef DWORD(WINAPI* ZW_SET_INFORMATION_THREAD)(HANDLE, DWORD, PVOID, ULONG);
void threadhidedebugger_test() {

HMODULE hModule;
ZW_SET_INFORMATION_THREAD ZwSetInformationThread;

hModule = GetModuleHandleA("Ntdll");
ZwSetInformationThread = (ZW_SET_INFORMATION_THREAD)GetProcAddress(hModule,"ZwSetInformationThread");
ZwSetInformationThread(GetCurrentThread(),17,0,0);

}

ZwSetInformationThread函数通过将HideFromDebugger结构设置为True, 来达到类似于设置ProcessDebugPort0x0从而使调试器和被调试进程断开的目的。

DebugObject

调试器与被调试进程建立关系有两种途径:

  1. 创建进程时设置DEBUG_PROCESS
  2. 调用DebugActiveProcess附加到某个已经运行的进程上。

DebugActiveProcess函数是对DbgUiConnectToDbg函数的Wrapper,DbgUiConnectToDbg函数创建一个DebugObject,并存储到NtCurrentTeb()->DbgSsReserved[1]。因此可以通过检测NtCurrentTeb()->DbgSsReserved[1]来判断是否存在调试器,若NtCurrentTeb()->DbgSsReserved[1]不为空就说明当前进程是一个用户态调试器的进程。

SystemKernelDebuggerInformation

函数ZwQuerySystemInformation的参数SystemInformation被设置为SystemKernelDebuggerInformation时,可以判断是否有系统调试去存在。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION
{
BOOLEAN DebuggerEnabled;
BOOLEAN DebuggerNotPresent;
}SYSTEM_KERNEL_DEBUGGER_INFORMATION,*PSYSTEM_KERNEL_DEBUGGER_INFORMATION;
typedef DWORD(WINAPI* ZW_QUERY_SYSTEM_INFORMATION)(DWORD,PVOID,ULONG,PULONG);
BOOL systemkerneldebuggerinformation_test() {

HINSTANCE hModule = GetModuleHandleA("Ntdll");
ZW_QUERY_SYSTEM_INFORMATION ZwQuerySystemInformation = \
(ZW_QUERY_SYSTEM_INFORMATION)GetProcAddress(hModule, "ZwQuerySystemInformation");
SYSTEM_KERNEL_DEBUGGER_INFORMATION Info = {0};

ZwQuerySystemInformation(35,&Info,sizeof(Info),NULL);

return (Info.DebuggerEnabled && !Info.DebuggerNotPresent);
}

用于检测以Debug方式启动的系统。

Hook & AntiHook

主要是Windows2000及Win9.X的情形,许多调用规则已经不一样了。。。

小节

文章最后给了一种通用调用ThreadHideFromDebugger的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
push    0
push 0
push 11
push -2
mov eax, 0C7
mov edx, esp
int 2E
mov eax, 0E5
mov edx, esp
int 2E
mov eax, 0EE
mov edx, esp
int 2E
mov eax, 136
mov edx, esp
int 2E
add esp, 10

小技巧

检测OD

  1. 特征码

OD 1.1的几个特征码:

1
2
0x401126->83 3D 1B 01
0x43AA7C->8D 8E 83 21
  1. 检测DBGHELP模块

检测一个进程有无加载DBGHELP.DLL。

  1. 查找窗口

FindWindowEnumWindowGetForeGroundWindow这几个方法可以查找窗口。

  1. 直接查找进程Ollydbg.exe

  2. SeDebugPrivilege方法

打开CSRSS.EXE进程间接使用SeDebugPrivilegel来判断进程是否被调试了。

  1. SetUnhandledExceptionFilter方法

  2. EnableWindow方法

使用EnabelWindow方法暂时锁定前台的窗口,此时调试器无法工作。

  1. BlockInput方法

调用BlockInput锁住键盘。

防止调试器附加

RING3调试器附加使用的是DebugActiveProcess函数,附加相关进程是先致谢ZwContinue函数,最后停在DbgBreakPoint函数。因此只需要对ZwContinue函数以及DbgBreakPoint函数进行检测就可以了。

父进程检测

正常程序启动时父进程应该是“Exploer.exe”、“cmd.exe”、“Services.exe”中的一个,因此只需要对一个进程的父进程进行检测,若不是上面的三个之一就可以认为被调试了。

绕过方法比较简单,跳过检测函数即可。

时间差

若程序开启的时间比较长,就可以认为一个程序被追踪了,当然,误报率比较高,所以可以作为其他检测的一个开启条件。

Trap Flag 检测

1
2
3
4
5
6
pushfd;
push eflags
or dword ptr [esp],100h;
popfd ;TF = 1
nop ;在这里安装SEH
jmp die ;执行到这里说明程序被追踪

双进程保护

RING3级别的调试中,一个进程只有一个调试器,因此可以使用该方法进行双进程保护。