2种内核级反用户态调试方法

0.前言

    很久前写过一些应用层的反调试的文章,这类反调试方法的好处是易于实现,但缺点是很容易被绕过----保护因此失效。我们的危机公关手段是:将反调试功能放到内核中用驱动程序实现,以增强程序的反调试能力。由于vista后patch guard的影响,因此本文以Xp系统为例,其他系统需要对代码中的硬编码进行调整。

1.调试对象(DEBUG_OBJECT)介绍

    当调试器创建进程时,会创建一系列调试消息,如:进程创建消息,模块加载消息。对于调试器附加到运行中进程,亦会依次产生这些消息(确切的说是杜撰调试消息)。内核将这些消息通过Debug Port对象发送给调试器。另外当被调试进程触发异常时,内核收到异常消息后,亦会通过Debug Port把消息传给调试器。可见Debug Port是联系调试器和被调试器的纽带。内核会为调试器进程创建一个DEBUG_OBJECT对象,并将对象地址保存在进程_EPROCESS!DebugPort中;当调试器创建/附加被调试进程时,内核会把同一个DEBUG_OBJECT对象的地址保存到被调试进程的_EPROCESS!DebugPort字段。
kd> dt _EPROCESS -y DebugPort
nt!_EPROCESS
   +0x0bc DebugPort : Ptr32 Void

2.方法1:DebugPort清零    

    基于上面的介绍,我们可以这样实现反调试功能:检测所有进程的_EPROCESS!DebugPort字段,如果发现该字段非空,可以当前系统中至少存在着调试器。出于演示目的,我简单的把_EPROCESS!DebugPort字段清0,这就把调试器或者被调试进程进行消息传递的纽带的一端给切断了。下面来看下代码:
#include <Ntifs.h>
#include <wdm.h>

#ifdef __cplusplus
extern "C" {
#endif

KSTART_ROUTINE DetectDbgThd;
HANDLE detectThdHnd = 0UL;

VOID DriverUnload(PDRIVER_OBJECT);

NTSTATUS DriverEntry(PDRIVER_OBJECT drvObj, PUNICODE_STRING regPath)
{
	NTSTATUS ldStatus = STATUS_SUCCESS;
	OBJECT_ATTRIBUTES thdAttr;
	CLIENT_ID cid;

	_asm int 3;

	UNREFERENCED_PARAMETER(regPath);

	drvObj->DriverUnload = DriverUnload;

	memset(&thdAttr, 0, sizeof(OBJECT_ATTRIBUTES));
	thdAttr.Length = sizeof(OBJECT_ATTRIBUTES);
	ldStatus = PsCreateSystemThread(&detectThdHnd, 0,
		&thdAttr, 0,
		&cid,
		DetectDbgThd, NULL);
	if (!NT_SUCCESS(ldStatus))
		return ldStatus;

	return STATUS_SUCCESS;
}

VOID DriverUnload(IN PDRIVER_OBJECT drvObj)
{
	UNREFERENCED_PARAMETER(drvObj);
	ZwClose(detectThdHnd);
	return;
}

PLIST_ENTRY GetPsActiveProcessHeadAddr()
{
	PLIST_ENTRY PsActiveProcessHeadAddr = NULL;
	NTSTATUS status;
	PEPROCESS eProc = NULL;

	status = PsLookupProcessByProcessId((HANDLE)4, &eProc);
	if (!NT_SUCCESS(status))
		return NULL;
#ifdef WIN32_XP		
	PsActiveProcessHeadAddr = ((PLIST_ENTRY)(((char*)eProc) + 0x88))->Blink;
#elif WIN32_7
	PsActiveProcessHeadAddr = ((PLIST_ENTRY)(((char*)eProc) + 0xb8))->Blink;
#endif
	//PsActiveProcessHeadAddr = eProc->ActiveProcessLinks->Blink;
	ObDereferenceObject(eProc);

	return PsActiveProcessHeadAddr;
}

VOID DetectDbgThd(void* thdCtx)
{
	PEPROCESS eProc = NULL;
	PLIST_ENTRY PsActiveProcessHeadAddr = NULL;
	LIST_ENTRY* pos = NULL;
	LARGE_INTEGER period = RtlConvertLongToLargeInteger(-10*1000);
	DWORD32* procDebugPortAddr = NULL;
	UNREFERENCED_PARAMETER(thdCtx);

	PsActiveProcessHeadAddr = GetPsActiveProcessHeadAddr();
	if (!PsActiveProcessHeadAddr)
		return;

	while(1)
	{
		pos = PsActiveProcessHeadAddr->Blink;
		while (pos != PsActiveProcessHeadAddr)
		{
			//eProc = (PEPROCESS)CONTAINER_OF(pos, EPROCESS, ActiveProcessLinks);
#ifdef WIN32_XP
			eProc = (PEPROCESS)((char*)pos - 0x88);
#elif WIN32_7
			eProc = (PEPROCESS)((char*)pos - 0xb8);
#endif
			//0xBC==EPROCESS!DebugPort
#ifdef WIN32_XP	
			procDebugPortAddr = (DWORD32*)(((char*)eProc) + 0xBC);
#elif WIN32_7
			procDebugPortAddr = (DWORD32*)(((char*)eProc) + 0xeC);
#endif			
			if(*procDebugPortAddr != 0x00UL)
			{
				//KillProcess(eProc);
				*procDebugPortAddr = 0x00UL;
			}

			pos = pos->Blink;
		}
		
		KeDelayExecutionThread(KernelMode, FALSE, &period);
	}
}

#ifdef __cplusplus
}
#endif
    DriverEntry函数仅仅创建DetectDbgThd线程后就退出。GetPsActiveProcessHeadAddr用以获得系统中进程链表地址。DetectDbgThd每间隔1s遍历系统中的所有进程,并获得进程对应的_EPROCESS结构。进程通过_EPROCESS!ActiveProcessLinks字段加入PsActiveProcessHead形成链表。由于DDK并没有导出这个结构,所以要通过windbg获取字段偏移,并在代码中硬编码,以下是Xp sp3该域的偏移:
kd> vertarget
Windows XP Kernel Version 2600 (Service Pack 3) UP Free x86 compatible
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 2600.xpsp.080413-2111
kd> dt _EPROCESS -y ActiveProcessLinks
nt!_EPROCESS
   +0x088 ActiveProcessLinks : _LIST_ENTRY
获得ActiveProcessLinks域的地址后可以马上获得_EPROCESS对象地址和_EPROCESS!DebugPort地址,之后直接把_EPROCESS!DebugPort指针值改为0即可实现DebugPort清零。
kd> dt _EPROCESS -y DebugPort
nt!_EPROCESS
   +0x0bc DebugPort : Ptr32 Void
    让我们加载驱动,以调试calc.exe为例,看下反调试效果(上面代码编译后生成的驱动名为DetectDbg.sys):
图1.加载驱动,并分别启动windbg.exe和calc.exe,准备附加到进程。

图2.windbg附加到calc后,每次中断调试目标,都会显示红框中的内容,意思是 调试器进入了挂起中断状态(准调试状态,对这个状态的解释可以参考张银奎<软件调试> 第10.6.7节)此时calc虽然处于被挂起状态,可以查看内存值,但不能被跟踪(如单步)和下断点。再次运行(F5)调试目标,calc恢复到运行状态。
附注:调试进程被挂起和恢复是因为调试子系统调用了DbgkpSuspendProcess/DbgkpResumeProcess将calc.exe挂起/恢复,这不需要经过DebugPort即可实现。至于可以查看内存值,是因为windbg只需通过ReadProcessMemory就能获得calc的内存值,也不需要DebugPort参与。

3.方法2:Teb!DbgSsReserve句柄清零

   这个方法其实是上面DebugPort清零的衍生版,网上暂时没有找到同样的实现,应该独我一家 生气(没仔细找是否有雷同)~不过这个标志位和DebugPort有点区别,首先只有调试器进程才有这个标志位;其次,它位于用户空间,由TEB保存。更进一步讲,位于调试器工作线程内部,调试器的UI线程也没有这个标志位;最后,它是一个句柄,指向内核为调试器进程创建的DEBUG_OBJECT对象。
    要实现Teb!DbgSsReserve句柄清零,首先要搜索具有调试特征的进程;搜索到目标进程后,要从当前线程空间Attach到目标进程空间,这样才能从目标进程空间读到有效的内存(虚拟内存);最后,用ZwClose关闭句柄。下面,我们来看下代码:
VOID DetectDbgThd(void* thdCtx)
{
	PEPROCESS eProc = NULL;
	PETHREAD eThd = NULL;
	PLIST_ENTRY PsActiveProcessHeadAddr = NULL;
	LIST_ENTRY* procPos = NULL;
	LIST_ENTRY* thdPos = NULL;
	LIST_ENTRY thdListHead;
	KAPC_STATE apcState;
	LARGE_INTEGER period = RtlConvertLongToLargeInteger(-10*1000);
	DWORD32* thdTebAddr = NULL;
	DWORD32* dbgSsReserved = NULL;
	
	UNREFERENCED_PARAMETER(thdCtx);

	PsActiveProcessHeadAddr = GetPsActiveProcessHeadAddr();
	if (!PsActiveProcessHeadAddr)
		return;

	while(1)
	{
		procPos = PsActiveProcessHeadAddr->Blink;
		while (procPos != PsActiveProcessHeadAddr)
		{
			//eProc = (PEPROCESS)CONTAINER_OF(pos, EPROCESS, ActiveProcessLinks);
#ifdef WIN32_XP
			//+0x088 EPROCESS!ActiveProcessLinks : _LIST_ENTRY
			eProc = (PEPROCESS)((char*)procPos - 0x88);
#endif

#ifdef WIN32_XP
			//+0x190 EPROCESS!ThreadListHead : _LIST_ENTRY
			//thdListHead = (LIST_ENTRY*)((char*)eProc+0x190);
			//memcpy(&thdListHead,((char*)eProc+0x190),sizeof(LIST_ENTRY));
#endif
			//pos = ListHead->Blink;
			thdPos = (LIST_ENTRY*)(*(DWORD32*)((char*)eProc+0x190));
			//while(pos != &ListHead)
			while(thdPos != (LIST_ENTRY*)((char*)eProc+0x190))
			{
#ifdef WIN32_XP
				//+0x22c ETHREAD!ThreadListEntry  : _LIST_ENTRY
				eThd = (PETHREAD)((char*)thdPos - 0x22c);
				//+0x020 ETHREAD!KTHREAD!Teb : Ptr32 Void
				thdTebAddr = (*(DWORD32*)((char*)eThd+0x20));
				if(!thdTebAddr)
					goto Next;
#endif
				KeStackAttachProcess(eProc, &apcState);

#ifdef WIN32_XP
				//+0xf20 Teb!DbgSsReserved    : [2] Ptr32 Void
				dbgSsReserved = ((char*)thdTebAddr+0xf20);
#endif				
				/*
				DbgSsReserved[0]:is reserved
				DbgSsReserved[1]:for debuggee process, DbgSsReserved[0] stand for handler of debug-object
				*/
				if(dbgSsReserved[1])
				{
					ZwClose(dbgSsReserved[1]);
					dbgSsReserved[1] = 0x00UL;
				}
				
				KeUnstackDetachProcess(&apcState);
			Next:					
				thdPos = thdPos->Blink;
			}
			
			procPos = procPos->Blink;
		}
		
		KeDelayExecutionThread(KernelMode, FALSE, &period);
	}
}
加载上面的代码后,调试器就无法打开和附加到目标进程。不过有个限制windbg.exe的实现有点特殊,DbgSsReserve字段一直为空,所以上面的代码对windbg无能为力~

代码链接:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值