Skip to content

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:

  1. Open Settings > Security > Google Play Protect
  2. Click the gear icon
  3. Disable "Scan apps with Play Protect"
  4. 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:

  1. Create a VirtualDisplay with tiny dimensions (5-15 pixels) via DisplayManager.createVirtualDisplay()
  2. Render a Presentation on the virtual display to keep it alive
  3. Create an AudioTrack and play random noise to acquire audio focus
  4. Create a MediaSession with a PendingIntent as the media button receiver
  5. The PendingIntent wraps a broadcast carrying the target activity Intent as an extra
  6. Dispatch MEDIA_PLAY key events via AudioManager.dispatchMediaKeyEvent()
  7. The system delivers the key event to the active MediaSession, which fires the PendingIntent
  8. The broadcast receiver extracts the Intent and calls context.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:

<uses-feature android:name="android.software.companion_device_setup"/>

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

  1. App declares companion_device_setup feature in manifest
  2. At runtime, scans for any nearby Bluetooth LE device (random beacon, headphones, smartwatch)
  3. Calls CompanionDeviceManager.associate() with a filter matching that device
  4. System shows a dialog: "Allow [app] to access [device name/MAC]?" -- the dialog mentions data access, not background activity privileges
  5. User taps "Allow" (the dialog text is opaque and does not mention BAL)
  6. App is registered as a companion device manager, added to BAL_ALLOW_ALLOWLISTED_COMPONENT allowlist
  7. 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_setup in <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, or NotificationManager instances directly (bypassing Context.getSystemService() which would expose the real package name)
  • Accessing private fields on BaseDexClassLoader for 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/maps or /proc/self/status file reads
  • Hardcoded AV package name strings
  • TelephonyManager calls 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