JavaScript Examples
Frida JavaScript Examples
Inject Arbitrary JS into a Node.js Process (V8 VM)
Prerequisites:
- Target: Node.js process (exposes
uv_*andv8::*symbols) - Mechanism: libuv async handle + V8 C++ API via
NativeFunction - Symbols used: mangled C++ names (GCC/Clang ABI)
Pattern: Schedule JS source strings via a libuv uv_async_t handle that fires on the Node.js event loop thread, then compile and run them using V8’s Script::Compile / Script::Run.
1const uv_default_loop = new NativeFunction(Module.getGlobalExportByName('uv_default_loop'), 'pointer', []);
2const uv_async_init = new NativeFunction(Module.getGlobalExportByName('uv_async_init'), 'int', ['pointer', 'pointer', 'pointer']);
3const uv_async_send = new NativeFunction(Module.getGlobalExportByName('uv_async_send'), 'int', ['pointer']);
4const uv_close = new NativeFunction(Module.getGlobalExportByName('uv_close'), 'void', ['pointer', 'pointer']);
5const uv_unref = new NativeFunction(Module.getGlobalExportByName('uv_unref'), 'void', ['pointer']);
6
7// V8 C++ API — mangled symbol names (GCC ABI)
8const v8_Isolate_GetCurrent = new NativeFunction(Module.getGlobalExportByName('_ZN2v87Isolate10GetCurrentEv'), 'pointer', []);
9const v8_Isolate_GetCurrentContext = new NativeFunction(Module.getGlobalExportByName('_ZN2v87Isolate17GetCurrentContextEv'), 'pointer', ['pointer']);
10const v8_HandleScope_init = new NativeFunction(Module.getGlobalExportByName('_ZN2v811HandleScopeC1EPNS_7IsolateE'), 'void', ['pointer', 'pointer']);
11const v8_HandleScope_finalize = new NativeFunction(Module.getGlobalExportByName('_ZN2v811HandleScopeD1Ev'), 'void', ['pointer']);
12const v8_String_NewFromUtf8 = new NativeFunction(Module.getGlobalExportByName('_ZN2v86String11NewFromUtf8EPNS_7IsolateEPKcNS_13NewStringTypeEi'), 'pointer', ['pointer', 'pointer', 'int', 'int']);
13const v8_Script_Compile = new NativeFunction(Module.getGlobalExportByName('_ZN2v86Script7CompileENS_5LocalINS_7ContextEEENS1_INS_6StringEEEPNS_12ScriptOriginE'), 'pointer', ['pointer', 'pointer', 'pointer']);
14const v8_Script_Run = new NativeFunction(Module.getGlobalExportByName('_ZN2v86Script3RunENS_5LocalINS_7ContextEEE'), 'pointer', ['pointer', 'pointer']);
15
16const NewStringType = { kNormal: 0, kInternalized: 1 };
17
18const pending = [];
19
20// Callback runs on the Node.js event loop thread — safe to call V8 API here
21const processPending = new NativeCallback(function () {
22 const isolate = v8_Isolate_GetCurrent();
23
24 const scope = Memory.alloc(24); // sizeof(v8::HandleScope) ~ 24 bytes
25 v8_HandleScope_init(scope, isolate);
26
27 const context = v8_Isolate_GetCurrentContext(isolate);
28
29 while (pending.length > 0) {
30 const item = pending.shift();
31 const source = v8_String_NewFromUtf8(isolate, Memory.allocUtf8String(item), NewStringType.kNormal, -1);
32 const script = v8_Script_Compile(context, source, NULL);
33 const result = v8_Script_Run(script, context);
34 }
35
36 v8_HandleScope_finalize(scope);
37}, 'void', ['pointer']);
38
39const onClose = new NativeCallback(function () {
40 Script.unpin(); // allow Frida script GC after handle is closed
41}, 'void', ['pointer']);
42
43// Register async handle; uv_unref so it doesn't keep the loop alive
44const handle = Memory.alloc(128); // sizeof(uv_async_t) ≤ 128 bytes
45uv_async_init(uv_default_loop(), handle, processPending);
46uv_unref(handle);
47
48// When this Frida script is unloaded, close the handle cleanly
49Script.bindWeak(handle, () => {
50 Script.pin();
51 uv_close(handle, onClose);
52});
53
54// Public API: enqueue a JS source string for execution in Node.js V8
55function run(source) {
56 pending.push(source);
57 uv_async_send(handle);
58}
59
60// Usage
61run('console.log("Hello from Frida");');
Key points:
NativeFunctionwraps raw C/C++ exports; argument types must match exactly.NativeCallbackcreates a C-callable function pointer from a JS closure.Memory.allocreturns aNativePointer; size must cover the native struct.Script.pin()/Script.unpin()prevent premature GC while async work is in flight.Script.bindWeak(obj, fn)firesfnwhenobjis about to be collected — used for cleanup.
Trace Function Calls in a Perl 5 Process
Prerequisites:
- Target: process linking
libperl(e.g.,perl, embedded Perl host) - Hook point:
Perl_pp_entersub— called on every Perl sub invocation - Struct layout depends on pointer size (
Process.pointerSize)
Perl internal offsets used:
| Field | Offset | Notes |
|---|---|---|
SV.sv_flags (u32) | pointerSize + 4 | Bottom byte is sv_type |
PVGV.xgv_gp->gp_egv->sv_any namehek | 4 * pointerSize | Body pointer + NAMEHEK offset |
SVt_PVGV = 9 — the SV type for a glob (named sub reference).
1const pointerSize = Process.pointerSize;
2const SV_OFFSET_FLAGS = pointerSize + 4;
3const PVGV_OFFSET_NAMEHEK = 4 * pointerSize;
4
5const SVt_PVGV = 9;
6
7Interceptor.attach(Module.getGlobalExportByName('Perl_pp_entersub'), {
8 onEnter(args) {
9 // args[0] = PerlInterpreter* (aTHX)
10 const interpreter = args[0];
11 const stack = interpreter.readPointer(); // PL_stack_sp points to top of stack
12 const sub = stack.readPointer(); // CV* (code value) at top of stack
13
14 const flags = sub.add(SV_OFFSET_FLAGS).readU32();
15 const type = flags & 0xff;
16 if (type === SVt_PVGV) {
17 // Named sub: read GV name from HEK (hash entry key)
18 console.log(GvNAME(sub) + '()');
19 // Note: console.log() per-call is slow; for high-frequency tracing
20 // buffer events and batch-send with send() instead.
21 }
22 // Anonymous subs (type !== SVt_PVGV) are not handled here
23 }
24});
25
26// Read the name of a GV (glob value) from its embedded HEK
27// HEK layout: [len:u32][hash:u32][key:utf8...][NUL] → string starts at offset 8
28function GvNAME(sv) {
29 const body = sv.readPointer();
30 const nameHek = body.add(PVGV_OFFSET_NAMEHEK).readPointer();
31 return nameHek.add(8).readUtf8String();
32}
Key points:
Perl_pp_entersubreceives the Perl interpreter pointer asargs[0](theaTHXconvention).- The interpreter’s stack pointer is dereferenced to get the CV (code value) being called.
sv_flags & 0xffextractssv_type; onlySVt_PVGV(9) carries a symbol-table name.- HEK (Hash Entry Key) string data begins at byte offset 8 (after 4-byte length + 4-byte hash).
- For production tracing, replace
console.logwithsend()to batch-deliver events.