Anti-Analysis Techniques¶
Detecting and evading analysis environments, security tools, and human researchers. Nearly every modern Android banking trojan implements multiple layers of anti-analysis checks before executing any malicious behavior. The goal is simple: if the malware suspects it is being analyzed, it does nothing, resulting in a clean verdict from automated sandboxes and wasted hours for manual analysts.
See also: Play Store Evasion, Dynamic Code Loading, Persistence Techniques
MITRE ATT&CK
| ID | Technique | Tactic |
|---|---|---|
| T1633 | Virtualization/Sandbox Evasion | Defense Evasion |
| T1633.001 | System Checks | Defense Evasion |
| T1627 | Execution Guardrails | Defense Evasion |
| T1627.001 | Geofencing | Defense Evasion |
| T1406 | Obfuscated Files or Information | Defense Evasion |
| T1628 | Hide Artifacts | Defense Evasion |
| T1630 | Indicator Removal on Host | Defense Evasion |
| T1629 | Impair Defenses | Defense Evasion |
T1633 covers emulator/sandbox detection via build properties, telephony, sensors, and filesystem artifacts. T1627 covers execution guardrails including geofencing, delayed activation, and SIM checks. T1406 covers string encryption, packing, and code obfuscation. T1628/T1630 cover hiding the app icon, clearing logs, and removing forensic artifacts. T1629 covers disabling Play Protect and security tools.
Emulator Detection¶
The most common first check. Emulators expose artifacts through build properties, hardware fingerprinting, telephony state, and sensor data that differ from physical devices.
Build Property Checks¶
private boolean isEmulator() {
return Build.FINGERPRINT.contains("generic")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| Build.HARDWARE.contains("goldfish")
|| Build.HARDWARE.contains("ranchu")
|| Build.PRODUCT.contains("sdk_gphone")
|| Build.BOARD.contains("unknown")
|| Build.HOST.startsWith("Build");
}
Families like Cerberus, Anatsa, and Hook check 10-20+ build properties. The check is trivial to implement but also trivial to bypass via Frida property spoofing on a physical device.
Telephony Checks¶
| Check | Emulator Value | Real Device Value |
|---|---|---|
getDeviceId() |
000000000000000 or null |
Valid IMEI |
getSimSerialNumber() |
Empty or 89014103211118510720 |
Valid ICCID |
getNetworkOperatorName() |
Android or empty |
Carrier name |
getSimOperator() |
Empty | MCC+MNC code |
getLine1Number() |
15555215554 (emulator default) |
Real number or empty |
getSubscriberId() |
Empty | Valid IMSI |
File System Artifacts¶
private boolean checkEmulatorFiles() {
String[] knownPaths = {
"/dev/socket/qemud",
"/dev/qemu_pipe",
"/system/lib/libc_malloc_debug_qemu.so",
"/sys/qemu_trace",
"/system/bin/qemu-props",
"/dev/goldfish_pipe"
};
for (String path : knownPaths) {
if (new File(path).exists()) return true;
}
return false;
}
Sensor-Based Detection¶
Trend Micro documented malware using motion sensor data to distinguish real phones from emulators. BatterySaverMobi and Currency Converter (discovered on Play Store) checked accelerometer readings -- emulators return static or zero values because they don't simulate physical motion. The malware only activated its dropper payload after detecting non-zero accelerometer variance over time.
SpinOk SDK used gyroscope and magnetometer data as anti-emulation checks before activating its data harvesting across 193 apps with 451 million downloads.
Hardware Property Checks¶
| Property | Emulator | Physical |
|---|---|---|
BOARD |
unknown, goldfish |
Device-specific |
BOOTLOADER |
unknown |
Version string |
DEVICE |
generic, generic_x86 |
Device codename |
HARDWARE |
goldfish, ranchu |
qcom, exynos, etc. |
| CPU ABI | x86, x86_64 (common in AVDs) |
arm64-v8a (most real devices) |
| Battery temperature | Static (usually 0) | Varies with use |
| Battery status | Always CHARGING |
Varies |
Root and Magisk Detection¶
Banking trojans detect rooted devices for two reasons: to avoid analysis environments (analysts use rooted devices), and to determine available exploitation paths.
Common Root Checks¶
private boolean isRooted() {
String[] paths = {
"/system/app/Superuser.apk",
"/system/xbin/su",
"/system/bin/su",
"/sbin/su",
"/data/local/xbin/su",
"/data/local/bin/su"
};
for (String path : paths) {
if (new File(path).exists()) return true;
}
try {
Runtime.getRuntime().exec("su");
return true;
} catch (IOException e) {
return false;
}
return false;
}
Magisk Detection¶
| Technique | What It Detects | Bypass |
|---|---|---|
Check for /sbin/.magisk, /data/adb/magisk |
Magisk installation directory | MagiskHide / Shamiko |
Mount namespace inspection (/proc/self/mounts) |
Magisk mount overlays | DenyList + Shamiko |
Check ro.boot.vbmeta.device_state |
Unlocked bootloader | Cannot spoof without re-locking |
PackageManager.getInstalledPackages() for com.topjohnwu.magisk |
Magisk Manager app | Randomize package name (built-in Magisk feature) |
SELinux status (getenforce) |
Permissive mode (common on rooted) | Enforce mode on properly configured root |
SafetyNet / Play Integrity¶
Some malware checks Play Integrity attestation before executing, refusing to operate on devices that fail hardware attestation. This is unusual (most malware avoids Google APIs) but has been observed in families that want to ensure they are running on a real, unmodified device to maximize fraud success.
Hooking Framework Detection¶
Hooking frameworks let analysts (and attackers) intercept function calls at runtime. Detecting them is a priority for both malware resisting analysis and protectors defending banking apps. The detection landscape has evolved significantly: early approaches targeted specific frameworks by name, while modern protectors use generic techniques that catch any inline hook regardless of the framework that placed it.
Evolution¶
| Phase | Era | Approach | What It Catches |
|---|---|---|---|
| 1 | 2017-2020 | Framework-specific signatures | Frida by name (frida-agent strings, port 27042, process name) |
| 2 | 2020-2022 | Evasion tools emerge | Renamed Frida binaries, Gadget injection, ZygiskFrida bypass phase-1 checks |
| 3 | 2022-present | Generic inline hook detection | Any framework: Frida, Dobby, ShadowHook, ByteHook, custom hooks |
The shift from phase 1 to phase 3 is the most important trend. Protectors that only check for frida-agent strings are trivially bypassed. Protectors that verify function prologue integrity catch everything.
Hooking Frameworks¶
| Framework | Type | Footprint | Where It Shows Up |
|---|---|---|---|
| Frida | Full instrumentation runtime | Heavy (JS engine, server process, agent library) | Primary RE tool, automated analysis pipelines |
| Dobby | Native inline hooking library | Minimal (~300 KB .so, no server, no ports) | Zygisk modules (PlayIntegrityFix, root hiding), game mods, malware |
| ShadowHook | Native inline hooking (ByteDance) | Minimal | ByteDance apps internally, open-source adoption growing |
| xHook | PLT/GOT hooking (iQIYI) | Minimal | Stable PLT hooking, widely adopted in Chinese app ecosystem. Predecessor to ByteHook. |
| ByteHook | PLT/GOT hooking (ByteDance) | Minimal | PLT-level interception, less invasive than inline |
| LSPlant | ART Java method hooking | Minimal | LSPosed framework, Xposed module ecosystem |
| Pine | ART Java method hooking | Minimal | Alternative to LSPlant for Java-level hooks |
Frida's detection surface is large: a server process, a full JavaScript runtime injected as a shared library, network ports, and characteristic strings in memory. Dobby and similar lightweight frameworks trade flexibility for stealth -- they compile as a small native library with no server, no JS engine, and no characteristic strings. The analyst controls the .so name. The only artifacts are modified function prologues and allocated trampoline memory pages.
Frida-Specific Detection¶
| Technique | Implementation | Reliability |
|---|---|---|
| Default port scan | Connect to localhost:27042 (frida-server default) |
Low (easily changed) |
/proc/self/maps scan |
Search memory mappings for frida-agent, frida-gadget |
Medium (bypassed by renaming) |
| Process enumeration | List running processes for frida-server, frida-helper |
Medium (bypassed by renaming) |
| Named pipe detection | Check /proc/self/fd/ for linjector pipes |
Medium |
pthread enumeration |
Scan thread names for Frida-related strings (gmain, gdbus) |
Medium-High |
| Library detection | Enumerate loaded libraries for Frida agent patterns | Medium |
These signature-based checks are the easiest to bypass: rename the binary, change the port, use ZygiskFrida to inject Frida Gadget via Zygisk instead of running a server. Patch /proc/self/maps reads via Frida itself to filter out Frida strings. Shamiko hides root and Frida artifacts from DenyList processes.
Xposed / LSPosed Detection¶
| Technique | What It Detects |
|---|---|
| Stack trace inspection | de.robv.android.xposed in call stack |
| Class check | XposedBridge class loadable via reflection |
/proc/self/maps |
XposedBridge.jar mapped into process |
| Exception handler check | Xposed hooks modify exception handling chain |
| ART method structure | LSPlant modifies ART internal method structures; integrity checks on ArtMethod fields detect this |
LSPosed operates through Zygisk, making traditional Xposed detection (class names, JAR in maps) ineffective. Detection now requires checking ART internals for method structure modifications.
Generic Inline Hook Detection¶
These techniques catch any framework that modifies native function code in memory, regardless of name or origin. This is where the arms race currently sits.
Prologue integrity checking: The protector stores hashes of the first 16-32 bytes of critical functions at initialization. Periodically (or before sensitive operations), it re-reads those bytes and compares. Any inline hook overwrites the prologue with a branch instruction, breaking the hash. This is the most reliable detection method.
uint8_t expected_prologue[16] = { /* stored at build/init time */ };
uint8_t current_prologue[16];
memcpy(current_prologue, (void*)target_function, 16);
if (memcmp(expected_prologue, current_prologue, 16) != 0) {
// Function has been hooked
}
Anonymous executable memory scanning: Inline hooking frameworks allocate executable memory pages for trampolines. These appear in /proc/self/maps as anonymous regions with execute permission (r-xp with no backing file). Normal processes have very few anonymous executable pages. A protector scanning for these can detect Dobby, Frida Interceptor, ShadowHook, and any other inline hook framework.
Instruction pattern detection: On ARM64, a Dobby hook replaces the function prologue with LDR Xn, [PC, #offset] followed by BR Xn (load absolute address, branch to it). Frida's Interceptor uses a similar pattern. Scanning function entry points for these characteristic instruction sequences detects hooks without knowing which framework placed them.
.text section integrity: Compare the in-memory .text section of loaded libraries against the on-disk copy. Any byte difference indicates patching. More expensive than prologue checking but catches hooks placed anywhere in a function, not just the entry point.
Timing analysis: A hooked function traverses the trampoline (relocated original instructions, jump back), adding nanoseconds of latency. Tight timing loops around sensitive functions can reveal the overhead, though this is noisy in practice.
How Protectors Implement Detection¶
| Protector | Detection Approach |
|---|---|
| Promon SHIELD | RASP-focused. Runtime integrity checks on critical functions, environment fingerprinting. Primarily behavioral detection rather than signature scanning. |
| Arxan (Digital.ai) | Guard network: dozens of native guard functions that verify each other's integrity in a mesh. Hooking one guard triggers detection by others. Prologue integrity is checked across the guard network. |
| LIAPP | Aggressive multi-layer: Frida-specific signature checks, Magisk-aware root detection, and native function integrity verification. Server-side token validation adds a layer that client-side hooks cannot bypass. |
| Appdome | Multi-vector framework detection: signature scanning for known frameworks, generic inline hook detection, memory map analysis. Covers Frida, Xposed, Dobby, and custom hooks. |
| DexProtector | Native bridge with integrity checks. Anti-Frida and anti-debug at the native layer. White-box crypto module (vTEE) adds cryptographic verification that hooks cannot fake without the key material. |
Bypass Strategies¶
| Detection Type | Bypass Approach |
|---|---|
| Frida-specific signatures | Rename binaries, change ports, use ZygiskFrida or Frida Gadget |
/proc/self/maps scanning |
Hook fopen/fgets to filter results, or use kernel-level hiding (Shamiko) |
| Prologue integrity | Frida Stalker (copies and instruments code blocks without modifying originals) |
| Anonymous RWX pages | Allocate trampolines in existing RX regions or use code-cave injection |
.text integrity |
Kernel-level hooking below the protector's visibility |
| Guard networks (Arxan) | Identify and patch all guards simultaneously, or hook the integrity check function itself |
| Timing analysis | Minimize trampoline overhead, add compensating delays |
The current state of the art: protectors layer multiple detection types so that bypassing one doesn't disable all. Analysts counter by combining Zygisk-based injection (avoids process-level artifacts), Frida Stalker (avoids prologue modification), and Shamiko (hides root and maps entries). Neither side has a decisive advantage.
Debugger Detection¶
private boolean isDebugged() {
if (Debug.isDebuggerConnected()) return true;
if (Debug.waitingForDebugger()) return true;
try {
BufferedReader reader = new BufferedReader(
new FileReader("/proc/self/status"));
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("TracerPid:")) {
int pid = Integer.parseInt(line.substring(10).trim());
if (pid != 0) return true;
}
}
} catch (Exception ignored) {}
return false;
}
Timing-Based Detection¶
Malware measures execution time of code blocks. Under a debugger or instrumentation framework, execution is significantly slower. MITRE ATT&CK T1497.003 documents this as "Time Based Evasion."
long start = SystemClock.uptimeMillis();
performDecoyComputation();
long elapsed = SystemClock.uptimeMillis() - start;
if (elapsed > THRESHOLD) {
// Likely under instrumentation
}
AV and Security App Detection¶
Malware checks for installed security apps to decide whether to activate. Some families disable Play Protect via accessibility before proceeding.
Package Name Checks¶
private boolean hasSecurityApp() {
String[] avPackages = {
"com.avast.android.mobilesecurity",
"com.eset.ems2.gp",
"com.kaspersky.security.cloud",
"com.bitdefender.security",
"org.malwarebytes.antimalware",
"com.symantec.mobilesecurity",
"com.lookout",
"com.zimperium.zips",
"com.trendmicro.tmmspersonal",
"com.drweb.pro"
};
PackageManager pm = getPackageManager();
for (String pkg : avPackages) {
try {
pm.getPackageInfo(pkg, 0);
return true;
} catch (NameNotFoundException ignored) {}
}
return false;
}
Play Protect Suppression¶
Multiple families use accessibility to disable Google Play Protect:
- Open
Settings > Security > Google Play Protect - Click the gear icon
- Disable "Scan apps with Play Protect"
- Confirm the dialog
Anatsa, Cerberus, Hook, and Xenomorph all implement this flow. See Notification Suppression for how malware also suppresses Play Protect warnings.
Geographic and Locale Checks¶
Malware avoids executing in researcher-common locales or countries where the operator has no targets. Covered in depth in Play Store Evasion.
| Check | API | Spoofing Difficulty |
|---|---|---|
| SIM country | TelephonyManager.getSimCountryIso() |
High (requires physical SIM) |
| Network country | TelephonyManager.getNetworkCountryIso() |
High (VPN doesn't change this) |
| MCC (Mobile Country Code) | TelephonyManager.getSimOperator().substring(0,3) |
High (requires physical SIM from target country) |
| IP geolocation | Server-side check | Medium (VPN changes IP) |
| System locale | Locale.getDefault() |
Low (Settings change) |
| Timezone | TimeZone.getDefault() |
Low (Settings change) |
Anatsa campaigns specifically avoid Eastern European and Chinese IP ranges. Mandrake used C2-side geofencing to avoid delivering payloads to non-target regions entirely.
MCC-Based Geo-Targeting¶
The Mobile Country Code (first 3 digits of the SIM operator string) provides a reliable country indicator that survives VPN, locale changes, and timezone spoofing. Adware and data exfiltration SDKs use MCC whitelists or blacklists to selectively enable malicious behavior:
String mcc = telephonyManager.getSimOperator().substring(0, 3);
Set<String> blocklist = parseConfigBlocklist();
if (blocklist.contains(mcc)) {
return;
}
startExfiltration();
This pattern targets regions with weaker privacy enforcement while avoiding countries where regulatory action is likely. The whitelist/blacklist is typically fetched from a remote config endpoint, allowing operators to adjust targeting without app updates. Combined with SDK version checks (e.g., disable on Android 12+ where restrictions are tighter), this creates multi-layered execution guardrails.
HTTP Proxy Detection¶
Ad fraud and data exfiltration SDKs check for HTTP proxies as an anti-analysis measure. Security researchers commonly route traffic through intercepting proxies (Burp Suite, mitmproxy) for analysis:
boolean isProxied() {
String host = System.getProperty("http.proxyHost");
String port = System.getProperty("http.proxyPort");
return host != null && !host.isEmpty();
}
If a proxy is detected, the SDK refuses to initialize, preventing analysts from capturing network traffic. This check is trivial to bypass via Frida (hook System.getProperty to return null for proxy keys), but it's effective against automated sandbox environments that route all traffic through a proxy by default.
Install Referrer Gating¶
Malware that connects to the Google Play Install Referrer API before executing any malicious code. The payload only activates if the referrer connection succeeds and the referrer data contains valid attribution timestamps (e.g., click timestamp exceeds a threshold). This means sideloaded APKs never trigger the payload: when an analyst downloads the APK from VirusTotal and installs it on an emulator, there is no referrer data, so the malicious chain never fires.
This is highly effective against dynamic analysis because most sandboxes install APKs via adb install, which provides no referrer context. Only installs through a real ad campaign or the Play Store with attribution data activate the malware.
InstallReferrerClient client = InstallReferrerClient.newBuilder(context).build();
client.startConnection(new InstallReferrerStateListener() {
@Override
public void onInstallReferrerSetupFinished(int responseCode) {
if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
ReferrerDetails details = client.getInstallReferrer();
if (details.getReferrerClickTimestampSeconds() > THRESHOLD) {
activatePayload();
}
}
}
});
Bypass: use Frida to hook InstallReferrerClient and return synthetic referrer data with valid timestamps.
Background Activity Launch Bypass (MediaSession)¶
Android 10 (API 29) introduced restrictions on launching activities from the background. Malware bypasses this by abusing the media key event handling system:
- Create a
VirtualDisplaywith tiny dimensions (5-15 pixels) viaDisplayManager.createVirtualDisplay() - Render a
Presentationon the virtual display to keep it alive - Create an
AudioTrackand play random noise to acquire audio focus - Create a
MediaSessionwith aPendingIntentas the media button receiver - The
PendingIntentwraps a broadcast carrying the target activityIntentas an extra - Dispatch
MEDIA_PLAYkey events viaAudioManager.dispatchMediaKeyEvent() - The system delivers the key event to the active
MediaSession, which fires thePendingIntent - The broadcast receiver extracts the
Intentand callscontext.startActivity()
The system allows this because media button handling is considered a legitimate foreground interaction. The VirtualDisplay keeps a "display" active, and the AudioTrack gives the MediaSession audio focus. The entire chain is invisible: the virtual display is a few pixels and the "audio" is random noise.
This technique was patched in Android 15 (API 35). On Android 10-14, it remains effective for popping up permission request screens, phishing overlays, or notification access settings from the background without user interaction.
Background Activity Launch Bypass (CompanionDeviceManager)¶
A technique that works on Android 15 (API 35) with zero dangerous permissions. The app abuses CompanionDeviceManager to gain a BAL allowlist exemption, enabling background activity launches that Android's restrictions would otherwise block.
Prerequisites¶
A single <uses-feature> declaration in the manifest:
This is not a permission. It does not require user approval at install, does not appear in Settings > App Permissions, and Google Play does not flag it. No BLUETOOTH_SCAN, no REQUEST_COMPANION_RUN_IN_BACKGROUND, no dangerous permissions of any kind.
Attack Chain¶
- App declares
companion_device_setupfeature in manifest - At runtime, scans for any nearby Bluetooth LE device (random beacon, headphones, smartwatch)
- Calls
CompanionDeviceManager.associate()with a filter matching that device - System shows a dialog: "Allow [app] to access [device name/MAC]?" -- the dialog mentions data access, not background activity privileges
- User taps "Allow" (the dialog text is opaque and does not mention BAL)
- App is registered as a companion device manager, added to
BAL_ALLOW_ALLOWLISTED_COMPONENTallowlist - App can now call
startActivity()from background services on Android 15
The association has no expiry and persists across reboots. The dialog gives no indication that accepting grants background activity launch privileges. Combined with invisible foreground services (denied POST_NOTIFICATIONS), the user has zero awareness that the app is launching activities from background.
Two-Step Trampoline Launch¶
With the BAL exemption, observed adware uses a two-step trampoline to display ads from a background service:
Step 1 -- Background to trampoline (via BAL_ALLOW_ALLOWLISTED_COMPONENT): The foreground service calls startActivity() targeting an obfuscated trampoline activity. This is allowed because the app is on the companion device allowlist.
Step 2 -- Trampoline to ad SDK (via BAL_ALLOW_VISIBLE_WINDOW): The trampoline activity is now visible, so it launches the ad SDK activity. This is also allowed because the calling app now has a visible window.
The BAL exemption is per-UID, so all activities within the same APK (same UID) share the exemption. The trampoline pattern is not strictly necessary for BAL purposes -- the ad SDK activity in the same APK would also be allowed. The trampoline likely exists because the ad SDK's own launch logic goes through service or broadcast intermediaries that may not carry the BAL exemption context, or because the native code controlling the flow uses a generic launch-then-redirect pattern.
Brute-Force Fallback¶
Apps without a companion association spam startActivity() every 30 seconds. BAL blocking is deterministic -- without an exemption, every attempt is blocked. The volume is not about overcoming blocking but about ensuring the app launches an ad immediately if circumstances change (e.g., the app briefly enters the foreground via a notification tap, or the user grants a companion association). With multiple apps in a fleet (e.g., 14 apps at 30-second intervals), the device generates hundreds of blocked attempts per hour. These apps simultaneously request CompanionDeviceManager.associate() dialogs -- if the user accepts any of them, that app joins the allowlist and starts showing fullscreen ads.
Social Engineering the Dialog¶
The CompanionDevice dialog shows an opaque Bluetooth MAC address with no context about what permissions are being granted. Adware can present multiple dialogs in rapid succession, training the user to tap "Allow" reflexively.
Detection¶
companion_device_setupin<uses-feature>without legitimate companion device functionality (no Bluetooth UI, no wearable integration)CompanionDeviceManager.associate()calls in utility apps (cleaners, PDF readers, WiFi analyzers)- Active companion associations visible via
adb shell dumpsys companiondevice - Revoke associations via
adb shell cmd companiondevice disassociate <userId> <packageName> <macAddress>
Hidden API Bypass¶
AndroidHiddenApiBypass (org.lsposed.hiddenapibypass) is an open-source library that circumvents Android's hidden API restrictions introduced in Android 9. Normally, apps cannot call @hide annotated methods or directly instantiate system service classes. The library has multiple bypass variants: the original uses Unsafe memory operations, while the newer LSPass variant uses Property.of() to achieve the same result without Unsafe.
In malware, it enables:
- Creating
DisplayManager,TelecomManager, orNotificationManagerinstances directly (bypassingContext.getSystemService()which would expose the real package name) - Accessing private fields on
BaseDexClassLoaderfor classloader injection - Calling hidden methods that the public API does not expose
Legitimate use exists in Xposed/LSPosed modules and root apps. In a non-root app from an unknown developer, it is a strong indicator of API abuse. Look for HiddenApiBypass.newInstance() or HiddenApiBypass.invoke() calls.
Security Patch Date Behavioral Branching¶
Malware checks Build.VERSION.SECURITY_PATCH against a threshold date to vary its behavior based on the device's security level. Newer security patches mean stricter OS restrictions (notification channels, background service limits, alarm restrictions), so the malware selects different code paths:
| Device Age | Techniques Used |
|---|---|
| Older patches | Direct techniques: visible notifications, simple AlarmManager, standard service starts |
| Newer patches | Evasion techniques: TelecomManager fake calls, VirtualDisplay invisible rendering, HiddenApiBypass for direct system service instantiation |
This is adaptive malware that detects the security posture of the device and chooses the most effective attack path. The threshold date roughly corresponds to when specific Android security restrictions were enforced. Combined with Build.VERSION.SDK_INT checks, it creates multi-layered execution guardrails that maximize compatibility across device populations.
Instrumentation Hijack¶
Android's Instrumentation class is a testing framework hook that intercepts Activity lifecycle callbacks before they reach the Activity itself. Malware abuses this by declaring an <instrumentation> element in the manifest targeting its own package, then implementing the Instrumentation subclass with all lifecycle methods as native JNI calls.
<instrumentation
android:name="com.example.MyInstrumentation"
android:targetPackage="com.example.myapp" />
public class MyInstrumentation extends Instrumentation {
static { System.loadLibrary("payload"); }
@Override
public native void onCreate(Bundle arguments);
@Override
public native void onStart();
}
When the system creates the app process, it instantiates the Instrumentation class before any Activity runs. The onCreate() and onStart() callbacks fire before the app's own lifecycle, giving the malware first-mover control over the entire process. Because the methods are native, the actual logic is hidden in a .so library, invisible to DEX-level static analysis.
Combined with packing, this creates a two-layer hiding strategy: the packer encrypts the DEX, and the Instrumentation class pushes critical logic into native code. Even after unpacking, the decompiled Java shows only native method stubs with no implementation.
Detection: <instrumentation> elements in the manifest where targetPackage matches the app's own package. Legitimate Instrumentation usage (testing frameworks like AndroidJUnitRunner) targets a different package (the app under test) and is not shipped in production APKs.
APK and Manifest Corruption¶
Malformed ZIP Headers¶
TrickMo uses malformed ZIP files combined with JSONPacker. The corrupted ZIP structure causes analysis tools (apktool, JADX, unzip) to fail or produce incomplete output, while the Android runtime tolerates the malformations and installs the APK normally. Cleafy documented this as a deliberate anti-analysis layer across 40+ variants.
Manifest Corruption¶
SoumniBot injects malformed compression parameters into AndroidManifest.xml. Android's parser tolerates the corruption; analysis tools crash. Kaspersky documented three specific techniques: invalid compression method values, invalid manifest size declarations, and oversized namespace strings.
Oversized DEX Headers¶
Some families pad DEX files with junk data that exceeds parser buffer sizes in analysis tools but is safely ignored by ART.
Code-Level Obfuscation¶
| Technique | Effect | Families |
|---|---|---|
| String encryption | C2 URLs, package names encrypted at rest, decrypted at runtime | Anatsa (DES), Mandrake, most families |
| Reflection-based API calls | Method names resolved via strings at runtime, invisible to static analysis | Octo, Xenomorph |
| Native code for sensitive ops | Key operations in .so libraries, harder to decompile |
Mandrake (OLLVM), Octo2 |
| Control flow flattening | Switch-based dispatch obscures actual execution order | Commercial packers, DexGuard |
| Dead code injection / DEX bloat | Junk methods/classes inflate the codebase, waste analyst time | Joker, crypter outputs |
| Class/method renaming | a.b.c.d() instead of meaningful names |
Nearly universal (ProGuard/R8 baseline) |
| Dynamic class loading | Payload classes loaded from encrypted assets or C2 at runtime | Anatsa, Necro, SharkBot |
String Encryption Patterns¶
String encryption is the single most common obfuscation technique. Several distinct patterns appear across the ecosystem, from open-source Gradle plugins to custom native implementations.
XOR-Based Schemes¶
The majority of Android string obfuscation uses XOR with a repeating key. Variations differ in encoding and key management:
| Pattern | Algorithm | Reversal |
|---|---|---|
| Base64 + XOR | Base64.decode(ciphertext) XOR Base64.decode(key) |
Extract key from decryptor class, apply same XOR |
| Hex + XOR | Hex string to byte pairs, XOR with repeating key | Parse hex, XOR with key |
| Single-byte XOR | Each character XOR'd with a constant (e.g., 0x10) |
Trivial: XOR all strings with the constant |
| Nibble swap + XOR | ((b & 0x0F) << 4) \| ((b >> 4) & 0x0F) then XOR |
Reverse nibble swap after XOR decode |
StringFog¶
StringFog is an open-source Gradle plugin that automatically encrypts all string literals in DEX bytecode at build time. A ClassVisitor replaces every string constant load with an encrypted byte array and an injected call to a static decrypt method.
The default algorithm is cyclic XOR:
public static String decrypt(String ciphertext, String key) {
byte[] data = Base64.decode(ciphertext, Base64.DEFAULT);
byte[] keyBytes = Base64.decode(key, Base64.DEFAULT);
for (int i = 0; i < data.length; i++) {
data[i] = (byte) (data[i] ^ keyBytes[i % keyBytes.length]);
}
return new String(data);
}
The key is compiled into the app. Reversal: extract the key from the StringFogImpl class (or hook it with Frida), then batch-decrypt all constants. Katalina (Dalvik bytecode emulator) can deobfuscate StringFog automatically via emulation.
StringFog appears in both legitimate apps (protecting API keys) and malware (hiding C2 URLs, permission strings, SharedPreferences keys). Artifact: com.github.megatronking.stringfog in build dependencies, com.github.megatronking.stringfog.xor.StringFogImpl in DEX.
NPStringFog¶
NPStringFog is a stripped-down variant associated with NP Manager (a Chinese APK patching tool). It uses hex-encoded strings XOR'd with a hardcoded key (commonly "npmanager" or a custom key). Unlike StringFog, NPStringFog has no Gradle plugin integration -- it operates at the bytecode level during repackaging.
Structure: a large StringPool class with hundreds of static methods, each returning one decoded string via NPStringFog.d(). The d() method often has multiple polymorphic overloads with unused parameters (boolean, int) that serve as junk to confuse decompilers.
Observed in the wild in banking trojans and NFC relay malware targeting Turkish and Korean users.
Native Library Decryption¶
The most resistant pattern delegates string decryption to a native .so library. The DEX code calls a JNI method with encrypted byte arrays; the native code decrypts and returns plaintext. A Java-level XOR fallback handles cases where the native library fails to load.
static String decrypt(byte[] data, byte[] key) {
try {
return nativeDecrypt(data, key);
} catch (UnsatisfiedLinkError e) {
byte[] result = new byte[data.length];
for (int i = 0; i < data.length; i++) {
result[i] = (byte) (data[i] ^ key[i % key.length]);
}
return new String(result);
}
}
This pattern forces analysts to reverse both Java and native code. The native implementation may use AES, custom ciphers, or hardware-bound keys. Reversing requires IDA/Ghidra on the .so or Frida hooks on the JNI call boundary.
Frida-Based Batch Decryption¶
For any XOR-based scheme, hooking the decrypt method at runtime recovers all strings in a single run:
Java.perform(function() {
var StringFog = Java.use("com.github.megatronking.stringfog.xor.StringFogImpl");
StringFog.decrypt.implementation = function(data, key) {
var result = this.decrypt(data, key);
console.log("[StringFog] " + result);
return result;
};
});
Adapt the class name to match the target's specific obfuscator.
Visual Confusion Obfuscation¶
An obfuscation technique that renames classes, methods, and packages using visually similar characters: uppercase O, lowercase o, and zero 0 (e.g., OooO0O0, o0000Ooo, Oooo0OO). Unlike standard R8/ProGuard which uses short alphabetic names (a, b, a0), this pattern is deliberately adversarial, designed to make manual analysis painful because all identifiers look identical at a glance.
When this obfuscation style appears, it indicates deliberate anti-analyst intent rather than standard build-time optimization. R8/ProGuard obfuscation is functional (reduce APK size); visual confusion obfuscation is hostile.
DEX Bloat¶
A deliberate anti-analysis technique where the APK is inflated with thousands of junk classes and methods that serve no functional purpose. The goal is to overwhelm analysts and automated tools: decompilers slow down, class lists become unnavigable, and pattern matching produces excessive false positives.
A typical implementation generates hundreds of identical classes, each containing 100-150 empty or trivially returning methods. For example, 741 junk classes with 144 methods each produces 106,704 junk methods. The junk code may be invoked once at startup (e.g., a single callEntry() call in Application.onCreate()) to prevent dead code elimination by R8, but the methods themselves do nothing meaningful.
The technique is effective because:
- Decompilers like JADX take significantly longer to process bloated APKs
- Class/method lists in analysis tools become unusable (thousands of identically structured entries)
- Automated string extraction and pattern matching returns noise from junk classes
- Analysts cannot quickly distinguish real code from padding without runtime analysis
Unlike legitimate code expansion from library bundling, DEX bloat classes follow uniform patterns: identical method signatures, no cross-references to real app code, and trivial method bodies. Detection: look for large clusters of classes with identical structure, uniform method counts, and no meaningful call graph connections to the app's real functionality.
Domain Generation Algorithms¶
Octo2 introduced DGA-based C2 resolution, generating domain names algorithmically so that blocking individual domains is ineffective. The DGA seed and algorithm are embedded in a dynamically loaded native library, adding another analysis layer.
Families by Anti-Analysis Depth¶
| Family | Emulator | Root/Magisk | Frida | Debugger | AV Check | Geo | Obfuscation |
|---|---|---|---|---|---|---|---|
| Anatsa | Yes | Yes | Yes | Yes | Yes | Yes | DES strings, native loader |
| Mandrake | Yes | Yes | Yes | Yes | Yes | Yes | OLLVM native, multi-year dormancy |
| Octo/Octo2 | Yes | Yes | Yes | Yes | Yes | Yes | DGA, native library decryption |
| Hook | Yes | Yes | Yes | Yes | Yes | Yes | Inherited from ERMAC lineage |
| TrickMo | Yes | Minimal | No | No | Yes | Yes | Malformed ZIP, JSONPacker |
| Cerberus | Yes | Yes | Yes | Yes | Yes | Yes | Play Protect disable |
| SpyNote | Yes | Yes | Yes | Yes | Yes | Minimal | Restricted settings bypass |
| GodFather | Yes | Yes | Yes | Yes | Yes | Yes | Multi-language targeting |
Academic Research¶
| Paper | Year | Key Finding |
|---|---|---|
| A Comprehensive Survey on Android Anti-Reversing and Anti-Analysis | 2024 | Systematic taxonomy of 32 anti-analysis subcategories across 5 major categories |
| DroidMorph | 2024 | 1,771 morphed variants achieved 51.4% detection rate, meaning half evaded all AV |
| AVPASS | 2017 | Leaked AV detection models to generate evasive variants; 56/58 AVs bypassed |
Detection During Analysis¶
Static Indicators
- Multiple
Build.*property checks concentrated in a single method /proc/self/mapsor/proc/self/statusfile reads- Hardcoded AV package name strings
TelephonyManagercalls not related to app functionality- Native library with OLLVM indicators (flattened control flow)
- Encrypted string arrays with runtime decryption routines
Dynamic Indicators
- App silently exits or shows benign behavior in emulator but activates on physical device
- Delayed C2 contact (hours/days after install)
- Port scanning on localhost (Frida detection)
- Rapid file existence checks across
/dev/,/system/,/sbin/ getInstalledPackages()called early in app lifecycle