goauld — Android / arm64 dynamic instrumentation
Named after the Goa'uld: a symbiont that takes over a host.
Quick start · vs Frida · Architecture · Recipes · JS API parity · Design
goauld is a Rust toolkit for instrumenting Android on AArch64. You inject an agent into a process, then load Frida-shaped JavaScript (Interceptor, Java, Process, Module, Memory, Arm64Writer, send).
| Platform | Android + arm64 only — no x86, ARM32, iOS, or desktop targets |
| Version | workspace 0.1.6 — agent / injector / host print goauld_proto::version_info() (0.1.6 (git:… built:…)) |
| Engines | QuickJS (default) or Symbiote — compile-time exclusive |
| Reference | Frida — goauld is a smaller alternative, not a replacement |
- On-device inject —
goauld-injectorptraces the target anddlopenslibgoauld_agent.so - Frida-shaped JS — familiar globals; see
docs/JS_API_PARITY.mdfor gaps - Native hooks —
Interceptor.attach/replace, GOT/PLT, inline arm64 patches - Java / ART —
Java.use, Technique-A method hooks,attach java/trace javaAPI streams - Syscall trace —
PTRACE_SYSCALLwithout loading the agent (injector alone) - Arm64Writer / Relocator — emit and relocate AArch64 code from JS
- Two JS backends — QuickJS for the full surface; Symbiote as a pure-Rust experiment
# 1. Host CLI
cargo build -p goauld-host --release
# 2. On-device arm64 artifacts (NDK version pinned in ndk.txt)
./scripts/build-android.sh # QuickJS → dist/android-arm64/
./scripts/build-android.sh symbiote # optional second engine
# 3. Rooted emulator (example AVD: Goauld_API34)
./scripts/emulator.sh start
adb root && adb shell setenforce 0
# 4. Demo APK
./scripts/build-java-target.sh
adb install -r testapps/java-target/build/java-target.apk
adb shell am start -n com.example.javatarget/.TargetPKG=com.example.javatarget
INJ=dist/android-arm64/goauld-injector
SO=dist/android-arm64/libgoauld_agent.so
# Inject once per process
cargo run -p goauld-host --release -- inject \
--package "$PKG" --injector "$INJ" --agent "$SO" --stage-into-app
PID=$(adb shell pidof -s "$PKG" | tr -d '\r')
# Attach a script (host exits when --expect-send matches — that is normal)
cargo run -p goauld-host --release -- attach \
--pid "$PID" --port 27046 \
--script scripts/fixtures/java_toast.js \
--expect-send toast-shown --max-wait-secs 10Re-inject usually needs a fresh process (am force-stop + start again). After --expect-send, logcat may show goauld session ended: failed to fill whole buffer — expected when the host closes the socket.
Frida is the mature, cross-platform reference: Stalker, Gum, frida-server / frida-gadget, and the ecosystem of tools and scripts people already use. If you need that coverage, use Frida.
goauld is a narrower Android / arm64 alternative. It reuses a Frida-shaped JS surface so scripts feel familiar. It is not feature-complete Frida.
Snapshot below: API 34 emulator (arm64-v8a), same JS workload, Date.now median of 3, Frida 17.18.0. Lower is better. Re-run with scripts/bench-js-engines.py — numbers move with build and device.
| goauld / QuickJS | Frida 17.18.0 | |
|---|---|---|
| Script load (session already up) | 2.6 ms | 2.0 ms |
| Inject (adb push of injector + agent, then ptrace, every time) | 370 ms | — |
Attach (frida-server already running; 56 MB server push not included) |
— | 53 ms |
| Integer add | 48 ns/op | 74 ns/op |
| Property get | 109 ns/op | 147 ns/op |
| Call | 112 ns/op | 145 ns/op |
readU32 |
230 ns/op | 2.2 µs/op |
| String append | 2.7 µs/op | 1.7 µs/op |
| Object alloc | 1.0 µs/op | 0.9 µs/op |
send |
2 µs/msg | 14 µs/msg |
| Agent PSS at idle | ~1.6 MB | ~6 MB |
Those two startup rows are not the same operation. goauld’s 370 ms repeats the push; Frida’s 53 ms is only attach after frida-server is already up.
Frida stays ahead on attach, strings, allocation, and everything this table does not measure (other ABIs, Stalker, CLI, existing scripts). On this device QuickJS was quicker on tight loops, memory reads, and send. Symbiote is a second pure-Rust backend of the same API and is still well behind both.
┌──────────────┐ adb push / shell ┌─────────────────────────┐
│ goauld-host │ ──────────────────► │ goauld-injector (arm64) │ ← runs ON the phone
│ (desktop) │ │ ptrace + remote dlopen │
└──────┬───────┘ └───────────┬─────────────┘
│ adb forward │ injects
▼ ▼
TCP ↔ abstract socket ┌─────────────────────────┐
│ libgoauld_agent.so │
│ hooks + JS + ART │
└─────────────────────────┘
Ptrace must run on-device. The desktop CLI never attaches to app processes itself; it deploys the injector and agent, then runs the injector as root (or via adb root).
| Crate | Role |
|---|---|
goauld-proto |
Wire protocol |
goauld-inject |
Ptrace / remote-call library (into the on-device injector) |
goauld-injector |
On-device arm64 binary — inject / syscall trace |
goauld-native-hook |
Agent: arm64 inline + GOT/PLT hooks |
goauld-art-bridge |
Agent: ArtMethod / JNI hooks |
goauld-script |
Agent: Frida-shaped JS (QuickJS or Symbiote) |
goauld-agent |
Agent cdylib (libgoauld_agent.so) |
goauld-host |
Desktop CLI (adb orchestrator + script attach) |
Disassembler: sibling arm_disassembler. Deeper notes: docs/ARCHITECTURE.md.
Compile-time mutually exclusive features on goauld-script / goauld-agent:
| Feature | Backend | Notes |
|---|---|---|
quickjs (default) |
rquickjs |
Full Frida-shaped surface + e2e fixtures |
symbiote |
symbiote-core + symbiote-jit |
Pure-Rust; path dep ../../symbiote |
./scripts/test-js-engines.sh both
./scripts/test-symbiote-emulator.sh # Symbiote agent → dist/android-arm64-symbiote/Pattern: inject once, then attach scripts. Replace SOME.js / SOME-OK as needed.
cargo run -p goauld-host --release -- attach \
--pid "$(adb shell pidof -s com.example.javatarget | tr -d '\r')" \
--port 27046 \
--script scripts/fixtures/java_toast.js \
--expect-send toast-shown --max-wait-secs 10On-device PTRACE_SYSCALL — no agent .so, injector only. Decoded paths, sockaddrs, buffer previews. Does not use PTRACE_O_EXITKILL (app should survive detach).
cargo run -p goauld-host --release -- attach syscall \
--pid "$(adb shell pidof -s com.example.javatarget | tr -d '\r')" \
--max-events 400 --max-wait-secs 60
cargo run -p goauld-host --release -- trace syscalls \
--package com.example.javatarget \
--injector dist/android-arm64/goauld-injector \
--duration-secs 10 \
--filter openat,connect,write,readHooks ART invoke stubs; emits android-api events (method, shorty, decoded args). Direct AOT-to-AOT calls that never hit those stubs are invisible.
# Fresh process: restart + inject + trace
cargo run -p goauld-host --release -- trace java \
--package com.example.javatarget \
--injector dist/android-arm64/goauld-injector \
--agent dist/android-arm64/libgoauld_agent.so \
--stage-into-app \
--filter 'android.,androidx.,java.,javax.,com.android.' \
--max-events 200 --max-wait-secs 20
# Already injected
cargo run -p goauld-host --release -- attach java \
--pid "$(adb shell pidof -s com.example.javatarget | tr -d '\r')" \
--java-hooks 'com.example.javatarget.Target.hookMe:(I)I' \
--max-events 50 --max-wait-secs 20# Package inspector
--script scripts/fixtures/inspect_package.js --expect-send inspect-ok
# Technique-A Java hook
--script scripts/fixtures/java_hook.js --expect-send java-hook-installed
# Native Interceptor
--script scripts/fixtures/interceptor_attach.js --expect-send interceptor-attach-ok
--script scripts/fixtures/interceptor_replace.js --expect-send interceptor-replace-ok
# Arm64Writer recipes
--script scripts/fixtures/arm64_writer_examples.js --expect-send arm64-examples-ok
# Smoke
--script scripts/fixtures/hi.js --expect-send hi
--script scripts/fixtures/process_module_memory.js --expect-send ptmm-okMinimal writer / relocator sketch:
var code = Memory.alloc(Process.pageSize);
Memory.protect(code, Process.pageSize, 'rwx');
var w = new Arm64Writer(code);
w.putLdrRegU64(Register.x0, 99);
w.putRet();
w.flush();
Memory.patchCode(target, 16, function (code) {
var w = new Arm64Writer(code);
w.putInstruction((0xD2800000 | (7 << 5)) >>> 0); // MOVZ X0, #7
w.putRet();
w.flush();
w.dispose();
});./scripts/stress_comm.sh
./scripts/stress_comm.sh --mode fridaTiny cousin of androguard/freedump: enumerate
ranges in the target, stream chunks over the goauld session, write
%x-%x.dump + info.freedump (same layout freedump uses locally).
# Agent already injected
python3 scripts/freedump.py --pid "$(adb shell pidof -s com.example.javatarget | tr -d '\r')" \
-o /tmp/goauld-dumps
# Inject then dump (QuickJS dist)
python3 scripts/freedump.py --package com.example.javatarget --inject \
--dist dist/android-arm64 -o /tmp/goauld-dumps
# rw- only, 64 KiB chunks, stop at 32 MiB
python3 scripts/freedump.py --pid 1234 -o /tmp/goauld-dumps \
--prot 'rw-' --chunk 65536 --max 33554432Agent fixture: scripts/fixtures/freedump.js.
| Script | Purpose | --expect-send |
|---|---|---|
hi.js |
send("hi") |
hi |
java_toast.js |
Framework toast | toast-shown |
java_hook.js |
Hook Target.hookMe |
java-hook-installed |
java_api.js |
Java.* smoke |
java-api-ok |
java_perform.js |
Java.perform / performNow |
java-perform-ok |
inspect_package.js |
Classes / prefs / storage | inspect-ok |
interceptor_*.js |
Native Interceptor | interceptor-*-ok |
arm64_writer*.js |
Writer / relocator | arm64-*-ok |
process_module_memory.js |
Process / Module / Memory | ptmm-ok |
memory_scan.js / memory_patch.js |
Scan / patchCode | memory-*-ok |
thread_api.js / thread_backtrace.js |
Threads / backtrace | thread-api-ok / backtrace-ok |
misc_apis.js |
console / hexdump / timers / gc | misc-apis-ok |
trace_java_api.js |
Android API tracing | via trace java |
stress_*.js |
Comm stress | via stress |
freedump.js |
Process memory dump | via scripts/freedump.py |
./scripts/run_milestone.sh all
./scripts/run_e2e_apk.sh
./scripts/test-symbiote-emulator.sh| # | Goal | Status |
|---|---|---|
| 1 | Injection round-trip | OK on emulator |
| 2 | Native hook plumbing | Host unit OK; live via Interceptor |
| 3 | send("hi") round-trip |
OK |
| 4 | Interceptor.attach from JS |
OK |
| 5 | Java method hook (Technique A) | OK on java-target |
| 6 | APK e2e (java-target) |
syscalls, inject, fixtures, attach / trace java |
| 7 | Non-rooted hardening | pending |
| 8 | Binder tracing | pending |
APK e2e (scripts/run_e2e_apk.sh) covers syscalls, inject, hi / ptmm, toast / java-api / inspect, android-api, Technique-A hooks, Interceptor, and trace java on a rooted emulator.
| Doc | Contents |
|---|---|
docs/JS_API_PARITY.md |
Frida surface coverage |
docs/ARCHITECTURE.md |
Design notes |
docs/INTERNALS.html |
Internals (HTML) |
./scripts/emulator.sh start | start --gui | stop | status