// Command usbmux-proxy ist ein Man-in-the-Middle Unix-socket proxy zwischen // einem Client (z.B. TechLockdown) und Apple's `/var/run/usbmuxd`-Daemon. // // Architektur: // // Client --[unix socket /tmp/usbmux-proxy]--> proxy --[unix socket /var/run/usbmuxd]--> usbmuxd // | // v // /tmp/usbmux-capture-*.log // // Usage: // // # Terminal 1 — start proxy // ./bin/rebreak-usbmux-proxy // // # Terminal 2 — run TL through proxy (kein sudo nötig!) // USBMUXD_SOCKET_ADDRESS=unix:///tmp/usbmux-proxy \ // open ~/Downloads/TechLockdown-supervise-mac-arm64.app // // Log-Output: hex+ascii dump für jeden gesendeten/empfangenen byte. // Plus: usbmux-frame-parsing (16-byte header + payload), plist-detection. package main import ( "bytes" "encoding/binary" "flag" "fmt" "io" "net" "os" "strings" "sync" "sync/atomic" "time" "howett.net/plist" ) const ( defaultProxyPath = "/tmp/usbmux-proxy" defaultRealPath = "/var/run/usbmuxd" ) var ( flagProxyPath = flag.String("proxy", defaultProxyPath, "unix-socket-pfad für proxy") flagRealPath = flag.String("real", defaultRealPath, "unix-socket-pfad zum echten usbmuxd") flagLogPath = flag.String("log", "", "log-output (default: /tmp/usbmux-capture-TIMESTAMP.log)") flagQuiet = flag.Bool("q", false, "kein stdout output, nur file-log") sessionCounter atomic.Uint64 ) func main() { flag.Parse() logPath := *flagLogPath if logPath == "" { logPath = fmt.Sprintf("/tmp/usbmux-capture-%s.log", time.Now().Format("20060102-150405")) } logFile, err := os.Create(logPath) if err != nil { fmt.Fprintln(os.Stderr, "log create:", err) os.Exit(1) } defer logFile.Close() logger := &mitm{ out: logFile, quiet: *flagQuiet, } logger.printf("== usbmux-proxy starting — proxy=%s → real=%s, log=%s\n", *flagProxyPath, *flagRealPath, logPath) // Cleanup any stale socket os.Remove(*flagProxyPath) listener, err := net.Listen("unix", *flagProxyPath) if err != nil { fmt.Fprintln(os.Stderr, "listen:", err) os.Exit(1) } defer listener.Close() defer os.Remove(*flagProxyPath) logger.printf("== listening — point USBMUXD_SOCKET_ADDRESS=unix://%s\n", *flagProxyPath) for { clientConn, err := listener.Accept() if err != nil { logger.printf("accept error: %v\n", err) continue } sessionID := sessionCounter.Add(1) go handleSession(sessionID, clientConn, logger) } } type mitm struct { mu sync.Mutex out *os.File quiet bool } func (m *mitm) printf(format string, args ...interface{}) { m.mu.Lock() defer m.mu.Unlock() s := fmt.Sprintf(format, args...) m.out.WriteString(s) if !m.quiet { os.Stderr.WriteString(s) } } func handleSession(sessionID uint64, client net.Conn, logger *mitm) { defer client.Close() logger.printf("\n=== SESSION %d START (client connected) ===\n", sessionID) daemon, err := net.Dial("unix", *flagRealPath) if err != nil { logger.printf("[session %d] dial daemon: %v\n", sessionID, err) return } defer daemon.Close() var wg sync.WaitGroup wg.Add(2) // Client → Daemon go func() { defer wg.Done() pipeWithLog(sessionID, "C→D", client, daemon, logger) daemon.Close() // signal other side }() // Daemon → Client go func() { defer wg.Done() pipeWithLog(sessionID, "D→C", daemon, client, logger) client.Close() }() wg.Wait() logger.printf("=== SESSION %d END ===\n", sessionID) } // pipeWithLog liest from src + writes to dst, logging each chunk. func pipeWithLog(sessionID uint64, dir string, src, dst net.Conn, logger *mitm) { buf := make([]byte, 64*1024) for { n, err := src.Read(buf) if n > 0 { chunk := buf[:n] logger.printf("\n--- [session %d] %s | %d bytes | %s ---\n", sessionID, dir, n, time.Now().Format("15:04:05.000")) dumpChunk(chunk, logger) if _, werr := dst.Write(chunk); werr != nil { logger.printf("write err: %v\n", werr) return } } if err != nil { if err != io.EOF { logger.printf("read err: %v\n", err) } return } } } // dumpChunk format-aware: tries to detect usbmux-frame, plist, etc. // Falls back to hex+ascii. func dumpChunk(data []byte, logger *mitm) { // 1) Try usbmux-frame parsing (16-byte header + payload) if len(data) >= 16 { length := binary.LittleEndian.Uint32(data[0:4]) version := binary.LittleEndian.Uint32(data[4:8]) request := binary.LittleEndian.Uint32(data[8:12]) tag := binary.LittleEndian.Uint32(data[12:16]) if length > 16 && int(length) <= len(data) && version <= 2 && request <= 100 { payload := data[16:length] logger.printf(" [usbmux-frame] len=%d ver=%d req=%d tag=%d payload=%d bytes\n", length, version, request, tag, len(payload)) tryParsePlist(payload, logger) // remaining bytes if int(length) < len(data) { logger.printf(" [+%d bytes after frame]\n", len(data)-int(length)) dumpHex(data[length:], logger, 4) } return } } // 2) Try mobilebackup2 / lockdown 4-byte length-prefix + plist if len(data) >= 4 { length := binary.BigEndian.Uint32(data[0:4]) if int(length)+4 <= len(data) && length > 0 && length < 1024*1024 { payload := data[4 : 4+length] logger.printf(" [length-prefixed] len=%d payload=%d bytes\n", length, len(payload)) tryParsePlist(payload, logger) if int(length)+4 < len(data) { logger.printf(" [+%d bytes after frame]\n", len(data)-int(length)-4) dumpHex(data[int(length)+4:], logger, 4) } return } } // 3) Raw hex dump dumpHex(data, logger, 4) } // tryParsePlist — try XML or binary plist parse, log readable form. func tryParsePlist(data []byte, logger *mitm) { if bytes.HasPrefix(data, []byte(" len(data) { end = len(data) } hex := "" ascii := "" for j := i; j < end; j++ { hex += fmt.Sprintf("%02x ", data[j]) if data[j] >= 32 && data[j] < 127 { ascii += string(data[j]) } else { ascii += "." } } hex += strings.Repeat(" ", lineLen-(end-i)) logger.printf("%s%04x: %s | %s\n", prefix, i, hex, ascii) } } func indent(s, pad string) string { lines := strings.Split(s, "\n") for i, line := range lines { if line != "" { lines[i] = pad + line } } return strings.Join(lines, "\n") }