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无能为力~

代码链接:

前言 上一次发布过的程序:【首发】检测文件的占用,具有学习和商业价值(By超用户),可以使用,仿电脑管家 正文 对于怎么枚举文件句柄 ,上一帖子对此有介绍,核心代码大概如下:如果 (ZwQueryObject (handle, #ObjectTypeInformation, unicode, 0, size) ≠ #STATUS_INVALID_HANDLE )' 只要不是无效的,为什么,详细看下面的注释 ' 参数 ' Handle ' 对象的一个句柄来获取信息。 ' ObjectInformationClass ' 指定一个OBJECT_INFORMATION_CLASS返回值的类型决定了信息在ObjectInformation缓冲区。 ' ObjectInformation ' 一个指向caller-allocated缓冲接收请求的信息。 ' ObjectInformationLength ' 指定的大小,以字节为单位,ObjectInformation缓冲区。 ' ReturnLength ' 一个指向变量的指针,接收的大小,以字节为单位,请求的关键信息。如果NtQueryObject STATUS_SUCCESS返回,返回的变量包含的数据量。如果NtQueryObject返回STATUS_BUFFER_OVERFLOW或STATUS_BUFFER_TOO_SMALL,您可以使用变量的值来确定所需的缓冲区大小。 ' 返回值 ' NtQueryObject返回STATUS_SUCCESS或适当的错误状。可能的错误状码包括以下: ' 返回代码 描述 ' STATUS_ACCESS_DENIED ' 有足够的权限来执行该cha询。 ' STATUS_INVALID_HANDLE ' 提供对象句柄无效。 ' STATUS_INFO_LENGTH_MISMATCH ' 信息长度不足以容纳数据。 unicode = 取空白字节集 (size) ZwQueryObject (handle, #ObjectTypeInformation, unicode, size, 0)' 读取信息的unicode文本 RtlUnicodeStringToAnsiString (ansi, unicode, 真)' 编码转换 ' RtlUnicodeStringToAnsiString例程将给定Unicode字符串转换成一个ANSI字符串。 str = 指针到文本 (ansi.Buffer) ' RtlFreeAnsiString常规版本存储由RtlUnicodeStringToAnsiString分配。 ' 参数 ' AnsiString ' 指针ANSI字符串缓冲区由RtlUnicodeStringToAnsiString以前分配的。 RtlFreeAnsiString (ansi) str = “无法获取”' 无效的怎么获取…… 返回 (str) 这一次呢更新了一个RemoteCloseHandle ,大概的原理是什么呢? 同时也采用了一些比较骚的方法,这方法的限制较多,但是对于32位进程就很有效果。 NtClose在MSDN的大概介绍 1. NtClose is a generic routine that operates on any type of object. 2. Closing an open object handle causes that handle to become invalid. The system also decrements the handle count for the object and checks whether the object can be deleted. The system does not actually delete the object until all of the object's handles are closed and no referenced pointers remain. 3. A driver must close every handle that it opens as soon as the handle is no longer required. Kernel handles, which are those that are opened by a system thread or by specifying the OBJ_KERNEL_HANDLE flag, can be closed only when the previous processor mo
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值