// 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:
- Observe everything interesting an app does (network, crypto, keychain, device/privacy, filesystem, process) with provenance and without crashing the host.
- Intercept - rewrite responses, block hosts, fake return values, defeat pinning/jailbreak checks - under a rule engine that is observe-by-default.
- Reach code that resists ordinary hooking - statically-linked libraries, non-
@objcSwift, custom boundaries. - Be scriptable - everything the GUI does is also available over MCP.
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
.ipaof 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
- Launch Ophanim and drop in an
.ipa. It re-signs, injects, and installs the app. - 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.
- Launch the app from within Ophanim.
- 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.
| Category | Embedded | Sibling |
|---|---|---|
| Network · Process · Device/Privacy · Crypto | ✔ | ✔ |
| Custom ObjC / Swift / inline hooks · Rules · Sinks | ✔ | ✔ |
| Filesystem | ✔ full | ◑ NSFileManager |
Keychain (SecItem*) | ✔ | ✗ |
| Jailbreak bypass + logging | ✔ both | bypass 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 withlog 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" }; }
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.
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.