# How the ipatool Hook System Intercepts Memory Access During Emulation

> Discover how ipatool's hook system intercepts memory access during emulation using Unicorn engine callbacks and Go integration. Learn about the advanced techniques employed for precise memory monitoring.

- Repository: [Majd/ipatool](https://github.com/majd/ipatool)
- Tags: internals
- Published: 2026-09-06

---

**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](https://github.com/unicorn-engine/unicorn) as its emulation backend. Since Unicorn is written in C, the project uses [purego](https://github.com/ebitengine/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`](https://github.com/majd/ipatool/blob/main/internal/sap/unicorn/hook.go):

```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:

```go
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`](https://github.com/majd/ipatool/blob/main/internal/sap/unicorn/engine.go) wraps `uc_hook_add` through purego's function binding:

```go
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:

```go
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`](https://github.com/majd/ipatool/blob/main/internal/sap/unicorn/hook.go), it looks up and executes the original Go callback:

```go
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`](https://github.com/majd/ipatool/blob/main/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

```go
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`](https://github.com/majd/ipatool/blob/main/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`](https://github.com/majd/ipatool/blob/main/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.