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 like GetFunctionForVMAddr and GetPointerAtAddress for virtual memory translation.
  • arm64-cgo/disassemble – Low-level ARM64 decoder that transforms 32-bit instruction words into fully-typed Instruction structs containing operands and immediate values.
  • ipsw disass package – 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 the pkg/disass orchestration layer.
  • The MachoDisass.Triage() method in pkg/disass/macho.go drives 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.go structures and dyld.File.GetPatchInfo to expose export and location patches within shared cache images.
  • Entitlement workflows rely on internal/utils/macos.go to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →