在edr或者其他类型的安全软件我们通常要监测当前系统的内核驱动的加载,通常使用的方法是PsSetLoadImageNotifyRoutine设置模块加载回调例程来监控ring3模块以及ring0模块的加载,回调函数 eLoadImageNotifyRoutine 的第二个参数判断,如果 PID是0 ,则表示加载驱动,如果PID非零,则表示加载DLL。此方法的优点是:
更底层 方法简单通用
缺点当然也就是函数太底层,第二就是方法太通用几乎做过进程、线程监控的搞安全内核开发的人基本都晓得,也很容易被发现,而且也会被摘链,而失效。第三到回调函数这步骤的时候有可能内核已经被加载,被加载的内核驱动的入口点已经执行完毕。 本篇文章将会探索一种新方法去监测并且控制内核模块的加载。首先我们要讲解内核加载驱动的过程。 写一个demo的驱动,然后使用VMware双机调试来调试驱动。(VMware双机调试的方法如果不会可以baidu)
连接被调试虚拟机后,在windbg里输入sxe ld demo驱动的名字.sys
然后go,如果加载系统要加载这个驱动windbg会自动停下来。
然后输入kb
可以看到内核里加载的时候会开启一个单独的线程去加载驱动
00 fffff80004b1748d : fffff880
0456b8a0 fffff880031ac0d0 00000000
00000001 fffff80004b74dfe : nt!DebugService2+0x5
01 fffff800
04b74ecb : fffff880031ac000 fffffa80
016de070 fffff8800456b9b8 00000000
00000007 : nt!DbgLoadImageSymbols+0x4d 02 fffff80004e47bfd : fffffa80
00eeee20 fffff8a00000001c fffff800
04d84a30 fffff8800456b888 : nt!DbgLoadImageSymbolsUnicode+0x2b
03 fffff800
04e6286b : fffff880031ac000 fffff880
0456b8f8 0000000000000000 fffff880
0456b8d8 : nt!MiDriverLoadSucceeded+0x2bd 04 fffff80004e64ebd : fffff880
0456b9b8 0000000000000000 00000000
00000000 0000000000000000 : nt!MmLoadSystemImage+0x80b
05 fffff800
04e65875 : 0000000000000001 00000000
00000000 0000000000000000 fffffa80
0231c1e0 : nt!IopLoadDriver+0x44d 06 fffff80004a8b161 : fffff800
00000000 ffffffff8000077c fffff800
04e65820 fffffa80006db040 : nt!IopLoadUnloadDriver+0x55
07 fffff800
04d21166 : 0000000000000000 fffffa80
006db040 0000000000000080 fffffa80
006b71d0 : nt!ExpWorkerThread+0x111 08 fffff80004a5c486 : fffff800
04bf6e80 fffffa80006db040 fffffa80
006da680 0000000000000000 : nt!PspSystemThreadStartup+0x5a
09 00000000
00000000 : fffff8800456c000 fffff880
04566000 fffff8800456ae60 00000000
00000000 : nt!KiStartSystemThread+0x16 这是调试的时候被断点断下来的堆栈,我们需要回到加载驱动的地方,所以要打开源代码,在驱动的入口点DriverEntry按F9设置断点。
然后f5继续执行,之后就会停在驱动的入口点
紫红色表示已经运行到断点位置。 当我们使用!process 命令时会看到当前上下文是system
再次使用kb可以发现现在执行到入口点的栈的上下文是
01 fffff8800456b960 fffff800
04e65875 nt!IopLoadDriver+0xa07 02 fffff8800456bc30 fffff800
04a8b161 nt!IopLoadUnloadDriver+0x55 03 fffff8800456bc70 fffff800
04d21166 nt!ExpWorkerThread+0x111 04 fffff8800456bd00 fffff800
04a5c486 nt!PspSystemThreadStartup+0x5a 05 fffff8800456bd40 00000000
00000000 nt!KiStartSystemThread+0x16 可以发现在 nt!IopLoadDriver+0xa07的位置是执行入口点 使用U命令,可以查看汇编代码
fffff80004e6546e 488bd6 mov rdx,rsi fffff80004e65471 488bcb mov rcx,rbx fffff80004e65474 ff5358 call qword ptr [rbx+58h] fffff80004e65477 4c8b15627bdaff mov r10,qword ptr [nt!PnpEtwHandle (fffff80004c0cfe0)] fffff80004e6547e 8bf8 mov edi,eax call qword ptr [rbx+58h]这句代码就是执行被加载驱动的模块入口点函数
看汇编代码第一个参数rcx就是rbx,大致我们可以明确的就是rbx就是DriverEntry的DRIVER_OBJECT参数,所以就有了rbx+58h就是DRIVER_OBJECT的DriverInit,为了印证我们的猜测,在IDA下看rbx就是DRIVER_OBJECT的结构体,而这里的call执行的就是DriverInit
Call v29->DriverInit(v29, v32);
有了这样的过程,我们是不是就可以探索新的方法去控制驱动的加载呢?答案是肯定的。查看IDA,可以发现在执行驱动入口点之前这个过程内核会处理很多东西,比如分配内存啊,创建驱动的内核对象啊,驱动的权限的判断,创建内核镜像啊等等操作,凡是可以控制的地方我们都可以研究下,今天我们主要研究是内核Object这个东西,众所周知windows内部管理着很多的Object,windows专门有个内核对象管理器,我们通常说的文件、进程、线程、管道、油槽、内核Image等等都属于Object,windows在使用额时候总是会先CreateObject 然后在插入这个Object到object管理器中,成功了才会继续执行,所以在加载内核模块镜像的时候也一定会创建一个Object,然后在插入这个对象。
因为DRIVER_OBEJCT就是一个Object,我们可以通过追踪DRIVER_OBEJCT的生成来劫持控制驱动的加载。
查看IDA分析过程,对IopLoadDriver的函数分析可以发现,在调用入口点之上确实有个ObInsertObject的函数,而且该函数插入的就是DERIVER_OBJECT对象
v10 = ObInsertObject(v21, 0i64, 1u, 0, 0i64, &Handle);
有了这个函数我们就可以控制对象了,怎么控制呢?答案很简单,对象的回调函数。
ObInsertObject的函数内部是会经过每种对象类型对象的回调函数设置的。下面分析怎么到达过滤回调callback的。
在ObInsertObject内部会先调用ObInsertObjectEx函数
在ObInsertObjectEx内部会调用ObpCreateHandle
此时的第一个参数是0,而在ObpCreateHandle函数内部会调用 v51 = ObpPreInterceptHandleCreate(Objecta, Attributes, &v70, &ThreadCallbackListHead);
ObpPreInterceptHandleCreate函数就是我之前说的在调用当前对象类型的callback函数。
所以调用路径是
有ObInsertObject就一定会有ObCreateObject这个函数,往上继续翻阅就会看到
v10 = ObCreateObject( KeGetCurrentThread()->PreviousMode, IoDriverObjectType, &ObjectAttributes, 0, 0i64, 0x188u, 0, 0, &v74), 创建的是IoDriverObjectType这种类型的对象,理论上是可以在代码上对IoDriverObjectType注册callback,而且这个对象微软是可以外部直接链接的,不需要使用搜索的方法去寻找这个对象类型,下面就是检验理论猜想。
在之前的demo实例在加上一段代码注册回调
Globals.ob_operation_registrations.ObjectType = IoDriverObjectType; Globals.ob_operation_registrations.Operations |= OB_OPERATION_HANDLE_CREATE; Globals.ob_operation_registrations.Operations |= OB_OPERATION_HANDLE_DUPLICATE; Globals.ob_operation_registrations.PreOperation = CBTdPreOperationCallback; Globals.ob_operation_registrations.PostOperation = CBTdPostOperationCallback; Globals.ob_registration.Version = ObGetFilterVersion(); Globals.ob_registration.OperationRegistrationCount = 1; //CBObRegistration.Altitude = CBAltitude; Globals.ob_registration.RegistrationContext = NULL; Globals.ob_registration.OperationRegistration = &(Globals.ob_operation_registrations);
Status = ObRegisterCallbacks ( &(Globals.ob_registration), &(Globals.registration_handle) // save the registration handle to remove callbacks later ); if ( NT_SUCCESS(Status)) { Globals.ob_protect_installed = TRUE; } 然后继续使用双机调试,在入口点下断点
进入断点后,继续单步F10,执行过注册后status返回0表示注册成功(注意这里我是用了一些hack手法所以成功了,方法暂时不公开)
显示驱动启动成功
在我们设置的回调函数的地方下断点,看看加载驱动的时候是否会进入回调,在虚拟机里安装sysmon这个软件,他是会加载一个文件驱动的。
成功断下来了
查看PreInfo->Object 使用dt nt!_DRIVER_OBJECT PreInfo->Object
确实是sysmon的驱动sysmonDrv,他的入口点是sysmonDrv+1e058,看来这个方法确实有效,接下来我们要把driver_init设置为0,尝试修改 Eq xxxxxxx 0 修改成0
接下来直接f5,成功蓝屏
因为我们把入口点设置为0,所以一到执行入口点就蓝屏,说明我们可以从代码上控制驱动加载。Kb后显示堆栈,驱动执行路径
RetAddr : Args to Child : Call Site
00 fffff800042b3477 : fffffa80
029c81c0 fffffa80029c81c0 00000000
00000000 00000000000007ff : 0x0
01 fffff800
042b3875 : 0000000000000001 00000000
00000000 0000000000000000 fffffa80
029c82f8 : nt!IopLoadDriver+0xa07 02 fffff80003ed9161 : fffff8a0
00000000 ffffffff80000f54 fffff800
042b3820 fffffa8000711680 : nt!IopLoadUnloadDriver+0x55
03 fffff800
0416f166 : 0000000000000000 fffffa80
00711680 0000000000000080 fffffa80
006ed1d0 : nt!ExpWorkerThread+0x111 04 fffff80003eaa486 : fffff800
04044e80 fffffa8000711680 fffffa80
00711b60 0000000000000000 : nt!PspSystemThreadStartup+0x5a
05 00000000
00000000 : fffff8800457a000 fffff880
04574000 fffff88004578590 00000000
00000000 : nt!KiStartSystemThread+0x16 也就是call 0 ,我们设置的驱动入口下面我就方便修改demo驱动代码去控制驱动的加载了入口点。 在CBTdPreOperationCallback的函数里加一个修改DriverInit的数值,然后赋值为我们自定义的FakeDriverEntry函数
实现FakeDriverEntry函数如下:
下面再次开启双机调试,同样在虚拟机里执行sysmon –i
断点断在了回调函数的DRIVER_OBJECT pDriverObj = DRIVER_OBJECT)PreInfo->Object; 查看PDriverObj对象
U 入口点的函数u 0xfffff880`02ac2058
下面单步执行后会修改sysmonDrv驱动的DriverInit入口点 同时在FakeDriverEntry下断点
直接f5, 断点就直接断在了我们的Fake函数里
使用kb命令查看堆栈
确实执行了call,继续F5,这时我们观察sysmon命令行返回
Sysmon installed. SysmonDrv installed. StartService failed for SysmonDrv Failed to start the driver: Stopping the service failed:
Sysmon的驱动加载失败了,说明成功的控制了驱动的加载,从而证明了这种方案的可行。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课