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

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
}