How the ipatool Hook System Intercepts Memory Access During Emulation

The ipatool hook system uses the Unicorn engine's callback mechanism bridged to Go via purego, storing callbacks in a sync.Map and invoking them through a C-ABI trampoline to intercept every memory read and write during iOS binary emulation.

The hook system in majd/ipatool enables fine-grained observation and control of emulated iOS binaries by bridging the Unicorn engine's native hooking capabilities with Go code. This architecture allows the tool to intercept memory operations, track instruction execution, and simulate system behaviors required for App Store interaction analysis.

Hook Architecture Overview

At its core, ipatool leverages the Unicorn engine as its emulation backend. Since Unicorn is written in C, the project uses purego to call Unicorn's API from Go without CGO. This design choice enables cross-platform builds while maintaining direct access to low-level emulation hooks.

The hook system implements a callback registry pattern that maps between Unicorn's C-style callbacks and Go functions:

  • Native Unicorn hooks identify callbacks via a uintptr user-data parameter
  • ipatool stores the actual Go functions in a thread-safe sync.Map
  • A trampoline function bridges the two worlds at runtime

Hook Registration Flow

Step 1: Generate Unique Hook IDs

Each hook receives a unique identifier through an atomic counter defined in internal/sap/unicorn/hook.go:

var codeHookID uintptr

This counter increments atomically to ensure thread-safe ID generation even when multiple hooks are registered concurrently.

Step 2: Store the Go Callback

The user-provided function is stored in one of several package-level maps depending on hook type:

var codeHookCallbacks sync.Map // for instruction execution hooks

For memory-access hooks, a similar map stores callbacks with the signature func(address uint64, size uint32, data []byte, isWrite bool).

Step 3: Register with Unicorn via purego

The Engine struct in internal/sap/unicorn/engine.go wraps uc_hook_add through purego's function binding:

hook, err := engine.AddCodeHook(0x1000, 0x2000, func(pc uint64, size uint32) {
    fmt.Printf("executed instruction at 0x%x (size %d)\n", pc, size)
})

For memory operations, AddMemHook accepts a hook type constant combining flags like UC_HOOK_MEM_READ | UC_HOOK_MEM_WRITE, an address range, and the callback:

memReadHook, err := engine.AddMemHook(
    unicorn.UC_HOOK_MEM_READ,
    0x0, 0xffffffffffffffff,
    func(addr uint64, size uint32, data []byte) {
        fmt.Printf("read %d bytes from 0x%x\n", size, addr)
    })

Step 4: Trampoline Invocation

The trampoline, created via purego.NewCallback, receives control when Unicorn triggers the hook. As implemented in internal/sap/unicorn/hook.go, it looks up and executes the original Go callback:

callback, ok := codeHookCallbacks.Load(userData)
if ok {
    callback.(CodeHook)(address, size)
}

For memory hooks, the trampoline additionally handles the data byte slice and isWrite boolean to convey operation details.

Step 5: Callback Execution in Go

The Go callback runs with full access to the emulator state. It can:

  • Inspect accessed memory contents
  • Modify memory before the read completes (for UC_HOOK_MEM_READ)
  • Override write values (for UC_HOOK_MEM_WRITE)
  • Return an error to abort emulation
  • Log operations for analysis

Step 6: Hook Lifecycle Management

Each Hook object maintains the Unicorn handle and callback ID. Calling Hook.Close() triggers cleanup in three locations:

  • uc_hook_del removes the hook from Unicorn
  • The entry is deleted from the callback map
  • engine.hooks.ids is cleared in the engine's internal hookState

Memory Hook Types and Capabilities

ipatool supports the full range of Unicorn memory hook types:

Hook Constant Trigger Condition Use Case in ipatool
UC_HOOK_MEM_READ Before a memory read completes Intercepting data fetches, implementing guard pages
UC_HOOK_MEM_WRITE After a memory write completes Tracking memory modifications, implementing copy-on-write
UC_HOOK_MEM_READ_UNMAPPED Read from unmapped address Handling iOS kernel lazy allocation, detecting null dereferences
UC_HOOK_MEM_WRITE_UNMAPPED Write to unmapped address Implementing mmap behavior, stack growth simulation
UC_HOOK_MEM_FETCH_UNMAPPED Execute from unmapped address Detecting jumps to invalid code, JIT compilation hooks

These hook types can be combined with bitwise OR for a single callback handling multiple conditions.

Practical Implementation in Machine Shims

The internal/sap/machine/shims.go file demonstrates production usage of the memory hook system. It installs hooks to:

  • Intercept libc functions like _memcpy and _memmove by hooking their entry points with code hooks, then monitoring their memory operations
  • Simulate iOS kernel services by watching for specific memory access patterns that indicate system call arguments being prepared
  • Apply memory protection checks that mirror iOS's vm_protect behavior using read/write hooks to enforce permission boundaries

Code Example: Complete Memory Monitoring Setup

package main

import (
    "fmt"
    "github.com/majd/ipatool/v2/internal/sap/unicorn"
)

func monitorMemoryAccess(engine *unicorn.Engine) error {
    // Monitor all heap reads (typical iOS heap range)
    heapReadHook, err := engine.AddMemHook(
        unicorn.UC_HOOK_MEM_READ,
        0x100000000, 0x1ffffffff, // 4GB-8GB range
        func(addr uint64, size uint32, data []byte) {
            fmt.Printf("[HEAP READ] 0x%x (%d bytes)\n", addr, size)
        },
    )
    if err != nil {
        return err
    }
    defer heapReadHook.Close()

    // Monitor stack writes (grows downward)
    stackWriteHook, err := engine.AddMemHook(
        unicorn.UC_HOOK_MEM_WRITE,
        0x7ff000000000, 0x7fffffffffff, // typical stack region
        func(addr uint64, size uint32, data []byte) {
            fmt.Printf("[STACK WRITE] 0x%x (%d bytes) = %x\n", 
                addr, size, data[:min(int(size), 8)])
        },
    )
    if err != nil {
        return err
    }
    defer stackWriteHook.Close()

    return nil
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

Performance Considerations

The hook system introduces overhead proportional to hook granularity:

  • Broad hooks (full address space) maximize coverage but trigger frequent context switches between Unicorn and Go
  • Narrow hooks (specific addresses/ranges) minimize overhead for targeted monitoring
  • Callback complexity directly impacts emulation speed; simple logging has minimal cost, while memory inspection adds latency

The sync.Map lookup in the trampoline adds a small constant cost per hook invocation. For hot paths, ipatool typically uninstalls temporary hooks after use rather than maintaining permanent broad-range monitors.

Summary

  • Unicorn engine provides the underlying emulation and native hook mechanism for ipatool's memory intercept capabilities
  • purego bridges C ABI to Go without CGO, enabling the trampoline callback pattern in internal/sap/unicorn/hook.go
  • Atomic ID generation and sync.Map storage provide thread-safe callback registration and lookup
  • Hook types cover reads, writes, fetches, and unmapped access patterns for comprehensive memory monitoring
  • Lifecycle management via Hook.Close() ensures clean resource release through uc_hook_del and map cleanup
  • Machine shims in internal/sap/machine/shims.go demonstrate production patterns for intercepting libc operations and simulating iOS kernel behavior

Frequently Asked Questions

What is the Unicorn engine and why does ipatool use it?

The Unicorn engine is a lightweight CPU emulator framework based on QEMU. ipatool uses Unicorn because it provides accurate ARM64 emulation with built-in hooking capabilities, allowing the tool to run and analyze iOS binaries without requiring actual Apple hardware or a full iOS kernel.

Why does ipatool use purego instead of CGO for Unicorn bindings?

purego enables dynamic loading of the Unicorn shared library and calling its functions at runtime without CGO. This approach eliminates the need for a C toolchain during builds, produces fully static Go binaries, and simplifies cross-compilation across platforms including macOS, Linux, and Windows.

Can multiple hooks be registered for the same memory address?

Yes. Unicorn supports multiple hooks on overlapping address ranges, and each receives an independent callback invocation. ipatool's hook system preserves this capability—callbacks execute in registration order, and any hook can abort the memory operation by returning an error that propagates through the trampoline.

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 →