VLC 3.0.23 VP9 分辨率切换越界写崩溃 PoC 深度解析:64x64 → 64x8192 帧高突变触发陈旧 slice-thread entries 分配

原创2026-10-02 14:36:57806 阅读
文章标签:示例工程网络安全渗透测试

VLC 3.0.23 VP9 分辨率切换越界写崩溃 PoC 深度解析:64x64 → 64x8192 帧高突变触发陈旧 slice-thread entries 分配

本篇技术指南围绕仓库内 vlc-vp9-reschange-crash-poc 这一紧凑型崩溃复现器展开:它用纯标准库 Python 生成一个仅 405 字节的 VP9 IVF 媒体样本,在 VLC 3.0.23(Windows x64)内置的 FFmpeg VP9 解码器中稳定触发 slice-thread 进度数组的越界零写。读完本文,你将掌握该崩溃条件的完整成因链条(sb_rows 计算、陈旧分配与重置循环)、样本的二进制结构与校验方式,以及如何在本地用 VLC 二进制安全重放并解读崩溃码。

概述:一个 405 字节的崩溃复现器

该 PoC 的核心是一份嵌入 Python 脚本中的 VP9 IVF 样本,包含两帧关键数据:

  • 第 1 帧:64x64
  • 第 2 帧:64x8192

两帧保持相同的 VP9 tile-column 布局(样本文件名中的 tc0 即 tile columns = 0),但第二帧的帧高从 64 突变到 8192。正是"帧高变化而 tile-column 数量不变"这一组合,使 VLC 3.0.23 附带的 FFmpeg VP9 解码器在解码第二帧时命中了陈旧(stale)的 slice-thread 进度分配。

复现器本身极小:poc.py 依赖 Python 标准库(argparse、base64、hashlib、json、subprocess、pathlib),无需任何第三方包,生成样本后以 JSON 输出路径、SHA256 哈希与大小,并可选地拉起本地 VLC 进程进行重放。需要强调的是,README 明确标注其研究状态为"不完整、持续进行中"(Research status: incomplete and continuing),它定位为崩溃复现器而非完整利用链。

漏洞机理:slice-thread 进度数组的陈旧分配

entries 数组按当前帧的超级块行数分配

VP9 解码器以切片(slice)并行方式解码,需要一个逐行记录各切片解码进度的数组,README 中称为 entries。关键点在于:该数组的分配大小由当前帧的超级块(superblock)行数决定,而超级块是固定 64x64 像素的块。

对于 64x64 的首帧:

sb_rows = (64 + 63) >> 6 = 1
entries allocation = 1 * sizeof(atomic_int) = 4 bytes

即首帧只需要 1 个 atomic_int(4 字节)的进度槽。对于后续的 64x8192 帧:

sb_rows = (8192 + 63) >> 6 = 128

新帧需要 128 个进度槽。

分辨率改变为何不触发重新分配

正常情况下,分辨率变化会触发解码器内部状态的重置与重新分配;但该路径的特殊之处在于:当 tile-column 数量不发生变化时,entries 数组不会被重新分配,仍然保留首帧按 1 行分配的 4 字节大小,形成陈旧分配。这解释了"必须同时满足帧高变化 + tile-column 稳定"才能复现的条件——若 tile-column 数量变化,分配路径会被刷新,越界条件也就不存在。

重置循环:对陈旧分配的固定模式零写

解码新帧时,VP9 的 slice-thread 重置循环按新帧的 sb_rows 逐行清零:

for (i = 0; i < s->sb_rows; i++)
    atomic_store(&s->entries[i], 0);

新帧 sb_rows = 128,而 entries 仍是首帧的 4 字节分配,于是第二帧的解码过程变成一串 4 字节零写,持续越过原本的 4 字节分配边界。在 Windows 上的 VLC 3.0.23 进程中,具体结果取决于堆布局与运行时状态,观测到的表现包括:堆损坏终止(0xC0000374)与访问违规(0xC0000005)。

从样本到崩溃:IVF 容器与 VP9 帧结构

生成后的样本可在本地直接按 IVF 容器格式解析验证(解析逻辑与 poc.py 中内嵌样本的字节布局一致):

字段 值 说明
文件总大小 405 字节 与 README 声明一致
文件魔数 DKIF IVF 容器标识
版本 / 头长 0 / 32 字节 标准 IVF 头
FourCC VP90 VP9 编码
容器宽 / 高 64 / 64 IVF 头保留首帧尺寸
帧率 rate=1, scale=1 时间戳单位为 1s
帧数 2 第 0、1 帧

两帧在文件中的布局(偏移从 IVF 头 32 字节后开始):

帧 帧载荷大小 时间戳 数据偏移
第 0 帧(64x64) 94 字节 0 32
第 1 帧(64x8192) 255 字节 1 138

一个值得注意的细节:IVF 容器头部仍记录 64x64(容器头不随帧更新),真正的分辨率变化是编码在 VP9 帧的比特流内部的——两帧的未压缩头前缀(0x82 0x49 0x83 0x42 ...)一致,差异体现在后续的帧尺寸与压缩头区域,这与 README"第二帧改变帧高、保持 tile-column 布局稳定"的描述吻合。

使用方式:生成与校验样本

生成默认的 IVF 样本(输出 vp9_reschange_64x64_to_64x8192_tc0.ivf):

python poc.py

生成到自定义路径:

python poc.py -o sample.ivf

可选地拉起本地 VLC 重放:

python poc.py --vlc "C:\Path\To\VLC\vlc.exe"

脚本在写出样本前会先做内嵌数据的完整性校验:对 base64 解码后的字节计算 SHA256,若与 EXPECTED_SHA256 不符则直接抛出 RuntimeError("embedded sample hash mismatch"),防止样本被篡改。预期哈希为:

F26BDEFBDFD0B44359E314E0BFDE7AEA979D29F80F598749DCCA68AB34F54649

脚本输出结构化的 JSON:

{
  "sample": "/abs/path/vp9_reschange_64x64_to_64x8192_tc0.ivf",
  "sha256": "F26BDEFBDFD0B44359E314E0BFDE7AEA979D29F80F598749DCCA68AB34F54649",
  "size": 405,
  "vlc": { "...": "仅指定 --vlc 时出现" }
}

CLI 参数一览(见 poc.py 的 argparse 定义):

参数 默认值 说明
-o, --output vp9_reschange_64x64_to_64x8192_tc0.ivf 样本输出路径
--vlc 无 可选,vlc.exe 绝对路径,用于本地重放
--timeout 8(秒) VLC 子进程超时,超时判定为 timeout

VLC 重放:命令行参数逐一解析

当传入 --vlc 时,poc.py 的 run_vlc() 会构造一套适合无人值守重放的参数集,并在 VLC 所在目录(cwd=str(vlc.parent))启动子进程:

vlc.exe -I dummy --dummy-quiet --ignore-config --no-media-library
       --play-and-exit --run-time 2 --no-one-instance
       --no-qt-privacy-ask --no-qt-error-dialogs --no-crashdump
       --no-audio --vout dummy sample.ivf vlc://quit

各参数作用:

参数 用途
-I dummy 使用无界面 dummy 交互模块,便于自动化
--dummy-quiet 抑制 dummy 模块输出
--ignore-config 忽略用户配置,保证复现环境一致
--no-media-library 禁用媒体库,避免干扰
--play-and-exit --run-time 2 播放后退出,最多运行 2 秒
--no-one-instance 允许多实例,不与既有 VLC 冲突
--no-qt-privacy-ask / --no-qt-error-dialogs 关闭隐私询问与错误弹窗,防止进程挂起等待交互
--no-crashdump 关闭崩溃转储弹窗
--no-audio / --vout dummy 禁用音频、使用 dummy 视频输出,聚焦解码路径
vlc://quit 播放结束后主动退出

子进程结束后,结果中会包含 status(clean / crash:<类型> / nonzero / timeout)、returncode 与十六进制形式 returncode_hex、elapsed 耗时,以及 stdout_tail / stderr_tail(各取末尾 2000 字符)。

崩溃观测与分类

poc.py 内置了一张 Windows 崩溃码表,用于把子进程返回码映射为可读类型:

十六进制返回码 含义 分类结果
0xC0000005 访问违规(access violation) crash:access_violation
0xC0000374 堆损坏(heap corruption) crash:heap_corruption
0xC0000409 栈缓冲区溢出(stack buffer overrun) crash:stack_buffer_overrun

返回码通过 code32() 做 32 位掩码归一化后参与分类;返回码为 0 判定 clean,超时判定 timeout,其余返回码归为 nonzero。

README 还记录了本地插桩观测到的具体证据:在 plugins/codec/libavcodec_plugin.dll 中,陈旧重置循环位于 RVA 0x698a5c,以固定零写模式对陈旧 entries 分配执行写入。针对 64x64 -> 64x8192 样本的直接存储追踪显示:

  • 共 129 次 entries 存储;
  • 其中 127 次越过请求的 4 字节分配;
  • 其中 114 次越过分配器原始可用块(raw usable block)。

可见越界写入规模远超分配本身,且已穿透到堆块的可用区域之外。README 同时提醒:在 Windows VLC 3.0.23 上,进程的具体表现(堆损坏终止或访问违规)取决于堆布局与运行时状态,并非每次重放都呈现同一崩溃码。

测试目标、适用前提与研究边界

README 声明该复现器针对以下环境验证:

  • VLC media player 3.0.23 for Windows x64
  • 解码模块:plugins/codec/libavcodec_plugin.dll
  • VP9 解码器源码谱系:FFmpeg 4.4.x 的 VP9 解码器

关键解码器行为是"分辨率变化但不改变 tile-column 数量时 entries 分配保持陈旧"。因此复现的有效性前提是:目标 VLC 附带的 libavcodec 模块保持上述分配/重置逻辑,且运行在 Windows 平台(堆损坏/访问违规的观测结果依赖平台堆行为)。

仓库同时明确界定:这是一个紧凑的崩溃复现器,关于该原语(primitive)完整可利用性的研究尚不完整、仍在继续。请在自有的本地测试环境中运行——生成媒体文件的唯一用途是复现并研究解码器故障路径,不应在生产环境或他人设备上使用。

延伸阅读

登录后查看全文
exploitarium