// 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 [, ] // → DLMessageVersionExchange ["DLVersionsOk", ] // ← 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, }) }