反跟踪技术上——侦测调试器

BeingDebugged标记

最简单也最常用的反调试方法就是使用IsDebuggerPresent函数了,它返回一个标志位BeingDebugged,这个标志位存在于PEB(进程环境块)中,偏移位置为0x2

IsDebuggerPresent(void)

{

return NtCurrentPeb()->BeingDebugged;

}

要找到BeingDebugged在当前进程的地址,就得先找到PEB的地址,PEB的地址是无法直接获取到的,我们可以通过TEB(线程环境块)来找到PEB。Windows在调入进程、创建线程时,操作系统会为每个线程分配TEB,且fs段寄存器总是被设置为使得地址fs:[0]指向当前线程的TEB数据。

在TEB结构偏移0x30处就是PEB的地址了。而偏移0x18处则指向了TEB本身。

加密与解密中提供了从外部代码去除某进程BeingDebugged标志的方式,可以作为调试器的插件去除这种反调试方法。但BeingDebugged标记在生成时还留下了一些后患。

LdrpInitialize函数是一个新进程的初始线程开始在用户态执行的最早代码,在这个函数内部有一段这样的代码:

if (Peb->BeingDebugged) {

Peb->NtGlobalFlag |= FLG_HEAP_ENABLE_FREE_CHECK |

FLG_HEAP_ENABLE_TAIL_CHECK |

FLG_HEAP_VALIDATE_PARAMETERS;

这说明BeingDebugged被设置为1后,NtGlobalFlag也被设置了一个标记,通过调试可发现这个标记为0x70。

那么NtGlobalFlag有没有向BeingDebugged一样留下痕迹呢?是有的,在初始化堆的函数RtlCreateHeap中可以找到相应代码。

if (NtGlobalFlag & FLG_HEAP_VALIDATE_PARAMETERS) {

Flags |= HEAP_VALIDATE_PARAMETERS_ENABLED;

}

...

if (DEBUG_HEAP(Flags)) {

return RtlDebugCreateHeap(Flags, ···);

}

DEBUG_HEAP是一个宏,总之在这里会返回1,然后进入RtlDebugCreateHeap函数。RtlDebugCreateHeap内部实际上又调用了RtlCreateHeap,不过这次Flags参数又多了几个属性,分别是HEAP_SKIP_VALIDATION_CHECKS, HEAP_TAIL_CHECKING_ENABLED, HEAP_FREE_CHECKING_ENABLED。第一个属性使得 RtlCreateHeap和RtlDebugCreateHeap不会再继续重复的相互调用。

与后两个标记有关的有这样一段代码,

if (Heap->Flags & HEAP_FREE_CHECKING_ENABLED) {

RtlFillMemoryUlong( (PCHAR)(BusyBlock + 1), Size & ~0x3, ALLOC_HEAP_FILL);

}

if (Heap->Flags & HEAP_TAIL_CHECKING_ENABLED) {

RtlFillMemory( (PCHAR) ReturnValue + Size,

CHECK_HEAP_TAIL_SIZE,

CHECK_HEAP_TAIL_FILL)

BusyBlock->Flags |= HEAP_ENTRY_FILL_PATTERN;

}

#define ALLOC_HEAP_FILL 0xBAADF00D

#define FREE_HEAP_FILL 0xFEEEFEEE

#define CHECK_HEAP_TAIL_FILL 0xAB

意思是会用这3种数据来填堆,即当堆中多次重复出现这3种数据时,就表示在调试状态中。这些东西通常被称为HeapMagic

除此之外,Flags在RltCreateHeap函数后面还有一些感染操作,使得Heap->Flags和Heap->ForceFlags都附带了调试信息,通常在程序正常执行的情况下,Flags为2,ForceFlags为0;而在调试状态下,Flags为0x50000062,ForceFlags为0x40000060

所以,从结论上来说,一个BeingDebugged标记使得NtGlobalFlag、HeapFlags、HeapForceFlags、HeapMagic都染上了调试状态信息,都可以作为反调试的手段,不过若是在一开始就处理掉BeingDebugged,就不会有后续发生了。

Native态的反调试

注:在用户态中,Zw和Nt是同一个函数的两个名字

CheckRemoteDebuggerPresent

除了IsDebuggerPresent函数外,微软还提供了一个用于检测调试器的函数,CheckRemoteDebuggerPresent,它的源码我就懒得写了,但一个事实是它与BeingDebugged无关,CheckRemoteDebuggerPresent函数调用了NtQueryInformationProcess函数去查询标号7的ProcessDebugPort标志,这个值是系统用来与调试器通信的端口句柄,若这个值不为0,就说明进程处于调试状态。若这个值为0,系统就无法对用户态调试器发送调试事件通知,调试器也就无法工作了。且这个端口句柄是只有为0时,才能进行设置,若已经不为零了,是不可以进行修改的,所以不能通过直接修改这个标志方法来达到反调试的目的。

ThreadHideFromDebugger

可以通过ZwSetInformationThread函数来设置线程的某些属性,这个函数的第2个参数时THREADINFOCLASS结构,它是一个枚举类型,最后一个标号字段就是ThreadHideFromDebugger,通过为线程设置ThreadHideFromDebugger,可以禁止某线程产生调试事件。

那么到底是怎么让调试器停止工作的呢?这里也不是修改了DebugPort,但是却达到了同样的效果,它执行了一行代码Thread->HideFromDebugger = true;

而在另一个函数中表现出了HideFromDebugger与DebugPort的关系

Port = PsGetCurrentThread()->HideFromDebugger ? NULL : Process->DebugPort;

if ( !Port || KeGetPreviousMode() == KernelMode ){

return;

}

...之后的代码是发送LOAD_DLL_DEBUG_EVENT事件

所以无论是DebugPort为0,或是HideFromDebugger为ture,这个函数都会直接从中间返回,系统不会对调试器发送LOAD_DLL_DEBUG_EVENT事件,也对之后的事一无所知。

DebugObject

DbgUiConnectToDbg函数是调试器与被调试进程建立调试关系一定会调用的函数,在DbgUiConnectToDbg函数中,有这样一行代码

if (NtCurrentTeb()->DbgSsReserved[1]) return STATUS_SUCCESS;

说明调试器创建了一个DebugObject对象,并存储在NtCurrenTeb()->DgbSsReserved[1]中,可以通过检测这个标志来判断调试状态

DebugObject是通过ObCreateObject来生成的,所以与DebugObject相关的API就是ZwQueryObject了,这个函数查询并返回内核对象信息,声明如下:

ZwQueryObject( __in_opt HANDLE Handle,

__in OBJECT_INFORMATION_CLASS ObjectInformationClass,

__out_opt PVOID ObjectInformation,

__in ULONG ObjectInformationLength,

__out_opt PULONG ReturnLength )

这个函数的使用方法挺麻烦的,这也是Windows API的特性吧。。。要使用该函数必须指定第二个参数,即ObjectInformationClass,它是一个枚举类型,标号3为ObjectAllTypesInformation,返回系统所有对象信息,并把链表指针返回给第三个参数。

使用方式:

1.先获取链表大小

ZwQueryObject(NULL, ObjectAllTypesInformation, &Size, sizeof(Size) );

2.再分配内存

void *Buf = NULL;

Buf = VirtualAlloc(NULL, lSize, MEM_REERVE | MEM_COMMIT, PAGE_READWRITE);

3.获取内核对象链表(本质上是结构体数组吧)

ZwQueryObject(NULL, ObjectAllTypesInformation, Buf, Size, NULL);

POBJECT_ALL_INFORMATION p = (POBJECT_ALL_INFORMATION)Buf;

4.遍历方法

UCHAR *pObjectLocationInfo = (UCHAR*)p->ObjectTypeInformation

pObgjectTypeInfo = (POBJECT_TYPE_INFORMATION)pObjectLocationInfo

pObjectLocationInfo = (UCHAR*)pObjectTypeInfo->TypeName.Buffer

pObjectLocationInfo += pObjectTypeInfo->TypeName.Length

pObjectLocationInfo=(UCHAR*)(((ULONG)pObjLocationInfo & ~3)+sizeof(ULONG))

遍历时,只需判断pObjectTypeInfo->TypeName.Buffer与"DebugObject"字符串是否相等即可,这样就能获得系统中调试器的数量。

SystemKernelDebuggerInformation

ZwQuerySystemInformation函数可以用来查询是否有系统调试器的存在,只需要把第一个参数设置为35,表示SystemKernelDebuggerInformation,然后第二个参数是一个结构体SYSTEM_KERNEL_DEBUGGER_IFNORMATION的指针,用于接收返回的状态信息

TLS回调函数反调试

所谓TLS回调函数是指,每当创建/终止进程的线程时会自动调用执行的函数,包括主线程的创建同样如此。TLS回调函数之所以能用来反调试,就是因为它的执行先于EP,也就是说,如果未听说过这个技术,那么程序根本到不了入口点就已经被反调了。

在使用TLS回调函数时,要用到以下预处理指令

#pragma comment(linker, "/INCLUDE:__tls_used")

预处理器通过这条指令告诉链接器要使用TLS

#pragma data_seg(".CRT$XLX")

#pragma data_seg()

CRT表示C Runtime机制,X表示随机数,L表示TLS Callback section,最后一个X可以换成B-Y的任意字符

在实际的测试过程中,我发现vs2017的debug版本可以成功运行,但release版本却不行,Google一番后,发现再多添一行预处理指令可以解决:

#pragma comment (linker, "/INCLUDE:_p_tls_callback")

p_tls_callback的定义是下面这样的:

PIMAGE_TLS_CALLBACK p_tls_callback[] = { tls_callback1 };

即必须告诉链接器TLS函数地址表的位置才行,不过具体原因不是很清楚。。。

除了在写代码时使用TLS,也可以后期修改可执行程序,只要对PE文件结构熟悉,就很容易做到,以后有机会可以尝试以下。

ETC

显然,还有一种十分简单的反调试方式,它不对系统或进程进行调试状态的检查,而是对我们常用的调试工具进行检测,通常可以通过检测进程名并将其与ida,ollydbg,x64dbg这样的字符串进行比较,并能直接通过常用的Windows API来完成这些操作,如FindWindow(),CreateToolhelp32Snapshot()这样的函数,甚至可以通过检测计算机名(GetComputerName()),可执行文件路径(GetCommandLine())以及虚拟机进程名等来判断测试情况。

不过相对的,容易使用的反调试自然也容易被破解,这种基于对比明文的反调试如果没有进行信息隐藏,直接patch掉用来对比的关键字符串即可

猜你喜欢

转载自blog.csdn.net/qq_35713009/article/details/86603668
今日推荐