Implementiert eigenen MobileBackup2-Restore-Trick zur Supervision-Übernahme von aktivierten iOS-Geräten ohne Factory-Reset. Foundation für DiGA-Phase-G Lock-Layer-Stack (non-removable Apps, non-removable Profiles, OnDemand-VPN- Toggle-Lock) auf Consumer-iPhones. Verifiziert end-to-end auf: - iPhone Air (iPhone18,4, iOS 26.5): TL→ReBreak re-supervise ✅ - Olfa iPhone 14 Pro (iPhone15,3, iOS 26.4.2): TL→ReBreak re-supervise ✅ Key empirische Findings: 1. Find-My-iPhone MUSS off sein (ErrorCode 211 sonst) → Stolen Device Protection (SDP) zwingt FMI an seit iOS 17.3+ 2. SupervisorHostCertificates DARF NICHT in CloudConfigurationDetails sein für fresh-supervise auf activated unsupervised devices (sonst partial-apply) 3. MCInstall.SetCloudConfiguration firet 14002 auf allen activated devices → MobileBackup2-Restore-Trick ist der einzige Weg 4. TL's extracted-embed-bytes != TL's wire-output (Runtime-Mutation) → Verbatim-Kopieren reicht nicht Reverse-Engineering basiert komplett auf: - Apple's public protocol docs (devicemanagement, mobilebackup2 schemas) - libimobiledevice (open-source reference impl) - TL public-distributed binary (interop-RE, legal per US-DMCA-1201 + EU-2009/24) Structure: cmd/supervise/ — main CLI (check, cloud-config, supervise, cert-info, unsupervise) cmd/dump-artifacts/ — diagnostic helper (no device needed) cmd/usbmux-proxy/ — MITM-proxy for TL-traffic-capture (debug) cmd/tl-patcher/ — patches TL's hard-coded usbmuxd path (debug) internal/dlmessage/ — DLMessage wire-protocol (4-byte BE length + plist) internal/mobilebackup2/— mobilebackup2-service impl (BaseVersionExchange, Hello, Restore, ServeFiles + TL-extracted templates) internal/cloudconfig/ — CloudConfigurationDetails.plist builder (cert-less, 25 keys matching TL's runtime-output) internal/cert/ — auto-gen + persist supervisor-cert in ~/.rebreak-supervise/ internal/mcinstall/ — MCInstall.GetCloudConfiguration für state-checks internal/device/ — go-ios DeviceEntry wrapper internal/afclock/ — AFC sync-lock auf /com.apple.itunes.lock_sync internal/notification_proxy/ — PostNotification (syncWillStart/etc) internal/preflight/ — FMI/Activation/OS-version pre-checks internal/supervise/ — End-to-end Flow-Orchestrierung (MobileBackup2 default, MCInstall via REBREAK_FORCE_MCINSTALL=1) Pending für volle Productization (Phase G): - Fresh-supervise direkt-empirisch auf truly-unsupervised iPhone testen (heute Nacht nur durch Inferenz aus TL-Verhalten gestützt) - Auto-MDM-enroll-Step nach Supervise (ConfigurationURL oder cfgutil-style) - DiGA-Onboarding-Flow + Lyra-Coach für FMI/SDP-Disable - Multi-Device-Validation (Modelle, iOS-Versionen) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
236 lines
6.9 KiB
Go
236 lines
6.9 KiB
Go
// Manifest.db Runtime-Generator — baut die SQLite-DB die iOS während
|
|
// Restore liest um zu wissen welche Files im Backup liegen.
|
|
//
|
|
// Schema (Apple-public, verifiziert aus TechLockdown's extracted Manifest.db):
|
|
//
|
|
// CREATE TABLE Files (
|
|
// fileID TEXT PRIMARY KEY,
|
|
// domain TEXT,
|
|
// relativePath TEXT,
|
|
// flags INTEGER, -- 1=file, 2=directory
|
|
// file BLOB -- NSKeyedArchive-encoded MBFile metadata
|
|
// );
|
|
// CREATE TABLE Properties (
|
|
// key TEXT PRIMARY KEY,
|
|
// value BLOB
|
|
// );
|
|
//
|
|
// fileID-Berechnung: hex(SHA1(domain + "-" + relativePath))
|
|
package mobilebackup2
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha1"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"howett.net/plist"
|
|
|
|
_ "modernc.org/sqlite" // SQL-driver registration
|
|
)
|
|
|
|
// SystemGroupDomain — Apple's container-identifier für ConfigurationProfiles.
|
|
const SystemGroupDomain = "SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles"
|
|
|
|
// File-entry für Manifest.db.
|
|
type DBEntry struct {
|
|
Domain string
|
|
RelativePath string
|
|
IsDirectory bool
|
|
Size int64 // 0 für directories
|
|
Mode uint32 // POSIX mode (0o755 für dirs, 0o644 für files default)
|
|
}
|
|
|
|
// BuildManifestDB erstellt eine in-memory SQLite-DB, fügt die Einträge ein,
|
|
// und returnt die fertige DB als bytes (lesbar via plistlib-äquivalenten Code
|
|
// oder direkt von iOS's MobileBackup2-Service).
|
|
func BuildManifestDB(entries []DBEntry) ([]byte, error) {
|
|
// SQLite-driver hat keinen In-Memory-to-bytes Pfad direkt — wir nutzen
|
|
// eine tmp-Datei, lesen sie nach dem Schreiben.
|
|
tmpFile, err := os.CreateTemp("", "manifest-*.db")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("manifest_db: tmpfile: %w", err)
|
|
}
|
|
tmpPath := tmpFile.Name()
|
|
tmpFile.Close()
|
|
defer os.Remove(tmpPath)
|
|
defer os.Remove(filepath.Join(filepath.Dir(tmpPath), filepath.Base(tmpPath)+"-shm"))
|
|
defer os.Remove(filepath.Join(filepath.Dir(tmpPath), filepath.Base(tmpPath)+"-wal"))
|
|
|
|
db, err := sql.Open("sqlite", tmpPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("manifest_db: open: %w", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Schema
|
|
schema := []string{
|
|
`CREATE TABLE Files (
|
|
fileID TEXT PRIMARY KEY,
|
|
domain TEXT,
|
|
relativePath TEXT,
|
|
flags INTEGER,
|
|
file BLOB
|
|
)`,
|
|
`CREATE TABLE Properties (
|
|
key TEXT PRIMARY KEY,
|
|
value BLOB
|
|
)`,
|
|
}
|
|
for _, stmt := range schema {
|
|
if _, err := db.Exec(stmt); err != nil {
|
|
return nil, fmt.Errorf("manifest_db: schema: %w", err)
|
|
}
|
|
}
|
|
|
|
// Insert entries
|
|
insert, err := db.Prepare(`INSERT INTO Files (fileID, domain, relativePath, flags, file) VALUES (?, ?, ?, ?, ?)`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("manifest_db: prepare: %w", err)
|
|
}
|
|
defer insert.Close()
|
|
|
|
for _, e := range entries {
|
|
fileID := ComputeFileID(e.Domain, e.RelativePath)
|
|
flags := 1
|
|
if e.IsDirectory {
|
|
flags = 2
|
|
}
|
|
mbfileBlob, err := EncodeMBFile(e.RelativePath, e.IsDirectory, e.Size, e.Mode)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("manifest_db: MBFile %s: %w", e.RelativePath, err)
|
|
}
|
|
if _, err := insert.Exec(fileID, e.Domain, e.RelativePath, flags, mbfileBlob); err != nil {
|
|
return nil, fmt.Errorf("manifest_db: insert %s: %w", e.RelativePath, err)
|
|
}
|
|
}
|
|
|
|
// Close + flush
|
|
if err := db.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Read file back as bytes
|
|
data, err := os.ReadFile(tmpPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("manifest_db: read tmp: %w", err)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// ComputeFileID — Apple's fileID-Berechnung: SHA1(domain + "-" + relativePath).
|
|
//
|
|
// Empirisch verifiziert mit TL's extracted Manifest.db:
|
|
//
|
|
// domain="SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles"
|
|
// relativePath=""
|
|
// → fileID="9581eb754ee03b7f535293caf770235f0f37f8a8"
|
|
func ComputeFileID(domain, relativePath string) string {
|
|
h := sha1.New()
|
|
h.Write([]byte(domain + "-" + relativePath))
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
// EncodeMBFile — empirisch verifiziert gegen TL's bplist_02 (CloudConfigurationDetails-Entry):
|
|
//
|
|
// LastModified: 1554229750 (2019-04-02)
|
|
// LastStatusChange: 1554229750
|
|
// Birth: 1554229750
|
|
// InodeNumber: 51802
|
|
// UserID: 501
|
|
// GroupID: -2
|
|
// Flags: 0
|
|
// Mode: 33188 (0o100644 für regular file, 0o40755 für dir)
|
|
// ProtectionClass: 4
|
|
// Size: <variable>
|
|
// RelativePath: UID(2)
|
|
// $class: UID(3)
|
|
//
|
|
// NOTE 2026-05-27: TL-Embed-Werte für DIR-MBFiles (Size=0, ProtClass=0/4,
|
|
// Mode=040000 für Library) wurden ausgetestet — iPhone bail't ohne Reboot.
|
|
// Schlussfolgerung: TL mutiert die Embed-Werte zur Runtime, der Extract ist
|
|
// nicht 1:1 verwendbar. Rolled back zur originalen "skip Size/ProtClass für
|
|
// dirs"-Logik die wenigstens Reboot in Restore-Mode triggerte.
|
|
func EncodeMBFile(relativePath string, isDir bool, size int64, mode uint32) ([]byte, error) {
|
|
// Mode: full POSIX mode = file-type-bits | perm-bits
|
|
if mode == 0 {
|
|
if isDir {
|
|
mode = 0o40755 // directory + 0755
|
|
} else {
|
|
mode = 0o100644 // regular file + 0644
|
|
}
|
|
} else if mode < 0o100000 {
|
|
// Just perm-bits given — add file-type bits
|
|
if isDir {
|
|
mode |= 0o40000
|
|
} else {
|
|
mode |= 0o100000
|
|
}
|
|
}
|
|
|
|
mbFileObj := map[string]interface{}{
|
|
"$class": plist.UID(3),
|
|
"LastModified": int64(1554229750), // TL's timestamp (2019-04-02)
|
|
"LastStatusChange": int64(1554229750),
|
|
"Birth": int64(1554229750),
|
|
"GroupID": int64(-2),
|
|
"UserID": int64(501),
|
|
"InodeNumber": int64(51802),
|
|
"Flags": int64(0),
|
|
"Mode": int64(mode),
|
|
"RelativePath": plist.UID(2),
|
|
}
|
|
if !isDir {
|
|
mbFileObj["Size"] = size
|
|
mbFileObj["ProtectionClass"] = int64(4)
|
|
}
|
|
|
|
// NSKeyedArchive envelope
|
|
archive := map[string]interface{}{
|
|
"$archiver": "NSKeyedArchiver",
|
|
"$version": int64(100000),
|
|
"$top": map[string]interface{}{"root": plist.UID(1)},
|
|
"$objects": []interface{}{
|
|
"$null", // 0
|
|
mbFileObj, // 1
|
|
relativePath, // 2
|
|
map[string]interface{}{ // 3
|
|
"$classname": "MBFile",
|
|
"$classes": []interface{}{"MBFile", "NSObject"},
|
|
},
|
|
},
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
enc := plist.NewEncoderForFormat(&buf, plist.BinaryFormat)
|
|
if err := enc.Encode(archive); err != nil {
|
|
return nil, fmt.Errorf("encode MBFile: %w", err)
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// DefaultRestoreEntries returnt die 4 Standard-Entries für Cloud-Config-
|
|
// Injection (matched TL's extracted Manifest.db):
|
|
//
|
|
// 1. root dir (relativePath="")
|
|
// 2. Library
|
|
// 3. Library/ConfigurationProfiles
|
|
// 4. Library/ConfigurationProfiles/CloudConfigurationDetails.plist (datei, size variable)
|
|
func DefaultRestoreEntries(cloudConfigSize int64) []DBEntry {
|
|
return []DBEntry{
|
|
{Domain: SystemGroupDomain, RelativePath: "", IsDirectory: true},
|
|
{Domain: SystemGroupDomain, RelativePath: "Library", IsDirectory: true},
|
|
{Domain: SystemGroupDomain, RelativePath: "Library/ConfigurationProfiles", IsDirectory: true},
|
|
{
|
|
Domain: SystemGroupDomain,
|
|
RelativePath: "Library/ConfigurationProfiles/CloudConfigurationDetails.plist",
|
|
IsDirectory: false,
|
|
Size: cloudConfigSize,
|
|
Mode: 0o644,
|
|
},
|
|
}
|
|
}
|