http.sys—UlpParseNextRequest中连接数组容量整数溢出(CWE-190)导致内核池溢出

admin 2026-07-14 05:40:35 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 该漏洞是http.sys驱动中UlpParseNextRequest函数的整数溢出问题(CWE-190),导致内核池缓冲区溢出(CWE-122)。根因是16位加法无溢出检查,当capacity接近0xFFFF时加5会回绕,导致后续分配过小,引发溢出。补丁通过RtlUShortAdd函数添加溢出检查,并由KIR特性标志控制。攻击者可通过HTTP监听器远程触发,影响IIS、WinRM等服务。建议系统管理员确保KIR标志启用以防范此漏洞。 综合评分: 97 文章分类: 漏洞分析


cover_image

http.sys — UlpParseNextRequest 中连接数组容量整数溢出(CWE-190)导致内核池溢出

The Drift Corpus The Drift Corpus

securitainment

2026年7月13日 18:50

在小说阅读器读本章

去阅读

原文链接:

  • “https://byteray-ai.github.io/drift-corpus/item/http_a10f1434-http-report-20260629-001610.html”

1. 概述

| 项目 | 值 | | — | — | | 未修补二进制 | http_unpatched.sys | | 已修补二进制 | http_patched.sys | | 整体相似度 | 0.9896 | | 匹配函数 | 3205 | | 变更函数 | 6 | | 相同函数 | 3199 | | 未匹配 (未修补 → 已修补) | 0 / 0 |

结论:一个微小但关键的补丁,围绕每个TCP连接 HTTP 请求追踪数组的 capacity 字段引入了 16 位溢出检查,封堵了 http.sys中一个可远程触达的内核池溢出。

在 6 个变更函数中,只有 1 个 (UlpParseNextRequest (sub_1c000e4a0)→ UlpParseNextRequest (sub_1c000e550)) 与安全相关。其余是重定位到新地址的相同函数(寄存器/偏移漂移)或启发式误配对,外加无害的配置管道(新增 MaxHeadersCount注册表参数)。实际修复集中在请求处理循环中:已修补版本调用标准安全整数库例程 RtlUShortAdd (sub_1c001c6e4)执行 capacity + 5并带溢出检测,且此检查路径由 KIR 特性标志 UxKirRefBufferOverflowCheck (data_1c0078d80)选择。当该标志关闭时,已修补版本仍运行原始未检查的 add cx, 5路径,因此该修复是一个 KIR 门控的安全路径,而非无条件替换。


2. 漏洞摘要

严重 — 整数溢出 (CWE-190) → 内核池缓冲区溢出 (CWE-122)

受影响函数:UlpParseNextRequest (sub_1c000e4a0)(未修补)、UlpParseNextRequest (sub_1c000e550)(已修补)—— HTTP 请求处理主循环。

根因:

该函数为每个连接维护一个动态的 HTTP 请求/区间 (range) 对象数组。数组在连接对象上追踪三个小字段:

  • +0x630

    — WORDcapacity(容量)

  • +0x632

    — WORDcount(计数)

  • +0x638

    — PVOIDbuffer 指针(位于 NonPagedPoolNx

每当 count >= capacity时,代码扩张数组:分配一块大小为 (capacity * 8) + 0x28的新池缓冲区,复制旧条目,释放旧缓冲区,然后用** 16 位加 5 **写入新容量:

*(conn + 0x630) = capacity + 5;     // 16 位加法,无溢出检查

capacity是一个 WORD。一旦它达到 0xFFFB..0xFFFF,加 5 会回绕到 0x0000..0x0004。在** 下一次 **扩张事件时,分配大小由这个微小的回绕后容量计算(例如 1*8 + 0x28 = 0x30字节),但 count仍从其先前值(约 0xFFFC+)继续攀升。紧接着的下一条条目写入

mov qword [rax + rcx*8], rsi   ; rcx = count (很大), rax = 微小缓冲区

便会以 8 字节指针的步长走出新池分配的末端——一个大型的、由攻击者驱动的、内核池溢出。

为何可被利用:索引 (count) 和步长 (8 字节) 完全由攻击者控制,攻击者只需在长连接上发送更多 HTTP 请求/区间,即可控制数组的增长节奏。缓冲区位于 NonPagedPoolNx,其相邻分配常包含其他连接对象、头部或池元数据。

补丁的作用:已修补循环调用标准 RTL 安全加法库例程 RtlUShortAdd (sub_1c001c6e4),它在 32 位算术中执行 capacity + 5,将截断后的 16 位结果与输入比较,并用 cmp ax, cx; jb检测回绕。回绕时钳制为 0xFFFF并返回 STATUS_INTEGER_OVERFLOW (0xC0000095);调用方在负状态值时退出,因此溢出的容量永远不会抵达 ExAllocatePoolWithTagPriority。此检查路径在运行时由 KIR 特性标志 UxKirRefBufferOverflowCheck (data_1c0078d80)选择(来自 WIL 特性分级查询)。当标志禁用时,已修补版本回退到原始未检查的 add cx, 5路径 (0x1c000ea02),该路径与被漏洞利用的代码逐字节相同。未修补版本只有那条未检查路径,且没有对 RtlUShortAdd的调用。

攻击者可达的入口点:任何经由 http.sys的 HTTP 监听器(IIS、HTTP API v2、WinRM、Print Spooler 的 HTTP API 等)。

调用链:

  1. 远程客户端向目标 HTTP 端点打开 TCP 连接。

  2. TDI/WSK

    接收将字节交给 http.sys

  3. http.sys

    的 IRP 分发将请求路由进请求处理主循环。

  4. 调用方 UlpAuthenticateRequestCompletion (sub_1c01176a0)→ UlResumeParsing (sub_1c000a940)→ UlpHandleRequest (sub_1c000e1a0)→ UlBeginOpaqueMode (sub_1c002dc84)→ UlpParseNextRequest (sub_1c000e4a0)(脆弱循环)。

  5. 循环内部,ExAllocatePoolWithTagPriority以回绕后的欠尺寸容量被调用。

  6. 在 mov qword [rax+rcx*8], rsi(0x1c000e8d7) 处的下一条条目写入破坏了相邻的 NonPagedPoolNx内存。


3. 伪代码 Diff

// === 未修补: UlpParseNextRequest (sub_1c000e4a0) ===========================================
void process_request(conn_t *conn, request_t *req) {
    uint16_t count    = *(uint16_t *)(conn + 0x632);
    uint16_t capacity = *(uint16_t *)(conn + 0x630);

    if (count >= capacity) {
        size_t   new_size = (size_t)capacity * 8 + 0x28;   // 使用当前 capacity
        void    *new_buf  = ExAllocatePoolWithTagPriority(..., new_size, ...);
        if (!new_buf) return;

        memcpy(new_buf, conn->buf, capacity * 8);
        ExFreePoolWithTag(conn->buf);
        conn->buf = new_buf;

        // *** BUG: 16 位加法, 无溢出检查 ***
        *(uint16_t *)(conn + 0x630) = capacity + 5;        // 超过 0xFFFF 后回绕!
    }

    // 以 *单调递增* 的 count 为索引
    conn->buf[count] = req;                                // 若 capacity 已回绕则为 OOB 写入
    *(uint16_t *)(conn + 0x632) = count + 1;
}

// === 已修补: UlpParseNextRequest (sub_1c000e550) =============================================
// RtlUShortAdd 是标准 RTL 安全整数库例程, 非 http.sys 自定义辅助函数.
NTSTATUS RtlUShortAdd(uint16_t cur, uint16_t addend, uint16_t *out) {  // sub_1c001c6e4
    uint32_t sum = (uint32_t)cur + addend;                // 32 位加法
&nbsp; &nbsp; if ((uint16_t)sum < cur) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// cmp ax,cx ; jb
&nbsp; &nbsp; &nbsp; &nbsp; *out = 0xFFFF; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 钳制
&nbsp; &nbsp; &nbsp; &nbsp; return STATUS_INTEGER_OVERFLOW; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 0xC0000095
&nbsp; &nbsp; }
&nbsp; &nbsp; *out = (uint16_t)sum;
&nbsp; &nbsp; return STATUS_SUCCESS;
}

void process_request(conn_t *conn, request_t *req) {
&nbsp; &nbsp; uint16_t count &nbsp; &nbsp;= *(uint16_t *)(conn + 0x632);
&nbsp; &nbsp; uint16_t capacity = *(uint16_t *)(conn + 0x630);

&nbsp; &nbsp; if (count >= capacity) {
&nbsp; &nbsp; &nbsp; &nbsp; if (UxKirRefBufferOverflowCheck) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 新增 KIR 门控安全路径
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; uint16_t new_cap;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (RtlUShortAdd(capacity, 5, &new_cap) < 0) &nbsp; // 新增带溢出检查的加法
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; goto fail_request; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 新增回绕时退出
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; size_t new_size = (size_t)new_cap * 8; &nbsp; &nbsp; &nbsp; &nbsp; // 使用已校验的 capacity
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; void &nbsp;*new_buf &nbsp;= ExAllocatePoolWithTagPriority(..., new_size, ...);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (!new_buf) goto fail_request;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; memcpy(new_buf, conn->buf, count * 8);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (capacity > 1) ExFreePoolWithTag(conn->buf);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; *(uint16_t *)(conn + 0x630) = new_cap; &nbsp; &nbsp; &nbsp; &nbsp; // 安全, 已校验
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; conn->buf = new_buf;
&nbsp; &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // KIR 标志关闭: 原始未检查路径
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; size_t new_size = (size_t)capacity * 8 + 0x28;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; void &nbsp;*new_buf &nbsp;= ExAllocatePoolWithTagPriority(..., new_size, ...);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (new_buf) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; memcpy(new_buf, conn->buf, count * 8);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (capacity > 1) ExFreePoolWithTag(conn->buf);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; *(uint16_t *)(conn + 0x630) = capacity + 5; &nbsp;// 仍会 16 位回绕!
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; conn->buf = new_buf;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

&nbsp; &nbsp; conn->buf[count] = req;
&nbsp; &nbsp; *(uint16_t *)(conn + 0x632) = count + 1;
}

关键要点:

  • 未修补版本中危险的一行是未设防的 capacity + 5
  • 检查路径在 RtlUShortAdd (sub_1c001c6e4)返回成功前绝不信任加法结果,并将校验后的值用于分配大小、存储容量和条目写入。
  • 已修补版本保留了原始未检查的 add cx, 5路径作为回退(当 UxKirRefBufferOverflowCheck (data_1c0078d80)禁用时采用),因此该修复依赖于该 KIR 标志被启用。

4. 汇编分析

未修补 — 脆弱的分配块

; --- 读取两个 16 位字段 ---
0x1c000e82f | movzx &nbsp; eax, word [rdi+0x632] &nbsp; &nbsp; &nbsp;; eax = count
0x1c000e836 | movzx &nbsp; ecx, word [rdi+0x630] &nbsp; &nbsp; &nbsp;; ecx = capacity
0x1c000e83d | cmp &nbsp; &nbsp; ax, cx
0x1c000e840 | jb &nbsp; &nbsp; &nbsp;0x1c000e8cd &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 若 count < capacity, 跳过增长
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; (这是路径中唯一的尺寸检查)

; --- 使用当前 capacity 计算新分配大小 ---
0x1c000e846 | lea &nbsp; &nbsp; rdx, [rcx*8 + 0x28] &nbsp; &nbsp; &nbsp; &nbsp; ; rdx = capacity*8 + 0x28 &nbsp; <-- 盲目信任
0x1c000e85c | call &nbsp; &nbsp;qword [rel 0x1c0086c50] &nbsp; &nbsp; ; ExAllocatePoolWithTagPriority
0x1c000e86b | test &nbsp; &nbsp;rax, rax
0x1c000e86e | je &nbsp; &nbsp; &nbsp;0x1c000e8e9 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 分配失败 -> 退出

; --- (通过 memmove 复制旧数据, 释放旧缓冲区) ---
; ... (为清晰省略) ...

; --- BUG: capacity 更新是 16 位加法, 无溢出检测 ---
0x1c000e8ad | movzx &nbsp; ecx, word [rdi+0x630] &nbsp; &nbsp; &nbsp; ; 重新加载 capacity
0x1c000e8bb | add &nbsp; &nbsp; cx, 5 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 16 位加法; 若 cx >= 0xFFFB 则回绕
0x1c000e8bf | mov &nbsp; &nbsp; [rdi+0x630], cx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 存储回绕后的 capacity, 无检查

; --- 条目写入: count 是索引, capacity 已不再相关 ---
0x1c000e8cd | movzx &nbsp; ecx, ax &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; ecx = count (上方已设, 仍很大)
0x1c000e8d0 | mov &nbsp; &nbsp; rax, qword [rdi+0x638] &nbsp; &nbsp; &nbsp;; rax = 当前缓冲区指针
0x1c000e8d7 | mov &nbsp; &nbsp; qword [rax+rcx*8], rsi &nbsp; &nbsp; &nbsp;; *** OOB 写入 ***
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; rsi = 条目指针, rcx*8 = 索引
0x1c000e8db | inc &nbsp; &nbsp; word [rdi+0x632] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; count++

注解:

  • 0x1c000e83d

    0x1c000e840:路径中唯一的比较。它告诉函数 是否增长;它验证增长算术。

  • 0x1c000e846

    lea rdx, [rcx*8 + 0x28]信任回绕后的 capacity。

  • 0x1c000e8d7

    :破坏性的写入。一旦 capacity已回绕并进行了一次新的微小分配,rcx(count) 约为 0xFFFC,因此 rcx*8 ≈ 0x7FFE0字节,远超新缓冲区末端。

已修补 — 安全路径与 RTL 安全加法例程 RtlUShortAdd (sub_1c001c6e4)

在 UlpParseNextRequest中,增长判定后跟随一个 KIR 标志测试来选择检查路径:

0x1c000e8ec | movzx &nbsp; ecx, word [rdi+0x630] &nbsp; &nbsp; &nbsp; ; capacity (被加数)
0x1c000e8f3 | cmp &nbsp; &nbsp; [rdi+0x632], cx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; count < capacity? &nbsp;-> 跳过增长
0x1c000e8fa | jb &nbsp; &nbsp; &nbsp;0x1c000ea14
0x1c000e900 | cmp &nbsp; &nbsp; UxKirRefBufferOverflowCheck, r14b &nbsp;; KIR 标志 == 1 ?
0x1c000e907 | jz &nbsp; &nbsp; &nbsp;0x1c000e994 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 标志关闭 -> 旧未检查路径
0x1c000e90d | lea &nbsp; &nbsp; r8, [rbp+pusResult] &nbsp; &nbsp; &nbsp; &nbsp; ; 检查路径
0x1c000e911 | call &nbsp; &nbsp;RtlUShortAdd
0x1c000e916 | test &nbsp; &nbsp;eax, eax
0x1c000e918 | js &nbsp; &nbsp; &nbsp;0x1c000ea36 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 溢出 -> 退出
0x1c000e91e | movzx &nbsp; r15d, [rbp+pusResult] &nbsp; &nbsp; &nbsp; ; 已校验的新容量
0x1c000e926 | mov &nbsp; &nbsp; edx, r15d
0x1c000e92e | shl &nbsp; &nbsp; rdx, 3 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; NumberOfBytes = new_cap*8
0x1c000e938 | call &nbsp; &nbsp;ExAllocatePoolWithTagPriority
...
0x1c000e98a | mov &nbsp; &nbsp; [rdi+0x630], r15w &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 存储已校验的容量

RTL 安全加法例程本身:

0x1c001c6e4 | lea &nbsp; &nbsp; eax, [rcx+0x5] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 32 位: capacity + 5
0x1c001c6e7 | cmp &nbsp; &nbsp; ax, cx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 低 16 位是否倒退?
0x1c001c6ea | jb &nbsp; &nbsp; &nbsp;0x1c001c6f1 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 是 -> 溢出路径
0x1c001c6ec | movzx &nbsp; edx, ax &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 否 -> 使用安全值
0x1c001c6ef | jmp &nbsp; &nbsp; 0x1c001c6f6
0x1c001c6f1 | mov &nbsp; &nbsp; edx, 0xffff &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 钳制路径: edx = 0xFFFF
0x1c001c6f6 | sbb &nbsp; &nbsp; eax, eax &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 若 CF(溢出) 则 eax = 0xFFFFFFFF
0x1c001c6f8 | mov &nbsp; &nbsp; word [r8], dx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 存储已校验的容量
0x1c001c6fc | and &nbsp; &nbsp; eax, 0xc0000095 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 掩入 STATUS_INTEGER_OVERFLOW
0x1c001c701 | retn

注解:

  • lea eax, [rcx+5]

    在 32 位中执行加法,因此回绕可被检测。

  • cmp ax, cx; jb

    是经典的无符号 16 位”加完后变小”溢出测试。

  • sbb eax, eax

    将 CF 转为 0/-1,然后 and产生 0(成功)或 0xC0000095(溢出)。

  • 调用方以非负返回值门控分配,因此不良大小永远不会传入 ExAllocatePoolWithTagPriority

  • 0x1c000e994

    处的回退(当 UxKirRefBufferOverflowCheck关闭时采用)是原始代码:lea rdx, [rcx*8+0x28]分配,然后 add cx, 5mov [rdi+0x630], cx于 0x1c000ea02,无溢出检查。


5. 触发条件

要在未修补驱动上触达并触发该 bug:

  1. 触达目标。

    找出主机上任何由 http.sys支撑的 HTTP 服务(默认端口 80、443、5985/WinRM、41792/Print HTTP,或任何 http.sysURL 预留)。打开单个持久 TCP 连接。

  2. 驱动数组增长。

    每个请求(或在某些路径中,每个 Range 或分块段)添加一个条目。数组从小开始,每次填满时按 5 个容量槽增长。

  3. 到达回绕阈值。

    capacity 必须从其初始值攀升到 ~0xFFFB。按每次填满增长 5 个槽计算,这需要大约 ~13,107 次增长事件,即同一连接上发送约 0xFFFB个总请求/区间条目。

  4. 跨越边界。

    一旦加法后的值回绕(capacity+5溢出超过 0xFFFF),_下一次_ 触发增长的请求将分配一块由回绕值决定大小的缓冲区(典型约 0x30字节),而 count仍约为 0xFFFC

  5. 写入末端之外。

    任意单个额外请求都会导致 mov qword [rax+rcx*8], rsi在新微小分配起始处偏移 count*8的位置写入 8 字节——远在缓冲区之外。

  6. 可观察效果。

    一旦被破坏的池下次被检查,预期会发生内核模式 bugcheck。可能的停止代码:

  7. KERNEL_DATA_INPAGE_ERROR

    (0x7A) 或

  8. PAGE_FAULT_IN_NONPAGED_AREA

    (0x50) —— 故障虚拟地址位于高位 0xFFFF_FFFF_xxxx_xxxx范围,由 count*8对欠尺寸的 NonPagedPool 区域计算得出,或

  9. BAD_POOL_HEADER

    (0x19) / POOL_CORRUPTION—— 若被破坏的是小分配相邻的池元数据。

关于节奏的说明:

  • 使用 HTTP 管线化或 分块 POST在单个连接上推送多个条目而无需往返;这最小化了到达 65k 条目的挂钟时间。

  • HTTP Range 请求

    尤其有吸引力,若每段的路径汇入同一数组(Range: bytes=0-1,2-3,4-5,...每个请求产生多个条目)。

  • 某些端点会限流;将攻击分散到多个指向同一 http.sys实例的套接字 帮助,因为数组是每连接独立的。


6. 利用原语与开发说明

原语:固定 8 字节步长的 受控内核池溢出。攻击者:

  • 选择 count(因此选择距缓冲区起始的偏移 count*8),
  • 提供被写入的 8 字节指针值(请求/区间对象指针,攻击者通过先前内容间接控制),

从而允许对欠尺寸的 NonPagedPoolNx分配进行有节制的越界写入。

转化为完整利用:

  1. 堆整形 (Heap grooming)。

    用一个可控的受害对象回收欠尺寸分配相邻的位置。在 NonPagedPoolNx中好的候选包括:

  2. HAL

    PRCB 相邻结构(避免),

  3. Token

    /Process对象(大小不匹配——通常不行),

  4. Pipe

    实例、Event对象、Semaphore对象或其他连接作用域的结构(可以)。

  5. 最佳目标是池头之后前 8 字节含函数指针的对象。

  6. 伪造破坏。

    溢出进受害对象的虚表/函数指针/LIST_ENTRY,将其替换为指向攻击者可控数据的指针。

  7. 击败 KASLR。http.sys

    默认不泄露内核指针。要么:

  8. 利用另一驱动中的信息泄露原语(如 Win32k、gdi)获取内核基址,或

  9. 用地址可经确定性分配模式推断的对象喷射 NonPagedPoolNx

  10. 代码执行。

    一旦命中受控函数指针,重定向到位于 nt!Ke*gadget 中的 ROP/JOP 链,或——若 SMEP/SMAP 允许仅数据攻击——破坏 _TOKEN指针以实现提权,而无需执行用户态 shellcode。

需考虑的缓解措施:

  • kASLR:

    对代码执行流必须被击败;仅数据的 token 交换变体可绕过它。

  • SMEP / SMAP:

    阻断 ret-into-userland 流;内核 ROP 变体可绕过它们。

  • CFG(内核 CFG、CET):

    约束间接调用目标。选择 CFG 保护调用站点之外的破坏目标(如用于任意写入原语的 LIST_ENTRY卸载、I/O 完成回调)。

  • HVCI / VBS:

    将许多 ROP/JOP 链转为 hypervisor 故障。仅数据的 token 交换仍可行。

  • 池头完整性(Win10+):

    随机化并验证 POOL_HEADER;纯元数据破坏更难。优先破坏 对象字段,而非池头。

现实结果:从低权限本地上下文的高影响 提权至 SYSTEM,以及与独立信息泄露结合时的 远程内核崩溃 / 远程 LPE


7. 调试器 PoC 演练

假设 WinDbg/KD 已附加到 未修补的 http.sys,符号通过本地 PDB 或以下偏移解析。

断点

bp http_unpatched+0xE4A0 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; UlpParseNextRequest (sub_1c000e4a0) 入口 — 确认请求抵达循环
bp http_unpatched+0xE82F &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; capacity/count 读取 — 实时观察 16 位字段
bp http_unpatched+0xE846 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 大小计算: lea rdx,[rcx*8+0x28]
bp http_unpatched+0xE85C &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; ExAllocatePoolWithTagPriority 调用 — 检查 rdx (大小)
bp http_unpatched+0xE8CD &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 索引准备 — 在 OOB 写入前观察 ecx (=count)
bp http_unpatched+0xE8D7 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; *** OOB 写入本身 ***
bp nt!ExAllocatePoolWithTagPriority

若 http.sys加载基址非 0x1c0000000,替换为等效 RVA:从驱动基址起 +0x1A0+0x82F+0x846+0x85C+0x8CD+0x8D7

每个断点检查什么

  • 在 +0xE82Frdi

    = 连接对象。转储字段:

&nbsp; &nbsp; dp rdi+0x630 L1 &nbsp; &nbsp; ; 位 [0..15] = capacity, 位 [16..31] = count
&nbsp; &nbsp; dq rdi+0x638 L1 &nbsp; &nbsp; ; 当前缓冲区指针

确认 count在攀升,capacity以 5 为步长递增。

  • 在 +0xE846rcx

    = 增长时的 capacity。rdx(capacity*8)+0x28注意回绕:当 rcx≥ 0xFFFB时,_下一次_ 命中此断点将出现 rdx接近 0x30(微小)—— 那就是确凿的分配。

  • 在 +0xE85Crdx

    是请求的大小。确认其相对先前分配小得不合理。

  • 在 +0xE8D7

  • rax

    = 新的(微小)缓冲区指针。

  • ecx

    = count —— 这是乘数。

  • 在 WinDbg 中计算目标虚拟地址:? rax + rcx*8。任何超出 rax数百字节以上即确认 OOB。

  • 对正在写入的字节设置观察点:ba w8 <addr>

触发设置(用户模式)

  1. 向目标 HTTP 端口打开一个持久 TCP 套接字。
  2. 构造一个创建多个数组条目的 HTTP 请求。http.sys最简单路径:
&nbsp; &nbsp; &nbsp; POST / HTTP/1.1
&nbsp; &nbsp; &nbsp; Host: <target>
&nbsp; &nbsp; &nbsp; Transfer-Encoding: chunked
&nbsp; &nbsp; &nbsp; Range: bytes=0-1,2-3,4-5,6-7,8-9,...

每个逗号分隔的 range 创建一个条目。或者,在同一套接字上管线化多个请求而无需等待响应。

  1. 循环直到添加约 65,535 个条目。+0xE4A0处的函数应在每个请求上被命中;[rdi+0x630]处的 capacity 字段应以 5 为步长攀升直到回绕。
  2. 一旦回绕发生,再发送 一个请求以在 +0xE8D7触发 OOB 写入。

预期观察

  • 在 bugcheck 前,ba w8对被破坏地址的观察点触发,写入值为 rsi(请求指针)。
  • bugcheck 应不久后发生,故障指令要么在 nt!ExpPool...例程中,要么在 nt!KiPageFault中,要么在相邻被破坏对象的方法内。
  • 最可能的停止代码:0x50 PAGE_FAULT_IN_NONPAGED_AREA0x19 BAD_POOL_HEADER或 0xC2 BAD_POOL_CALLER
  • 检查故障地址:其形式为 tiny_buffer + count*8,远超出实际分配。

结构 / 偏移说明

连接对象布局 (相关字段):
&nbsp; +0x630 &nbsp;WORD &nbsp; capacity &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 16 位, 增长时 +5
&nbsp; +0x632 &nbsp;WORD &nbsp; count &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 16 位, 每条目 +1
&nbsp; +0x634 &nbsp;? &nbsp; &nbsp; &nbsp;(填充)
&nbsp; +0x638 &nbsp;PVOID &nbsp;buffer &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; NonPagedPoolNx 指针数组
&nbsp; 每个条目: 8 字节 (指向请求/区间对象的指针)

RTL 安全加法例程 (仅出现在已修补版本):
&nbsp; RtlUShortAdd (sub_1c001c6e4) (cx=当前capacity, dx=加数(5), r8=&输出新capacity)
&nbsp; 返回: 成功 0, 溢出 0xC0000095

8. 变更函数 — 完整分诊

  • UlpParseNextRequest (sub_1c000e4a0)→ UlpParseNextRequest (sub_1c000e550)

    (相似度 0.9142,安全相关):脆弱的请求处理循环。已修补版本新增一个 KIR 门控的安全增长路径:cmp UxKirRefBufferOverflowCheck (data_1c0078d80)测试在两条路径间选择——一条调用 RTL 安全加法例程 RtlUShortAdd (sub_1c001c6e4)的检查路径(回绕时以 STATUS_INTEGER_OVERFLOW退出,并从已校验的 capacity 分配),以及为 KIR 回滚保留的原始未检查 add cx, 5路径。UlpCheckTrailerLength行为未变;它是一个独立函数,仅从 sub_1c001b7b4移动到 sub_1c001c70c,不被增长逻辑调用。

  • UxDuoProcessCompleteCatalog (sub_1c00b77c0)→ UxDuoProcessCompleteCatalog (sub_1c00b8950)

    (相似度 0.9663,行为性):HTTP 请求体/分块处理状态机。变更是寄存器重命名和从 0x1c0076xxx到 0x1c0077xxx的偏移漂移(数据段重定位)外加轻微控制流重构。无安全影响。

  • UxReadHttp11ParserSettings (sub_1c00d8940)→ UxReadHttp11ParserSettings (sub_1c00d9b80)

    (相似度 0.911,行为性):配置初始化。读取注册表参数(MaxRequestBytesMaxFieldLength、…)并新增带特性标志 UxKirMaxHeadersCountLimit (data_1c0078d81)的新 MaxHeadersCount参数。非直接安全相关,但提供了一个有趣的 衰减旋钮:若 MaxHeadersCount默认 0x32、最大 0xFFFF,则推动数组超过配置上限的攻击者在已修补版本上可能触发更早的拒绝。对修补系统上的触发开发值得注意。

  • UxStartEnvironmentModule (sub_1c015e480)↔ wil_details_FeatureReporting_ReportUsageToServiceDirect (sub_1c001bce0)

    (相似度 0.2783,表面/启发式):算法不匹配——由调用序列启发式配对,非真实 diff。视为独立函数。

  • UlpCheckTrailerLength (sub_1c001b7b4)↔ WPP_SF_qddd (sub_1c0097654)

    (相似度 0.1667,启发式误配对):两个不相关函数由调用引用匹配配对。UlpCheckTrailerLength本身是同一函数,在已修补版本中重定位到 sub_1c001c70cWPP_SF_qddd是一个 WPP/ETW 追踪包装器(sub_1c0096654→ sub_1c0097654)。非语义 diff。

  • WPP_SF_qddd (sub_1c0096654)↔ RtlUShortAdd (sub_1c001c6e4)

    (相似度 0.0082,启发式误配对):未修补侧的 WPP_SF_qddd是 WPP/ETW 追踪包装器;已修补侧的 RtlUShortAdd是已修补循环调用的标准 RTL 安全加法库例程。它们是被错误配对的不同函数。RtlUShortAdd在修复中的作用已在 UlpParseNextRequest (sub_1c000e4a0)发现中涵盖。

关于非安全变更的折叠说明:全部六个函数中唯一有意义的 diff 是 (a) 添加到 UlpParseNextRequest (sub_1c000e4a0)的、由 KIR 标志 UxKirRefBufferOverflowCheck (data_1c0078d80)门控的溢出检查增长路径(其调用 RTL 库例程 RtlUShortAdd (sub_1c001c6e4)),以及 (b) UxReadHttp11ParserSettings中的新 MaxHeadersCount注册表参数。其余皆为数据段重定位、寄存器分配或 diff 工具误配对。


9. 未匹配函数

无。两个 removed和 added列表均为空。修复实现于已修补的请求处理循环中,外加 RTL 安全加法例程 RtlUShortAdd (sub_1c001c6e4)—— 该例程仅出现在已修补版本中,且因调用引用启发式而非作为新增列出,被配对到一个不相关的未修补函数。

含义:没有移除的消毒器,也没有移除的检查。补丁是增加性的:一条由 KIR 标志 UxKirRefBufferOverflowCheck (data_1c0078d80)守护的新溢出检查增长路径。未修补二进制对此 capacity + 5加法无任何溢出检查。注意已修补二进制仍包含原始未检查路径,当 KIR 标志禁用时可抵达。


10. 置信度与注意事项

置信度:高。

理由:

  • 脆弱算术在反汇编中一目了然:16 位加法紧随其后的是 32 位安全分配大小计算,中间无任何比较。

  • 补丁引入了教科书式的溢出检测惯用法 (lea; cmp; jb),恰好针对此加法。

  • mov qword [rax+rcx*8], rsi

    处的 OOB 写入使用刚刚追踪 count的同一寄存器,使数据流不言自明。

所做假设:

  • 每连接数组在真实系统上确实可达约 65,535 个条目。补丁附带默认 MaxHeadersCount为 0x32、最大 0xFFFF,这表明微软相信数组在某些配置中 确实能合法增长到 0xFFFF。驱动增长的精确攻击者可控输入(Range 头部 vs. 管线化请求 vs. 分块段)是从上下文推断的;请在你的实验室中通过追踪哪个调用方递增 count 字段来验证。
  • 数组位于 NonPagedPoolNx。这与 ExAllocatePoolWithTagPriority调用及函数其他地方缺乏 IRQL 检查一致,但请在运行时通过池标记(!pool!poolfind)确认。
  • 调用链 UlpAuthenticateRequestCompletion (sub_1c01176a0)→ UlResumeParsing (sub_1c000a940)→ UlpHandleRequest (sub_1c000e1a0)→ UlBeginOpaqueMode (sub_1c002dc84)→ UlpParseNextRequest (sub_1c000e4a0)可从远程 HTTP 流量抵达。该链取自交叉引用;在断言远程可达性之前,请用实时调试器中的断点验证每一步。

研究者在编写 PoC 前应验证:

  1. 通过在新连接上转储 [rdi+0x630][rdi+0x632][rdi+0x638]并将其与请求计数关联,确认连接对象布局。
  2. 识别每请求恰好添加一个条目的 精确请求形态——测试 Range、分块和管线化变体。
  3. 测量到达 65k 条目的真实吞吐量;预期某些中介(代理、TLS 终结器)会限制 range/管线深度,可能迫使使用许多独立的 keep-alive 请求。
  4. 确认池位置和相邻对象:在欠尺寸分配后立即捕获池遍历,以识别现实可利用的受害对象。
  5. 在启用了 Driver Verifier 的 VM 中验证 bugcheck 条件和池破坏模式(以更早、更干净地捕获溢出)。

免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:securitainment The Drift Corpus The Drift Corpus《http.sys — UlpParseNextRequest 中连接数组容量整数溢出(CWE-190)导致内核池溢出》

评论:0   参与:  0