chahinebrini 01374c426e feat(supervise-magic): TechLockdown-clone v1 — supervise iPhones without erase
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>
2026-05-27 01:55:10 +02:00

316 lines
11 KiB
Go

// Package mobilebackup2 implementiert Apple's MobileBackup2-Protocol für
// das `com.apple.mobile.mobilebackup2`-Service.
//
// Reverse-engineered aus TechLockdown's MobileBackup2Client + libimobiledevice
// reference. Protokoll: DLMessage-Frames mit Restore-Command + file-serve-loop.
//
// Architektur:
//
// ┌─────────────┐ DLMessage ┌────────────────┐
// │ Our Code │ ────────────▶│ mobilebackup2 │
// │ (Restore + │ │ (iPhone service)│
// │ file-loop) │ ◀────────────│ │
// └─────────────┘ └────────────────┘
//
// Restore-Flow:
// 1. ServiceOpen ("com.apple.mobile.mobilebackup2")
// 2. BaseVersionExchange (verhandle Protocol-Version)
// 3. Start (sende "Restore" ProcessMessage mit Options)
// 4. ServeFiles-Loop:
// - Receive DLMessage from device
// - Switch on type (ContentsOfDirectory, DownloadFiles, ...)
// - Respond with our embedded backup-files
// - Loop until DLMessageDisconnect
// 5. Reboot (triggered by separate diagnostics-Service)
package mobilebackup2
import (
"errors"
"fmt"
ios "github.com/danielpaulus/go-ios/ios"
"howett.net/plist"
"github.com/raynis/rebreak-supervise-magic/internal/dlmessage"
)
// Service-Name aus TL-Binary-Strings: "com.apple.mobilebackup2" (OHNE .mobile.)
// Old iOS hatte "com.apple.mobile.mobilebackup2", aber iOS 7+ ohne.
const serviceName = "com.apple.mobilebackup2"
// Supported protocol versions. libimobiledevice's standard ist {2.0, 2.1}.
// Wir nutzen genau das was libimobiledevice nutzt (iOS-bewährt).
var supportedVersions = []float64{2.0, 2.1}
// Client kapselt eine offene MobileBackup2-Session.
type Client struct {
deviceConn ios.DeviceConnectionInterface
dl *dlmessage.Conn
// negotiated state
protocolVersion float64
}
// Open startet die MobileBackup2-Session via Lockdown.
//
// CAVEAT: mobilebackup2 service braucht EscrowBag im StartService-Request
// (go-ios's ConnectToService schickt das nicht). Wir bauen custom-StartService
// + port-connect + SSL-enable selbst.
func Open(device ios.DeviceEntry) (*Client, error) {
// Lockdown-Session öffnen
lockdown, err := ios.ConnectLockdownWithSession(device)
if err != nil {
return nil, fmt.Errorf("mobilebackup2: lockdown session: %w", err)
}
defer lockdown.Close()
// PairRecord lesen — enthält EscrowBag
pairRecord, err := ios.ReadPairRecord(device.Properties.SerialNumber)
if err != nil {
return nil, fmt.Errorf("mobilebackup2: read pair record: %w", err)
}
if len(pairRecord.EscrowBag) == 0 {
return nil, fmt.Errorf("mobilebackup2: no EscrowBag in pair record — re-pair iPhone with passcode unlock")
}
// Custom StartService request mit EscrowBag
req := map[string]interface{}{
"Label": "rebreak-supervise-magic",
"Request": "StartService",
"Service": serviceName,
"EscrowBag": pairRecord.EscrowBag,
}
if err := lockdown.Send(req); err != nil {
return nil, fmt.Errorf("mobilebackup2: send StartService: %w", err)
}
respBytes, err := lockdown.ReadMessage()
if err != nil {
return nil, fmt.Errorf("mobilebackup2: read StartService response: %w", err)
}
// Parse response
var resp struct {
Port uint16
Service string
EnableServiceSSL bool
Error string
}
if _, err := plistUnmarshal(respBytes, &resp); err != nil {
return nil, fmt.Errorf("mobilebackup2: parse StartService response: %w", err)
}
if resp.Error != "" {
return nil, fmt.Errorf("mobilebackup2: StartService error: %s", resp.Error)
}
if resp.Port == 0 {
return nil, fmt.Errorf("mobilebackup2: StartService returned no port")
}
// Connect zur returned port via usbmux
muxConn, err := ios.NewUsbMuxConnectionSimple()
if err != nil {
return nil, fmt.Errorf("mobilebackup2: new usbmux: %w", err)
}
if err := muxConn.Connect(device.DeviceID, resp.Port); err != nil {
return nil, fmt.Errorf("mobilebackup2: muxConn.Connect(port=%d): %w", resp.Port, err)
}
deviceConn := muxConn.ReleaseDeviceConnection()
// SSL-Enable falls service das verlangt (mobilebackup2 immer SSL)
if resp.EnableServiceSSL {
if err := deviceConn.EnableSessionSsl(pairRecord); err != nil {
deviceConn.Close()
return nil, fmt.Errorf("mobilebackup2: enable SSL: %w", err)
}
}
// WICHTIG: nicht deviceConn.Conn() nutzen — das returnt die raw TCP socket
// ohne TLS-Layer. Wir wrappen deviceConn als ReadWriter via Send/Reader.
return &Client{
deviceConn: deviceConn,
dl: dlmessage.New(&deviceConnReadWriter{c: deviceConn}),
}, nil
}
// deviceConnReadWriter adaptiert go-ios's DeviceConnectionInterface zum io.ReadWriter.
// Send() schickt via TLS-Layer (wenn SSL enabled), Reader() liest dito.
type deviceConnReadWriter struct {
c ios.DeviceConnectionInterface
}
func (d *deviceConnReadWriter) Write(p []byte) (int, error) {
if err := d.c.Send(p); err != nil {
return 0, err
}
return len(p), nil
}
func (d *deviceConnReadWriter) Read(p []byte) (int, error) {
return d.c.Reader().Read(p)
}
// plistUnmarshal — helper für StartService-response-parsing.
func plistUnmarshal(data []byte, v interface{}) (int, error) {
return plist.Unmarshal(data, v)
}
// Close beendet die Session.
func (c *Client) Close() error {
if c.deviceConn != nil {
c.deviceConn.Close()
c.deviceConn = nil
}
return nil
}
// BaseVersionExchange — iOS 26-style: **DEVICE initiates**, host responds.
//
// Reverse-engineered aus TL's MobileBackup2Client.BaseVersionExchange
// disassembly (safesurfer.go calls): ReceivePacket → memequal → SendPacket →
// ReceivePacket → memequal.
//
// Flow:
//
// ← DLMessageVersionExchange [<device-version-major>, <device-version-minor>]
// → DLMessageVersionExchange ["DLVersionsOk", <chosen-version>]
// ← DLMessageVersionExchange (confirmation)
func (c *Client) BaseVersionExchange() error {
// Step 1: iPhone sendet zuerst seine version-info.
t, args, err := c.dl.Receive()
if err != nil {
return fmt.Errorf("mobilebackup2: receive initial VersionExchange: %w", err)
}
if t != dlmessage.TypeVersionExchange {
return fmt.Errorf("mobilebackup2: expected VersionExchange initial, got %s args=%v", t, args)
}
fmt.Printf("[mb2] iPhone initial VersionExchange: %v\n", args)
// Parse device-version (typically [major, minor] or [chosen_version, ...])
deviceMajor := uint64(2)
if len(args) > 0 {
if v, ok := args[0].(uint64); ok {
deviceMajor = v
}
}
// Step 2: send our acceptance with DLVersionsOk + chosen version
// matching device's major (or 2.1 as default).
chosenVersion := 2.1
if deviceMajor > 0 && deviceMajor < 100 {
chosenVersion = float64(deviceMajor) + 0.1
}
if err := c.dl.Send(dlmessage.TypeVersionExchange, "DLVersionsOk", chosenVersion); err != nil {
return fmt.Errorf("mobilebackup2: send DLVersionsOk: %w", err)
}
c.protocolVersion = chosenVersion
// Step 3: receive confirmation
t2, args2, err := c.dl.Receive()
if err != nil {
return fmt.Errorf("mobilebackup2: receive VersionExchange confirmation: %w", err)
}
fmt.Printf("[mb2] VersionExchange confirmation: type=%s args=%v\n", t2, args2)
return nil
}
// ProtocolVersion returnt die verhandelte Version (gültig nach BaseVersionExchange).
func (c *Client) ProtocolVersion() float64 {
return c.protocolVersion
}
// SendRequest — generischer ProcessMessage-Sender mit MessageName + extras.
// Beispiel:
//
// c.SendRequest("Hello", map[string]interface{}{"SupportedProtocolVersions": [2.1]})
func (c *Client) SendRequest(messageName string, extra map[string]interface{}) error {
cmd := map[string]interface{}{
"MessageName": messageName,
}
for k, v := range extra {
cmd[k] = v
}
return c.dl.SendProcessMessage(cmd)
}
// SendStatusResponse — generischer StatusResponse-Sender.
func (c *Client) SendStatusResponse(code int, errStr string, extra map[string]interface{}) error {
return c.dl.SendStatusResponse(code, errStr, extra)
}
// Receive — generischer DLMessage-Receiver (gibt Type-String + Args zurück).
// Caller muss auf Message-Type-string switchen.
func (c *Client) Receive() (string, []interface{}, error) {
return c.dl.Receive()
}
// Hello sendet das übliche Hello-Handshake nach VersionExchange.
//
// → ProcessMessage {MessageName: "Hello", SupportedProtocolVersions: [2.0, 2.1]}
// ← StatusResponse [0, "___EmptyParameterString___", {}]
//
// Returns ErrHandshakeRejected wenn Device nicht-zero-error returnt.
func (c *Client) Hello() error {
if err := c.SendRequest("Hello", map[string]interface{}{
"SupportedProtocolVersions": supportedVersions,
}); err != nil {
return err
}
t, args, err := c.dl.Receive()
if err != nil {
return fmt.Errorf("hello: %w", err)
}
if t != dlmessage.TypeProcessMessage && t != dlmessage.TypeStatusResponse {
return fmt.Errorf("hello: unexpected response type: %s", t)
}
// Bei StatusResponse: erstes arg ist error-code (uint64). 0 = OK.
if t == dlmessage.TypeStatusResponse && len(args) > 0 {
if code, ok := args[0].(uint64); ok && code != 0 {
return fmt.Errorf("hello rejected: code=%d args=%v", code, args)
}
}
return nil
}
// ErrHandshakeRejected — Device rejected unser Hello.
var ErrHandshakeRejected = errors.New("mobilebackup2: hello rejected by device")
// SendHello — sendet das Hello-Handshake (DLMessageProcessMessage{MessageName:Hello}).
// Required nach VersionExchange + VOR Restore-Command (libimobiledevice-protocol).
//
// Apple responds with Status [0, "___EmptyParameterString___", {}] bei Erfolg.
func (c *Client) SendHello() error {
if err := c.SendRequest("Hello", map[string]interface{}{
"SupportedProtocolVersions": supportedVersions,
}); err != nil {
return fmt.Errorf("hello send: %w", err)
}
t, args, err := c.dl.Receive()
if err != nil {
return fmt.Errorf("hello recv: %w", err)
}
fmt.Printf("[mb2] Hello response: type=%s args=%v\n", t, args)
return nil
}
// Start initiiert eine Restore-Operation. Übergibt das target-UDID + Restore-Options.
// Device wird dann files anfragen via DLMessageDownloadFiles / DLContentsOfDirectory.
// Diese muss caller via Receive-Loop bedienen (siehe ServeFiles).
func (c *Client) Start(targetUDID string, options map[string]interface{}) error {
// TL's exakte minimal-set (aus binary strings): nur 4 Felder.
// Plus "Apply": true — könnte triggern dass iPhone tatsächlich applied
// (gefunden als string in TL-Binary neben CloudConfig/CloudProvider/SetCloudProvider).
defaultOptions := map[string]interface{}{
"RemoveItemsNotRestored": true,
"RestoreDontCopyBackup": false, // iPhone soll backup fetchen
"RestorePreserveSettings": true,
"RestoreSystemFiles": true,
}
for k, v := range options {
defaultOptions[k] = v
}
return c.SendRequest("Restore", map[string]interface{}{
"TargetIdentifier": targetUDID,
"SourceIdentifier": targetUDID,
"Options": defaultOptions,
})
}