frida CLI
# List processes
frida-ps # local
frida-ps -U # USB (Android)
frida-ps -H host:port # remote
frida-ps -a # with paths
# Attach to process
frida -n com.target.app # by name
frida -p 1234 # by PID
frida -U -n com.target.app # USB device
# Spawn (start + inject before main)
frida -f ./challenge # spawn binary
frida -U -f com.target.app # spawn Android
frida -f ./challenge --no-pause # don't pause at entry
# Load script
frida -f ./challenge -l hook.js
frida -p 1234 -l hook.js --no-pause
frida-trace
# Trace function calls automatically
# Creates JS handlers in __handlers__/
# Trace by module!function
frida-trace -n challenge -i "strcmp"
frida-trace -n challenge -i "*check*" # wildcard
frida-trace -n challenge -i "libc.so!*"
# USB Android
frida-trace -U -f com.app -j '*!*verify*' # Java method
frida-trace -U -f com.app -i '*crypt*' # native
# frida-discover — list function names
frida-discover -n challenge
# frida-kill
frida-kill -n com.target.app
JSBasic hook
// hook.js — intercept any function
// By name (exported function)
const strcmp = Module.getExportByName(null, 'strcmp');
Interceptor.attach(strcmp, {
onEnter(args) {
// args[0], args[1] ... are NativePointers
console.log('strcmp:',
args[0].readUtf8String(),
args[1].readUtf8String()
);
},
onLeave(retval) {
console.log(' → returned:', retval.toInt32());
// Force return value:
retval.replace(0); // return 0 = equal
}
});
JSHook by address
// Hook by absolute or relative address
// No PIE: absolute address
const target = ptr('0x401234');
// PIE: base + offset
const base = Module.getBaseAddress('challenge');
const target = base.add('0x1234'); // offset from base
Interceptor.attach(target, {
onEnter(args) {
console.log('hit!');
console.log('rdi:', this.context.rdi);
console.log('rsi:', this.context.rsi);
console.log('rsp:', this.context.rsp);
}
});
// this.context has all registers:
// rax, rbx, rcx, rdx, rdi, rsi
// r8-r15, rsp, rbp, rip, eflags
JSModify args & return
// Modify argument before function runs
Interceptor.attach(checkFunc, {
onEnter(args) {
// Replace first arg with "admin"
const newArg = Memory.allocUtf8String('admin');
args[0] = newArg;
// Replace integer arg
args[1] = ptr(1337);
}
});
// Force return value
Interceptor.attach(target, {
onLeave(retval) {
retval.replace(1); // return 1
retval.replace(ptr('0x1337')); // return pointer
}
});
// Completely replace function
Interceptor.replace(target, new NativeCallback(
() => 1, // always return 1
'int', []
));
JSRead memory
const p = ptr('0x404060');
// Read various types
p.readU8() // 1 byte unsigned
p.readS8() // 1 byte signed
p.readU16() / p.readU32() / p.readU64()
p.readS16() / p.readS32() / p.readS64()
p.readFloat() / p.readDouble()
p.readPointer() // read pointer (arch-width)
p.readUtf8String() // null-terminated UTF-8
p.readUtf8String(20) // max 20 bytes
p.readUtf16String() // UTF-16 (Windows)
p.readByteArray(16) // 16 raw bytes → ArrayBuffer
// Dereference
p.readPointer().readUtf8String() // follow pointer chain
JSWrite & allocate
const p = ptr('0x404060');
// Write values
p.writeU8(0x41)
p.writeU32(0xdeadbeef)
p.writeUtf8String('new_value')
p.writeByteArray([0x90, 0x90, 0xc3]) // NOP NOP RET
// Allocate new memory
const buf = Memory.alloc(64) // 64 bytes
const str = Memory.allocUtf8String('hello')
const arr = Memory.allocByteArray([1,2,3])
// Make memory executable
Memory.protect(p, 4096, 'rwx')
// Pointer arithmetic
p.add(8) // p + 8
p.sub(4) // p - 4
p.and(0xfff)
p.or(1)
p.xor(ptr('0xff'))
JSMemory scan
// Scan for byte pattern
Memory.scan(
Module.getBaseAddress('libc.so.6'),
0x100000, // size to scan
'2f 62 69 6e 2f 73 68', // /bin/sh
{
onMatch(address, size) {
console.log('/bin/sh at', address);
},
onComplete() {}
}
);
// Scan with wildcards
Memory.scan(base, 0x1000, '48 ?? ?? ?? ?? c3', {
onMatch(addr) { console.log(addr); }
});
// Synchronous scan (returns array)
const results = Memory.scanSync(base, 0x1000,
'48 8b 3d ?? ?? ?? ??');
results.forEach(r => console.log(r.address));
NativeFunction — call native
// Call an existing native function from JS
const puts = new NativeFunction(
Module.getExportByName(null, 'puts'),
'int', // return type
['pointer'] // arg types
);
puts(Memory.allocUtf8String('hello from frida'));
// Type strings: void int uint32 uint64
// pointer float double bool char
// int8 int16 int32 int64
// uint8 uint16 uint32 uint64
// Call function at arbitrary address
const check = new NativeFunction(
ptr('0x401234'), 'int', ['pointer', 'int']
);
console.log(check(Memory.allocUtf8String('test'), 42));
Interceptor.replace — full replacement
// Replace function completely with JS callback
Interceptor.replace(
Module.getExportByName(null, 'strcmp'),
new NativeCallback(
function(s1, s2) {
console.log('strcmp hooked:',
s1.readUtf8String(),
s2.readUtf8String());
return 0; // always equal
},
'int', // return type
['pointer', 'pointer'] // arg types
)
);
// Restore original
Interceptor.revert(target);
Interceptor.flush(); # apply all pending
Module
// List loaded modules
Process.enumerateModules().forEach(m =>
console.log(m.name, m.base, m.size)
);
// Get specific module
const mod = Process.getModuleByName('libc.so.6');
mod.base // base address
mod.size // size
mod.path // full path
// Get base (shortcut)
Module.getBaseAddress('challenge')
Module.getBaseAddress('libc.so.6')
// Get export address
Module.getExportByName('libc.so.6', 'system')
Module.getExportByName(null, 'puts') // any module
// List exports
Module.enumerateExports('challenge').forEach(e =>
console.log(e.name, e.address)
);
Process & threads
// Process info
Process.id // PID
Process.arch // 'x64' | 'arm64' | 'ia32'
Process.platform // 'linux' | 'windows' | 'darwin'
Process.pageSize // usually 4096
Process.pointerSize // 8 (64-bit) or 4 (32-bit)
// Memory ranges
Process.enumerateRanges('r-x').forEach(r =>
console.log(r.base, r.size, r.file?.path)
);
// Threads
Process.enumerateThreads().forEach(t =>
console.log(t.id, t.state, t.context.rip)
);
// Find address info
Process.findModuleByAddress(ptr('0x401234'))
Android native hooks
// Hook JNI function (native method)
const lib = Module.getBaseAddress('libnative.so');
// JNI naming: Java_package_Class_method
const checkFunc = Module.getExportByName(
'libnative.so',
'Java_com_example_app_MainActivity_check'
);
Interceptor.attach(checkFunc, {
onEnter(args) {
// args[0] = JNIEnv* args[1] = jobject
// args[2..] = actual Java arguments
console.log('input:', args[2].readUtf8String());
},
onLeave(retval) {
retval.replace(1); // return true
}
});
SSL pinning bypass
// Universal Android SSL unpin
// Source: https://codeshare.frida.re/@pcipolloni/universal-android-ssl-pinning-bypass-with-frida/
frida -U -f com.target.app -l ssl-unpin.js
// Quick TrustManager bypass
Java.perform(() => {
const TrustManager = Java.registerClass({
name: 'com.frida.TrustManager',
implements: [Java.use('javax.net.ssl.X509TrustManager')],
methods: {
checkClientTrusted: (chain, authType) => {},
checkServerTrusted: (chain, authType) => {},
getAcceptedIssuers: () => []
}
});
// ... install via SSLContext
});
JSHook Java methods
Java.perform(() => {
// Get class
const MainActivity = Java.use('com.example.app.MainActivity');
// Hook instance method
MainActivity.checkPassword.implementation = function(pass) {
console.log('Password attempt:', pass);
// Call original
const result = this.checkPassword(pass);
console.log('Result:', result);
return true; // override return
};
// Hook overloaded method (specify signature)
MainActivity.check.overload('java.lang.String').implementation
= function(s) {
console.log(s);
return true;
};
});
JSJava utilities
Java.perform(() => {
// Call static method
const Log = Java.use('android.util.Log');
Log.d('FRIDA', 'hello from JS');
// Create new object
const StringBuilder = Java.use('java.lang.StringBuilder');
const sb = StringBuilder.$new();
sb.append('test');
console.log(sb.toString());
// Enumerate class instances
Java.choose('com.example.app.SecretClass', {
onMatch(instance) {
console.log(instance.secretKey.value); // field
},
onComplete() {}
});
// Enumerate loaded classes
Java.enumerateLoadedClasses({
onMatch: name => { if(name.includes('check')) console.log(name); }
});
});
pythonPython host script
import frida, sys
SCRIPT = """
Interceptor.attach(Module.getExportByName(null, 'strcmp'), {
onEnter(args) {
const s1 = args[0].readUtf8String();
const s2 = args[1].readUtf8String();
if (s2 && s2.includes('picoCTF')) {
send({flag: s2});
}
}
});
"""
def on_message(msg, data):
if msg['type'] == 'send':
print('[FLAG]', msg['payload'])
elif msg['type'] == 'error':
print('[ERROR]', msg['stack'])
device = frida.get_local_device()
pid = device.spawn(['./challenge'])
session = device.attach(pid)
script = session.create_script(SCRIPT)
script.on('message', on_message)
script.load()
device.resume(pid)
sys.stdin.read() # keep running
Common CTF patterns
// 1. Dump strcmp arguments to find flag
Interceptor.attach(Module.getExportByName(null, 'strcmp'), {
onEnter(args) {
try {
const a = args[0].readUtf8String();
const b = args[1].readUtf8String();
if (a || b) console.log(`strcmp(${a}, ${b})`);
} catch(e) {}
}
});
// 2. Dump all strings read from file
Interceptor.attach(Module.getExportByName(null, 'fgets'), {
onLeave(retval) {
if (retval.isNull()) return;
console.log('fgets:', this.args[0].readUtf8String());
}
});
// 3. Trace all calls + args (any function)
const target = ptr('0x401234');
Interceptor.attach(target, {
onEnter(args) {
console.log('called!',
'rdi='+this.context.rdi,
'rsi='+this.context.rsi);
}
});
QUICK REFERENCE →
frida -f ./bin -l hook.js spawn+inject
· frida-trace -n bin -i "strcmp" auto-trace
· Interceptor.attach(addr, {onEnter, onLeave})
· args[0].readUtf8String() read string arg
· retval.replace(0) override return
· Module.getBaseAddress('bin').add(offset) PIE addr
· Memory.scanSync(base, size, pattern) find bytes
· Java.perform(() => Java.use('class').method.implementation = ...) Android