Runtime — Frida sessions, Medusa & Mango toolboxes, Memory Inspector
Everything dynamic in MedusaNexus runs through one Frida session per project. This document covers the cable: how the session attaches, how scripts get stacked, how events stream back, and how the Memory Inspector exposes live process memory through the API.
For static analysis (apktool / jadx / mobsf / ghidra) see SPEC.md.
For iOS-specific workflows (IPA decrypt / patch / re-sign) see IOS.md.
TL;DR — the dynamic loop
[ START SESSION ] POST /v1/projects/{id}/dynamic/start
│
▼
FridaSession.start() frida.spawn() + attach() + load N scripts
│
▼ ┌─────────────────────────┐
tooling script auto-loaded │ Memory Inspector goes │
(rpc.exports for memory) │ live · session.mem ready│
│ └─────────────────────────┘
▼
send({channel: "ssl_pin", ...}) ─────► on_message → 2 fan-outs:
· live SSE stream → UI console
· dynamic_events DB (durable)
│
▼
/v1/dynamic/sessions/{sid}/memory/scan ─► Memory.scanSync
/v1/dynamic/sessions/{sid}/memory/read ─► ptr.readByteArray
/v1/dynamic/sessions/{sid}/memory/write ─► ptr.writeByteArray (rollback echoed)The Dynamic tab in the web UI drives the same endpoints; the REPL
/dynamic, /memory, /recipes slash commands are thin clients
over the same HTTP API.
Frida session lifecycle
Start
POST /v1/projects/{project_id}/dynamic/start
Content-Type: application/x-www-form-urlencoded
hooks=ssl_pinning_bypass,crypto_logger&recipes=SSL/pinning_universal&spawn=true&device=| Field | Default | Notes |
|---|---|---|
hooks | "" | csv of auto-hook names from /v1/projects/{id}/hooks (generated by HookGenerator from the static surface). |
recipes | "" | csv of recipe slugs from /v1/recipes — built-ins (android_universal_pinning) or Medusa-disk modules (encryption/cipher_1). Each is wrapped in an IIFE so per-recipe globals don’t collide. |
spawn | true | true → frida.spawn() then attach + resume (required for early-injection like SSL pinning bypass). false → attach to already-running pid. |
device | first USB | adb-style serial; forwarded to get_device(id). |
Returns the session snapshot plus stream_url for SSE:
{
"session_id": "a4b8c2",
"project_id": "PRJ-355151DF",
"package": "com.target.bank",
"state": "attached",
"pid": 12345,
"device": "00008030-001A28941A82802E",
"scripts": ["ssl_pinning_bypass", "recipe::SSL/pinning_universal"],
"tooling": true,
"stream_url": "/v1/projects/PRJ-355151DF/dynamic/stream?session_id=a4b8c2",
"log": [...]
}Failure modes
| HTTP | Cause |
|---|---|
503 frida not installed | pip install frida missing on the server. |
503 no USB device | Phone unplugged, USB-debug not authorised, or frida-server not running. |
500 spawn failed | Package not installed on the device, or frida-server rejected. |
400 unknown hook 'x' | Hook name didn’t match any auto-hook from /v1/projects/{id}/hooks. |
400 unknown recipe 'x' | Recipe slug not in /v1/recipes (built-ins) or ~/.mnexus/tools/medusa/modules/. |
Stop
POST /v1/projects/{project_id}/dynamic/stop
session_id=a4b8c2Unloads every script, detaches the session, kills the spawned PID when we owned it. Idempotent — double-stop after the UI sees the end-of-stream event doesn’t error.
Live stream (SSE)
GET /v1/projects/{project_id}/dynamic/stream?session_id=a4b8c2
Accept: text/event-streamReplays the last 50 in-memory log entries first (so a late subscriber
sees attached instead of an empty pane), then one frame per
send({...}) from any loaded script. Event names mirror the
channel field:
event: log
data: {"ts": 1747000000.123, "channel": "nexus", "line": "[NEXUS] session active · pid=12345"}
event: ssl_pin
data: {"ts": 1747000010.456, "channel": "ssl_pin", "source_script": "ssl_pinning_bypass",
"payload": {"host": "api.bank.com", "lib": "okhttp", "outcome": "bypassed"}}
event: end
data: {"reason": "detached", "error": null}Internal poll loop runs at 1s (so detach is seen quickly); heartbeat
byte (: heartbeat) emitted only after 15s of silence to keep
proxies open.
Recipe stacking
Multiple recipes load into the same Frida session, each wrapped in an IIFE so per-recipe globals don’t collide. The wrapper looks like:
(function () {
try {
// …recipe source…
} catch (e) {
try { send({ channel: 'error', source: '<recipe-name>',
description: '' + (e && (e.message || e)),
stack: (e && e.stack) || '' }); } catch (_) {}
}
})();Two pinning bypass recipes that both declare var CP = …CertificatePinner
no longer blow each other up; a syntax error in one recipe still
lets the rest of the session load, and the error fires through the
SSE stream tagged with the recipe name.
Medusa Runtime Toolbox (RUNTIME tab)
Different surface than the Dynamic tab — the Runtime tab lets the analyst run ad-hoc Medusa-flavoured actions against the project package without composing a full session. Each action generates a Frida script the analyst can copy or auto-load:
| Action | Generates |
|---|---|
enumerate <class glob> | Java class enumerator script. Returns class names matching the glob. |
describe <class> | Method dump for one Java class. |
jtrace <class.method> | Interceptor.attach + arg/return log. |
libs | Process.enumerateModules() dump. |
spawn_log | Lifecycle tracer (Activity.onCreate, etc.). |
Backend: POST /v1/projects/{id}/runtime/script with {action, target}.
The generator lives in mnexus.intelligence.runtime_scripts.
Mango Toolbox
A second strip on the Runtime tab covering the three Mango deltas Nexus didn’t already do:
| Mango command | Endpoint | Notes |
|---|---|---|
decodeflag | POST /v1/mango/decode-flags | Intent / Receiver / PendingIntent / Content namespaces. Disambiguates the classic 0x10000000 collision. |
diff <pkg> | GET /v1/projects/{id}/manifest-diff | Structured surface diff — not Mango’s textual manifest comparison. See diff workflow. |
deeplink [--poc] | POST /v1/projects/{id}/mango/deeplink/fire + GET /v1/projects/{id}/mango/deeplink/poc | Fire on device + standalone HTML PoC page. |
APK Patcher
Same tab. Three Android patches via apktool + apksigner:
| Patch | What it does |
|---|---|
debuggable | Flips android:debuggable=true on <application>. Enables jdb attach. |
cleartext_traffic | Flips android:usesCleartextTraffic=true. Lets you proxy plain-HTTP endpoints. |
user_ca_trust | Injects res/xml/network_security_config.xml trusting user-installed CAs. Unblocks Burp/Caido/Moxy MITM on Android 7+. |
Tool detection at runtime: missing apktool → preview mode (shows
what would change, no APK produced). Missing apksigner → falls
back to jarsigner (v1 only) with a warning. Missing both →
unsigned APK + warning.
Endpoint: POST /v1/projects/{id}/patch with patches=debuggable,user_ca_trust.
REPL: /patch apk debuggable,user_ca_trust.
Memory Inspector
Auto-injected tooling script per FridaSession (when tooling: true
in the start response). Exposes four RPC methods that drive the
endpoints below:
GET /v1/dynamic/sessions/{sid}/memory/modules
POST /v1/dynamic/sessions/{sid}/memory/scan {pattern, module?, max_results?}
POST /v1/dynamic/sessions/{sid}/memory/read {address, size}
POST /v1/dynamic/sessions/{sid}/memory/write {address, hex}
POST /v1/dynamic/sessions/{sid}/memory/trace {ranges: [{base, size}, …]}
DELETE /v1/dynamic/sessions/{sid}/memory/traceToken-swap workflow (the talk’s recipe)
1. /dynamic start --recipes ios_ssl_kill_switch # break TLS so we see traffic
2. (capture victim's JWT via Moxy)
3. /memory modules # find your app's module
4. /memory scan "65 79 4a 68" --module YourBank # "eyJ…" = JWT header
→ returns N addresses
5. /memory read 0x10f234000 256 # confirm it's the right token
6. /memory write 0x10f234000 "65 79 4a … <victim bytes>" # overwrite
→ previous_hex echoed back for rollback
7. App now sends requests as the victim — backend has no way to tellprevious_hex is the rollback artefact: keep it, paste it into a
second /memory write to revert.
Pattern syntax (Frida)
| Pattern | Meaning |
|---|---|
65 79 4a 68 | exact bytes (here: ASCII eyJh) |
aa ?? bb | one-byte wildcard |
aa ?b cd | half-byte wildcard (nibble) |
Source: Frida Memory.scan docs.
Safety
Memory writes can crash the target — the API does NOT gate. The UI
runs a confirmation dialog before the request; the REPL prompts
overwrite N byte(s) at <addr>? [y/N]. Pentester is in charge.
Trace (MemoryAccessMonitor)
Single-shot per page detection — useful when the question is “when
does the app touch this byte range?” rather than “what’s in it now?”.
Each first read/write/execute on a guarded page fires a
mem_trace event on the SSE stream:
{"channel": "mem_trace", "operation": "read",
"address": "0x10f234020", "from": "0x10009ab10",
"range_base": "0x10f234000", "range_index": 0,
"pages_total": 1, "pages_completed": 1}Once a page traps, its protection is restored — the monitor isn’t continuous logging, it’s “tell me the FIRST time this is touched”. Re-arm via another POST if you want another shot.
POST /v1/dynamic/sessions/{sid}/memory/trace
{"ranges": [{"base": "0x10f234000", "size": 4096}]}
DELETE /v1/dynamic/sessions/{sid}/memory/traceThe Dynamic console renders mem_trace lines in the same stream as
ssl_pin / nexus events, no separate viewer needed.
Diff workflow
Two complementary diffs between scans of the same package:
Manifest diff — surface delta
GET /v1/projects/{id}/manifest-diff[?against=<other_pid>]
Compares the parsed AttackSurface:
- exported components (added / removed / changed by export/permission flag flip)
- deeplinks (added / removed)
- permissions (added / removed)
- URL schemes (iOS) (added / removed)
- native libraries (by arch + path)
- SSL pinning posture (
detected_before/detected_after, library name)
Findings diff — security delta
GET /v1/projects/{id}/findings-diff[?against=<other_pid>]
Identity-keyed on (title, location) so severity drift on a same-identity
finding is changed, not added + removed. Summary tracks:
severity_escalated— head’s severity is worse than baseseverity_relieved— head’s severity is betterremediation_added— base had no fix-text, head shipped one
Auto-pick rule (no ?against=): the most recent non-self Project
with the same package_name, ordered by updated_at desc.
REPL: /diff manifest or /diff findings.
Recipes catalogue
/v1/recipes returns built-in + Medusa-disk recipes:
[
{"name": "android_universal_pinning", "origin": "builtin", "category": "SSL",
"platform": "android", "description": "Universal-ish okhttp + TrustManager bypass."},
{"name": "ios_ssl_kill_switch", "origin": "builtin", "category": "SSL",
"platform": "ios", "description": "Neutralise NSURLSession + Security framework pinning."},
{"name": "encryption/cipher_1", "origin": "medusa", "category": "ENCRYPTION",
"platform": "android", "description": "Cipher hook — log SecretKeySpec ctor args."}
]- Built-ins ship hardcoded in
mnexus.recipes.BUILTIN_RECIPES. - Medusa recipes walk
~/.mnexus/tools/medusa/modules/recursively (set byscripts/setup.shwhen it clones the upstream repo). - Slug for nested modules is
<parent_dir>/<stem>(soencryption/cipher_1.med→encryption/cipher_1).
The Dynamic tab’s “MEDUSA RECIPES” picker auto-filters by the project’s platform.
Endpoints reference
| Endpoint | Purpose |
|---|---|
POST /v1/projects/{id}/dynamic/start | Attach Frida + load hooks + recipes |
POST /v1/projects/{id}/dynamic/stop | Detach session, kill spawned PID |
GET /v1/projects/{id}/dynamic/stream?session_id= | SSE event stream |
GET /v1/projects/{id}/dynamic/events?session_id= | Polling-mode session snapshot |
POST /v1/projects/{id}/dynamic/events | Ingest endpoint for events from external Frida runs |
GET /v1/dynamic/sessions/{sid}/memory/modules | Loaded modules |
POST /v1/dynamic/sessions/{sid}/memory/scan | Memory.scanSync wrapper |
POST /v1/dynamic/sessions/{sid}/memory/read | readByteArray (hex out) |
POST /v1/dynamic/sessions/{sid}/memory/write | writeByteArray (returns previous_hex) |
POST /v1/projects/{id}/runtime/script | Medusa action generator (jtrace, enumerate, …) |
POST /v1/mango/decode-flags | Android flag decoder |
GET /v1/projects/{id}/manifest-diff | Surface diff vs prior scan |
GET /v1/projects/{id}/findings-diff | Security-delta diff vs prior scan |
POST /v1/projects/{id}/mango/deeplink/fire | adb am start with URI |
GET /v1/projects/{id}/mango/deeplink/poc | HTML PoC page |
POST /v1/projects/{id}/patch | APKPatcher (debuggable / cleartext / user-CA) |
GET /v1/mango/patcher/supported | APK patch catalogue |
GET /v1/recipes | Recipe catalogue |
GET /v1/recipes/{slug}/script | Recipe source code |
REPL reference
The slash commands map 1:1 to the endpoints above. Run /help in
mnexus to see the full list. Highlights for the runtime workflow:
/dynamic start --hooks ssl_pinning_bypass --recipes SSL/pinning_universal
/dynamic status
/dynamic stop
/memory modules
/memory scan "65 79 4a 68" --module YourBank --max 50
/memory read 0x10f234000 256
/memory write 0x10f234000 "65 79 4a 68 …"
/recipes SSL
/diff manifest
/diff findings
/patch apk user_ca_trust,debuggableFor iOS-only paths (/decrypt-ios, /patch ipa) see IOS.md.