How to Use debugserver and SSH for Interaction with Jailbroken Devices
The ipsw CLI provides a Go-based toolchain that combines SSH automation with USB-muxed debugserver connections to install, resign, and remotely debug processes on jailbroken iOS devices.
The blacktop/ipsw repository delivers a comprehensive solution for iOS debugging that bridges SSH-based device preparation with low-level GDB-compatible debugging protocols. By leveraging the debugserver daemon present on iOS devices, developers can attach to running processes, inspect memory, and control execution flow without Xcode. This workflow specifically targets jailbroken devices where elevated privileges allow attachment to any process, utilizing pure Go implementations for cross-platform compatibility.
Architecture of the ipsw Debugserver Stack
The workflow divides cleanly between SSH-based device preparation and USB-based debugging communication, with each layer implemented as a discrete Go package.
SSH Transport and Device Preparation
The internal/ssh/ssh.go module handles authentication, secure file transfer, and remote command execution. It provides ssh.NewSSH to establish connections with either password or key-based authentication, including a known-hosts callback that safely handles new host keys. This layer also contains the ResignDebugserver function which applies custom entitlements to the binary using codesign.
Debugserver Installation Logic
The command implementation in cmd/ipsw/cmd/ssh/ssh_debugserver.go orchestrates the installation flow. It first checks for existing binaries at /usr/libexec/debugserver, falls back to extracting debugserver from macOS Developer Disk Images when necessary, and handles the complete deployment pipeline including permission setting (chmod 0755) and diagnostic configuration.
USB Service Client
Located in pkg/usb/debugserver/debugserver.go, this component creates a lockdownd service connection for com.apple.debugserver. The debugserver.NewClient(udid) function establishes the USB-muxed socket and exposes high-level methods like Send, Recv, and Request for GDB packet communication.
GDB Packet Engine
The pkg/usb/debugserver/gdbserver.go file implements the wire protocol, handling packet framing ($payload#checksum), checksum validation, and byte-level I/O operations. This engine parses responses from debugserver and formats commands according to the GDB remote serial protocol specification.
Installing debugserver via SSH
Before USB debugging can commence, the device must host a properly signed debugserver binary with elevated entitlements. The ipsw toolchain automates this via SSH.
The installation process follows this sequence:
- Detection: Verify if
/usr/libexec/debugserverexists on the target device - Extraction: On macOS hosts, mount the Developer Disk Image and extract the binary if missing
- Resigning: Apply entitlements from
internal/ssh/data/debugserver.plistusingutils.CodeSignWithEntitlementsto grant thetask_for_pid-allowprivilege - Deployment: Copy the binary to
/usr/libexec/and set executable permissions - Configuration: Optionally enable private log data and crash log symbolication by writing plists to
/Library/Preferences/Logging/and/Library/Preferences/
package main
import (
"fmt"
"log"
"github.com/blacktop/ipsw/internal/ssh"
)
func installDebugserver() error {
// Configure SSH connection to jailbroken device
cfg := &ssh.Config{
Host: "192.168.1.10",
Port: "22",
User: "root",
Pass: "alpine",
Insecure: true, // Skip known-hosts verification for testing
}
client, err := ssh.NewSSH(cfg)
if err != nil {
return fmt.Errorf("ssh connection failed: %w", err)
}
defer client.Close()
// Check if debugserver exists
err = client.RunCommand("test -f /usr/libexec/debugserver")
if err != nil {
// Binary missing - extract from DDI and resign
// This calls internal logic equivalent to:
// ssh.ResignDebugserver("/path/to/extracted/debugserver")
log.Println("debugserver not found, extraction/resigning required")
}
// Enable additional diagnostics
if err := client.EnablePrivateLogData(); err != nil {
log.Printf("Warning: could not enable private logging: %v", err)
}
if err := client.EnableSymbolication(); err != nil {
log.Printf("Warning: could not enable symbolication: %v", err)
}
return nil
}
Communicating with debugserver Over USB
Once the binary resides on the device, the ipsw client communicates via USB using the lockdownd service. Unlike SSH, this connection provides the low-latency, interrupt-driven communication necessary for interactive debugging.
The client supports standard GDB remote commands:
vAttach;<pid>: Attach to a process by hexadecimal PIDg: Read general registersm<addr>,<len>: Read memoryc: Continue executions: Single stepqLaunchSuccess: Query launch status
package main
import (
"fmt"
"log"
"github.com/blacktop/ipsw/pkg/usb/debugserver"
)
func attachToProcess(udid string, pid int) error {
// Create USB client for specific device
client, err := debugserver.NewClient(udid)
if err != nil {
return fmt.Errorf("failed to create debugserver client: %w", err)
}
defer client.Close()
// Attach to target process (PID as hexadecimal)
attachCmd := fmt.Sprintf("vAttach;%x", pid)
resp, err := client.Request(attachCmd)
if err != nil {
return fmt.Errorf("attach request failed: %w", err)
}
log.Printf("Attach response: %s", resp)
// Read all general purpose registers
regs, err := client.Request("g")
if err != nil {
return fmt.Errorf("failed to read registers: %w", err)
}
log.Printf("Register state: %s", regs)
// Continue execution
_, err = client.Request("c")
if err != nil {
return fmt.Errorf("continue command failed: %w", err)
}
return nil
}
Complete End-to-End Workflow
Combining both phases creates a seamless pipeline: SSH prepares the device environment while USB handles the debugging session. The following implementation demonstrates extracting a debugserver, installing it with proper entitlements, and immediately attaching to a target process.
package main
import (
"fmt"
"github.com/blacktop/ipsw/internal/ssh"
"github.com/blacktop/ipsw/pkg/usb/debugserver"
)
func debugSession(udid, host string, targetPID int) error {
// Phase 1: SSH Preparation
sshCfg := &ssh.Config{
Host: host,
Port: "22",
User: "root",
Pass: "alpine",
Insecure: true,
}
sshClient, err := ssh.NewSSH(sshCfg)
if err != nil {
return fmt.Errorf("ssh connection error: %w", err)
}
defer sshClient.Close()
// Ensure debugserver is present and properly signed
// ResignDebugserver applies entitlements from internal/ssh/data/debugserver.plist
if err := ssh.ResignDebugserver("/tmp/debugserver"); err != nil {
return fmt.Errorf("resigning failed: %w", err)
}
// Deploy to device
if err := sshClient.CopyToDevice("/tmp/debugserver", "/usr/libexec/debugserver"); err != nil {
return fmt.Errorf("copy failed: %w", err)
}
if err := sshClient.RunCommand("chmod 0755 /usr/libexec/debugserver"); err != nil {
return fmt.Errorf("chmod failed: %w", err)
}
// Enable comprehensive logging
_ = sshClient.EnablePrivateLogData()
_ = sshClient.EnableSymbolication()
// Phase 2: USB Debugging
dsClient, err := debugserver.NewClient(udid)
if err != nil {
return fmt.Errorf("debugserver client error: %w", err)
}
defer dsClient.Close()
// Attach and begin debugging
cmd := fmt.Sprintf("vAttach;%x", targetPID)
if _, err := dsClient.Request(cmd); err != nil {
return fmt.Errorf("attach failed: %w", err)
}
return nil
}
Summary
- SSH Layer: The
internal/ssh/ssh.gopackage handles authentication, file transfer, and remote command execution with support for both password and key-based methods. - Binary Preparation:
cmd/ipsw/cmd/ssh/ssh_debugserver.goautomates detection, extraction from Developer Disk Images, and resigning with entitlements stored ininternal/ssh/data/debugserver.plist. - USB Communication:
pkg/usb/debugserver/debugserver.goestablishes lockdownd service connections, whilepkg/usb/debugserver/gdbserver.goimplements the GDB packet protocol for reliable debugging transmission. - Workflow: Combine SSH for one-time device setup with USB connections for low-latency debugging sessions, enabling attachment to any process on jailbroken devices.
Frequently Asked Questions
How does ipsw handle debugserver code signing for jailbroken devices?
The ipsw toolchain uses ssh.ResignDebugserver in internal/ssh/ssh.go to apply a custom entitlements plist embedded at internal/ssh/data/debugserver.plist. This plist contains the task_for_pid-allow entitlement required to attach to arbitrary processes on jailbroken devices. The utility writes these entitlements to a temporary file and invokes codesign with the --entitlements flag, allowing the binary to bypass standard iOS code signing restrictions when running on a jailbroken system.
What is the difference between the SSH and USB connections in this workflow?
SSH serves as the transport layer for device preparation—copying files, setting permissions, and installing the debugserver binary—while USB (specifically the lockdownd service) provides the debugging channel. The USB connection in pkg/usb/debugserver/debugserver.go creates a direct socket to com.apple.debugserver that supports the GDB remote protocol's interrupt-driven communication model, essential for breakpoints and stepping, whereas SSH operates over TCP/IP and lacks the real-time characteristics required for interactive debugging.
Can this workflow function on non-macOS hosts?
Yes, the SSH components and USB debugging client are pure Go and function on Linux, Windows, and macOS. The only platform-specific limitation occurs when the target device lacks /usr/libexec/debugserver, requiring extraction from a macOS Developer Disk Image (DDI). This extraction step, handled in the installation logic, checks the host OS at runtime and only executes on macOS systems where the DDI mounting utilities are available.
What GDB commands are supported by the ipsw debugserver client?
The client supports the full GDB remote serial protocol implemented in pkg/usb/debugserver/gdbserver.go, including process attachment (vAttach), register reading (g), memory read/write (m/M), continue (c), single step (s), and launch status queries (qLaunchSuccess). The Request method in the client handles packet formatting with proper checksums, while raw Send and Recv methods allow transmission of arbitrary GDB packets for extended debugging scenarios.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →