To detect community and modified Frida builds in an iOS app, check what instrumentation changes inside your process rather than looking for the word “Frida”. Enumerate loaded images through dyld and compare them with your build, test the code of sensitive functions for inline hooks and trampolines, and look for agent threads, local listening ports and unexpected exception ports. Pair those checks with signature and Team ID checks, because on a stock device Frida needs a re-signed app. Confirm the result server-side with App Attest. This guide covers iOS only.
For the cross-platform overview and Android techniques, see detecting modified Frida builds on Android and iOS. The product control is covered on anti-Frida.
The two iOS routes
Frida reaches an iOS app in two different ways, and modified builds target both.
| Route | Precondition | Main evidence |
|---|---|---|
| Frida server | Jailbroken device | Agent code in your process, agent threads, server listening port, jailbreak artefacts |
| Embedded Gadget (“jailed”) | App unpacked, Gadget added, app re-signed | Extra dynamic library, different Team ID and provisioning profile, agent threads |
On a jailbroken device the server injects an agent into the running app. On a stock device there is no injection route, so the attacker repackages the app with Gadget and re-signs it with their own certificate, usually a free or paid developer account, an enterprise certificate or a sideloading tool. The signing change is unavoidable on this route, which makes it the most dependable evidence.
Gadget limits on stock devices
Stock iOS enforces code signing on executable memory. Frida documents that when Gadget runs with code_signing set to required, its Interceptor API is unavailable, so inline hooks cannot be written. Attackers work around this by launching the re-signed app under a debugger, which needs the get-task-allow entitlement in a development profile. Both facts give you checks: a development entitlement or embedded development profile in what should be an App Store build, and a debugger attached at runtime.
iOS detection techniques
Each technique below lists what it looks for and how modified builds try to evade it. Use several together.
Loaded-image enumeration
Walk the image list with _dyld_image_count and _dyld_get_image_name, and register _dyld_register_func_for_add_image to see images loaded after launch. Compare each path with the frameworks your app ships and the system libraries. An unknown dylib inside your app bundle, or one loaded from an unexpected path, is a strong signal.
How variants evade it: Gadget can be given any file name, which Frida’s own documentation names as a way to avoid name checks. Compare against an allowlist of what you ship, not a blocklist of names. Some injectors map code without registering it with dyld, so also scan for executable memory regions that do not belong to any listed image.
Inline hooks and trampolines
An inline hook replaces the first instructions of a function with a branch to the attacker’s code. Check the prologues of your sensitive functions, and of the system functions they rely on, for unexpected branches, or compare them with the bytes in the signed binary on disk. For Objective-C, check that method implementations of sensitive classes still resolve to your own image with dladdr, which catches swizzling.
How variants evade it: hooks can move to a function you do not check, or to a callee deeper in the chain. Check integrity at the point of use, just before the sensitive operation, not only at launch.
Thread and port artefacts
The agent starts its own threads. Enumerate your task’s threads and read their names with pthread_getname_np. Upstream builds use names such as gum-js-loop and gmain. A Frida server or listening Gadget accepts connections on local TCP port 27042 by default.
How variants evade it: modified builds rename threads, change the port or run Gadget scripts autonomously with no port at all. Treat these as supporting signals and compare thread counts with a clean baseline of your own app.
Exception ports
Debuggers and some instrumentation tools register their own Mach exception handlers for the task. Read the task’s exception ports with task_get_exception_ports and flag handlers your app did not install. Combine the result with a sysctl check of the P_TRACED flag for an attached debugger.
How variants evade it: an attacker with code execution can hook the query itself. Run the check from code that is integrity-checked, and send the result to your server rather than deciding only on the device.
Signing and environment checks
Read the embedded embedded.mobileprovision and the code-signing Team ID at runtime. An App Store build carries no embedded provisioning profile, and its Team ID is yours. A profile with get-task-allow, a foreign Team ID or a changed bundle ID points to a re-signed copy. Jailbreak checks add context for the server route but do not prove instrumentation on their own.
Confirm on the server
Every in-app check can be patched by someone who controls the process. Use App Attest (DCAppAttestService) so your server receives an Apple-backed assertion that a request came from a genuine instance of your app, then combine that with the in-app instrumentation signal. App Attest is unavailable in some contexts, such as the simulator and some app extensions, so check isSupported and decide what the server does without it.
Otukan detects runtime instrumentation in iOS apps and pairs those checks with signature checks, so a re-signed copy carrying Gadget is caught on a stock device. The signal goes to your response policy, and Otukan applies the response you configure to the sensitive flow. Signal handling is covered on how to handle runtime threat signals, and re-signing on preventing mods that require re-signing iOS apps.
Test against real variants
- Record the exact tool release or fork, how it was built and which route it uses. Note whether the device is jailbroken and whether a debugger is attached.
- Run your signed release build without instrumentation and record images, threads and timing as the clean baseline.
- Prove the harness reaches a harmless, app-owned test function before judging detection.
- Repeat for each variant, changing one thing at a time: renamed dylib, renamed threads, moved port, autonomous script, re-signed Gadget build.
- Record each outcome as detected, harness failed, signal absent or app failed.
- Remove the instrumentation and confirm recovery under your policy.
- Swift, Objective-C and native code paths each have their own test entry.
- App extensions that touch sensitive data are tested separately from the main app.
- Crash reporting, analytics and approved SDKs run without a false signal.
- Simulator results are kept apart from physical-device results.
- Each result records device, iOS version, app version, tool build and reviewer.