HookDetection.getStatus() · check id hooks · signature pack 2026.08.1
This check is adversarial in a way the others are not. An attacker running a hooking framework can modify the code performing the detection — including this check. Detection is not guaranteed and cannot be. What these signals buy is cost: they raise the effort from "attach and go" to "attach, then hide". Any product claim beyond that would be false.
Indicators that this process is being instrumented:
- dynamic instrumentation — an agent mapped into the process, or worker threads injected by one;
- managed-code hooking frameworks — their classes loadable, their frames on the call stack, or their libraries mapped in;
- symbol redirection — standard library symbols resolving inside libraries other than the ones that should provide them.
- This process only. No other process is examined.
- No network probing. Scanning for a listening instrumentation port is a well-known technique and it is not used here: it would mean a security library opening sockets on a user's device, and the signal is not worth that trade. It is also trivially defeated by changing a port number.
- No attempt to block or unhook anything. The toolkit reports; countermeasures belong to the application, and in-process anti-tamper generally loses to an attacker with native access.
| ID | Indicator | Confidence | Sources |
|---|---|---|---|
RNSEC-RUNTIME-HOOK-001 |
Dynamic instrumentation agent or injected thread | medium | /proc/self/maps, /proc/self/task/<tid>/comm |
RNSEC-RUNTIME-HOOK-002 |
Managed-code hooking framework | medium | Class loadability, call-stack frames, mapped libraries |
RNSEC-RUNTIME-HOOK-003 |
Symbol resolves inside an unexpected library | medium | dlsym + dladdr, through the native probe |
Each reads more than one source, because a framework that hides one often does not bother with
another. HOOK-001 answers from threads alone if the memory map is unreadable, and vice versa.
A common approach to native hook detection compares the first bytes of a function against expected prologue patterns. That varies by architecture, compiler, and Android version, and it produces false positives on legitimate instrumentation.
HOOK-003 instead asks where a symbol actually lives: resolve open, read, connect, dlopen
and friends, then ask dladdr which library provided the address. If open resolves inside an
injected library, the symbol has been redirected. This survives the attacker renaming their library,
which the name-matching signals do not.
Only the library filename is compared, never the full path. Android moves bionic between
/system/lib64 and /apex/com.android.runtime/... across versions, so comparing paths would report
every modern device as hooked. There is a regression test for exactly that.
No signal here is rated high, and that is deliberate. Every one depends on a name, a path, or a
runtime fact that an attacker with native access can influence. Three medium signals corroborating
each other reach high at the check level through the aggregator's normal rules — but no single
indicator earns it alone.
| Result | Meaning |
|---|---|
secure + high |
Every probe ran; no indicator fired. The strongest statement available — and still not proof the process is uninstrumented. |
detected + high |
Two or more independent signals agree. |
detected + medium |
One signal fired. |
unknown + low |
A probe was blocked. Inconclusive, not clean. |
- Legitimate instrumentation. Some APM, crash-reporting and A/B-testing SDKs hook native or
managed functions.
HOOK-003may flag them, and it is not wrong to — the process genuinely is hooked. Whether that matters is the application's call. - Substring matching in memory maps.
HOOK-001matches map lines by substring, so an unrelated library whose name contains one of the markers would fire. Thread names are matched exactly for this reason —gmainis a marker,gmainthreadis not, and there is a test asserting that. - Class presence without activity. A framework class being loadable does not prove it is doing anything, which is why it is one of three evidence kinds rather than sufficient alone.
- A framework that renames its libraries and threads defeats
HOOK-001and the name-matching parts ofHOOK-002. - An attacker who patches these detectors, the aggregator, or the JavaScript that calls them defeats everything here. This is not a hypothetical: it is the normal case for a determined adversary.
- Hooks placed on functions not in the checked symbol list are invisible to
HOOK-003. /procvisibility restrictions blindHOOK-001; it then reportsindeterminate, which turns the check intounknownrather thansecure.
- Everything runs inside the process being attacked. That is the ceiling, and no amount of additional signals moves it.
- The signature lists date quickly.
signatureVersionin the result metadata records which list produced a result. - A real trust decision belongs on a server, informed by hardware-backed attestation. In-process hook detection informs that decision; it does not substitute for it.
This document deliberately does not describe how to defeat any of these checks.
const hooks = await HookDetection.getStatus();
if (hooks.status === 'detected' && hooks.confidence === 'high') {
// Proportionate: refuse high-value operations, require server-side
// re-authentication, log a security event with the signal ids.
}Because a determined attacker can defeat this check, the sensible design is to treat a clean result as worth little and a positive result as worth a lot. Use it to raise friction on specific operations, and put the actual gate on your server.
android/src/test/java/com/rnsecurity/detectors/hook/HookDetectorsTest.kt — 17 cases covering each
detector against an instrumented process, a clean process, partially blocked probes, the exact-match
thread-name rule, the bionic-path-independence rule, and a redirected symbol alongside unresolvable
ones.