How ipsw Disassembles and Analyzes Mach-O Binaries: Patching and Entitlements Explained
ipsw combines the go-macho parser, a low-level ARM64 decoder, and the pkg/disass orchestration layer to parse Mach-O headers, decode instructions, resolve symbols, extract Swift strings, handle dyld cache patches, and re-sign binaries with custom entitlements.
The blacktop/ipsw repository provides a self-contained Go toolkit for deep Mach-O binary analysis. Unlike external wrapper tools, ipsw implements its own disassembly stack that handles everything from immediate operand extraction to dyld shared cache patch enumeration and entitlement management.
The Core Architecture of ipsw's Mach-O Analysis
ipsw's analysis engine rests on three integrated libraries that handle distinct layers of the Mach-O format.
The Three-Pillar Stack
go-macho– Parses Mach-O headers, load commands, symbols, and sections. Provides critical helpers likeGetFunctionForVMAddrandGetPointerAtAddressfor virtual memory translation.arm64-cgo/disassemble– Low-level ARM64 decoder that transforms 32-bit instruction words into fully-typedInstructionstructs containing operands and immediate values.ipsw disasspackage – Orchestrates the parser and decoder, walking the binary to collect immediates, resolve symbols, demangle Swift/Objective-C names, discover patches, and manage code signing workflows.
How ipsw Disassembles Mach-O Binaries Step-by-Step
The disassembly process follows a pipeline defined in pkg/disass/macho.go, centered around the MachoDisass type.
Loading and Initialization
Disassembly begins by creating a MachoDisass object that wraps a *macho.File from go-macho alongside a disass.Config struct. The configuration specifies start addresses, architecture slices, demangling preferences, and color output options.
cfg := &disass.Config{
StartAddress: m.Entry,
Demangle: true,
Color: true,
Data: m.Data,
}
eng := disass.NewMachoDisass(m, cfg)
Instruction Triaging and Immediate Extraction
The MachoDisass.Triage() method drives the analysis. It reads raw data via d.Data(), feeds each 4-byte word to disassemble.Decompose, and records every address used as an immediate operand. This includes branch targets, literal references, and ADRP/ADD or ADRP/LDR instruction pairs.
The triage phase produces a map Addresses[address] → imm that serves as the foundation for all subsequent analysis.
Symbol Resolution and Context Enrichment
After triage completes, ipsw resolves raw addresses to human-readable symbols. The FindSymbol, IsFunctionStart, and IsBranchLocation methods query the Mach-O symbol table and dyld cache information. When quiet mode is disabled, the engine populates Locations[imm] for addresses inside known functions and builds AddrDetails structs for section-specific metadata including segment names, section indices, and flags.
The symbols package caches these lookups to accelerate repeated queries during large binary analysis.
Swift and Objective-C Metadata Extraction
The FindSwiftStrings method scans the instruction stream for Swift string object patterns (swift.StringObject layout) and Objective-C class/method references. Using the swift helper from go-macho/pkg/swift, it decodes string structures and maps runtime metadata sections back to their originating high-level constructs.
Handling Patches in Dyld Shared Caches
ipsw provides deep visibility into Apple's dyld shared cache patch mechanisms, essential for analyzing system binaries or creating delta patches.
Patch Info Structures
When analyzing a dyld shared cache, ipsw reads patch info structures defined in pkg/dyld/types.go. These include CachePatchInfoV*, CachePatchableExportV*, and related variants that describe export patches and location patches within cache images.
Accessing Patch Data via the dyld Package
The dyld package exposes patch information through helper methods on cache images. When the MachoDisass engine targets a cache image, it invokes dyld.File.GetPatchInfo to enumerate image patches, export patches, and location patches. These are surfaced via the Patch and PatchableExport interfaces.
if img.HasPatches() {
patches := img.GetPatches()
// Process patch entries
}
Entitlement Handling and Code Re-Signing
Binary modification invalidates code signatures, so ipsw provides utilities to extract existing entitlements and apply new ones during the analysis workflow.
Extracting Existing Entitlements
The internal/commands/ent/ent.go package implements high-level commands that parse a binary's code signature and print embedded entitlements. This allows analysts to inspect sandboxing rules and capability declarations before patching.
Re-signing with Custom Entitlements
For binaries requiring modification, internal/utils/macos.go provides CodeSignWithEntitlements. This function uses macOS's native codesign tool to apply new entitlements from a plist file, with fallback handling for XML format conversions. The CLI exposes this via the --entitlements flag in cmd/ipsw/cmd/macho/macho_disass.go, ensuring patched binaries remain valid for system execution.
ipsw codesign MyApp --entitlements ./MyApp.entitlements.xml
Practical Implementation Examples
Disassembling a Mach-O File via Go API
This example demonstrates loading a binary, running the triage phase, extracting Swift strings, and generating assembly output:
package main
import (
"log"
"github.com/blacktop/go-macho"
"github.com/blacktop/ipsw/pkg/disass"
)
func main() {
m, err := macho.Open("MyApp")
if err != nil {
log.Fatalf("open macho: %v", err)
}
defer m.Close()
cfg := &disass.Config{
StartAddress: m.Entry,
Demangle: true,
Color: true,
Data: m.Data,
}
eng := disass.NewMachoDisass(m, cfg)
if err := eng.Triage(); err != nil {
log.Fatalf("triage: %v", err)
}
if ss, err := eng.FindSwiftStrings(); err == nil {
for addr, s := range ss {
log.Printf("Swift string @0x%x: %s", addr, s)
}
}
log.Println(disass.Disassemble(eng))
}
CLI Disassembly with Symbol Resolution and Patching
Analyze a specific function, apply a binary patch, and re-sign with entitlements:
# Disassemble the main function from the arm64 slice
ipsw macho disass MyApp \
--arch arm64 \
--symbol main \
--demangle \
--color
# Apply a binary patch at a specific offset
ipsw patch apply MyApp \
--offset 0x1234 \
--bytes DE AD BE EF
# Re-sign with custom entitlements
ipsw codesign MyApp --entitlements ./MyApp.entitlements.xml
Extracting Dyld Cache Patches Programmatically
Enumerate all patched images within a shared cache:
package main
import (
"log"
"github.com/blacktop/ipsw/pkg/dyld"
)
func main() {
cache, err := dyld.Open("/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e")
if err != nil {
log.Fatalf("open cache: %v", err)
}
defer cache.Close()
for _, img := range cache.Images {
if img.HasPatches() {
patches := img.GetPatches()
log.Printf("%s has %d patch(es)", img.Name, len(patches))
}
}
}
Summary
- ipsw implements a complete Mach-O analysis stack in Go using
go-macho,arm64-cgo/disassemble, and thepkg/disassorchestration layer. - The
MachoDisass.Triage()method inpkg/disass/macho.godrives instruction decoding, immediate extraction, and address mapping. - Symbol resolution leverages the Mach-O symbol table and dyld cache information to map virtual addresses to function names and section metadata.
- Swift and Objective-C analysis occurs via
FindSwiftStrings, which detects string objects and runtime metadata patterns. - Patch handling uses
pkg/dyld/types.gostructures anddyld.File.GetPatchInfoto expose export and location patches within shared cache images. - Entitlement workflows rely on
internal/utils/macos.goto extract existing entitlements and re-sign modified binaries with custom capability lists.
Frequently Asked Questions
How does ipsw handle universal binaries with multiple architectures?
ipsw's go-macho library automatically parses universal binary headers and exposes individual architecture slices. When using the CLI, the --arch flag (e.g., --arch arm64) selects the specific slice for disassembly, and the MachoDisass constructor receives the appropriate *macho.File representing that slice.
Can ipsw disassemble binaries without symbol tables?
Yes. While symbol tables enhance output via FindSymbol and IsFunctionStart, the Triage() phase operates purely on instruction decoding. The disassemble.Decompose function processes raw bytes into Instruction structs regardless of debug information, though address-to-name resolution will show raw virtual addresses rather than function names.
What patch types does ipsw detect in dyld shared caches?
According to pkg/dyld/types.go, ipsw detects image patches, export patches (CachePatchableExportV*), and location patches (CachePatchInfoV*). These cover pointer rebasing, bind fixes, and rebase/bind optimizations used by the dynamic linker during cache loading.
How does ipsw preserve binary validity after modification?
ipsw uses CodeSignWithEntitlements in internal/utils/macos.go to invoke macOS's native codesign utility after any binary modification. This strips the invalid existing signature and applies a new ad-hoc or identity-based signature, optionally embedding a custom entitlements plist to maintain or extend the binary's capability declarations.
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 →