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>
188 lines
5.8 KiB
Go
188 lines
5.8 KiB
Go
// Package dlmessage implementiert Apple's DLMessage RPC-Protocol das von
|
|
// MobileBackup2, NotificationProxy und ähnlichen Apple-Services genutzt wird.
|
|
//
|
|
// Wire format:
|
|
//
|
|
// [4 bytes: total length big-endian] [XML plist payload]
|
|
//
|
|
// Payload ist ein plist-Array:
|
|
//
|
|
// [<DLMessage-type-string>, <arg1>, <arg2>, ...]
|
|
//
|
|
// Beispiel für DLMessageProcessMessage:
|
|
//
|
|
// ["DLMessageProcessMessage", {<command-dict>}]
|
|
//
|
|
// Reference: libimobiledevice's MobileBackup2-Protocol-Reverse-Engineering
|
|
// + verifiziert aus TechLockdown's libimobiledevice.MobileBackup2Client
|
|
// Symbol-Liste (Receive, SendFiles, Start, BaseVersionExchange).
|
|
package dlmessage
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"howett.net/plist"
|
|
)
|
|
|
|
// DLMessage-Type-Konstanten — Apple's enum für ProcessMessage-Arten.
|
|
const (
|
|
TypeVersionExchange = "DLMessageVersionExchange"
|
|
TypeDeviceReady = "DLMessageDeviceReady"
|
|
TypeProcessMessage = "DLMessageProcessMessage"
|
|
TypeStatusResponse = "DLMessageStatusResponse"
|
|
TypeDisconnect = "DLMessageDisconnect"
|
|
TypePing = "DLMessagePing"
|
|
|
|
// File-Operations
|
|
TypeContentsOfDirectory = "DLContentsOfDirectory"
|
|
TypeDownloadFiles = "DLMessageDownloadFiles"
|
|
TypeUploadFiles = "DLMessageUploadFiles"
|
|
TypeCopyItem = "DLMessageCopyItem"
|
|
TypeRemoveItems = "DLMessageRemoveItems"
|
|
TypeCreateDirectory = "DLMessageCreateDirectory"
|
|
TypeMoveItems = "DLMessageMoveItems"
|
|
TypeGetFreeDiskSpace = "DLMessageGetFreeDiskSpace"
|
|
)
|
|
|
|
// Conn ist ein Wrapper über eine generische bidirektionale Connection
|
|
// die DLMessage-Frames lesen/schreiben kann. Die underlying Connection
|
|
// kommt von go-ios's DeviceConnectionInterface.
|
|
type Conn struct {
|
|
rw io.ReadWriter
|
|
}
|
|
|
|
// New erzeugt einen Conn der DLMessages über `rw` sendet/empfängt.
|
|
func New(rw io.ReadWriter) *Conn {
|
|
return &Conn{rw: rw}
|
|
}
|
|
|
|
// DebugMode dumpt alle Send/Receive-bytes als hex. Aktiviere via env REBREAK_DLMSG_DEBUG=1.
|
|
var DebugMode = false
|
|
|
|
// Send schreibt eine DLMessage als [length-prefix][xml-plist].
|
|
//
|
|
// args wird zu einem plist-Array gemacht. Erstes Element ist der
|
|
// Message-Type-String, danach kommen die ProcessMessage-Daten.
|
|
//
|
|
// Beispiel:
|
|
//
|
|
// conn.Send(TypeProcessMessage, map[string]interface{}{
|
|
// "MessageName": "Hello",
|
|
// "ProtocolVersion": "2.1",
|
|
// })
|
|
func (c *Conn) Send(messageType string, args ...interface{}) error {
|
|
arr := make([]interface{}, 0, 1+len(args))
|
|
arr = append(arr, messageType)
|
|
arr = append(arr, args...)
|
|
|
|
var buf bytes.Buffer
|
|
enc := plist.NewEncoderForFormat(&buf, plist.XMLFormat)
|
|
if err := enc.Encode(arr); err != nil {
|
|
return fmt.Errorf("dlmessage: encode plist: %w", err)
|
|
}
|
|
|
|
// length-prefix als big-endian uint32
|
|
payload := buf.Bytes()
|
|
hdr := make([]byte, 4)
|
|
binary.BigEndian.PutUint32(hdr, uint32(len(payload)))
|
|
|
|
if DebugMode {
|
|
fmt.Printf("[dlmsg→] %d bytes payload (header: %x)\n", len(payload), hdr)
|
|
preview := payload
|
|
if len(preview) > 500 {
|
|
preview = preview[:500]
|
|
}
|
|
fmt.Printf("[dlmsg→] %s\n", string(preview))
|
|
}
|
|
if _, err := c.rw.Write(hdr); err != nil {
|
|
return fmt.Errorf("dlmessage: write header: %w", err)
|
|
}
|
|
if _, err := c.rw.Write(payload); err != nil {
|
|
return fmt.Errorf("dlmessage: write payload: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Receive liest eine DLMessage und returnt den Type-String + die restlichen
|
|
// Array-Elemente als []interface{}.
|
|
func (c *Conn) Receive() (messageType string, args []interface{}, err error) {
|
|
// 4-byte length-prefix
|
|
hdr := make([]byte, 4)
|
|
if _, err = io.ReadFull(c.rw, hdr); err != nil {
|
|
return "", nil, fmt.Errorf("dlmessage: read header: %w", err)
|
|
}
|
|
length := binary.BigEndian.Uint32(hdr)
|
|
if length == 0 {
|
|
return "", nil, errors.New("dlmessage: zero-length frame")
|
|
}
|
|
if length > 64*1024*1024 {
|
|
return "", nil, fmt.Errorf("dlmessage: frame too large (%d bytes)", length)
|
|
}
|
|
|
|
// payload
|
|
payload := make([]byte, length)
|
|
if _, err = io.ReadFull(c.rw, payload); err != nil {
|
|
return "", nil, fmt.Errorf("dlmessage: read payload: %w", err)
|
|
}
|
|
|
|
if DebugMode {
|
|
fmt.Printf("[dlmsg←] %d bytes payload (header: %x)\n", len(payload), hdr)
|
|
preview := payload
|
|
if len(preview) > 500 {
|
|
preview = preview[:500]
|
|
}
|
|
fmt.Printf("[dlmsg←] %s\n", string(preview))
|
|
fmt.Printf("[dlmsg←] hex: %x\n", preview[:min(80, len(preview))])
|
|
}
|
|
|
|
// decode plist-array
|
|
var arr []interface{}
|
|
if _, derr := plist.Unmarshal(payload, &arr); derr != nil {
|
|
return "", nil, fmt.Errorf("dlmessage: parse plist: %w", derr)
|
|
}
|
|
if len(arr) == 0 {
|
|
return "", nil, errors.New("dlmessage: empty array")
|
|
}
|
|
|
|
t, ok := arr[0].(string)
|
|
if !ok {
|
|
return "", nil, fmt.Errorf("dlmessage: first element not string: %T", arr[0])
|
|
}
|
|
return t, arr[1:], nil
|
|
}
|
|
|
|
// SendProcessMessage sendet `DLMessageProcessMessage` mit dem command-dict.
|
|
// Convenience für den häufigsten Send-Pattern.
|
|
func (c *Conn) SendProcessMessage(cmd map[string]interface{}) error {
|
|
return c.Send(TypeProcessMessage, cmd)
|
|
}
|
|
|
|
// SendStatusResponse sendet `DLMessageStatusResponse` mit error-code + msg.
|
|
func (c *Conn) SendStatusResponse(errorCode int, errorStr string, errorDict map[string]interface{}) error {
|
|
return c.Send(TypeStatusResponse, errorCode, errorStr, errorDict)
|
|
}
|
|
|
|
// ReceiveProcessMessage erwartet als Antwort einen DLMessageProcessMessage und
|
|
// returnt das command-dict. Strict — wirft Error bei anderem Type.
|
|
func (c *Conn) ReceiveProcessMessage() (map[string]interface{}, error) {
|
|
t, args, err := c.Receive()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if t != TypeProcessMessage {
|
|
return nil, fmt.Errorf("dlmessage: expected ProcessMessage, got %s (args: %v)", t, args)
|
|
}
|
|
if len(args) == 0 {
|
|
return nil, errors.New("dlmessage: ProcessMessage has no payload")
|
|
}
|
|
dict, ok := args[0].(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("dlmessage: ProcessMessage payload not a dict: %T", args[0])
|
|
}
|
|
return dict, nil
|
|
}
|