Gadget
Frida Gadget
Overview
- What it is: A shared library (
FridaGadget.dylib/libgadget.so) loaded into a target process for instrumentation. - When to use: When the Injected mode (i.e.,
fridaCLI /frida.attach()) is not viable — e.g., jailed iOS, non-debuggable Android, or restricted environments. - Activation: Gadget starts when the dynamic linker executes its constructor function (before
main()).
Loading Methods
| Method | Description |
|---|---|
| Source modification | Call dlopen() on Gadget in your own code |
| Binary patching | Use tools like insert_dylib to embed Gadget into an existing binary |
LD_PRELOAD / DYLD_INSERT_LIBRARIES | Environment variable injection on Linux/macOS |
Binary Naming
- The Gadget binary can be renamed to anything (useful for anti-detection bypass).
- The config file must match the binary name with a
.configextension.- Example:
FridaGadget.dylib→FridaGadget.config
- Example:
Config File
- Encoding: UTF-8
- Format: JSON object
- Location: Same directory as the Gadget binary (with matching name +
.configextension)
Root-Level Keys
| Key | Type | Default | Description |
|---|---|---|---|
interaction | object | {"type": "listen"} | Which interaction mode to use |
teardown | string | "minimal" | Cleanup on unload: "minimal" (skip thread/memory cleanup) or "full" (complete teardown). Use "full" if Gadget will be explicitly unloaded at runtime. |
runtime | string | "default" | JavaScript runtime: "default", "qjs" (QuickJS), or "v8" |
code_signing | string | "optional" | iOS code signing: "optional" or "required" |
code_signing Details
"optional": Frida assumes it can modify code in memory and run unsigned code (default for most platforms)."required": Required to run on jailed iOS without a debugger attached. Side effect: Interceptor API becomes unavailable. To use Interceptor on jailed iOS, attach a debugger before Gadget loads — the relaxed code-signing state persists even after detaching.
Platform-Specific Notes
Android (non-debuggable apps)
The package manager only copies files from /lib if they match:
- Name starts with
lib - Name ends with
.so - Name is
gdbserver
Workaround: Rename config file to comply with .so suffix rule.
lib/
└── arm64-v8a/
├── libgadget.config.so ← config file renamed with .so suffix
└── libgadget.so
iOS (Xcode)
Xcode typically places FridaGadget.dylib inside a Frameworks/ subdirectory. Gadget will also search the parent directory of Frameworks/ for the config file. This means the config can sit next to the app binary:
MyApp.app/
├── MyApp ← app binary
├── FridaGadget.config ← config can live here
└── Frameworks/
└── FridaGadget.dylib
Interaction Types
1. Listen (Default)
Gadget exposes a frida-server-compatible interface. Existing CLI tools (frida, frida-trace, etc.) work without modification.
- Process list: Contains only one entry —
Gadget - App identifier: Always
re.frida.Gadget - Default behavior: Constructor blocks until you
attach()or callresume()afterspawn()→attach()→ instrument.
Default config:
1{
2 "interaction": {
3 "type": "listen",
4 "address": "127.0.0.1",
5 "port": 27042,
6 "on_port_conflict": "fail",
7 "on_load": "wait"
8 }
9}
All supported keys:
| Key | Type | Default | Description |
|---|---|---|---|
address | string | "127.0.0.1" | Interface to listen on. Use "0.0.0.0" for all IPv4, "::" for all IPv6 |
port | number | 27042 | TCP port |
certificate | string | — | PEM-encoded public+private key (multi-line or filesystem path). Enables TLS. Server accepts any client cert. |
token | string | — | Secret token for authentication. Clients must present this token. |
on_port_conflict | string | "fail" | "fail": abort if port taken. "pick-next": try consecutive ports. |
on_load | string | "wait" | "wait": block until client connects and resumes. "resume": start immediately (attach later). |
origin | string | — | Required “Origin” header value, protects against unauthorized cross-origin browser access. |
asset_root | string | — | Filesystem directory to serve as static files over HTTP/HTTPS. Default: nothing served. |
2. Connect
Gadget connects outbound to a running frida-portal, joining its process cluster. The portal’s control interface (same protocol as frida-server) lets controllers enumerate_processes() and attach() remotely.
- Blocking behavior: Only blocks if spawn-gating is enabled (
Device.enable_spawn_gating()). Otherwise proceeds after joining the portal cluster.
Default config:
1{
2 "interaction": {
3 "type": "connect",
4 "address": "127.0.0.1",
5 "port": 27052
6 }
7}
All supported keys:
| Key | Type | Default | Description |
|---|---|---|---|
address | string | "127.0.0.1" | Portal host (cluster interface). Supports IPv4 and IPv6. |
port | number | 27052 | Portal cluster interface TCP port |
certificate | string | — | PEM-encoded CA public key (multi-line or filepath). Required if portal has TLS enabled. Server cert must match or derive from this CA. |
token | string | — | Auth token to present to portal. Interpretation depends on portal implementation (fixed secret for frida-portal binary, or custom OAuth etc. via API). |
acl | array of strings | — | Access Control List. Only controllers whose tag matches an entry can interact with this process. Example: ["team-a", "team-b"]. Requires custom portal API with tagging logic. |
Advanced: For custom authentication, per-node ACLs, and app-specific protocol messages, instantiate
PortalServiceprogrammatically instead of using thefrida-portalCLI.
3. Script
Loads a single JavaScript file from the filesystem before the program’s entrypoint. Fully autonomous — no external Frida client needed.
Minimal config:
1{
2 "interaction": {
3 "type": "script",
4 "path": "/home/user/explore.js"
5 }
6}
Script skeleton with lifecycle hooks:
1rpc.exports = {
2 init(stage, parameters) {
3 // stage: "early" (first load) or "late" (reload)
4 // parameters: object from config, or {}
5 console.log('[init]', stage, JSON.stringify(parameters));
6
7 Interceptor.attach(Module.getGlobalExportByName('open'), {
8 onEnter(args) {
9 const path = args[0].readUtf8String();
10 console.log('open("' + path + '")');
11 }
12 });
13 },
14 dispose() {
15 // Called when script is unloaded (process exit, Gadget unload, or reload)
16 console.log('[dispose]');
17 }
18};
Lifecycle behavior:
init(stage, parameters)is called before program entrypoint. Gadget waits for it to return (supports returning aPromisefor async work — e.g.,Socket.connect()).stagevalues:"early": Gadget was just loaded for the first time"late": Script is being reloaded from disk
dispose()is called on unload. Optional.rpc.exportsis optional entirely — only needed if lifecycle awareness is required.- Debugging:
console.log(),console.warn(),console.error()→ stdout/stderr.
All supported keys:
| Key | Type | Default | Description |
|---|---|---|---|
path | string | required | Filesystem path to script. Can be relative to Gadget binary location. On iOS, relative paths first resolve against the app’s Documents/ directory (supports iTunes file sharing / AFC). |
parameters | object | {} | Arbitrary data passed as second argument to init() |
on_change | string | "ignore" | "ignore": load once. "reload": monitor file and reload on change. Recommended: "reload" during development. |
4. ScriptDirectory
Loads multiple scripts from a directory. Each script is treated as a plugin. Optional per-script filtering limits which processes load each script — useful for system-wide instrumentation with per-app targeting.
Minimal config:
1{
2 "interaction": {
3 "type": "script-directory",
4 "path": "/usr/local/frida/scripts"
5 }
6}
All supported keys:
| Key | Type | Default | Description |
|---|---|---|---|
path | string | required | Directory containing scripts. Can be relative to Gadget binary. Scripts must use .js extension. |
on_change | string | "ignore" | "ignore": scan directory once. "rescan": monitor and rescan on change. Recommended: "rescan" during development. |
Per-script config file (e.g., twitter.js → twitter.config):
| Key | Type | Default | Description |
|---|---|---|---|
filter | object | — | Load criteria (OR logic — any one match triggers load). Keys: executables (array), bundles (array), objc_classes (array). |
parameters | object | {} | Passed to init() as second argument |
on_change | string | "ignore" | "ignore" or "reload" (same as Script interaction) |
Example — macOS Twitter tweak:
/usr/local/frida/scripts/twitter.js:
1const { TMTheme } = ObjC.classes;
2
3rpc.exports = {
4 init(stage, parameters) {
5 ObjC.schedule(ObjC.mainQueue, () => {
6 TMTheme.switchToTheme_(TMTheme.darkTheme());
7 });
8 },
9 dispose() {
10 ObjC.schedule(ObjC.mainQueue, () => {
11 TMTheme.switchToTheme_(TMTheme.lightTheme());
12 });
13 }
14};
/usr/local/frida/scripts/twitter.config:
1{
2 "filter": {
3 "executables": ["Twitter"],
4 "bundles": ["com.twitter.twitter-mac"],
5 "objc_classes": ["Twitter"]
6 }
7}
Filter is OR-logic: script loads if executable name is Twitter or bundle ID is com.twitter.twitter-mac or an ObjC class named Twitter is loaded. For stability, prefer filtering on bundle ID. Use objc_classes as a fallback for apps without bundle IDs.
Quick Reference: Interaction Type Comparison
| Feature | Listen | Connect | Script | ScriptDirectory |
|---|---|---|---|---|
| Requires external Frida client | Yes | Yes (via portal) | No | No |
| Autonomous instrumentation | No | No | Yes | Yes |
| Blocks on startup | Yes (until attach/resume) | Only if spawn-gating enabled | Yes (until init() returns) | Yes (until all init() return) |
| Multi-script support | No | No | No | Yes |
| Script hot-reload | No | No | on_change: reload | on_change: rescan/reload |
| Port/network required | Yes | Yes | No | No |
Related
- Frida Modes — overview of Injected, Embedded (Gadget), and Preloaded modes
- JavaScript API: rpc —
rpc.exportsreference - JavaScript API: Socket.connect() — async socket API usable in
init() - frida-trace — CLI tool that works with Listen mode
- insert_dylib — tool for patching dylibs to load Gadget