// documentation

Ophanim Documentation

Everything you need to import an app, choose what to capture, intercept its behavior, and drive the whole thing from a script. For the high-level overview, see the home page.

Introduction

Ophanim is a macOS dynamic-analysis and security-testing tool for iOS apps. You give it an .ipa; it re-signs the app, rewrites its Mach-O so an instrumentation runtime loads into it, converts it to a Mac Catalyst process, and runs it natively on Apple Silicon. Once running, Ophanim can observe and actively intercept the app's behavior from inside its own process.

The fundamental constraint that shapes everything: the analyst's UI and the target app are separate processes. You cannot attach a debugger to the closed app and expect to see its decrypted traffic or fake a keychain item cleanly at scale. The only vantage point that sees plaintext, in-process state, and ObjC/Swift dispatch is code running inside the target process - so Ophanim's core is an injected in-process agent.

Its goals, in priority order:

  1. Observe everything interesting an app does (network, crypto, keychain, device/privacy, filesystem, process) with provenance and without crashing the host.
  2. Intercept - rewrite responses, block hosts, fake return values, defeat pinning/jailbreak checks - under a rule engine that is observe-by-default.
  3. Reach code that resists ordinary hooking - statically-linked libraries, non-@objc Swift, custom boundaries.
  4. Be scriptable - everything the GUI does is also available over MCP.
Authorized use only. Ophanim is for analyzing software you own or are authorized to test (security research, app QA, CTF, reverse-engineering your own dependencies). Re-signing breaks server-side attestation by design, so it cannot be used to defeat licensing or impersonate a genuine device to a server.

Requirements

  • An Apple Silicon Mac (M1 or newer).
  • macOS 12.0+.
  • Xcode 26+ with the iOS platform installed (xcodebuild -downloadPlatform iOS) - required to build from source.
  • An .ipa of the app you intend to analyze.

Building from source

The top-level build script builds the Galgal runtime and the sibling agent, deep ad-hoc re-signs everything, and installs the app:

# build Ophanim.app (+ Galgal runtime + sibling agent), re-sign, and
# install to ~/Applications/Ophanim.app
$ ./build-ophanim.sh Release

# deploy the injected runtime to ~/Library/Frameworks
# (where hosted apps load it from)
$ ./scripts/deploy-runtime.sh

Other useful scripts:

  • Galgal/build-galgal.sh - build just the runtime framework (+ input plugin), retagged to Mac Catalyst.
  • Galgal/build-agent.sh - build just the standalone sibling agent dylib.
  • TestApp/build-testapp.sh - build the bundled test harness (OphanimTest.ipa) that exercises every capture category.
  • scripts/integration-test.sh - install/launch the harness and assert each category captures.

Quick start

  1. Launch Ophanim and drop in an .ipa. It re-signs, injects, and installs the app.
  2. Open the app's Instrumentation settings: enable the engine, pick categories and sinks, choose the injection method, and (optionally) author interception rules or custom ObjC/Swift hooks.
  3. Launch the app from within Ophanim.
  4. View captured events in View log… or with:
    $ log stream --predicate 'subsystem == "be.ophanim"'

Capture categories

Captured events carry a category, a capture layer (how it was intercepted), an API name, a one-line summary, structured key/value fields, an optional request/response body, and a disposition recording whether anything was modified.

network

HTTP(S) requests and decrypted response bodies (via NSURLSession swizzle and a custom NSURLProtocol), raw socket and DNS (connect / getaddrinfo), Secure-Transport / BoringSSL plaintext (SSL_read / SSL_write), and certificate-pinning bypass with logging.

keychain

SecItem* access - adds, queries, updates, deletes - with the item attributes that drove each call. Embedded-only (it's a C API with no ObjC fallback).

crypto

CommonCrypto symmetric operations (CCCrypt) and HMACs (CCHmac) - algorithm, mode, key length, and in/out sizes.

device & privacy

Vendor and advertising identifiers, location, pasteboard, App Attest / DeviceCheck, biometrics, and the camera / microphone / photos / contacts permission prompts. Many of these can be faked rather than merely observed.

filesystem

File access and jailbreak-path probes. Embedded mode captures at the C level (open / stat / access / rename / unlink); sibling mode falls back to NSFileManager-level capture.

process

dlopen / fork / posix_spawn and inter-app / URL launches.

jailbreak

Detection of and bypass for common root/jailbreak detectors, with logging of each probe (logging is embedded-only). Use list_jailbreak_detectors over MCP to enumerate what's recognized.

Injection modes

Ophanim has two ways to get the instrumentation engine into the target:

  • Embedded (default) - the engine runs inside the Galgal runtime that's already loaded into the app. Full capture, no extra re-sign.
  • Sibling - a standalone agent dylib is injected alongside the runtime via a second LC_LOAD_DYLIB (the app is re-signed). Use it when instrumentation should be independent of the runtime.

The engine deliberately only interposes C symbols the runtime doesn't already own, so the two never collide. That's why keychain and raw C-level filesystem stay embedded-only, and sibling falls back to NSFileManager-level filesystem capture.

CategoryEmbeddedSibling
Network · Process · Device/Privacy · Crypto
Custom ObjC / Swift / inline hooks · Rules · Sinks
Filesystem✔ full◑ NSFileManager
Keychain (SecItem*)
Jailbreak bypass + logging✔ bothbypass only

Output sinks & the log viewer

Captured events can be written to any combination of sinks:

  • NDJSON - one JSON event per line, for machine processing.
  • Plain text - a human-readable one-line-per-event log.
  • os_log - under subsystem be.ophanim, viewable with log stream / Console.app.

The built-in log viewer streams events live with category coloring and disposition tags, and the same events are queryable over MCP with query_events. See the screenshots below.

Interception & the rules engine

Every hook routes through a policy decision. By default it observes - logged, original behavior unchanged. When a rule matches, it can instead:

  • block - prevent the call from running;
  • returnReplaced - replace the return value / response (body + status + headers for HTTP(S));
  • argsModified - rewrite the input arguments;
  • delayed - delay the call before it runs;
  • faulted - force the call to fail.

A static rule replaces the same thing every time. A rule can also carry a JavaScript body (evaluated via JavaScriptCore) for per-call logic - the script sees each call's context (URL, headers, request body, fields) and can return a different result per call.

// per-call rule body: only replace the balance for one account,
// pass everything else through untouched
function handle(ctx) {
  if (ctx.url.includes("/v2/account/balance")) {
    const body = JSON.parse(ctx.responseBody);
    if (body.accountId === "acct_1001") {
      body.available = "1337.00";
      return { action: "returnReplaced", responseBody: JSON.stringify(body) };
    }
  }
  return { action: "observe" };
}
Not every layer can intercept. Active modification is available for HTTP(S) responses, device & privacy values, inline-hooked functions, and ObjC/Swift hooks (block/delay only - those are void methods). The raw plaintext layers - TLS read/write, socket/DNS, crypto, keychain, and dlopen/fork/posix_spawn - are observe-only: they're captured through a lock-free ring on a drain thread, so the original call has already returned by the time the event is processed.

Custom hooks

Beyond the built-in categories, you can hook arbitrary boundaries three ways:

Objective-C swizzle

Swizzle any ObjC (class, selector) boundary. Block or delay the call (void methods have no return to replace). Set over MCP with set_objc_hooks.

Native-Swift vtable patch

Patch overridable native-Swift methods at the vtable, reaching non-@objc Swift that ordinary swizzling can't see. Set with set_swift_hooks.

Inline machine-code hook

Inline-hook arbitrary arm64 machine code by address, symbol, module + offset, or byte signature - reaching statically-linked, stripped, or static-dispatch functions. Intercept or modify the return, log, and render NSData / NSString argument registers as bodies or fields. Inline hooks are gated behind a master "Enable inline hooks" toggle (OFF by default) and patch function code in memory copy-on-write, in-process only. Set with set_inline_hooks.

All custom hooks route through the same rule engine and disposition model as the built-in categories, and none of them touch the app's code on disk.

Scripting (MCP)

Ophanim --mcp speaks the Model Context Protocol over stdio (or an HTTP port). It exposes tools so analysis can be driven programmatically by an agent or script:

  • list_apps / launch_app - find a bundle id and run it.
  • get_config / set_config - read & write per-app capture config.
  • set_rules - author interception rules (static or scripted).
  • set_objc_hooks / set_swift_hooks / set_inline_hooks - custom hooks.
  • list_presets / apply_preset - list and apply capture presets.
  • list_jailbreak_detectors - enumerate recognized root/jailbreak detectors.
  • analyze_app / app_imports / find_symbols - inspect a binary's import/symbol surface.
  • tail_events / query_events - stream or query captured behavior.
$ Ophanim --mcp          # stdio transport

# a typical scripted flow:
list_apps
analyze_app          be.target.app    # imports / symbol surface
apply_preset         be.target.app network-deep
set_rules            be.target.app    # observe-by-default + targeted replace
launch_app           be.target.app
tail_events          --category network keychain crypto

Screenshots

Ophanim's SwiftUI front-end uses a "hackery" terminal theme - dark green-shifted slate surfaces, phosphor-green accents with a purple secondary, system monospace everywhere, and optional CRT theatrics (scanlines, a slow refresh sweep, digital rain, and the many-eyed "watcher"). The interface below is rendered in that same theme.

App Library - Ophanim
TargetApp
Banking
SocialX
Wallet
GameCo
StreamR
DeliverX
VPN
Messenger
TestHarness
Fig. 1 - The App Library. With IPA sources removed, the library is the whole window; drop an .ipa to re-sign, inject, and install.
Instrumentation - TargetApp
Enable hackingTurn the Ophanim engine on for this app. Relaunch to apply.
Enable inline hooksarm64 machine-code patch · OFF by default; hooks stay inert until on
Capture backtracesRecord the calling stack for ObjC-level events. Adds overhead.
Open log window on launch
CAPTURE CATEGORIES
network keychain crypto device privacy filesystem process jailbreak
OUTPUT SINKS
ndjson plain text os_log
INJECTION METHOD
embedded sibling
Fig. 2 - Per-app instrumentation settings: master enable, inline-hook gate, capture categories, output sinks, and injection method.
View log - be.ophanim · live
12:04:07networkNSURLSession.dataTask → POST api.target.app/v2/login 200 (1.2 KB)
12:04:07cryptoCCCrypt kCCEncrypt AES-256-CBC in=48 out=48
12:04:08keychainSecItemCopyMatching acct="auth.token" [returnReplaced]
12:04:08deviceidentifierForVendor → faked 00000000-DEAD-BEEF-0000-000000000000
12:04:09jailbreakstat("/Applications/Cydia.app") [blocked] → ENOENT
12:04:09networkTLS pinning challenge api.target.app [bypassed + logged]
12:04:10processdlopen("/usr/lib/libobjc.A.dylib")
12:04:10networkSSL_read api.target.app ← {"session":"…","flags":3} (842 B)
12:04:11deviceCLLocationManager.requestLocation [faulted]
12:04:11cryptoCCHmac kCCHmacAlgSHA256 key=32 data=128
12:04:12keychainSecItemAdd class=genp acct="refresh.token"
12:04:12networkgetaddrinfo("telemetry.target.app") [blocked]
Fig. 3 - The live log viewer. Each row carries a category (color-coded), API, summary, and disposition tag. The same events are written as NDJSON / plain text / os_log and are queryable over MCP.

FAQ

Does Ophanim need a jailbroken device?

No. There's no device at all - the iOS app runs natively on your Apple Silicon Mac as a Mac Catalyst process. Instrumentation happens in-process via the injected runtime.

Can it defeat licensing or spoof a device to a server?

No. Re-signing breaks server-side attestation by design. Ophanim is for analyzing software you own or are authorized to test - not for impersonating a genuine device to a server.

Will instrumentation change the app's behavior by accident?

No. The engine is observe-by-default: nothing is modified unless a rule you authored matches. Many capture layers are observe-only and physically cannot modify the call.

How do I reach a non-@objc Swift method or a statically-linked C function?

Use a native-Swift vtable patch for overridable Swift methods, or an inline machine-code hook (by address/symbol/offset/signature) for static-dispatch or stripped code. See Custom hooks.

Where are events stored?

In whichever sinks you enable: NDJSON and/or plain-text files, and/or os_log under subsystem be.ophanim.

License & credits

Ophanim is distributed under the GPLv3 license. See LICENSE in the repository.

Created by ytcracker and clord.

Built with these open-source components: inject (Mach-O load-command injection), PTFakeTouch (synthetic touch, vendored), DownloadManager, DataCache, CachedAsyncImage, SwiftSoup, Yams, and swift-atomics.