Persistent Frida Hooks on Android Without Repackaging
About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.
This research is published for educational and defensive purposes. The target sample is anonymized throughout.
Persistent Frida Hooks on Android Without Repackaging

The idea: Frida hooks that don’t need a PC
Frida is powerful enough that plenty of researchers live in it. The usual workflow is to write a JS script on the PC and apply it via spawn or attach. But can we make hook scripts persistent like Xposed — no PC dependency — and, further, pin a specific script to a specific app? Yes, thanks to the frida-gadget shared library.
The catch is that using frida-gadget normally means repackaging the target app to inject the .so. That’s fragile: apps increasingly verify their signature and crash after repackaging, and commercial packers don’t make it easier. Repackaging is a poor path from both the workflow and the protection angle. Is there a simpler, one-time solution? There is, and it comes from the app lifecycle.
In my earlier FART unpacking writeups I covered how a packed app boots: the moment you tap an icon, the first thing entered is the ActivityThread class. For unpacking you want to hook as late as possible to dump and fix already-decrypted dex. But for persistent hooking you want the opposite — as early as possible, otherwise you miss hooks on the app’s own classes, like the packer’s own class methods. So the injection point for frida-gadget must be no later than the app’s Application subclass calling attachBaseContext.
That gives us plenty of early hook points inside ActivityThread (anything before the Application.attachBaseContext call). And by keying off the process’s package name, we can inject different JS scripts into different apps. That’s the design behind FridaManager: persistent Frida hooks, plus per-app script selection driven by configuration.
Usage
-
Flash the FridaManager ROM. (Currently only a Nexus 5X / Android 7.1.2 build is published; an Android 10 build and more devices are planned.)
-
Install the FridaManager app and grant it SD-card read/write access. It’s a simple manager for choosing which script persists for which app:

-
Install the target app. Important: don’t open it yet, and grant it SD-card read/write access (it will read the config and the persisted JS file from the SD card).
-
Configure. The main screen has two panes: pick the target app on the left, then pick the JS script on the right (browse to the script’s directory and select it). A dialog confirms the pairing:

Then exit FridaManager and open the target app. (If you edit the JS file later, kill the app process and reopen — or re-apply the script — for the change to take effect.)
Hello world, persisted
Save this as helloworld.js and copy it to the phone’s SD card:
function Log(info) {
Java.perform(function () {
var LogClass = Java.use("android.util.Log");
LogClass.e("FridaManager", info);
})
}
function main() {
Log("hello fridamanager!");
Log("goodbye fridamanager!");
}
setImmediate(main);
It just actively calls Frida’s Log class to print the two strings. Configure it for a target app (one with SD-card permission) and open the app — the strings show up in logcat.
Tracing JNI registration (static + dynamic)
Save this as traceJNIRegisterNative.js:
function Log(info) {
Java.perform(function () {
var LogClass = Java.use("android.util.Log");
LogClass.e("FridaManager", info);
})
}
function readStdString(str) {
const isTiny = (str.readU8() & 1) === 0;
if (isTiny)
return str.add(1).readUtf8String();
return str.add(2 * Process.pointerSize).readPointer().readUtf8String();
}
function hook_register_native() {
var libartmodule = Process.getModuleByName("libart.so");
var PrettyMethodaddr = null;
var RegisterNativeaddr = null;
libartmodule.enumerateExports().forEach(function (symbol) {
//android7.1.2
if (symbol.name == "_ZN3art12PrettyMethodEPNS_9ArtMethodEb") {
PrettyMethodaddr = symbol.address;
}
});
var PrettyMethodfunc = new NativeFunction(PrettyMethodaddr, ["pointer", "pointer", "pointer"], ["pointer", "int"]);
Interceptor.attach(RegisterNativeaddr, {
onEnter: function (args) {
var ArtMethodptr = args[0];
this.JniFuncaddr = args[1];
var result = PrettyMethodfunc(ArtMethodptr, 1);
var stdstring = Memory.alloc(3 * Process.pointerSize);
ptr(stdstring).writePointer(result[0]);
ptr(stdstring).add(1 * Process.pointerSize).writePointer(result[1]);
ptr(stdstring).add(2 * Process.pointerSize).writePointer(result[2]);
this.funcnamestring = readStdString(stdstring);
Log("[RegisterJni begin]" + this.funcnamestring + "--addr:" + this.JniFuncaddr);
}, onLeave: function (retval) {
Log("[RegisterJni over]" + this.funcnamestring + "--addr:" + this.JniFuncaddr);
}
})
}
function main() {
Log("hello fridamanager!");
hook_register_native();
Log("goodbye fridamanager!");
}
setImmediate(main);
Configure a packed app with this script, open it, and you can watch every JNI binding — both static and dynamically registered:

The project (source and further updates) lives on GitHub: FridaManager.
Magisk module variant
For wider device support without flashing a custom ROM, FridaManager is also packaged as a Magisk module (paired with a modified frida-gadget that slips past app detection). The flow:
- Install the Magisk module.
- Install the Riru module (via Magisk Manager or locally).
- Reboot, then locally install the
riru-fridamanagermodule. - Reboot, then install the FridaManager app to manage app-to-script pairings.
If you update a JS script on the SD card, re-apply it in FridaManager and (ideally) reboot for the change to take effect.

What this technique exposes
The defensive framing is straightforward: the same ActivityThread-early injection that makes Frida persistence possible is also the window where anti-tamper code should be checking for foreign modules and unexpected dlopens. Persistence tools like this succeed precisely because the hook lands before the app’s own protections initialize — which is a reminder that early-native checks, not Java-layer checks, are what actually raise the bar against injection.
Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).
Get in touch