Gadget

Frida Gadget

Overview

Loading Methods

MethodDescription
Source modificationCall dlopen() on Gadget in your own code
Binary patchingUse tools like insert_dylib to embed Gadget into an existing binary
LD_PRELOAD / DYLD_INSERT_LIBRARIESEnvironment variable injection on Linux/macOS

Binary Naming

Config File

Root-Level Keys

KeyTypeDefaultDescription
interactionobject{"type": "listen"}Which interaction mode to use
teardownstring"minimal"Cleanup on unload: "minimal" (skip thread/memory cleanup) or "full" (complete teardown). Use "full" if Gadget will be explicitly unloaded at runtime.
runtimestring"default"JavaScript runtime: "default", "qjs" (QuickJS), or "v8"
code_signingstring"optional"iOS code signing: "optional" or "required"

code_signing Details


Platform-Specific Notes

Android (non-debuggable apps)

The package manager only copies files from /lib if they match:

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.

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:

KeyTypeDefaultDescription
addressstring"127.0.0.1"Interface to listen on. Use "0.0.0.0" for all IPv4, "::" for all IPv6
portnumber27042TCP port
certificatestringPEM-encoded public+private key (multi-line or filesystem path). Enables TLS. Server accepts any client cert.
tokenstringSecret token for authentication. Clients must present this token.
on_port_conflictstring"fail""fail": abort if port taken. "pick-next": try consecutive ports.
on_loadstring"wait""wait": block until client connects and resumes. "resume": start immediately (attach later).
originstringRequired “Origin” header value, protects against unauthorized cross-origin browser access.
asset_rootstringFilesystem 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.

Default config:

1{
2  "interaction": {
3    "type": "connect",
4    "address": "127.0.0.1",
5    "port": 27052
6  }
7}

All supported keys:

KeyTypeDefaultDescription
addressstring"127.0.0.1"Portal host (cluster interface). Supports IPv4 and IPv6.
portnumber27052Portal cluster interface TCP port
certificatestringPEM-encoded CA public key (multi-line or filepath). Required if portal has TLS enabled. Server cert must match or derive from this CA.
tokenstringAuth token to present to portal. Interpretation depends on portal implementation (fixed secret for frida-portal binary, or custom OAuth etc. via API).
aclarray of stringsAccess 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 PortalService programmatically instead of using the frida-portal CLI.


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:

All supported keys:

KeyTypeDefaultDescription
pathstringrequiredFilesystem 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).
parametersobject{}Arbitrary data passed as second argument to init()
on_changestring"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:

KeyTypeDefaultDescription
pathstringrequiredDirectory containing scripts. Can be relative to Gadget binary. Scripts must use .js extension.
on_changestring"ignore""ignore": scan directory once. "rescan": monitor and rescan on change. Recommended: "rescan" during development.

Per-script config file (e.g., twitter.jstwitter.config):

KeyTypeDefaultDescription
filterobjectLoad criteria (OR logic — any one match triggers load). Keys: executables (array), bundles (array), objc_classes (array).
parametersobject{}Passed to init() as second argument
on_changestring"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

FeatureListenConnectScriptScriptDirectory
Requires external Frida clientYesYes (via portal)NoNo
Autonomous instrumentationNoNoYesYes
Blocks on startupYes (until attach/resume)Only if spawn-gating enabledYes (until init() returns)Yes (until all init() return)
Multi-script supportNoNoNoYes
Script hot-reloadNoNoon_change: reloadon_change: rescan/reload
Port/network requiredYesYesNoNo