Skip to Content
Alpha — full Android pipeline + iOS toolkit + live dynamic loop. API still shifting; pin to commits in CI.
WorkflowsDynamic + Frida

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=
FieldDefaultNotes
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.
spawntruetruefrida.spawn() then attach + resume (required for early-injection like SSL pinning bypass). false → attach to already-running pid.
devicefirst USBadb-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

HTTPCause
503 frida not installedpip install frida missing on the server.
503 no USB devicePhone unplugged, USB-debug not authorised, or frida-server not running.
500 spawn failedPackage 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=a4b8c2

Unloads 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-stream

Replays 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:

ActionGenerates
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.
libsProcess.enumerateModules() dump.
spawn_logLifecycle 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 commandEndpointNotes
decodeflagPOST /v1/mango/decode-flagsIntent / Receiver / PendingIntent / Content namespaces. Disambiguates the classic 0x10000000 collision.
diff <pkg>GET /v1/projects/{id}/manifest-diffStructured 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/pocFire on device + standalone HTML PoC page.

APK Patcher

Same tab. Three Android patches via apktool + apksigner:

PatchWhat it does
debuggableFlips android:debuggable=true on <application>. Enables jdb attach.
cleartext_trafficFlips android:usesCleartextTraffic=true. Lets you proxy plain-HTTP endpoints.
user_ca_trustInjects 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/trace

Token-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 tell

previous_hex is the rollback artefact: keep it, paste it into a second /memory write to revert.

Pattern syntax (Frida)

PatternMeaning
65 79 4a 68exact bytes (here: ASCII eyJh)
aa ?? bbone-byte wildcard
aa ?b cdhalf-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/trace

The 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 base
  • severity_relieved — head’s severity is better
  • remediation_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 by scripts/setup.sh when it clones the upstream repo).
  • Slug for nested modules is <parent_dir>/<stem> (so encryption/cipher_1.medencryption/cipher_1).

The Dynamic tab’s “MEDUSA RECIPES” picker auto-filters by the project’s platform.


Endpoints reference

EndpointPurpose
POST /v1/projects/{id}/dynamic/startAttach Frida + load hooks + recipes
POST /v1/projects/{id}/dynamic/stopDetach 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/eventsIngest endpoint for events from external Frida runs
GET /v1/dynamic/sessions/{sid}/memory/modulesLoaded modules
POST /v1/dynamic/sessions/{sid}/memory/scanMemory.scanSync wrapper
POST /v1/dynamic/sessions/{sid}/memory/readreadByteArray (hex out)
POST /v1/dynamic/sessions/{sid}/memory/writewriteByteArray (returns previous_hex)
POST /v1/projects/{id}/runtime/scriptMedusa action generator (jtrace, enumerate, …)
POST /v1/mango/decode-flagsAndroid flag decoder
GET /v1/projects/{id}/manifest-diffSurface diff vs prior scan
GET /v1/projects/{id}/findings-diffSecurity-delta diff vs prior scan
POST /v1/projects/{id}/mango/deeplink/fireadb am start with URI
GET /v1/projects/{id}/mango/deeplink/pocHTML PoC page
POST /v1/projects/{id}/patchAPKPatcher (debuggable / cleartext / user-CA)
GET /v1/mango/patcher/supportedAPK patch catalogue
GET /v1/recipesRecipe catalogue
GET /v1/recipes/{slug}/scriptRecipe 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,debuggable

For iOS-only paths (/decrypt-ios, /patch ipa) see IOS.md.