Operating Renef
You are driving renef — a dynamic instrumentation engine for Android ARM64. Your job is to emit renef Lua/CLI that runs correctly the first time. If you've written Frida hooks, GameGuardian cheats, or game-hacking Lua, the concepts already transfer — you only need renef's syntax and workflow, which this skill makes exact. Do not guess API names; use the Syntax Contract below as ground truth, and run the Self-Lint before presenting any script.
For authorized security testing, app-hardening review, CTFs, and reverse-engineering of software you own or are permitted to analyze.
Start here for any real target: run probe.lua (bundled with this skill) to ground-truth the
actual API/behavior of this renef build, then follow the investigation loop in
references/methodology.md. Attempt and measure on-device — don't pre-decide what's possible —
and report status honestly: not attempted → installed but unverified → verified working. The
human drives the device/app/proxy and decides the goal; you do the enumeration, scripting,
error-parsing, and verification. Recipes below are worked examples of the loop, not a menu to
copy blindly (offsets/signatures are target-specific).
What you're driving
- Topology — read this before anything.
renefis a host client (your macOS/Linux shell) that drivesrenef_serverrunning on the Android device over an ADB-forwarded socket. You do not runrenefon the device, and you do notadb push hook.lua+adb shell renef …. That on-device pattern is only--localmode (Termux/SSH on a rooted phone). Default and assumed-here = host client → remote device. Lua scripts stay on the host and are sent over the wire by-l; device-side paths apply only under--local. - Your Lua runs inside the target process (injected
libagent.so, embedded Lua 5.4, memfd injection, no ptrace).Module.*,Memory.*, hook callbacks all execute in the app's address space — paths, PIDs, fds are the target's. - You drive it from the CLI REPL (
renef>) or one-shot via-l script.lua. - Hook/trace output is asynchronous — you must
watch(or launch with-w) or you see nothing.
Preflight (Step 0 — a gate, before you write, run, or "test" anything)
Confirm the host→device chain end-to-end. Until you do, you cannot test — deliver the script as explicitly unverified, never as "tested/working", and never invent run output.
adb deviceslists exactly one authorized device (more than one → require-d <id>; none → stop).- The
renefclient exists on the host:command -v renef(orrenef -h). renef_serveris running on the device:adb shell pidof renef_server. If absent, start it (adb shell su -c '/data/local/tmp/renef_server &') or rely on magisk-renef.- Root (or gadget mode for non-root), target is ARM64, and the package is installed (
la~<name>).
A "test" = you saw real hook output via -w/watch while exercising the app — not that the
script reads correctly. If any link above is missing, say which one and stop short of claiming a run.
The loop (always follow this)
- Connect to the target (table below).
- Locate the target code — resolve an offset by symbol or byte-pattern, never hardcode an
absolute runtime address; guard with
if not base then return end. - Install the smallest hook that proves the call fires (
printinonEnter). - Verify by watching output while you trigger the app (
-w/watch). - Iterate — add logic only after the minimal hook is confirmed.
For startup-time protections (root/integrity/debugger checks), connect with --pause (spawn gate)
so hooks install before the app's onCreate runs.
Connect modes
| Mode | Command | When |
|---|---|---|
| Spawn (root) | renef -s com.pkg -l hook.lua |
start fresh, hooks before app code |
| Attach (root) | renef -a <pid> -l hook.lua |
already running (adb shell pidof com.pkg) |
| Spawn gate | renef -s com.pkg -l bypass.lua --pause |
beat startup-time checks |
| Gadget (no root) | renef -g <pid> -l hook.lua |
non-root, APK patched with libagent.so |
| Local (on-device) | renef --local -s com.pkg -l /data/local/tmp/hook.lua |
Termux/SSH, SELinux-safe |
Every row except the last runs renef on the host against the device — only --local runs the
client on-device. Server must run on device first (adb shell /data/local/tmp/renef_server, or the
magisk-renef module auto-starts it). Engine: trampoline (default, any address) or --hook=pltgot (imported
functions only). Full flag list and REPL commands → references/cli.md.
Syntax Contract (ground truth — do not invent API)
Language is Lua 5.4. local; .. concatenation; string.format("0x%x", n); function(x) end;
tables {}; nil; ipairs/#t; integer hex 0x... No var/let/const, arrow fns, template
strings, or ===.
Native hook — target is (library, offset), never an absolute address:
hook("libc.so", 0x12340, {
-- args[0..7] = x0..x7 (first arg = args[0]); assign to modify
args[0] = 0x200
end,
-- return a value to REPLACE x0; `return retval` passes through; nothing = keep
return retval
end,
caller = "libnative.so" -- optional: restrict to this caller → PLT/GOT mode (imported fns only)
})
Java hook — class uses / (JNI), signature is mandatory:
hook("com/example/Cls", "method", "(Ljava/lang/String;)Z", {
-- args[0]=ArtMethod* (ignore) | args[1]=this (instance) OR first param (static) | args[2..]=params
local s = Jni.getStringUTF(args[2]) -- string args are raw pointers
-- args.skip = true -- skip the original entirely (bypass primitive)
end,
-- retval.raw = raw x0 (int) ; retval.value = decoded string (String-returning methods only)
return 1 -- see return table below
end
})
onEnter.args also exposes args.class/method/signature/isStatic. onLeave return options:
nil=keep · integer=set x0 (1 true / 0 false) · boolean=1/0 · Jni.newStringUTF("x")=new
String · {__jni_type="string"|"int"|"boolean", value=...}=typed · retval.raw=pass original.
Memory: Memory.scan(pat[,lib])/search (string or hex, ?? wildcards) → table of
{library,addr,offset,hex,ascii}. read(a,n) readU8/16/32/64(a) readStr(a[,n])/readString.
write(a,bytes) writeU8/16/32/64(a,v) patch(a,bytes) (auto-mprotect). There is no
Memory.writeString — write a string as bytes: Memory.write(addr, "text\0").
hexdump(target[,len]) → string, print() it.
Module: find(name)→base int|nil · list()→string · exports(name)→{name,offset}
(.dynsym) · symbols(name)→{name,offset} (.symtab, nil if stripped).
Java runtime (no hook): Java.use("p/C") → :call(m,sig,...) / :new(sig,...);
instance :call(m,sig,...); .raw = ART pointer; Java.registerClass{implements=,methods=};
Java.array("p/C",{...}).
Other globals: Jni.newStringUTF/getStringUTF/getStringLength/deleteGlobalRef ·
Thread.backtrace() (auto caller context in hooks) / Thread.id() · File.read/exists/readlink/ fdpath · Syscall.trace(...)/stop()/list() · OS.getpid/kill/tgkill/listdir · print /
console.log / color globals RED GREEN YELLOW BLUE MAGENTA CYAN WHITE RESET.
Script directive: __hook_type__ = "trampoline"|"pltgot" at top of file.
There is no
Interceptor,Java.perform,Process.*,ptr()/NativePointer, orgg.*namespace. Those belong to other tools — translate them (references/from-other-tools.md).
Share state between onEnter and onLeave with a closure-local (NOT args)
args does not exist in onLeave. Capture in a local declared just before hook(...):
for _, e in ipairs(Module.exports("libc.so")) do
if e.name == "open" then
local blocked = false -- closure-local
hook("libc.so", e.offset, {
local p = Memory.readString(args[0]); blocked = p and p:find("/su") ~= nil
end,
if blocked then return -1 end; return retval end
})
break
end
end
Self-Lint (run over every script before presenting it)
- Preflight passed (host
renef+adb devices+ devicerenef_server) before any claim of a test. Script invoked from the host (renef -s/-a/-g …), notadb shell renef/adb push(that's--local-only). If preflight didn't pass, the script is labeled unverified — no fabricated output. - Hook target is
(library, offset)— no absolute address passed tohook(). - Offset resolved by symbol (
Module.exports/symbols) or byte-pattern (Memory.search), not a hardcoded number — and guarded withif not base then return end. - Java class uses
/separators; signature is present and exact(params)ret. - Static vs instance arg index: instance method first param =
args[2]; static method first param =args[1](verified). Native first arg =args[0]. Getting this wrong reads the wrong value silently — the #1 logic bug. - State shared across callbacks via a closure-
local, never viaargsinonLeave. -
onLeavereturns to modify; native pass-through usesreturn retval. Spoof a return inonLeave— do not mutate the input arg inonEnterto change a return value. - No
Memory.writeString(doesn't exist) — useMemory.write(a, "s\0"). Only documented globals (no Frida/GG names, noProcess.*). - Every hook wrapped in
safe()/pcallthat prints the error — renef reports failures as a bareERROR: Lua execution failedand aborts the rest of the script (seereferences/debugging.md). - No hot-function hooks (
open/read/write/system/popen/do_dlopen) installed while the app runs — they race and crash the agent. Put important bypasses first, risky hooks last. - Syntax pre-checked with
luac -p(renef won't give you the line); output seen via-w/watch.
Task → playbook
| User wants | Do |
|---|---|
| Override a Java boolean/int check (root, isVip, license) | Java hook, onLeave return 0/1 |
Bypass a void Java verify that throws (SSL checkServerTrusted) |
Java hook, onEnter args.skip = true |
| Replace a Java String return | onLeave return Jni.newStringUTF("...") |
| Trace/inspect a native function | resolve offset via Module.exports/symbols, minimal onEnter hook, watch |
| Hook a not-yet-loaded lib (Flutter, plugin) | hook do_dlopen in linker64, install on load |
| Game value cheat (find/edit/freeze a number) | Memory.scan → refine → writeU32; freeze via a hot hook → from-other-tools.md (GameGuardian) |
| Monitor file/network/syscalls | Syscall.trace{category="file"} or renef-strace, then watch |
| Patch/NOP native code | Memory.search pattern → Memory.patch(addr, bytes) |
| Port a Frida or GameGuardian script | references/from-other-tools.md |
Reference files (read on demand — don't load them all at once)
references/methodology.md— the investigation loop: how to solve a NEW bypass/hook yourself (classify → locate → probe mechanism → minimal hook → verify → choose primitive), decompose-to- smallest-goal, human/AI roles, honest status. Start here for anything not covered by a recipe.probe.lua— bundled diagnostic. Run it FIRST on a new target/build to measure the real API surface, error behavior, arg layout, and the reflection bridge instead of trusting notes.references/debugging.md— read this when anything fails. renef hides errors (ERROR: Lua execution failed/(no response)/Agent disconnected); how to recover the real message withpcall/load, the hot-hook crash race,--pausecaveats, verifying installs, connection recovery, and recognizing RASP/shielded apps.references/lua-api.md— full API: exact signatures, return shapes, JNI signature table, per-function notes. Read before writing a non-trivial script.references/cli.md— every CLI command/flag, connect modes, spawn gate, build/deploy, r2renef, python binding, magisk-renef.references/from-other-tools.md— translate Frida JS and GameGuardian (gg.*) / game-hacking Lua into renef. Includes a reusable value scan→refine→freeze helper.references/recipes.md— copy-paste scripts: SSL unpinning (Java/Flutter/native), root/debugger bypass, crypto logging, tracing, dlopen watch, memory cheats, syscall auditing, CTF.references/gotchas.md— pitfalls, doc/runtime discrepancies (JnivsJNI, undocumented helpers), troubleshooting (injection fails, hook not firing, SELinux), performance.