Legend:
[STATIC]= Static analysis |[SMALI]= Smali bytecode |[PATCH]= APK patching |[NATIVE]= Native library analysis |[CRYPTO]= Cryptography |[BYPASS]= Security bypass |[DEOBF]= Deobfuscation |[TOOL]= Tool usage
1. Extracting & Enumerating APK Files
What it means: An APK is just a ZIP archive. Extracting and enumerating its contents is the very first step of every Android assessment — before running a single line of code. Everything from API keys to certificate pins to database schemas can be found at this stage.
1.1 Getting the APK
# ─── Method 1: Pull from installed device/emulator ───
adb shell pm list packages | grep target
adb shell pm path com.target.app
# Output: package:/data/app/com.target.app-1/base.apk
adb pull /data/app/com.target.app-1/base.apk ./target.apk
# Split APKs (App Bundles)
adb shell pm path com.target.app
# May list: base.apk + split_config.*.apk
adb pull /data/app/com.target.app-1/base.apk .
adb pull /data/app/com.target.app-1/split_config.arm64_v8a.apk .
# ─── Method 2: APK download sites ───
# APKMirror: https://www.apkmirror.com
# APKPure: https://apkpure.com
# APK Combo: https://apkcombo.com
# ─── Method 3: From Play Store via Raccoon ───
# Raccoon (desktop) → login Google → download any Play Store APK
# ─── Method 4: From device using backup ───
adb backup -apk com.target.app
# → backup.ab → extract to get APK
1.2 APK Enumeration — First Steps
# ─── Basic file info ───
file target.apk # → Zip archive
unzip -l target.apk # list contents without extracting
sha256sum target.apk # hash for tracking
# ─── Extract APK ───
cp target.apk target.zip
unzip target.zip -d raw_extracted/
# OR
apktool d target.apk -o decompiled/ # preferred — decodes resources
# ─── Key files to examine immediately ───
cat decompiled/AndroidManifest.xml # components, perms, config
cat decompiled/res/values/strings.xml # app strings (often has secrets)
cat decompiled/res/xml/network_security_config.xml # SSL config
ls -la decompiled/assets/ # raw files, databases, scripts
ls -la raw_extracted/lib/ # native libraries
ls -la raw_extracted/META-INF/ # certificates
# ─── Quick enumeration script ───
PKG_DIR="decompiled"
echo "=== Exported Components ==="
grep -i "exported=\"true\"" $PKG_DIR/AndroidManifest.xml
echo "=== Debuggable? ==="
grep -i "debuggable" $PKG_DIR/AndroidManifest.xml
echo "=== Allow Backup? ==="
grep -i "allowBackup" $PKG_DIR/AndroidManifest.xml
echo "=== Cleartext Traffic? ==="
grep -i "cleartext\|usesCleartextTraffic" $PKG_DIR/AndroidManifest.xml
echo "=== API Keys in strings.xml ==="
grep -iE "key|secret|token|api|password" $PKG_DIR/res/values/strings.xml
echo "=== Endpoints ==="
grep -rh "https\?://" $PKG_DIR/ 2>/dev/null | sort -u | head -30
1.3 Automated Enumeration Tools
# ─── apkleaks — find secrets & endpoints ───
pip3 install apkleaks
apkleaks -f target.apk # find all endpoints + secrets
apkleaks -f target.apk -o report.json # save report
# ─── MobSF — full automated analysis ───
docker run -it -p 8000:8000 opensecurity/mobile-security-framework-mobsf
# Upload APK → instant static analysis report
# ─── androguard — Python library ───
pip3 install androguard
python3 << 'EOF'
from androguard.misc import AnalyzeAPK
a, d, dx = AnalyzeAPK("target.apk")
print("Package:", a.get_package())
print("Version:", a.get_androidversion_name())
print("Min SDK:", a.get_min_sdk_version())
print("Activities:", a.get_activities())
print("Permissions:", a.get_permissions())
EOF
# ─── apkid — identify build tools + obfuscators ───
pip3 install apkid
apkid target.apk
# → detects: ProGuard, DexGuard, R8, Flutter, React Native, Xamarin
2. Disassembling the APK
What it means: Disassembling converts the compiled .dex bytecode back into human-
readable form — either Smali (low-level assembly-like) or Java (via decompiler). These are
two different levels: Smali is accurate but verbose; Java decompilation is readable but
may have errors.
2.1 The Compilation Chain
Developer writes: Java / Kotlin source (.java / .kt)
↓
Compiled to: .class files (JVM bytecode)
↓
Compiled to: .dex files (Dalvik bytecode) ← inside APK
↓
Disassembled to: Smali (via apktool / baksmali) ← low level
Decompiled to: Java source (via jadx) ← high level
2.2 apktool — Disassemble to Smali
# ─── Disassemble (decode) ───
apktool d target.apk # output to target/ folder
apktool d target.apk -o my_output/ # custom output folder
apktool d target.apk -f -o output/ # force overwrite
apktool d target.apk --no-src # skip smali, decode resources only
apktool d target.apk --no-res # skip resources, smali only
# Output structure:
# output/
# ├── AndroidManifest.xml ← decoded (human readable!)
# ├── smali/ ← disassembled .smali files
# │ └── com/target/app/
# │ ├── MainActivity.smali
# │ ├── AuthManager.smali
# │ └── ...
# ├── res/ ← decoded resources
# └── assets/ ← raw assets
# ─── Recompile (after patching) ───
apktool b my_output/ -o patched.apk
# ─── Version check ───
apktool --version # should be 2.9+
2.3 jadx — Decompile to Java
# ─── CLI decompile ───
jadx target.apk -d output_java/ # decompile to Java
jadx target.apk -d output/ --show-bad-code # include decompile failures
# ─── GUI (BEST for analysis) ───
jadx-gui target.apk
# Features:
# - Full Java source tree
# - Search across all classes
# - Cross-references (who calls what)
# - Deobfuscation mapping
# - Export all sources
# jadx-gui hotkeys:
# Ctrl+F → search in current file
# Ctrl+Shift+F → search entire codebase
# Ctrl+Click → jump to definition
# Alt+← → go back
# X on method → find all usages
# ─── dex2jar + JD-GUI alternative ───
d2j-dex2jar target.apk -o target.jar # convert dex → jar
jd-gui target.jar # open with JD-GUI
# OR use IntelliJ IDEA to open the .jar directly
2.4 baksmali / smali — Low Level
# ─── baksmali: disassemble .dex → .smali ───
baksmali disassemble classes.dex -o smali_output/
# ─── smali: assemble .smali → .dex ───
smali assemble smali_output/ -o classes.dex
# ─── Extract dex from APK first ───
unzip target.apk classes.dex classes2.dex
baksmali disassemble classes.dex -o smali/
baksmali disassemble classes2.dex -o smali2/
3. Understanding Smali
What it means: Smali is the human-readable representation of Dalvik bytecode — the actual machine language Android executes. Unlike Java decompilation (which may have errors), Smali is 100% accurate. Patching APKs means editing Smali directly. You MUST understand Smali to reliably bypass security checks.
3.1 Smali Basics — Type Notation
# ─── Type descriptors ───
V → void
Z → boolean
B → byte
S → short
C → char
I → int
J → long (64-bit, takes 2 registers)
F → float
D → double (64-bit, takes 2 registers)
Ljava/lang/String; → String (L + full/class/path + semicolon)
Ljava/util/List; → List
[I → int[] (array of int)
[Ljava/lang/String; → String[]
# ─── Method signature ───
# Lpackage/ClassName;->methodName(ParamTypes)ReturnType
Lcom/target/app/AuthManager;->isAdmin(Ljava/lang/String;)Z
# param=String ^return=boolean
# ─── Field descriptor ───
# Lpackage/ClassName;->fieldName:Type
Lcom/target/app/Config;->API_KEY:Ljava/lang/String;
3.2 Smali Registers
# Registers are like variables — Dalvik is register-based VM
# Two types:
# v0, v1, v2... → local registers
# p0, p1, p2... → parameter registers
# p0 = 'this' (for instance methods)
# p1 = first parameter
# p2 = second parameter
# Example: method with 3 local vars + 2 params
.method public someMethod(II)V
.registers 5 # total = 3 local (v0-v2) + 2 param (p0=this, p1=int, p2=int)
const/4 v0, 0x0 # v0 = 0
move v1, p1 # v1 = p1 (first param)
add-int v2, p1, p2 # v2 = p1 + p2
return-void
.end method
3.3 Smali Instruction Reference
# ─── Constants ───
const/4 v0, 0x0 # v0 = 0 (4-bit constant)
const/16 v0, 0x10 # v0 = 16 (16-bit constant)
const v0, 0x1234 # v0 = 0x1234 (32-bit)
const-wide v0, 0x1234L # v0,v1 = 0x1234 (64-bit)
const-string v0, "hello" # v0 = "hello"
const/4 v0, 0x1 # v0 = true (boolean)
const/4 v0, 0x0 # v0 = false (boolean)
# ─── Move operations ───
move v0, v1 # v0 = v1
move-object v0, v1 # v0 = v1 (object reference)
move-result v0 # v0 = result of previous invoke
move-result-object v0 # v0 = result (object)
move-result-wide v0 # v0 = result (wide/64bit)
# ─── Arithmetic ───
add-int v0, v1, v2 # v0 = v1 + v2
sub-int v0, v1, v2 # v0 = v1 - v2
mul-int v0, v1, v2 # v0 = v1 * v2
div-int v0, v1, v2 # v0 = v1 / v2
rem-int v0, v1, v2 # v0 = v1% v2
add-int/lit8 v0, v1, 0x5 # v0 = v1 + 5
# ─── Comparisons ───
if-eq v0, v1, :label # if v0 == v1 goto label
if-ne v0, v1, :label # if v0 != v1 goto label
if-lt v0, v1, :label # if v0 < v1 goto label
if-gt v0, v1, :label # if v0 > v1 goto label
if-le v0, v1, :label # if v0 <= v1 goto label
if-ge v0, v1, :label # if v0 >= v1 goto label
if-eqz v0, :label # if v0 == 0 (null/false) goto label
if-nez v0, :label # if v0 != 0 (not null/true) goto label
# ─── Method calls ───
invoke-virtual {v0, v1}, Lcom/target/Class;->method(I)V
# ^obj ^arg ^void return
invoke-static {v0}, Lcom/target/Utils;->encrypt(Ljava/lang/String;)Ljava/lang/String;
# ^arg (no object — static method)
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
# constructor call
invoke-interface {v0, v1}, Ljava/util/List;->add(Ljava/lang/Object;)Z
# After invoke: get return value with move-result
invoke-virtual {v0}, Lcom/target/Auth;->isLoggedIn()Z
move-result v1 # v1 = boolean return value
# ─── Return ───
return-void # return void
return v0 # return v0 (int/bool/float)
return-object v0 # return object reference
return-wide v0 # return 64-bit value
# ─── Field access ───
iget v0, p0, Lcom/target/App;->mToken:Ljava/lang/String; # instance get
iput v0, p0, Lcom/target/App;->mToken:Ljava/lang/String; # instance put
sget v0, Lcom/target/Config;->API_KEY:Ljava/lang/String; # static get
sput v0, Lcom/target/Config;->API_KEY:Ljava/lang/String; # static put
# ─── Object creation ───
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
# ─── Array operations ───
new-array v0, v1, [I # v0 = new int[v1]
aput v2, v0, v3 # v0[v3] = v2
aget v2, v0, v3 # v2 = v0[v3]
array-length v0, v1 # v0 = v1.length
# ─── Type checks ───
instance-of v0, v1, Ljava/lang/String; # v0 = (v1 instanceof String)
check-cast v0, Ljava/lang/String; # (String) v0
3.4 Complete Smali Example — Reading a Real Method
// Original Java:
public boolean isAdmin(String username) {
if (username.equals("admin")) {
return true;
}
return false;
}
# Smali equivalent:
.method public isAdmin(Ljava/lang/String;)Z # Z = boolean return
.registers 3
# p0 = this
# p1 = username (String parameter)
# v0 = local variable
const-string v0, "admin" # v0 = "admin"
invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
# calls: username.equals("admin")
move-result v0 # v0 = result of equals()
if-eqz v0, :cond_0 # if v0 == 0 (false), goto cond_0
const/4 v0, 0x1 # v0 = true
return v0 # return true
:cond_0
const/4 v0, 0x0 # v0 = false
return v0 # return false
.end method
4. Reading Hardcoded Strings
What it means: Developers frequently hardcode sensitive values — API keys, encryption keys, passwords, endpoints, JWT secrets — directly into the app code. These are trivially extractable with string search or decompilation and are the most common high-severity finding in Android assessments.
4.1 Where Hardcoded Strings Hide
Location Tool to Read
──────────────────────────────────────────────────────────────
res/values/strings.xml cat / grep (after apktool decode)
Java/Kotlin source jadx-gui → search
Smali files grep -r "const-string"
assets/.json / .js cat / grep
assets/*.html cat / grep
BuildConfig.java jadx → search BuildConfig
Native .so files strings libname.so
Encrypted strings Frida hook at decrypt() call
React Native bundle cat assets/index.android.bundle
4.2 Finding Hardcoded Strings
# ─── Method 1: grep on decompiled Java (jadx output) ───
jadx target.apk -d java_out/
grep -rni "api.key\|apikey\|api_key" java_out/
grep -rni "password\|passwd\|secret" java_out/
grep -rni "token\|bearer\|authorization" java_out/
grep -rni "aws\|firebase\|google\|twilio\|stripe" java_out/
grep -rni "private.key\|-----BEGIN" java_out/
# Endpoints
grep -rh "http://\|https://" java_out/ | sort -u
grep -rh "\.com\|\.io\|\.net" java_out/ | grep '"' | sort -u
# ─── Method 2: grep on smali ───
apktool d target.apk -o smali_out/
grep -rn "const-string" smali_out/smali/ | grep -i "key\|secret\|pass\|token"
grep -rn "const-string" smali_out/smali/ | grep "http"
# ─── Method 3: strings on raw APK ───
strings target.apk | grep -i "api\|key\|secret\|token\|password"
strings target.apk | grep "http"
# ─── Method 4: strings in strings.xml ───
grep -i "key\|secret\|token\|api\|pass" decompiled/res/values/strings.xml
# ─── Method 5: apkleaks automated ───
apkleaks -f target.apk -v # verbose — all findings
apkleaks -f target.apk -o secrets.txt # save to file
4.3 Common Patterns in Java
// ─── Hardcoded API keys ───
private static final String API_KEY = "AIzaSyD1234567890abcdef";
private String mApiKey = "sk-proj-abc123";
public static final String SECRET = "mysupersecretkey123";
// ─── Hardcoded credentials ───
if (password.equals("admin1234")) { ... }
if (username.equals("backdoor") && pass.equals("letmein")) { ... }
// ─── Hardcoded encryption key ───
byte[] key = "1234567890123456".getBytes(); // AES key!
SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
// ─── Hardcoded JWT secret ───
String secret = "jwt_secret_hardcoded_2024";
Jwts.builder().signWith(SignatureAlgorithm.HS256, secret)...
// ─── BuildConfig constants ───
// These come from build.gradle:
// buildConfigField "String", "API_KEY", '"mykey123"'
// Access via:
String key = BuildConfig.API_KEY;
// Visible in jadx as:
// com/target/app/BuildConfig.smali
5. Bad Cryptography Implementation
What it means: Developers frequently implement cryptography incorrectly — using weak algorithms, hardcoded keys, predictable IVs, ECB mode, or broken custom crypto. Identifying these patterns lets you decrypt protected data or forge authentication tokens.
5.1 Common Crypto Mistakes
// ─── MISTAKE 1: Hardcoded encryption key ───
String key = "MyHardcodedKey!!"; // 16 bytes = AES-128
byte[] keyBytes = key.getBytes("UTF-8");
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
// FIX: derive key from secure random, store in Android Keystore
// ─── MISTAKE 2: ECB mode (no IV, patterns visible) ───
Cipher cipher = Cipher.getInstance("AES");
// Default = AES/ECB/PKCS5Padding — NEVER use ECB!
// ECB encrypts each block independently → pattern leakage
// FIX: Cipher.getInstance("AES/GCM/NoPadding") with random IV
// ─── MISTAKE 3: Hardcoded IV ───
byte[] iv = "1234567890123456".getBytes(); // static IV!
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// FIX: SecureRandom().nextBytes(iv) — random IV per encryption
// ─── MISTAKE 4: Weak algorithm ───
MessageDigest md = MessageDigest.getInstance("MD5"); // broken!
MessageDigest sha1 = MessageDigest.getInstance("SHA1"); // weak!
// FIX: use SHA-256 minimum, bcrypt/Argon2 for passwords
// ─── MISTAKE 5: Predictable seed ───
Random r = new Random(12345); // seeded with constant!
// FIX: SecureRandom() — uses OS entropy
// ─── MISTAKE 6: Storing key in SharedPreferences ───
prefs.edit().putString("enc_key", Base64.encode(key)).apply();
// FIX: Android Keystore System for key storage
// ─── MISTAKE 7: Custom XOR "encryption" ───
for (int i = 0; i < data.length; i++) {
data[i] = (byte)(data[i] ^ key[i % key.length]);
}
// Known-plaintext attack → recover key trivially!
5.2 Identifying Crypto in Smali/Code
# ─── Search for crypto usage in jadx output ───
grep -rn "AES\|DES\|Cipher\|SecretKey\|KeySpec\|MessageDigest" java_out/
grep -rn "getInstance.*AES\|getInstance.*DES\|getInstance.*ECB" java_out/
grep -rn "Base64\|getBytes.*UTF" java_out/
grep -rn "SecureRandom\|Random(" java_out/
grep -rn "Keystore\|KeyStore" java_out/
# ─── jadx-gui search ───
# Ctrl+Shift+F → search "AES/ECB" → find vulnerable cipher usage
# Ctrl+Shift+F → search "getBytes" → find key material from strings
5.3 Exploiting Common Crypto Weaknesses
# ─── Exploit hardcoded AES ECB key (Python) ───
from Crypto.Cipher import AES
import base64
key = b"MyHardcodedKey!!" # extracted from app
encrypted = base64.b64decode("BASE64_CIPHERTEXT_FROM_APP")
cipher = AES.new(key, AES.MODE_ECB)
decrypted = cipher.decrypt(encrypted)
print(decrypted.rstrip(b'\x00')) # strip padding
# ─── Exploit static IV (AES CBC) ───
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
key = b"HardcodedKey1234"
iv = b"1234567890123456" # static IV from code
ct = base64.b64decode("CIPHERTEXT")
cipher = AES.new(key, AES.MODE_CBC, iv)
pt = unpad(cipher.decrypt(ct), 16)
print(pt)
# ─── Crack MD5 password hash ───
import hashlib
# Found in APK: MD5 hash "5f4dcc3b5aa765d61d8327deb882cf99"
# hashcat: hashcat -m 0 hash.txt rockyou.txt
# john: john --format=raw-md5 hash.txt
6. Reversing Hybrid Apps
What it means: Hybrid apps (React Native, Cordova, Ionic, Flutter) bundle JavaScript or compiled cross-platform code alongside native Android code. The web layer is often not obfuscated — full source code, API keys, and business logic are visible in plaintext JS bundles.
6.1 React Native
# ─── Extract JS bundle ───
unzip target.apk assets/index.android.bundle
# This IS the full application code!
# ─── Read bundle ───
cat assets/index.android.bundle | head -100
# The bundle is a single minified JS file — beautify it:
npm install -g prettier
prettier --write index.android.bundle
# OR
cat index.android.bundle | node -e "
const fs = require('fs');
let c = '';
process.stdin.on('data', d => c += d);
process.stdin.on('end', () => {
const b = require('js-beautify');
console.log(b(c));
});
" > beautified.js
# ─── Search beautified bundle ───
grep -i "api.key\|apiKey\|secret\|token\|password\|baseUrl\|endpoint" beautified.js
grep -i "http://\|https://" beautified.js | sort -u
grep -i "AWS\|firebase\|stripe\|twilio\|sendgrid" beautified.js
# ─── Find source maps (if present) ───
find . -name "*.map" -o -name "sourcemap*"
# Source maps → reconstruct original source files!
6.2 Cordova / PhoneGap / Ionic
# ─── Web assets location ───
unzip target.apk -d extracted/
ls extracted/assets/www/
# Structure:
# assets/www/
# ├── index.html ← main HTML
# ├── js/app.js ← all JS code
# ├── js/vendor.js ← libraries
# └── environments/
# └── environment.prod.js ← prod config! (API keys, endpoints)
# ─── Read all JS ───
cat extracted/assets/www/js/app.js | node -e "..." > readable.js
grep -rni "apiKey\|secret\|token\|baseUrl" extracted/assets/www/
# ─── Common sensitive location ───
cat extracted/assets/www/environments/environment.prod.js
# Often contains:
# export const environment = {
# production: true,
# apiUrl: 'https://api.target.com',
# apiKey: 'HARDCODED_KEY_HERE', ← jackpot!
# };
6.3 Flutter
# ─── Flutter reversing (harder) ───
# Code compiled to native ARM → libapp.so
# Tool 1: blutter
git clone https://github.com/worawit/blutter
python3 blutter.py extracted/lib/arm64-v8a/libapp.so out_dir/
# Generates pseudo-code from Dart snapshot
# Tool 2: reFlutter (patch + recompile to intercept traffic)
pip3 install reflutter
reflutter target.apk
# Patches libflutter.so → redirects traffic to Burp
# Then recompile + sign + install
# Tool 3: strings on libapp.so
strings lib/arm64-v8a/libapp.so | grep -i "http\|api\|key\|secret"
# Tool 4: Ghidra analysis of libapp.so
# Load libapp.so → find Dart snapshot magic bytes
# → decompile Dart classes
6.4 Xamarin
# ─── .NET assemblies in assemblies/ ───
unzip target.apk assemblies/*.dll -d xamarin_dlls/
ls xamarin_dlls/
# ─── Decompile with ILSpy (Linux) ───
ilspycmd xamarin_dlls/MyApp.dll -o csharp_out/
# OR use dotPeek (Windows) / dnSpy
# ─── Search decompiled C# ───
grep -rni "ApiKey\|Secret\|Password\|Token" csharp_out/
grep -rni "http://" csharp_out/ | sort -u
7. Reading Obfuscated Code
What it means: ProGuard, R8, DexGuard, and DashO rename classes, methods, and fields to meaningless single letters (a, b, c...) to hinder reversing. Reading obfuscated code requires understanding patterns — following data flow, recognizing standard library calls, and using context clues to identify what obfuscated methods do.
7.1 Identifying Obfuscation
# ─── Check with apkid ───
apkid target.apk
# → [OBFUSCATOR] proguard
# → [OBFUSCATOR] r8
# → [OBFUSCATOR] dexguard (commercial — strongest)
# → [PACKER] jiagu (Tencent packer)
# ─── Visual indicators in jadx-gui ───
# Classes named: a, b, c, aa, ab, ac...
# Methods named: a(), b(), c()...
# Fields named: a, b, c...
# String encryption → calls to decrypt() methods
# Class count explosion → unused reflection wrappers
7.2 Reading Obfuscated Code — Strategy
// ─── Obfuscated code example ───
public class a {
private String a;
private int b;
public boolean a(String a, String b) {
return this.a(a).equals(b(b));
}
private String a(String a) {
// string decryption...
return c.a(a, this.a);
}
}
// ─── Reading strategy ───
// 1. Start from entry points you KNOW (MainActivity, onClick handlers)
// 2. Follow the call chain — not the names
// 3. Rename in jadx-gui: right-click → Rename
// e.g., class "a" → rename → "AuthManager"
// method "a(String,String)" → rename → "validateCredentials"
// 4. Look at WHAT it calls — standard library calls reveal purpose:
// .equals() call → string comparison
// Cipher.getInstance() → encryption
// SQLiteDatabase.query() → database access
// okhttp3.Request.Builder → HTTP request
// 5. Follow data flow — track where parameters go
7.3 Identifying Key Classes Despite Obfuscation
# ─── In jadx-gui: search for known patterns ───
# Find authentication class → search "login" or "password"
Ctrl+Shift+F → "password"
# Even in obfuscated code, string literals often remain!
# → finds class that handles passwords → that IS the auth class
# Find network class → search "okhttp" or "retrofit"
Ctrl+Shift+F → "OkHttpClient"
# → finds networking wrapper class
# Find crypto class → search "AES" or "Cipher"
Ctrl+Shift+F → "AES"
# ─── Look at the manifest ───
# Activity names are NOT obfuscated (Android requires real class names)
# MainActivity → trace to → which obfuscated classes it calls
7.4 ProGuard Mapping File
# If you can get the mapping.txt from developer/CI/CD:
# It maps obfuscated names back to originals!
# mapping.txt format:
# com.target.app.AuthManager → a.b.c:
# boolean validateLogin(String,String) → a
# String hashPassword(String) → b
# Use with jadx:
jadx target.apk -d out/ --deobf --deobf-map mapping.txt
# Find leaked mapping file:
# Check: GitHub repos, CI/CD artifacts, crash reporting (Firebase Crashlytics)
# Firebase Crashlytics sometimes exposes mapping.txt at:
# https://crashlytics.com/... (requires auth but worth checking)
8. Deobfuscating Code
What it means: When manual reading is too slow, automated deobfuscation tools and techniques can reconstruct meaningful names, decrypt string encryption routines, and undo packing to give you readable code faster.
8.1 jadx Built-in Deobfuscation
# jadx can auto-rename short names (a,b,c → cls_a, mth_b, fld_c)
jadx --deobf target.apk -d output/
jadx --deobf --deobf-min 2 --deobf-max 64 target.apk -d output/
# --deobf-min 2 = rename identifiers shorter than 2 chars
# In jadx-gui: Tools → Deobfuscation → Run
# Or: check "Enable deobfuscation" in preferences
8.2 String Encryption Bypass
// Common pattern: strings stored encrypted, decrypted at runtime
// Obfuscator: DexGuard, Stringer, etc.
// In obfuscated code:
String apiKey = a.b("Xk3mP9zQ..."); // a.b() = decryption function!
// ─── Method 1: Find and run decryption offline ───
// 1. Identify the decryption method (a.b in example)
// 2. Extract the method logic from smali
// 3. Reimplement in Python and decrypt all strings
// ─── Method 2: Frida — hook at runtime ───
// Hook the decryption function → print all decrypted strings
// Frida script: hook string decryption
Java.perform(function() {
// Hook the decrypt method (found via jadx analysis)
var DecryptClass = Java.use("a.b"); // obfuscated class name
DecryptClass.a.overload("java.lang.String").implementation = function(encrypted) {
var result = this.a(encrypted);
console.log("[DECRYPT] " + encrypted + " → " + result);
return result;
};
});
// Run:
// frida -U -f com.target.app --no-pause -l decrypt_hook.js
// → All decrypted strings printed in real-time!
8.3 Deobfuscation Tools
# ─── simplify — simplify obfuscated code via execution ───
# https://github.com/CalebFenton/simplify
java -jar simplify.jar -i classes.dex -o simplified.dex --max-passes 3
# ─── Deobfuscating React Native ───
npm install -g js-beautify
js-beautify index.android.bundle > readable.js
# Unminify online: https://unminify.com
# Prettier: prettier --parser babel --write bundle.js
# ─── jadx rename workflow (manual but effective) ───
# jadx-gui → right-click class → Rename → give meaningful name
# Auto-saves to .jadx/config/rename.xml
# Propagates rename across all usages
# ─── de4dot (.NET / Xamarin obfuscation removal) ───
de4dot MyObfuscatedApp.dll -o deobfuscated.dll
de4dot *.dll --det-attrs # auto-detect obfuscator
9. Reversing Shared Objects (.so)
What it means: Native libraries (.so files) contain compiled C/C++ ARM code. They
can't be decompiled to Java — you need binary reverse engineering tools (Ghidra, IDA Pro,
Radare2). They often contain cryptographic keys, license checks, and anti-tamper logic
that developers moved to native code specifically to be harder to analyze.
9.1 Initial .so Enumeration
# ─── Extract libraries ───
unzip target.apk lib/ -d extracted/
find extracted/lib/ -name "*.so"
# lib/
# ├── arm64-v8a/libnative.so ← 64-bit ARM (most modern devices)
# ├── armeabi-v7a/libnative.so ← 32-bit ARM
# └── x86_64/libnative.so ← x86 emulator
# ─── Basic info ───
file extracted/lib/arm64-v8a/libnative.so
readelf -h libnative.so # ELF header
readelf -d libnative.so # dynamic section (imports)
readelf -s libnative.so # symbol table (function names)
# ─── String extraction (quick wins) ───
strings libnative.so | grep -i "key\|secret\|password\|token\|http\|api"
strings libnative.so | grep -E "[A-Za-z0-9+/]{20,}={0,2}" # base64
strings libnative.so | grep -E "-----BEGIN" # PEM keys
# ─── Find exported JNI functions ───
readelf -s libnative.so | grep "Java_"
nm -D libnative.so | grep "Java_"
# Output: Java_com_target_app_NativeLib_decrypt
# → class: com.target.app.NativeLib
# → method: decrypt()
9.2 Ghidra Analysis
# ─── Setup Ghidra ───
# Download: https://ghidra-sre.org
# Run: ./ghidraRun
# ─── Analysis workflow ───
# 1. File → New Project → Import libnative.so
# 2. Select ARM/Thumb2 (armeabi-v7a) or AARCH64 (arm64-v8a)
# 3. Auto-analyze: YES (takes a few minutes)
# 4. Window → Symbol Table → filter "Java_" → find JNI functions
# 5. Double-click function → Decompiler window (right side)
# 6. Read pseudo-C code
# ─── Key analysis areas ───
# a. JNI functions (Java_*) → these are called from Java
# b. Functions called by JNI functions → encryption/validation logic
# c. String references → right-click → References → show in decompiler
# d. Cross-references → who calls this function?
# ─── Rename for clarity ───
# Click function name → press L → rename
# Add comments: right-click → Set EOL Comment
9.3 Radare2 / r2 Quick Analysis
# ─── Open and analyze ───
r2 libnative.so # open
> aaa # analyze all (takes time)
> afl # list all functions
> afl | grep Java # find JNI functions
> pdf @ sym.Java_com_target_app_NativeLib_decrypt # disassemble
# ─── Find strings ───
> iz # list strings in data sections
> iz~key # filter strings containing "key"
# ─── Search for byte patterns ───
> / secret # search string "secret"
> /x 414243 # search hex bytes
# ─── Analyze with rizin (modern r2 fork) ───
rizin libnative.so
> aaa
> pdf @ main
> agf @ main # ASCII call graph
9.4 Frida — Hook Native Functions
// Hook native function at runtime — no full reversing needed!
// Method 1: Hook by JNI function name (exported symbol)
var decrypt = Module.findExportByName("libnative.so", "Java_com_target_app_NativeLib_decrypt");
Interceptor.attach(decrypt, {
onEnter: function(args) {
// args = JNIEnv*, args = jclass/jobject, args[2+] = actual params[1]
console.log("[NATIVE decrypt] arg0: " + Java.vm.getEnv().getStringUTFChars(args, null).readCString());[2]
},
onLeave: function(retval) {
console.log("[NATIVE decrypt] result: " + Java.vm.getEnv().getStringUTFChars(retval, null).readCString());
}
});
// Method 2: Hook by address (found in Ghidra/r2)
var base = Module.findBaseAddress("libnative.so");
var funcAddr = base.add(0x1234); // offset from Ghidra analysis
Interceptor.attach(funcAddr, {
onEnter: function(args) {
console.log("Function called! arg0: " + args);
}
});
10. Reversing DLL Files
What it means: Xamarin apps bundle .NET DLLs inside the APK. These are decompilable back to near-perfect C# source code using .NET decompilers — effectively giving you the full source code of the application.
10.1 Extracting DLLs from APK
# ─── Xamarin apps → assemblies/ folder ───
unzip target.apk "assemblies/*" -d extracted/
ls extracted/assemblies/
# assemblies/
# ├── TargetApp.dll ← main app code
# ├── TargetApp.Core.dll ← business logic
# ├── Xamarin.Android.dll ← framework
# ├── Newtonsoft.Json.dll ← libraries
# └── ...
# ─── Note: modern Xamarin may compress DLLs ───
# Check if files are valid .NET assemblies:
file extracted/assemblies/TargetApp.dll
# If compressed: use xamarin-decompress tool
pip3 install lz4
python3 -c "
import lz4.block, sys
data = open(sys.argv,'rb').read()[1]
if data[:4] == b'XALZ': # Xamarin compressed
decompressed = lz4.block.decompress(data[8:], uncompressed_size=int.from_bytes(data[4:8],'little'))
open(sys.argv+'.dll','wb').write(decompressed)[1]
" assemblies/TargetApp.dll
10.2 Decompiling DLLs to C
# ─── ILSpy (CLI) — best open source option ───
# Install: dotnet tool install -g ilspycmd
ilspycmd TargetApp.dll > TargetApp_source.cs
ilspycmd TargetApp.dll -o output_dir/ # full decompile to files
# ─── dnSpy (Windows GUI — best for Windows) ───
# Open TargetApp.dll → full C# source with navigation
# Can EDIT and recompile directly!
# ─── dotPeek (JetBrains, free) ───
# Open DLL → navigate → export as project → open in Rider/VS
# ─── Telerik JustDecompile (free) ───
# ─── For .NET 6+ MAUI apps ───
# Main code in: lib/arm64-v8a/libMonoPosixHelper.so (check for ILBundle)
# Use dotnet-dump or dotnet-sos for runtime analysis
10.3 Analyzing Decompiled C# Code
// After decompile — search for same patterns as Java:
// ─── Hardcoded credentials ───
private const string ApiKey = "HARDCODED_API_KEY_123";
private string _adminPassword = "admin@123";
// ─── Insecure crypto ───
using (var aes = Aes.Create()) {
aes.Key = Encoding.UTF8.GetBytes("HardcodedKey1234"); // fixed key!
aes.IV = new byte; // all-zero IV![3]
aes.Mode = CipherMode.ECB; // ECB!
}
// ─── Authentication logic ───
public bool Authenticate(string username, string password) {
// Can read full auth logic → find bypass
if (username == "debug" && password == "debug123") return true; // backdoor!
return Database.ValidateUser(username, password);
}
11. Authentication Bypass (APK Patching)
What it means: APK patching = decompile → edit Smali → recompile → resign → install. This allows you to permanently bypass security checks — SSL pinning, root detection, authentication, license checks — without needing runtime tools like Frida.
11.1 APK Patching Workflow
# ─── Full workflow ───
# Step 1: Decode
apktool d target.apk -o patched_app/
# Step 2: Edit smali files
nano patched_app/smali/com/target/app/AuthManager.smali
# Step 3: Rebuild
apktool b patched_app/ -o patched.apk
# Step 4: Sign APK (REQUIRED — unsigned APK won't install)
# Generate keystore (once)
keytool -genkey -v -keystore my.keystore -alias mykey \
-keyalg RSA -keysize 2048 -validity 10000
# Sign APK
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \
-keystore my.keystore patched.apk mykey
# Align APK
zipalign -v 4 patched.apk aligned_patched.apk
# OR: Use uber-apk-signer (easier)
java -jar uber-apk-signer.jar -a patched.apk
# Step 5: Install
adb install aligned_patched.apk
11.2 Bypassing Authentication in Smali
// Target Java code (jadx decompile):
public boolean checkLogin(String username, String password) {
String hash = MD5.hash(password);
return database.query(username, hash);
}
# In Smali — find the method, make it always return true:
# BEFORE (original):
.method public checkLogin(Ljava/lang/String;Ljava/lang/String;)Z
.registers 4
# ... complex login logic ...
invoke-static {p2}, Lcom/target/MD5;->hash(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
invoke-virtual {p0, p1, v0}, Lcom/target/DB;->query(...)Z
move-result v0
return v0
.end method
# AFTER (patched — always returns true):
.method public checkLogin(Ljava/lang/String;Ljava/lang/String;)Z
.registers 4
const/4 v0, 0x1 # v0 = true
return v0 # always return true!
.end method
11.3 Bypass Login Screen (Activity Export)
<!-- AndroidManifest.xml patch: export admin activity -->
<!-- BEFORE: -->
<activity android:name=".AdminActivity"
android:exported="false"/>
<!-- AFTER (patch in decompiled manifest): -->
<activity android:name=".AdminActivity"
android:exported="true"/>
<!-- Rebuild → now accessible directly! -->
<!-- adb shell am start -n com.target.app/.AdminActivity -->
11.4 Bypass PIN / OTP Check
# Find the PIN validation method in smali
# e.g., method verifyPin(String)Z
# BEFORE:
.method public verifyPin(Ljava/lang/String;)Z
# ... comparison logic ...
invoke-virtual {v0, p1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
return v0
# AFTER (always true):
.method public verifyPin(Ljava/lang/String;)Z
.registers 2
const/4 v0, 0x1
return v0
.end method
12. Modifying Game Apps
What it means: Game apps are the most common patching target — modifying gold/coins values, unlocking premium features, removing ads, enabling god mode. The same APK patching technique applies — find the value or check in Smali, patch it.
12.1 Finding Game Values
# ─── Search for in-app purchase / premium checks ───
grep -rn "isPremium\|isPurchased\|isUnlocked\|hasCoins\|goldCount" java_out/
# ─── Common patterns ───
grep -rn "getCoins\|setCoins\|mGold\|mCoins\|mScore" java_out/
grep -rn "inAppPurchase\|billing\|BillingClient" java_out/
# ─── Search in smali ───
grep -rn "isPremium\|isUnlocked" patched_app/smali/ -l
# → com/target/game/PremiumManager.smali
12.2 Patching Game Values in Smali
// Target Java:
public boolean isPremiumUser() {
return this.sharedPrefs.getBoolean("is_premium", false);
}
public int getCoins() {
return this.sharedPrefs.getInt("coins", 0);
}
# Patch isPremiumUser() → always return true
.method public isPremiumUser()Z
.registers 2
const/4 v0, 0x1 # true
return v0
.end method
# Patch getCoins() → always return 999999
.method public getCoins()I
.registers 2
const v0, 0x000F423F # 999999 in hex
return v0
.end method
12.3 Memory Modification (Runtime — GameGuardian)
For games with server-side validation (can't just patch):
GameGuardian (root required):
1. Install GameGuardian
2. Attach to game process
3. Search current value (e.g., 100 coins)
4. Spend coins → now 95 → search 95
5. Narrow down → find memory address
6. Edit value → 999999
Limitations:
Server-side validation → coins reset on sync
Memory protection → encrypted values
Anti-cheat → ban detection
13. License Verification Bypass
What it means: Apps use Google Play Licensing (LVL) or custom license checks to verify the app was legitimately purchased. These checks happen in code — in Smali they are just conditional branches that can be flipped. Bypassing means patching the verification result to always return "licensed."
13.1 Google Play LVL Bypass
// LVL code pattern in Java:
mLicenseChecker.checkAccess(this);
// LicenseCheckerCallback:
public void allow(int policyReason) {
// License valid → unlock app
unlockFeatures();
}
public void dontAllow(int policyReason) {
// Not licensed → show purchase screen
showPurchaseDialog();
}
# Find: dontAllow() method → patch to call unlockFeatures() instead
# OR: Find: checkAccess() → patch to always call allow()
# Method 1: Make dontAllow behave like allow
.method public dontAllow(I)V # patched
.registers 2
invoke-virtual {p0}, Lcom/target/app/MainActivity;->unlockFeatures()V
return-void
.end method
# Method 2: Skip license check entirely
# Find the checkAccess() invocation in onCreate():
# BEFORE:
invoke-virtual {v0, p0}, Lcom/android/vending/licensing/LicenseChecker;->checkAccess(Lcom/android/vending/licensing/LicenseCheckerCallback;)V
# AFTER: comment out (replace with nop or remove the invoke):
# (delete the invoke line + move-result line)
13.2 Custom License Check Bypass
# App has custom: boolean isLicensed()
# In smali — find and patch to return true
# Search:
grep -rn "isLicensed\|checkLicense\|verifyLicense\|isPaid" smali/ -l
# Patch:
.method public isLicensed()Z
.registers 2
const/4 v0, 0x1 # always licensed!
return v0
.end method
13.3 In-App Purchase Check Bypass
# Find: isPurchased(String productId)Z or hasBought()Z
# BEFORE:
.method public isPurchased(Ljava/lang/String;)Z
.registers 3
# ... complex purchase validation ...
# reads from server or local receipt
# returns 0 (false) if not purchased
# AFTER:
.method public isPurchased(Ljava/lang/String;)Z
.registers 2
const/4 v0, 0x1
return v0
.end method
14. Root Detection Bypass (Static)
What it means: Apps detect root to prevent tampering and bypass. Static bypass (APK patching) permanently removes the root check from the code — unlike Frida which requires runtime injection every time. Once patched and signed, the app runs without ever performing root checks.
14.1 Finding Root Detection Code
# ─── Search in jadx output ───
grep -rn "isRooted\|isDeviceRooted\|checkRoot\|detectRoot" java_out/
grep -rn "su\"\|/su\|superuser\|SuperUser\|Magisk\|magisk" java_out/
grep -rn "Build.TAGS\|test-keys" java_out/
grep -rn "RootBeer\|rootbeer" java_out/ # popular root detection library
# ─── In smali ───
grep -rn "isRooted\|checkRoot\|/su\|superuser" smali/ -l
14.2 Common Root Detection Patterns to Patch
// Pattern 1: File existence check
boolean isRooted() {
String[] paths = {"/system/bin/su", "/system/xbin/su",
"/sbin/su", "/su/bin/su"};
for (String path : paths) {
if (new File(path).exists()) return true;
}
return false;
}
// Pattern 2: Build tags check
if (Build.TAGS != null && Build.TAGS.contains("test-keys")) {
// rooted!
}
// Pattern 3: Package check
try {
getPackageManager().getPackageInfo("com.topjohnwu.magisk", 0);
return true; // Magisk installed = rooted!
} catch (PackageManager.NameNotFoundException e) {
return false;
}
# ─── Patch isRooted() → always return false ───
.method public isRooted()Z
.registers 2
const/4 v0, 0x0 # false — not rooted!
return v0
.end method
# ─── Patch the if-check that uses isRooted() ───
# BEFORE: if-nez v0, :rooted_detected (if rooted, jump to block)
# AFTER: # (delete the if-nez line — fall through to normal code)
# OR: if-eqz v0, :rooted_detected (flip condition — never triggers)
# ─── Patch RootBeer library call ───
# Find: invoke-virtual {v0}, Lcom/scottyab/rootbeer/RootBeer;->isRooted()Z
# Follow the result and patch the branch that follows:
# BEFORE:
invoke-virtual {v0}, Lcom/scottyab/rootbeer/RootBeer;->isRooted()Z
move-result v1
if-nez v1, :cond_rooted # if rooted != 0 → detected!
# AFTER: change if-nez to if-eqz to flip condition
if-eqz v1, :cond_rooted # condition never triggers normally
14.3 Patch Emulator Detection
# Same pattern — find isEmulator() method and return false
# Common emulator checks:
grep -rn "isEmulator\|Build.FINGERPRINT\|Build.MODEL\|goldfish" smali/ -l
# Patch similarly:
.method public isEmulator()Z
.registers 2
const/4 v0, 0x0 # not an emulator!
return v0
.end method
14.4 Multi-Bypass Smali Patch Summary
Method Smali Pattern to Find Patch
───────────────────────────────────────────────────────────────────
isRooted() invoke..->isRooted()Z return false
isEmulator() invoke..->isEmulator()Z return false
isPremium() invoke..->isPremium()Z return true
isLicensed() invoke..->isLicensed()Z return true
checkPin(String) invoke..->checkPin(..)Z return true
verifySignature() invoke..->verifySignature()Z return true
isDebuggingEnabled() invoke..->isDebugging()Z return false
15. Cheat Sheet
APK Patching Full Workflow
# 1. Decode
apktool d target.apk -o out/
# 2. Find target
grep -rn "isRooted\|isPremium\|checkLicense" out/smali/ -l
# Open in jadx-gui → find method → note smali path
# 3. Edit smali
nano out/smali/com/target/app/SecurityManager.smali
# Patch: return true/false as needed
# 4. Rebuild
apktool b out/ -o patched.apk
# 5. Sign
java -jar uber-apk-signer.jar -a patched.apk --out signed/
# 6. Install
adb install -r signed/patched-aligned-debugSigned.apk
# 7. Verify
adb logcat | grep com.target.app
Smali Return Patches
# Return true (boolean):
const/4 v0, 0x1
return v0
# Return false (boolean):
const/4 v0, 0x0
return v0
# Return 0 (int):
const/4 v0, 0x0
return v0
# Return 999999 (int):
const v0, 0x000F423F
return v0
# Return null (object):
const/4 v0, 0x0
return-object v0
# Return void:
return-void
Tool Quick Reference
# Decompile
apktool d app.apk -o out/ # smali + resources
jadx-gui app.apk # Java source GUI
# String hunting
apkleaks -f app.apk -v # secrets + endpoints
strings lib/*.so | grep -i key # native strings
grep -rni "password\|secret" out/ # smali/java search
# Native analysis
readelf -s lib.so | grep Java_ # JNI exports
strings lib.so | grep -i "key" # quick strings
ghidraRun → import .so → analyze # full decompile
# Rebuild + sign
apktool b out/ -o patched.apk
java -jar uber-apk-signer.jar -a patched.apk
# Frida (runtime hook)
frida -U -f com.target.app --no-pause -l hook.js
objection -g com.target.app explore → android root disable
Smali Type Reference
Z → boolean I → int J → long (wide)
B → byte F → float D → double (wide)
S → short V → void C → char
L