滥用 Android 媒体管线与图像解析器

Tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE) 学习和实践 Azure 黑客技术:HackTricks Training Azure Red Team Expert (AzRTE)

支持 HackTricks

投递:消息应用 ➜ MediaStore ➜ 特权解析器

现代 OEM 构建会定期运行特权媒体索引器,这些索引器会为了 “AI” 或分享功能重新扫描 MediaStore。在 2025 年 4 月补丁之前的 Samsung 固件上,com.samsung.ipservice 会加载 Quram (/system/lib64/libimagecodec.quram.so) 并自动解析 WhatsApp(或其他应用)放入 MediaStore 的任何文件。实际上,攻击者可以发送一个伪装成 IMG-*.jpg 的 DNG,等待受害者点击 “download”(一键),即使用户从未打开图库,特权服务也会解析该 payload。

$ file IMG-2025-02-10.jpeg
TIFF image data ...
$ exiftool IMG-2025-02-10.jpeg | grep "Opcode List"
Opcode List 1 : [opcode 23], [opcode 23], ...

关键要点

  • 交付依赖于系统媒体的重新解析(而不是聊天客户端),因此继承了该进程的权限(对图库的完全读/写访问、放置新媒体的能力等)。
  • 任何通过 MediaStore 可达的图像解析器(vision widgets、wallpapers、AI résumé 功能等)如果攻击者能说服目标保存媒体,就会变成远程可达。

0-click DD+/EAC-3 解码路径 (Google Messages ➜ mediacodec sandbox)

现代消息栈也会在转录/搜索前自动解码 audio。在 Pixel 9 上,Google Messages 会在用户打开消息之前将收到的 RCS/SMS audio 交给位于 /vendor/lib64/libcodec2_soft_ddpdec.soDolby Unified Decoder (UDC),将 0-click 面扩大到媒体 codec。

关键解析约束

  • 每个 DD+ syncframe 最多有 6 个 block;每个 block 能将最多 0x1FF 字节的攻击者可控 skip data 复制到一个 skip buffer(≈ 每帧 0x1FF * 6 字节)。
  • skip buffer 会被扫描以查找 EMDFsyncword (0xX8) + emdf_container_length (16b) + 可变长度字段。emdf_payload_size 通过一个无界的 variable_bits(8) 循环解析。
  • EMDF payload 字节会在一个每帧自定义的 “evo heap” bump allocator 中分配,然后从受 emdf_container_length 限制的位读取器按字节复制。

Integer-overflow → heap-overflow 原语 (CVE-2025-54957)

  • ddp_udc_int_evo_malloc 通过 total_size += (8 - total_size) % total_sizealloc_size+extra 对齐到 8 字节,没有检测溢出。接近 0xFFFFFFFFFFFFFFF9..FF 的值在 AArch64 上会缩成很小的 total_size
  • 复制循环仍然使用来自 emdf_payload_size逻辑 payload_length,因此攻击者字节会覆盖 evo-heap 中该缩小块之后的数据。
  • 溢出长度由攻击者可选的 emdf_container_length 精确限定;溢出字节就是攻击者控制的 EMDF payload 数据。slab allocator 在每个 syncframe 重置,提供可预测的相邻布局。

次级读取原语 如果 emdf_container_length > skipl,EMDF 解析会读取已初始化 skip 字节之外的数据(OOB read)。单独看它会 leaks 零/已知媒体,但在破坏相邻堆元数据后,它可以读取回被破坏的区域以验证漏洞利用。

利用配方

  1. 构造 EMDF,使用巨大的 emdf_payload_size(通过 variable_bits(8))使 allocator 填充对齐环绕到一个很小的块。
  2. emdf_container_length 设为期望的溢出长度(≤ 总 skip 数据预算);在 EMDF payload 中放置溢出字节。
  3. 塑造每帧的 evo heap,使小分配位于解码器静态缓冲区(≈693 KB)或为每个 decoder 实例分配一次的动态缓冲区(≈86 KB)内目标结构之前。
  4. 可选地选择 emdf_container_length > skipl,在破坏后从 skip buffer 读回被覆盖的数据以进行验证。

Quram 的 DNG Opcode 解释器漏洞

DNG 文件在不同解码阶段嵌入三组 opcode 列表。Quram 复制了 Adobe 的 API,但其 Stage-3 对 DeltaPerColumn(opcode ID 11)的处理器信任攻击者提供的平面边界。

DeltaPerColumn 中失败的平面边界

  • 攻击者设置 plane=5125planes=5123,尽管 Stage-3 图像只暴露平面 0–2 (RGB)。
  • Quram 计算 opcode_last_plane = image_planes + opcode_planes 而不是 plane + count,并且从未检查结果的平面范围是否适合图像。
  • 因此循环会将 delta 写入 raw_pixel_buffer[plane_index],偏移由攻击者完全控制(例如,plane 5125 ⇒ 偏移 5125 * 2 bytes/pixel = 0x2800)。每个 opcode 将一个 16-bit float 值(0x6666)加到目标位置,产生一个精确的 heap OOB add 原语。

将增量转为任意写

  • 利用首先用 480 个畸形的 DeltaPerColumn 操作破坏 Stage-3 的 QuramDngImage.bottom/right,使后续的 opcode 将巨大的坐标当作在界内处理。
  • 然后将 MapTable opcodes(opcode 7)指向这些伪造的边界。使用全零的替换表或带有 -Inf deltas 的 DeltaPerColumn,攻击者可以将任一区域清零,然后应用额外的 deltas 来写入精确值。
  • 因为 opcode 参数存在于 DNG 元数据中,payload 可以在不直接触碰进程内存的情况下编码数十万次写操作。

Scudo 下的堆塑形

Scudo 按大小对分配进行分桶。Quram 恰好以相同的 0x30 字节块大小分配以下对象,因此它们位于相同区域(堆上以 0x40 字节间隔):

  • Stage 1/2/3 的 QuramDngImage 描述符
  • QuramDngOpcodeTrimBounds 和 厂商 Unknown opcodes(ID ≥14,包括 ID 23)

利用序列化分配以确定性地放置块:

  1. Stage-1 的 Unknown(23) opcode(20,000 条目)喷洒 0x30 块,之后这些块被释放。
  2. Stage-2 释放那些 opcode 并在被释放区域内放置一个新的 QuramDngImage
  3. 240 个 Stage-2 的 Unknown(23) 条目被释放,Stage-3 随即分配其 QuramDngImage 加上一个同样大小的新 raw pixel buffer,重用这些位置。
  4. 一个精心构造的 TrimBounds opcode 在列表 3 中首先运行,并分配另一个 raw pixel buffer,然后释放 Stage-2 状态,保证了“raw pixel buffer ➜ QuramDngImage”的相邻性。
  5. 另外 640 个 TrimBounds 条目被标记为 minVersion=1.4.0.1,因此 dispatcher 跳过它们,但它们的 backing objects 仍保持分配,后续成为原语目标。

该编排将 Stage-3 raw buffer 放在 Stage-3 QuramDngImage 之前,因此基于平面的溢出会翻转描述符内部字段,而不是随机崩溃其他状态。

重用厂商“Unknown” Opcodes 作为数据块

Samsung 在厂商特定 opcode ID(例如 ID 23)中保留了高位,这指示解释器 分配 该结构但跳过执行。利用者将这些休眠对象当作攻击者可控的堆块:

  • opcode 列表 1 和 2 的 Unknown(23) 条目用作连续的 scratchpad 来存放 payload 字节(JOP chain 在相对于 raw buffer 的偏移 0xf000 处,shell 命令在 0x10000)。
  • 因为解释器在处理列表 3 时仍然将每个对象视为 opcode,所以之后夺取某个对象的 vtable 就足以开始执行攻击者数据。

构造伪造的 MapTable 对象 & 绕过 ASLR

MapTable 对象比 TrimBounds 更大,但一旦布局损坏到位,解析器就会愉快地从 OOB 读取额外参数:

  1. 使用线性写原语部分覆盖一个 TrimBounds 的 vtable 指针,替换为一个伪造的 MapTable substitution table,该表将来自相邻 TrimBounds vtable 的低 2 字节映射到 MapTable vtable。受支持的 Quram 构建之间只有低字节差异,因此单个 64K 查找表可以覆盖七个固件版本和每个 4 KB 的 ASLR slide。
  2. 修补 TrimBounds 的其余字段(top/left/width/planes),使该对象在稍后执行时表现得像有效的 MapTable
  3. 在被置零的内存上执行伪造 opcode。因为 substitution table 指针实际引用另一个 opcode 的 vtable,输出字节就会变成来自 libimagecodec.quram.so 或其 GOT 的 leaked 低位地址。
  4. 再次应用 MapTable 过程,将这些两字节的 leaks 转换为指向 gadget 的偏移,例如 __ink_jpeg_enc_process_image+64QURAMWINK_Read_IO2+124qpng_check_IHDR+624 和 libc 的 __system_property_get 条目。攻击者有效地在其喷洒的 opcode 区域内重建完整地址,而无需本地内存泄露 API。

触发 JOP ➜ system() 转换

一旦 gadget 指针和 shell 命令在 opcode 喷洒区就位:

  1. 最后一轮 DeltaPerColumn 写入在 Stage-3 QuramDngImage 的偏移 0x22 上追加 0x0100,将其 raw buffer 指针移动 0x10000,使其现在引用攻击者的命令字符串。
  2. 解释器开始执行尾部的 1040 个 Unknown(23) opcode。第一个被破坏的条目其 vtable 已被替换为位于 0xf000 的伪造表,因此 QuramDngOpcode::aboutToApply 解析出假表中的 qpng_read_data(第 4 项)。
  3. 链式 gadget 完成的操作:加载 QuramDngImage 指针,加 0x20 指向 raw buffer 指针,解引用它,将结果拷贝到 x19/x0,然后通过被重写为 system 的 GOT 插槽跳转。因为 raw buffer 指针现在等于攻击者字符串,最终 gadget 在 com.samsung.ipservice 内执行 system(<shell command>)

关于分配器变体的说明

存在两类 payload:一个针对 jemalloc 调优,另一个针对 scudo。它们在 opcode 区块的排序上有差异以实现相邻性,但共享相同的逻辑原语(DeltaPerColumn bug ➜ MapTable zero/write ➜ bogus vtable ➜ JOP)。Scudo 禁用的 quarantine 使 0x30 字节 freelist 重用变得确定性,而 jemalloc 则依赖 tile/subIFD 大小来控制大小类。

参考

Tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE) 学习和实践 Azure 黑客技术:HackTricks Training Azure Red Team Expert (AzRTE)

支持 HackTricks