~/posts/binary/cve-2020-16898-rdnss-parser-boundaries.md

CVE-2020-16898:RDNSS 的八字节错位

从 RDNSS 长度奇偶性追到两阶段解析分歧,再区分 NdisGetDataBuffer 的直接访问与栈复制路径。用边界模型验证八字节错位,结合分片 RA 约束和 GS 崩溃证据限定影响。

date[31:24]
read[23:16]
6 分钟
cat[15:8]
二进制
目录
  1. 0x00RDNSS 的长度必须闭合
  2. 0x01两次遍历赋予尾部不同身份
  3. 0x02Route Information 承接未经同类检查的长度
  4. 0x03NdisGetDataBuffer 的复制分支改变结果
  5. 0x04分片、重组与协议接收条件
  6. 0x05用算术模型隔离边界错误
  7. 0x06修复与回归应恢复同一组不变量
  8. 0x07参考资料

CVE-2020-16898 涉及 2020 年 10 月安全更新前的 Windows IPv6 路由器通告(Router Advertisement,RA)处理。一个畸形 RDNSS 选项让两轮遍历相差 8 字节:验证阶段视作数据的尾部,在使用阶段变成了新的选项头,最终影响内核栈上的复制长度。

关键不只是 Length 字段的奇偶性,还包括 NET_BUFFER 是否连续,以及函数返回前的栈 cookie 检查。以下地址和崩溃值限定于该历史案例;具体 Windows build 与 tcpip.sys 哈希尚未对应,结论停留在解析错位和内存破坏,不延伸为跨版本的完整代码执行链。

RDNSS 的长度必须闭合

RDNSS 类型为 0x19。按照 RFC 8106 第 5.1 与 5.3.1 节,它由 8 字节固定头部和至少一个 16 字节 IPv6 地址组成。Length 以 8 字节为单位,合法值至少为 3,并满足 (Length - 1) % 2 == 0。

RDNSS 单地址布局BE
偏移名称类型大小值
0x00Typeuint810x19
0x01Lengthuint813
0x02Reserveduint162
0x04Lifetimeuint324
0x08AddressIPv60x10
sizeof(struct RDNSS) = 0x18(24 字节)

包含 N 个地址时,字节长度为 8 + 16*N,Length 为 1 + 2*N。偶数 Length 不只是“多了些填充”:RDNSS 结构本身没有供这一残余半地址使用的尾部字段。

设 Length 为 4。外层按 4*8 认定跨度为 32 字节,内层若用整数除法得到 (4-1)//2 == 1 个地址,就只消费 8+16 == 24 字节。

同一选项的两种边界4 行
Length 声明跨度 按完整地址消费 差值
3 24 24 0
4 32 24 8
5 40 40 0
6 48 40 8

两次遍历赋予尾部不同身份

入口 tcpip!Ipv6pHandleRouterAdvertisement 与后续 Ipv6pUpdateRDNSS 的配合,是定位边界分歧的重点。第一轮按声明长度检查选项;第二轮按类型消费内容。若两者的游标推进规则不同,“已验证的字节范围”并不保证“最终解释出的每个对象都已验证”。

案例中的两次读取起点如下。这里展示的是地址关系,不是可移植的函数偏移。

游标与字段解释证据4 行
观察项 值 含义
第一次选项起点 ffff920766042650 RDNSS 起点
后续选项起点 ffff920766042668 实际前进 0x18
声明跨度 0x20 与实际增量相差 8 字节
尾部标记 XXXXYYYY 首两个 0x58 分别被读取为 Type 和 Length

把偶数长度 RDNSS 的最后 8 字节作为新头部读取后,第二轮看到的选项序列已不是第一轮验证过的序列。严格地说,不是某个超长选项成功通过了自己的校验,而是对应的类型检查从未作用于这段字节。

Route Information 承接未经同类检查的长度

Route Information 类型为 0x18。RFC 4191 第 2.3 节 规定其 Length 为 1、2 或 3,取值还受 Prefix Length 约束,因此协议表示的最大长度是 24 字节。

直接放入一个超长 Route Information,早期校验会发现长度异常。让其头部落在 RDNSS 的残余 8 字节内,则形成另一条路径:第一轮仍把它看作 RDNSS 内容,第二轮才把它识别为 Route Information,并把其中的 Length 交给后续内存操作。

审查这类多阶段解析时,可以同时记录三个量:

  • 验证读取的起点。
  • 使用读取的起点。
  • 两个阶段各自计算跨度的公式。

只证明缓冲区“整体足够长”并不充分;还要证明被使用的类型和边界正是此前检查的那一个。

NdisGetDataBuffer 的复制分支改变结果

NdisGetDataBufferc
PVOID NdisGetDataBuffer(
    PNET_BUFFER NetBuffer,
    ULONG BytesNeeded,
    PVOID Storage,
    ULONG AlignMultiple,
    ULONG AlignOffset
);

Microsoft 的接口文档 要求调用者提供的 Storage 至少容纳 BytesNeeded 字节。函数在数据连续、满足访问条件时可以返回原有数据指针;数据不连续且提供了 Storage 时,会使用该区域组织所需数据。数据总量不足、映射资源不足等条件也会导致空指针返回。

本案例的调用点把固定大小栈区域用作 Storage,却根据重新解释出的 Length 计算 BytesNeeded。Length << 3 表示乘以 8;Length 为 0x22 时,请求长度为 0x110,即 272 字节,远大于正常 Route Information 的 24 字节上限。

因此需要同时证明两件事:输入长度到达了复制参数;本次调用实际进入了使用备用存储区的分支。只观察到 Length 偏大,还不足以解释栈覆盖。

分片、重组与协议接收条件

案例使用分片输入影响重组后的数据布局,从而触发非连续数据的复制路径。这里真正起作用的是 NET_BUFFER 和内存描述符链(MDL)的实际组织方式;网络上出现 Fragment Header,不自动等于目标位置一定跨越 MDL 边界。驱动、收包及重组实现都会影响这一结果。

RA 还具有链路范围及 Hop Limit 等接收条件。这个案例不支持“任意互联网位置都能直接触达目标解析器”的结论。

另一个独立约束是实际数据量:即便请求长度越过了栈区域容量,网络缓冲区也要有足够的后续字节,才可能进入相应复制。只有夸大的 Length 而没有足够数据,可能先走失败返回。复现时应检查接收与重组后的状态,而不是把某个分片尺寸当作跨环境常量。

用算术模型隔离边界错误

下面的模型只验证字节消费关系,不生成网络流量,也不模拟 Windows 内核。它穷举 8 位 Length 中从 3 到 255 的 253 个值,核对奇数的差值为 0、偶数的差值为 8,并检查案例指针增量与复制长度。

rdnss_boundaries.pypython
def rdnss_offsets(length_units):
    if not 3 <= length_units <= 255:
        raise ValueError("length outside the modeled range")
    declared = length_units * 8
    walked = 8 + ((length_units - 1) // 2) * 16
    return declared, walked

for length in range(3, 256):
    declared, walked = rdnss_offsets(length)
    assert declared - walked == (8 if length % 2 == 0 else 0)
assert rdnss_offsets(4) == (32, 24)
assert 0xffff920766042668 - 0xffff920766042650 == 0x18
assert 0x22 << 3 == 0x110 == 272
print("PASS: 253 lengths; even delta=8; odd delta=0; copy length=272")
边界模型的实际输出
$ python rdnss_boundaries.py
PASS: 253 lengths; even delta=8; odd delta=0; copy length=272

案例还记录到返回地址区域出现 0x4242424242424242,随后触发 BugCheck 0x139,参数 1 为 2,并进入 _report_gsfailure。Microsoft 对该参数的定义 对应栈 cookie 检测到的栈缓冲区越界。

分别评价每一层证据4 行
层次 本案例的证据边界
RDNSS 游标错位 指针增量与尾部字段解释相符;算术模型另行通过
未经同类检查的选项长度 两次遍历对尾部的对象身份不同
栈内存破坏 复制路径、填充值及安全检查失败相互支持
稳定代码执行 未完成这一证明;覆盖控制数据不等同于成功使用它

栈保护检测到破坏,并不否定之前发生了越界;反过来,发生越界也不证明已经越过栈保护。把这两种判断分开,才能准确表达结果。

修复与回归应恢复同一组不变量

系统维护应以 Microsoft 的 CVE-2020-16898 更新公告 选择对应版本的安全更新。对解析器本身,以下是从根因推导出的审查与回归条件,不是对某个未核对补丁的逐行描述。

边界检查清单4 步
  1. 1

    先验证结构合法性

    RDNSS 长度至少为 3,且 (Length - 1) % 2 == 0;合法地址数应与声明跨度精确匹配。

  2. 2

    固定每个选项的解释边界

    校验与消费复用同一套边界信息,类型解析器的消费量与外层声明跨度保持一致。

  3. 3

    在复制点约束备用存储

    比较 BytesNeeded 与真实 Storage 容量,而不是仅信任前序协议遍历。

  4. 4

    覆盖两种内存表示

    对连续与非连续缓冲区分别测试,同时覆盖数据不足、畸形长度、协议层提前丢弃等结果。

这条链的核心是三层约束发生分离:协议长度、解析游标和底层存储。审查时把三者重新对齐,才有机会在栈损坏之前定位真正失守的边界。

参考资料

NORMAL~/posts/binary/cve-2020-16898-rdnss-parser-boundaries.md§--
0%zh-CN