Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2020-15368 — CVE-2020-15368,又名“如何利用易受攻击的驱动程序” | Kitploit
工具/GitHubGitHub/stong/cve-2020-15368
权限提升漏洞分析漏洞利用Shellcode学习与教育Payload 开发二进制利用
GitHubstong/cve-2020-15368

CVE-2020-15368

CVE-2020-15368,又名“如何利用易受攻击的驱动程序”

查看仓库
5154914年前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

如何利用一个存在漏洞的Windows驱动程序

针对 CVE-2020-15368 的漏洞利用与概念验证(PoC)。Asrock 将 rweverything 驱动程序重新打包,用于其 RGB 控制器配置工具,并对其进行了签名。他们通过加密 ioctl 来“保护”这个驱动……呵呵。去年夏天我们偶然发现了这个 CVE,据我所知,该驱动程序至今仍未得到修补。其影响自然是内核中的任意代码执行等。那么,请享用这个“0day”吧,哈哈。

如果你想和我争论这是否是一个真正的、可靠的 CVE,请随时在 Twitter 上联系我,我们可以在公开的社交媒体上大吵一架,这对所有参与的人来说都会非常激动人心!如果你愿意,我甚至可以为这个漏洞购买一个域名。这都是营销!!!

总之,这个漏洞相当糟糕,所以我将用它作为教程,教你如何攻破一个典型的漏洞驱动。因此,这篇文章主要面向初学者。 你将学习如何利用一个存在漏洞的驱动程序。市面上还有很多其他像这样的垃圾驱动。世界就是你的牡蛎。好好玩吧。

免责声明:本出版物仅供教育目的。读者有责任遵守所有适用的地方、州和联邦法律。本出版物的作者不承担任何责任,也不对因使用本出版物中的软件而造成的任何误用或损害负责。

背景故事

在隔离期间,我和我的室友(Pear0、Codetector)在 Pear0 的新 Asrock 主板上瞎捣鼓。那亮红色的 LED 灯非常烦人,而且在 Linux 上又无法配置它们。因此,我们的计划是对控制它的 Windows 驱动程序进行逆向,并在 Linux 上复现 I/O 操作。

长话短说,没过多久我们就意识到这个驱动程序实际上只是一个通用的驱动程序,授予了对任何事物的任意读/写访问权限。这包括控制寄存器(如 CR3、CR4)、物理内存等。这类驱动程序是设计用作调试工具的,而且供应商的网站上也明确说明了这一点。

docs/lol.png

我们觉得这非常搞笑。第一次让计算机从用户态三次故障并硬重启是相当令人兴奋的。(第二十次可能就没那么兴奋了。)总之,我们报告了这个漏洞,然后就把它忘了一年。

环境搭建

作为一个内核新手,我当时想知道如何实际加载并与驱动程序交互。结果发现这非常简单。

你只需在 Process Hacker 中为驱动程序创建一个服务即可(显然,加载驱动程序需要管理员权限)。然后右键点击并启动它。是的,就是这么简单。

docs/processhacker.png

我们可以在 WinObjEx64 中查看我们的设备对象。

docs/processhacker.png

我们甚至可以在 FileTest 中操作这个设备。

docs/filetest.png

docs/filetest2.png

这三款工具都非常棒,尤其是 Process Hacker 和 FileTest。它们就像瑞士军刀,应该是每个 Windows 逆向工程师工具箱中的必备工具。例如,据我所知,Jonas L 就是在 FileTest 里瞎折腾,发现了无数 Windows 本地权限提升漏洞。所以 Windows 确实有一些很棒的瞎搞工具。我希望 Linux 上也有这种东西。

“安全”绕过

Rweverything 有一个 ioctl,它接受一个 ioctl 作为参数,用于控制要执行的操作(读内存、写内存、读 MSR 等),以及一个包含操作特定参数(如源地址、目标地址等)的联合体。当我们比较两个驱动程序的代码时:

docs/comparison.png

🤔🤔🤔🤔🤔🤔🤔🤔🤔

然而,该驱动程序通过要求所有 ioctl 调用都必须使用硬编码的 AES 密钥进行适当的加密,来做一个拙劣的“安全通过模糊化”尝试。整理后的代码如下所示:

root@kitploit:~
if ( IoControlCode == 0x22EC00 )
{

  char enc_key[32];
  memset(enc_key, 0, sizeof(enc_key));
  memmove(enc_key, "C110DD4FE9434147B92A5A1E3FDBF29A", 32ui64);
  memcpy(enc_key + 13, ioctl_args->key, 16);
  
  size_t cb_decrypted = 0;
  my_decrypted_cmd* decryptedCmd = NULL;
  DWORD iv_size = ioctl_args->iv_size;
  DWORD input_size = *(DWORD*)(ioctl_args + bufferLen - 6);

  // really just calls BCrypt API to get an AES implementation
  if ( (unsigned int)decrypt_ioctl_params(enc_key, 32, ioctl_args->iv, iv_size, ioctl_args + bufferLen - input_size - 6, input_size, &decryptedCmd, &cb_decrypted) )
  {
    // Decryption failed
    if ( decryptedCmd )
      ExFreePoolWithTag(decryptedCmd, 0);
    irp->IoStatus.Status = 0xC000000D;
    goto Fail_Out;
  }
  IoControlCode = decryptedCmd->opcode;
  Rweverything_Args = &decryptedCmd->args;
}
else // not 0x22EC00
{
  if ( IoControlCode != 0x22E858 &&
       IoControlCode != 0x22E860 &&
       IoControlCode != 0x22E800 &&
       IoControlCode != 0x22E804 )// whitelisted control codes
    IoControlCode = 0; // block everything else
}

该驱动程序允许一些无聊的操作(执行一些 PMIO),但所有“有趣”的控制代码都被这个解密例程阻挡。尽管有这个显式的白名单,它仍然包含了所有危险的 Rweverything 功能。这些危险功能本应被直接移除,而不是隐藏起来。

另外,有趣的是,它还允许用户指定部分密钥(???),我不知道这是出于什么原因。代码写得很糟糕。

总之,编写客户端代码来使用这个奇怪的加密 API,并向它传递我们想要的任意 ioctl 调用,是相对容易的。我就不详细说明这些细节了。

与驱动程序通信

我们打开一个驱动程序的句柄,并使用 DeviceIoControl 来调用 ioctl,这是非常标准的操作。

root@kitploit:~
HANDLE hDevice = CreateFileA("\\\\.\\GlobalRoot\\Device\\AsrDrv104", GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

// ... set up the encrypted ioctl data

BOOL result = DeviceIoControl(hDevice, 0x22EC00, ioctl_data, in_buf_size, out_buf, sizeof(out_buf), &bytes_returned, NULL);

现在我们已经能够与驱动程序中隐藏的 Rweverything 部分进行通信,我第一件想做的事情就是触发一次崩溃,以确认我的驱动客户端能正常工作。

实现这一目标最直接的方法是用垃圾数据覆盖 CR3。我知道你们中有些读者是新手,这没关系,所以我会详细解释。我也很笨,这可能会帮助你学习。如果你知道自己在做什么,可以跳过这部分。

在 x86 上,当启用分页时(现代操作系统几乎总是这样),CR3 指向顶级页表目录的物理基地址。如果你不知道这意味着什么,请去阅读维基百科上关于虚拟内存的文章。

当我们用垃圾数据(例如 0x0000000000000000)覆盖 CR3 时,TLB 会被刷新,当尝试执行下一条指令时,处理器(特别是 MMU)会尝试将指令指针转换为物理地址。地址转换可以看作是从 CR3 开始的一系列页表遍历。现在 CR3 指向物理地址 0,这个地址确实存在且可访问;但它极不可能是一个有效的页表(页表项必须遵循特定的结构)。

当这种情况发生时,我们会在地址转换时得到一个页错误。通常,CPU 会带我们到页错误处理程序的地址。但它是如何知道页错误处理函数地址的呢?这存储在一个称为中断描述符表(IDT)的内存数据结构中。处理器有一个寄存器(由 sidt 和 lidt 指令读写),它保存了 IDT 的虚拟地址。现在你看到问题了吗?为了处理页错误,我们首先需要再进行一次虚拟内存访问,从而再进行一次地址转换。

当然,我们的第二次地址转换也会出错。现在我们遇到了双重错误:一个在处理第一个页错误时发生的错误。这很严重,但仍然可以恢复——处理器会给我们最后一次恢复的机会。当然,这一次尝试也会被第三次也是最后一次的页错误——三重错误——无情地打断。此时,CPU 直接放弃,对机器进行硬重置。如果你在物理机上执行此操作,你现在很可能已经看到 BIOS 启动画面了。

现在,如果你有任何问题,我会给你那个我一直得到的答案:去阅读《Intel 手册第 3A 卷》(也就是圣经)。

利用驱动程序

好了,现在我们要如何实际利用这个驱动程序呢?环顾四周,我们看到了一个自由的任意物理内存读写原语。它基本上使用 MmMapIoSpace 映射你想要的任何物理地址,将你的缓冲区复制到该地址(或反过来),然后取消映射该地址。

小提示:当你试图在已附加内核调试器的情况下使用像我们这样愚蠢的参数调用 MmMapIoSpace 时,会导致错误检查。你可以通过在 WinDbg 中写入一个魔术字节来绕过这个问题。请参阅 exploit.cpp 中引用 MiShowBadMapper 的注释以了解更多信息。我其实不太清楚那到底是什么,也懒得去搞清楚。

我们如何利用这个原语在内核中获得代码执行呢?这个原语的主要问题是它操作的是物理内存。作为用户态程序,我们基本上不知道物理内存的布局——操作系统为我们处理了这一切。即使我们能获取某些内核数据结构或内核函数指针的虚拟地址,我们也完全不知道它们在物理地址空间中的位置。

一个想法是读取 CR3,读取页表,然后自己进行虚拟地址转换。这是个好主意。但行不通。这是因为 Windows 不再允许你使用 MmMapIoSpace 映射页表。所以我们需要更聪明一些。

我使用了 xeroxz 在 VDM 中的技术。它相当简单,但技术很巧妙。虽然我们不知道物理内存的布局,但我们仍然可以扫描所有物理内存,直到找到我们想要的东西。我们可以利用的一点是:页内容在物理上和虚拟上总是相同的:任何相对于页边界的偏移都是保留的。例如,如果我的页 0x7fff000000000XXX 映射到物理帧 0x0000000123456XXX,那么所有地址的 XXX 在物理和虚拟地址中都是相同的。所有页内结构都被保留;因此我们可以扫描某个我们想要覆盖的有趣的页。

我们最容易覆盖的东西可能是某个容易到达的系统调用或 ioctl 处理程序。在 Windows 上,有一个标准的 Beep() 函数可以让你的电脑发出蜂鸣声。信不信由你,这个功能是在一个名为 Beep.sys 的驱动程序中实现的,它提供了 Beep 设备(事实上,你可以在之前的 WinObjEx64 截图中看到它)。任何人都可以使用 Beep 设备,而且它很少被调用。所以让我们覆盖 Beep 的 ioctl 处理程序。

我们可以将 Beep.sys 放入 IDA 中,并查看 DeviceIoControl 处理程序。

docs/beep.png

在页偏移 0x270 处,我们有这段代码,其字节为 40 53 48 ...。这些字节都没有被重定位,所以扫描这个函数非常容易。如果有重定位的字节,我们需要将它们作为通配符。这与编写游戏外挂时进行签名扫描的思路相同。

所以,在扫描物理内存找到这段代码后,我们可以用我们自己的 shellcode 覆盖它。你还需要小心,因为物理内存中可能存在的这个页的多个副本(!),所以要找到所有副本。

此时,我们可以很容易地通过将我们进程的安全令牌与系统进程的令牌交换来提升权限,从而获得 nt authority\system 权限。不幸的是,Asrock 驱动程序无论怎样都要求管理员权限才能打开,所以这并不有趣。

对于我们来说,我们编写了一个基本的 shellcode,它分配并复制一个第二阶段载荷,然后生成一个新的内核线程。我们不能在我们覆盖的 Beep 处理程序中完成所有操作,因为 1)我们被限制在一个页面内,2)当我们尝试关闭 Beep 设备的句柄时会导致系统崩溃,因为我们同时也破坏了 Beep 设备中的其余代码。至于获取内核指针,这实际上很容易,因为如果我们好好请求,NtQuerySystemInformation 会免费给我们。

root@kitploit:~
__int64 __declspec(dllexport) __fastcall MyIRPHandler(struct _DEVICE_OBJECT* a1, IRP* irp)
{
    MyIrpStruct* user_data = (MyIrpStruct*)irp->AssociatedIrp.SystemBuffer;

    void* my_rwx = user_data->nt_ExAllocatePoolWithTag(NonPagedPoolExecute, user_data->payload_size, 'lmao');

    user_data->nt_memcpy(my_rwx, user_data->payload, user_data->payload_size);

    HANDLE hThread;
    user_data->nt_PsCreateSystemThread(&hThread, THREAD_ALL_ACCESS, NULL, NULL, NULL, (PKSTART_ROUTINE)my_rwx, NULL);

    user_data->nt_IofCompleteRequest(irp, 0);

    return 0;
}

所以我们快速修补 Beep,调用被覆盖的 ioctl 处理程序,然后取消修补 Beep。这样我们就安全地创建了一个执行我们代码的内核线程,而不会破坏系统上的其它任何东西。此时,我们可以映射我们自己的驱动程序或其他任何东西。

结论

我是一个糟糕的安全研究员,只是偶然发现了毫无价值的漏洞。感谢大家的阅读。请订阅我的 OnlyFans。

下载工具