[CVE-2024-43560] Windows 存储端口驱动程序权限提升漏洞
1. 背景知识
根据漏洞名称(Microsoft Windows Storage Port Driver)搜索确定对应文件为 storport.sys
,该驱动用于计算机和高性能存储设备之间的通信,定义了计算机如何和设备之间进行通信。
2. 补丁对比
系统版本:Win11 22H2 专业版
对比显示有 6 个函数存在修改:
经过对补丁修复前后的函数代码对比,代码差异主要存在于 Feature_
类函数调用上,此类函数用于与升级前代码兼容,通过检查某个全局变量变量是否执行升级后代码,其中函数 RaUnitStorageGetIdlePowerUpReason
除了调用 Feature_
类函数外,对数据的操作最多,因此判断漏洞可能存在于该函数中。
所以补丁修复后的代码中,如果某个全局变量未启用,就执行修复前代码,如果启用,就执行新增代码。因此可以直接比对这两者之间的区别,区别在于框选中的代码:
3. 漏洞原理与修复分析
虽然找到了修复代码的位置,但是从目前的代码中看不出有什么漏洞,比较值得注意的就是在比较 stackLocation->Parameters.Read.Length
的大小的时候,数值从 0x8 变成了 0xC,并且判断了 masterIrp
是否为 0,这部分肯定了漏洞有关。
我猜测了一些原因,但是因为已知信息太少,没办法确定,首先需要弄清楚这里的参数还有结构体是什么。
通过查询官方文档中关于 IRP 的结构介绍,发现 irp->AssociatedIrp.MasterIrp
中 MasterIrp
实际上是一个 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
,代码看起来很合理:
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);
可以看到此类请求的 InputBuffer
和 OutputBuffer
都是 PowerupReason
。 所以说stackLocation->Parameters.Read.Length
大概就是在检查这个缓冲区的大小。
那么修复前的代码究竟有什么问题呢?首先一个比较明显的问题就是,代码修复前,只检查了stackLocation->Parameters.Read.Length
是否小于 8,这显然是不对的,因为正常来说这个缓冲区大小应该是 12,不知道为什么这里要写成 8。
但是这样写真的有影响吗,一开始我没有看出来,可能从伪代码看不太清晰,我们直接看汇编:
修复前的代码在检查了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. 漏洞触发
有了上面的分析其实触发就比较简单了,可以看一下整个函数调用链
并不深,查看上层代码发现基本上确定请求类型之后就直接调用了漏洞函数,因此没有需要满足的其他条件,可以直接调用 DeviceIoControl
触发漏洞。
之前找到的示例代码中其实也构造了 irp 请求并调用了驱动,但是并没有使用 DeviceIoControl
的方式,因为它是操作系统的代码,所以在自己写 poc 代码的时候可以参考这个示例,但没办法直接挪用。
参考 IoBuildDeviceIoControlRequest
,DeviceIoControl
的调用如下所示:
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,找到一下内容:
虽然不知道具体都是什么,但是看起来 \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 代码这一块儿内容的的话,内容是很有限的,因为我之前的分析重点更多放在了漏洞原理上面,对利用代码不熟悉,所以这部分能力应该很快就能提升。