[CVE-2024-43560] Windows 存储端口驱动程序权限提升漏洞

Page content

1. 背景知识

根据漏洞名称(Microsoft Windows Storage Port Driver)搜索确定对应文件为 storport.sys,该驱动用于计算机和高性能存储设备之间的通信,定义了计算机如何和设备之间进行通信。

2. 补丁对比

系统版本:Win11 22H2 专业版

对比显示有 6 个函数存在修改:

image.png

经过对补丁修复前后的函数代码对比,代码差异主要存在于 Feature_ 类函数调用上,此类函数用于与升级前代码兼容,通过检查某个全局变量变量是否执行升级后代码,其中函数 RaUnitStorageGetIdlePowerUpReason 除了调用 Feature_ 类函数外,对数据的操作最多,因此判断漏洞可能存在于该函数中。

image.png

所以补丁修复后的代码中,如果某个全局变量未启用,就执行修复前代码,如果启用,就执行新增代码。因此可以直接比对这两者之间的区别,区别在于框选中的代码:

image.png

3. 漏洞原理与修复分析

虽然找到了修复代码的位置,但是从目前的代码中看不出有什么漏洞,比较值得注意的就是在比较 stackLocation->Parameters.Read.Length 的大小的时候,数值从 0x8 变成了 0xC,并且判断了 masterIrp 是否为 0,这部分肯定了漏洞有关。

我猜测了一些原因,但是因为已知信息太少,没办法确定,首先需要弄清楚这里的参数还有结构体是什么。

通过查询官方文档中关于 IRP 的结构介绍,发现 irp->AssociatedIrp.MasterIrpMasterIrp 实际上是一个 union 字段,定义为:

   union {
    struct _IRP     *MasterIrp;
    __volatile LONG IrpCount;
    PVOID           SystemBuffer;
  } AssociatedIrp;

这里比较合理的选择应该是 SystemBuffer 。因此接下来需要弄清楚在函数 RaUnitStorageGetIdlePowerUpReason 中,处理的是什么请求,对应的 SystemBuffer 中保存的又是什么数据结构。

在搜索关键字 Storage PowerUpReason 的时候,找到了这个页面以及这个页面,获得数据结构:

//
// IOCTL_STORAGE_GET_IDLE_POWERUP_REASON
//
// Input Buffer:
//      None.
//
// Output Buffer:
//      A STORAGE_IDLE_POWERUP_REASON structure specifying what caused the power up.
//
typedef enum _STORAGE_POWERUP_REASON_TYPE {
  StoragePowerupUnknown           = 0,
  StoragePowerupIO,
  StoragePowerupDeviceAttention
} STORAGE_POWERUP_REASON_TYPE, *PSTORAGE_POWERUP_REASON_TYPE;

typedef struct _STORAGE_IDLE_POWERUP_REASON {
    ULONG Version;                          // Structure version, should be set to 1 for Win8.
    ULONG Size;                             // Size of this structure in bytes.
    STORAGE_POWERUP_REASON_TYPE PowerupReason;   // The reason for the power up (see above).
} STORAGE_IDLE_POWERUP_REASON, *PSTORAGE_IDLE_POWERUP_REASON;

#define STORAGE_IDLE_POWERUP_REASON_VERSION_V1 1

虽然此时不能确定SystemBuffer 到底是什么结构,但是目前只得到这些信息,所以先把它设置为类型 PSTORAGE_IDLE_POWERUP_REASON ,代码看起来很合理:

image.png

stackLocation->Parameters.Read.Length 这个字段我其实不太确定什么意思,但是根据上下文猜测,应该就是在判断保存 PSTORAGE_IDLE_POWERUP_REASON 这块的缓冲区有多大,因为我同时找到了一段构造此类请求的代码

irp = IoBuildDeviceIoControlRequest(IOCTL_STORAGE_GET_IDLE_POWERUP_REASON,
                                    DeviceExtension->LowerPdo,
                                    PowerupReason,
                                    sizeof (STORAGE_IDLE_POWERUP_REASON),
                                    PowerupReason,
                                    sizeof (STORAGE_IDLE_POWERUP_REASON),
                                    FALSE,
                                    &event,
                                    &ioStatus);

可以看到此类请求的 InputBufferOutputBuffer 都是 PowerupReason。 所以说stackLocation->Parameters.Read.Length 大概就是在检查这个缓冲区的大小。

那么修复前的代码究竟有什么问题呢?首先一个比较明显的问题就是,代码修复前,只检查了stackLocation->Parameters.Read.Length 是否小于 8,这显然是不对的,因为正常来说这个缓冲区大小应该是 12,不知道为什么这里要写成 8。

但是这样写真的有影响吗,一开始我没有看出来,可能从伪代码看不太清晰,我们直接看汇编:

image.png

修复前的代码在检查了stackLocation->Parameters.Read.Length 是否小于 8 之后,也检查了 [sys_buf]+12 是否大于 [sys_buf] + sys_buf.size ,就是说根据 size 字段计算得到的 sys_buf 的末尾地址是否超过了 sys_buf 后 12 个字节的地址,只有在超过的情况下才会访问它的 PowerupReason字段 。看起来好像没问题,这不就是在检查 sys_buf 的大小有没有超过 12 个字节吗?然后我才意识到,这里获得的 sys_buf 的大小并不是来源于函数 IoBuildDeviceIoControlRequest 中的 InputBufferSize 参数,即通过 sizeof (STORAGE_IDLE_POWERUP_REASON) 得到的大小,而是来自参数 PowerupReason 中的 size 字段,这个字段是用户可控的!

如果攻击者在通过 IoBuildDeviceIoControlRequest 构造请求的时候传入的 PowerupReason 的大小只有 8,但是设置其中的 size 字段数值是 12,同样能够通过检查,但是在通过检查之后访问 PowerupReason 字段时,会出现越界访问,因为 PowerupReason 字段在偏移 8 个字节的位置。

因此这是一个越界写漏洞。

微软的修复方式其实就是将 8 改成了 12,同时更加规范的对 OutputBuffer(即同样作为输入的 PowerupReason) 中的内容进行了赋值,而不是直接信任来自 InputBuffer(同样是 PowerupReason)中的内容。

4. 漏洞触发

有了上面的分析其实触发就比较简单了,可以看一下整个函数调用链

image.png

并不深,查看上层代码发现基本上确定请求类型之后就直接调用了漏洞函数,因此没有需要满足的其他条件,可以直接调用 DeviceIoControl 触发漏洞。

之前找到的示例代码中其实也构造了 irp 请求并调用了驱动,但是并没有使用 DeviceIoControl 的方式,因为它是操作系统的代码,所以在自己写 poc 代码的时候可以参考这个示例,但没办法直接挪用。

参考 IoBuildDeviceIoControlRequestDeviceIoControl 的调用如下所示:

status = DeviceIoControl(  
    hDevice,  
    IOCTL_STORAGE_GET_IDLE_POWERUP_REASON,  
    PowerupReason,  
    sizeof (STORAGE_IDLE_POWERUP_REASON),  
    PowerupReason,  
    sizeof (STORAGE_IDLE_POWERUP_REASON),  
    &bytesReturned,  
    NULL  
);

只需要解决 hDevice,然后构造可以触发漏洞的 PowerupReason 即可。

在示例代码中,有一个参数 deviceExtension,类型是 PCDROM_DEVICE_EXTENSION,因此考虑 hDevice 传入的也是一个 CDROM 的句柄。

在 WinObj64.exe 中搜索 cdrom,找到一下内容:

image.png

虽然不知道具体都是什么,但是看起来 \Device\0000006e 更加合理。

因此只需要调用 NtCreateFile 获得设备句柄,然后构造好可以触发漏洞的 powerupReason,再调用 DeviceIoControl 即可。

但是这个漏洞的问题在于,虽然从定义上确实写入了超出PowerupReason 范围的内存,但是实际上系统分配的内存要超过 8 字节,因此绝大多数情况下,poc无法实现系统崩溃,漏洞触发是否成功只能通过调试发现。同时越界写入的内容并不可控,因此该漏洞的直接危害很小,获取可以通过其他漏洞组合实现利用。

// 检查保存的缓冲区长度,确实是 8
1: kd> dd ffffe28ff4a43f20 L1
ffffe28f`f4a43f20  00000008
// 缓冲区内容
1: kd> dd rdx l4
ffffe28f`f4c11f80  00000001 0000000c 00000000 00000000
// 包含 rdx 的 pool 的信息,大小有 96 字节
1: kd> !pool rdx
Pool page ffffe28ff4c11f80 region is Nonpaged pool
 ffffe28ff4c11050 size:   60 previous size:    0  (Allocated)  MmSe
 ......
 ffffe28ff4c11ef0 size:   60 previous size:    0  (Allocated)  MmSe
*ffffe28ff4c11f50 size:   60 previous size:    0  (Allocated) *IoSB Process: ffffe28ff4a340c0
        Owning component : Unknown (update pooltag.txt)

可以看到缓冲区地址是 rdx(ffffe28ff4c11f80),其所在 pool page 的区域是从 ffffe28ff4c11f50 开始的 96 个字节,也就是说虽然缓冲区名义上大小是 8 字节,但实际上可以容纳 48 个字节而不会发生崩溃。

5. 反思

其实在找到漏洞函数之后我就有些不自信了,因为没办法直接看出来漏洞原理,所以瞅了一眼微软提供的资料,如果没有这些资料,我还是会怀疑漏洞在这里,但可能会因为缺乏自信而找不到正确的路径。确信存在漏洞→找不到漏洞的原因在于数据结构不清晰,怎样才能建立起这其中更加清晰的连接?如果确信存在漏洞只是一种心理自证,能够明确是数据结构不对则需要一些经验和知识的积累。

想象我在没有资料的情况下会怎么处理这个漏洞:1)定位到漏洞函数后,需要更加确认,或者说以更加确信的态度去分析代码修复前后的差别,这样我可以找到图中的红框和黄框,并且注意到 8 和 12 的差别,这种数值的差别会进一步加强“这是漏洞函数”的结论;2)虽然有数值差别,但是我很难意识到这里面涉及到了一个缓冲区,即 MasterIrp 实际上 union 结构中的另一个字段,极大概率到这里我就停了。这里更多的是一种意识的培养,因此这一步只能假设我通过更多学习培养了这种意识;3)猜测到这里涉及到缓冲区之后,我会更有信心和动力往下搜索资料,去微软官网看文档一个很自然的步骤,大概率我也能够找到 PSTORAGE_IDLE_POWERUP_REASON 的相关内容,之后也能够顺利的确定漏洞原理;4)在漏洞利用阶段,阻碍的点在于获得文件句柄,文中我确定去找 CDROM 文件句柄的原因更像是按图索骥,因为知道要找 CDROM,所以关注到了源码中和 CDROM 相关的路径。无法确定的原因在于我只熟悉向 CreateFile 函数传递普通的文件路径参数,并不熟悉更加深入的系统编程。除此之外,我一开始在写 poc 代码的时候,使用的是 CreateFile 函数,但是这个函数会出错,必须使用 NtCreateFile 函数。其实如果只把知识点局限在写 poc 代码这一块儿内容的的话,内容是很有限的,因为我之前的分析重点更多放在了漏洞原理上面,对利用代码不熟悉,所以这部分能力应该很快就能提升。