Detecting Frida/Xposed at Runtime (Android)
Part 14, closing out Phase 3 of our Android security series. Part 13 covered R8/ProGuard hardening — static defenses. This post covers the dynamic threat that static hardening alone can’t stop: an attacker actively instrumenting your running app.
What you’re actually defending against
Frida and Xposed/LSPosed let someone hook into your app while it’s running — intercepting function calls, changing return values, bypassing checks like certificate pinning or license validation, entirely independent of how well-obfuscated your bytecode is. This is genuinely advanced tooling, and detecting it is a real cat-and-mouse game — every detection method below has known bypasses, and new detection methods get published (and broken) regularly. This post is about raising the cost meaningfully, not achieving certainty.
Layer 1: process and port checks
Frida’s default setup leaves a few classic fingerprints. None of these are hard to hide individually, but together they catch a lot of default, un-hardened Frida setups:
fun detectFridaServer(): Boolean {
// frida-server binds to 27042 by default
return try {
val socket = Socket()
socket.connect(InetSocketAddress("127.0.0.1", 27042), 200)
socket.close()
true // something is listening — likely frida-server
} catch (e: Exception) {
false
}
}
fun detectFridaProcessName(): Boolean {
val suspiciousNames = listOf("frida", "frida-server", "frida-agent")
return File("/proc").listFiles()?.any { procDir ->
val cmdline = File(procDir, "cmdline")
cmdline.exists() && suspiciousNames.any { name ->
runCatching { cmdline.readText().contains(name) }.getOrDefault(false)
}
} ?: false
}Known weakness: a determined attacker just runs frida-server on a non-default port and renames the binary. This catches the default, out-of-the-box case only.
Layer 2: memory map inspection
More resilient than port scanning — this checks whether Frida’s own libraries got loaded into your process, regardless of what port it’s using:
fun detectFridaInMemoryMaps(): Boolean {
return try {
val maps = File("/proc/self/maps").readText()
listOf("frida", "gum-js-loop", "gmain").any { maps.contains(it) }
} catch (e: Exception) {
false
}
}frida-gadget (the embedded/injected variant, as opposed to the standalone frida-server) doesn't open a network port at all — so this catches cases the port check misses. Known weakness: sophisticated setups can rename or hide these library signatures too.
Layer 3: native-side checks (harder to bypass than Java-layer checks)
Anything checked in Kotlin/Java can be hooked by Frida itself — which is a real irony worth sitting with. Moving detection into native code (JNI/NDK) raises the bar, because it takes more effort to hook native detection logic than to hook a Java method:
// Simplified native check — actual implementations should add
// obfuscation and anti-debugging around this, not ship it this readable
bool detect_frida_native() {
FILE* maps = fopen("/proc/self/maps", "r");
if (!maps) return false;
char line[512];
bool found = false;
while (fgets(line, sizeof(line), maps)) {
if (strstr(line, "frida") != nullptr) {
found = true;
break;
}
}
fclose(maps);
return found;
}The frontier: dirty-page detection
A newer technique worth knowing about, because it addresses a real gap in the checks above: frida-server inline-hooks parts of system_server (like libc.so) as soon as it starts — even before it ever attaches to your app specifically. That hooking triggers copy-on-write on those memory pages, and the pages stay marked "dirty" permanently, even if Frida later restores the original bytes. Checking /proc/[pid]/smaps for unexpectedly dirty pages in system libraries can catch frida-server's mere presence on the device, independent of whether it's currently attached to your process. This is more resilient than the checks above precisely because it doesn't rely on Frida's process name, port, or in-memory footprint inside your app.
The honest recommendation: don’t build this yourself for production
Everything above is genuinely useful to understand, and fine for a side project or learning exercise. But for a production app that actually needs this level of protection, hand-rolled checks like these get bypassed faster than you’d expect, and maintaining them against an evolving toolset is a real ongoing cost. Purpose-built RASP (Runtime Application Self-Protection) SDKs — such as freeRASP, Approov, or similar commercial options — maintain detection signatures against the current tooling landscape as an ongoing service, which a side project generally can’t keep pace with alone.
// Example shape using a RASP SDK instead of hand-rolled detection
Talsec.start(applicationContext, talsecConfig)
override fun onHookDetected() {
// Respond deliberately: log it, restrict sensitive actions,
// step-up auth (Part 9) - avoid an instant hard crash, which
// just teaches an attacker exactly which check they tripped
}What to actually do when you detect something
Resist the instinct to System.exit() immediately. An immediate crash is a gift to whoever's probing your app — it confirms exactly which check fired and when. A more resilient response: log the signal, feed it into the same risk-tier model from Part 11 (Play Integrity) and Part 12 (root detection), and let the combination of signals drive the response — restrict sensitive actions, require step-up auth, or silently degrade functionality rather than announcing the detection outright.
Quick checklist
- [ ] Detection layered — process/port, memory maps, and native-side checks together, not just one
- [ ] Understood as raising cost, not achieving certainty — no client-side check here is unbeatable
- [ ] Detection signal feeds a combined risk score (with Play Integrity + root signals), not a standalone gate
- [ ] Response to detection is deliberate (restrict, log, step-up auth) rather than an instant crash that reveals the check
- [ ] For genuinely high-value production apps, a maintained RASP SDK evaluated over hand-rolled detection
The one-line takeaway
Runtime instrumentation detection is real cat-and-mouse — every check here has a known bypass — so the goal isn’t an unbeatable app, it’s combining enough independent signals (Play Integrity, root indicators, hook detection) that casually and even moderately sophisticated tampering gets meaningfully harder and more expensive than it’s worth.
Get Khizar Khan’s stories in your inbox
Join Medium for free to get updates from this writer.
That closes Phase 3 — Runtime & Build-time Integrity: Play Integrity API → Root Detection → R8/ProGuard → Frida/Xposed Detection. Combined with Phase 1’s network foundations and Phase 2’s data/auth patterns, the series now covers the core layers most mobile security reviews actually check.









