Uses the Java reflection API to inspect runtime objects, read private fields, check method modifiers, and detect modifications to the ART virtual machine state caused by code injection and hooking.
Java reflection (java.lang.reflect) allows code to inspect and interact with classes, methods, and fields at runtime, including private and hidden members that are not accessible through normal API calls. In the context of security detection, reflection serves as a powerful introspection tool: hook frameworks must modify runtime data structures (method flags, class caches, callback lists, application metadata) for their hooks to function, and these modifications are observable via reflection. Unlike native-level detection (procfs, SVC), reflection operates within the ART virtual machine and can detect semantic changes to the Java runtime — such as a method's access flags being changed to native, a class's appComponentFactory being replaced, or a hook framework's internal cache being populated.
From a defender's perspective, reflection-based detection is valuable because it targets the fundamental requirement of hooking: the runtime state must be altered for hooks to work. Attackers cannot hook methods without modifying method metadata, cannot inject stub classes without modifying ApplicationInfo, and cannot load framework JARs without populating internal caches. Reflection reads these very data structures, creating a detection mechanism that is intrinsically tied to the hooking mechanism itself.
- Replace method implementation via native flag — Hook frameworks mark target methods with the
ACC_NATIVE(0x0100) flag in ART's internal method structure, redirecting execution from compiled Java bytecode to a native hook handler. This allows interception of any Java method call. - Register hook callbacks — The framework maintains callback lists (e.g.,
sHookedMethodCallbacks) mapping hooked methods to their replacement implementations. These data structures must persist in memory for hooks to function. - Populate method resolution caches — Hook frameworks use internal caches (e.g.,
methodCache) to speed up method lookups. These caches are populated at hook registration time and remain non-empty throughout the process lifetime. - Replace application component factory — Repackaging tools replace the app's
appComponentFactoryfield inApplicationInfowith a stub class name to intercept component instantiation. - Inject metadata — Repackaging tools add entries to
ApplicationInfo.metaDatabundle to store configuration and version information for the injected module.
| Artifact | Location | Indicator |
|---|---|---|
| Native method flag on non-native method | Method.getModifiers() |
Modifier.isNative() returns true for methods that should be Java-implemented (e.g., Runtime.exec(), ClassLoader.loadClass()) |
| Non-empty hook callback map | Framework's sHookedMethodCallbacks field |
Map size > 0 indicates active hooks are registered |
| Non-empty method cache | Framework's methodCache static field |
Cache containing entries proves methods have been resolved for hooking |
| Risk class loadable | ClassLoader.loadClass() |
Successfully loading a hook framework's bridge class proves the framework JAR is in the classpath |
| Replaced appComponentFactory | ApplicationInfo.appComponentFactory |
Field contains class name with repackaging tool identifiers (e.g., containing "lsp", "npatch", "Stub") |
| Injected metadata keys | ApplicationInfo.metaData |
Bundle contains unexpected keys added by the repackaging tool |
| JDWP debugger attached | Debug.isDebuggerConnected() |
Returns true when a Java debugger is connected via JDWP protocol |
| Hidden API access via SystemProperties | SystemProperties.get("ro.*") |
Returns values indicating modified system state |
// Hook framework modifies method at ART level
step_1: resolve target method (e.g., Activity.onCreate)
step_2: set method.access_flags |= ACC_NATIVE // mark as native
step_3: save original entry_point to backup
step_4: set method.entry_point = hook_trampoline
step_5: register callback in sHookedMethodCallbacks[method] = handler
step_6: cache method in methodCache for fast lookup
// Repackaging tool modifies ApplicationInfo
step_1: replace appComponentFactory = "com.tool.StubComponentFactory"
step_2: add metaData("npatch_version", "1.0")
| Evasion | Description |
|---|---|
| Clean up caches on detection | Clear methodCache and sHookedMethodCallbacks before detection code runs, repopulate after |
| Use non-standard field names | Obfuscate framework class and field names so reflection-based lookups for known names fail |
| Restore native flags temporarily | Temporarily clear ACC_NATIVE flag when detection is suspected, restore it afterward |
| Hook reflection API itself | Intercept Field.get(), Method.getModifiers(), or Class.forName() to return clean values |
| Avoid appComponentFactory | Use alternative injection vectors that don't require modifying ApplicationInfo fields |
The invariant is that hooking frameworks must modify observable Java runtime state to function. A method cannot be hooked without changing its flags or entry point. A framework JAR cannot be injected without being loadable from some ClassLoader. Application metadata cannot be used for configuration without being present in the ApplicationInfo.metaData bundle. Reflection provides direct read access to all of these data structures. The detection is inherently tied to the requirement of the attack: if the modifications are reverted to avoid detection, the hooks stop working.
| Property | Explanation |
|---|---|
| Intrinsic coupling | Detection targets the same data structures that must be modified for hooking to work — removal breaks the hooks |
| Multiple independent signals | Method flags, class loading, callback maps, metadata, and component factory provide independent detection vectors |
| Hidden API access | Using reflection to access hidden/private fields means detection can read internal framework state not intended for public access |
| Runtime verification | Detection occurs at the JVM level against live objects, not static analysis — obfuscation of class names on disk does not help if the runtime names are known |
| Remaining bypass surface | Framework can hook the reflection API itself (Field.get(), Method.getModifiers()); race conditions allow temporary cleanup during scanning window |
- Attempt to load risk classes — Use
ClassLoader.loadClass()with known hook framework class names (e.g.,de.robv.android.xposed.XposedBridge). If the class loads withoutClassNotFoundException, the framework JAR is present. - Read hook callback structures — If a framework class loads, use reflection to access its static hook registration fields (e.g.,
sHookedMethodCallbacks). If the map/collection is non-empty, hooks are actively registered. - Read method resolution caches — Access the framework's
methodCachefield to check for cached method resolutions — a non-empty cache proves hooking activity. - Check method modifiers — For critical Java methods (
Runtime.exec(),ClassLoader.loadClass(),Thread.sleep(), etc.), callmethod.getModifiers()and checkModifier.isNative(). These methods are implemented in Java bytecode; if they report as native, their implementation has been replaced by a hook. - Inspect ApplicationInfo fields — Read
context.applicationInfo.appComponentFactoryand check if it contains repackaging tool class name patterns. Readcontext.applicationInfo.metaDataand check for unexpected keys. - Check debug state — Call
Debug.isDebuggerConnected()to detect JDWP debugger attachment. - Enumerate runtime instances — Use hidden API
VMDebug.getInstancesOfClasses()to enumerate all live instances of framework classes. If instances exist, the framework is loaded and active.
// Check 1: Attempt to load risk class
try:
cls = ClassLoader.loadClass("de.robv.android.xposed.XposedBridge")
flag("framework_class_loadable", cls.getName())
// Check 2: Read hook callback map
field = cls.getDeclaredField("sHookedMethodCallbacks")
field.setAccessible(true)
callbacks = field.get(null)
if callbacks.size() > 0:
flag("active_hooks_registered", callbacks.size())
catch ClassNotFoundException:
pass // framework not loaded
// Check 3: Method modifier verification
methods_to_check = [
Runtime.class.getMethod("exec", String.class),
ClassLoader.class.getMethod("loadClass", String.class)
]
for method in methods_to_check:
if Modifier.isNative(method.getModifiers()):
flag("method_hooked_native", method.getName())
// Check 4: ApplicationInfo inspection
info = context.getApplicationInfo()
if info.appComponentFactory != null:
if info.appComponentFactory.contains("lsp") or
info.appComponentFactory.contains("npatch") or
info.appComponentFactory.contains("Stub"):
flag("repackaged_component_factory", info.appComponentFactory)
// Check 5: Debug state
if Debug.isDebuggerConnected():
flag("jdwp_debugger_attached")
| Scenario | Mitigation |
|---|---|
| Legitimate use of native methods in JNI-heavy apps | Only check methods known to be Java-implemented in AOSP source; maintain a whitelist of genuinely native framework methods |
| App framework using custom appComponentFactory | Only flag component factory names matching known repackaging tool patterns, not all custom factory names |
| Debug.isDebuggerConnected() during development | Check ApplicationInfo.FLAG_DEBUGGABLE; suppress JDWP detection for debug builds |
| ClassLoader.loadClass() false hits from library class names | Use exact fully-qualified class name matching; verify loaded class has expected fields/methods before flagging |
| Test/CI environments with debugging tools | Allow configuration to suppress reflection-based detection in known CI environments |