Decompiling DEX bytecode to readable Java/Kotlin

Kotlin code compiles to Dalvik Executable (DEX) bytecode. During a penetration test, we convert this bytecode into readable Java/Kotlin using decompilers like Jadx or Apktool. Obfuscation tools like R8 rename structural identifiers, but do not prevent us from mapping the code's execution flow:

# Decompile APK to Java classes and resource files
jadx -d decompiled_source app-release.apk
# Search for API endpoints and sensitive strings in decompiled code
grep -rnw "decompiled_source/sources" -e "api.simpalabs.com"

Bypassing native root detection and SSL pinning

Native Kotlin applications implement security controls to prevent debugging. We bypass common root detection checks (like RootBeer library integration) and custom SSL certificate pinning in the application's runtime. Using Frida, we hook the target classes and force them to return clean states:

Java.perform(function () {
  // Hook and bypass typical native root checker implementation
  const RootCheck = Java.use("com.scottyab.rootbeer.RootBeer");
  RootCheck.isRooted.implementation = function () {
    console.log("[Android Audit] Bypassing RootBeer detection class.");
    return false;
  };
});

Auditing Android Biometric Prompt implementation

A critical vulnerability in many banking apps is the weak enforcement of BiometricPrompt callbacks. If the application logic simply waits for the success callback to unlock the UI without validating a cryptographic signature, we hook the authentication listener to bypass fingerprint checks:

Java.perform(function () {
  const BiometricPrompt = Java.use("androidx.biometric.BiometricPrompt$AuthenticationResult");
  const CryptoObject = Java.use("androidx.biometric.BiometricPrompt$CryptoObject");
  
  // If the developer failed to pass a CryptoObject to the authenticate call,
  // we can instantiate an empty result and force the callback to trigger.
  console.log("[Android Audit] Hooking Biometric AuthenticationResult.");
});

The Fix: Developers must pass a BiometricPrompt.CryptoObject wrapped around a Cipher initialized with a KeyStore key. The key must require user authentication, forcing the hardware security module (HSM) to decrypt session secrets only upon successful biometric input.

Secure local storage (EncryptedSharedPreferences)

Traditional SharedPreferences write data in plain text XML files at `/data/data/package/shared_prefs`. For secure applications, developers must use Jetpack Security's EncryptedSharedPreferences. We inspect the app storage folder to verify if the MasterKey provider has been configured securely inside the Android Keystore:

# Connect to device shell and check local shared preferences XML
adb shell
cat /data/data/com.fintech.app/shared_prefs/secret_prefs.xml
Real-world finding

Biometric bypass on seed-stage wallet app

During an audit of a digital wallet app, we bypassed the biometric fingerprint screen in 30 seconds using a Frida script. The app executed the user interface transition directly inside the onAuthenticationSucceeded callback without verifying any cryptographic signature or KeyStore secrets. Fix priority: high. Remediated by binding the access token decryption to a hardware-backed CryptoObject.

Building a native Android application in Kotlin? Ensure your local controls are secure.

Book an Android Pentest

Frequently asked questions

How does ProGuard/R8 obfuscation affect Android security audits?

ProGuard and R8 rename classes, fields, and methods to short strings (a, b, c), which increases the time required for static analysis. However, it does not alter the application logic or API endpoints, which we verify through dynamic runtime interception and network analysis.

Why is native biometric authentication vulnerable in Android?

If the application checks biometrics purely by evaluating a true/false return value from the BiometricPrompt API, an auditor can use Frida to hook the callback and force it to return success, bypassing physical biometric checks.

How do you test Kotlin Coroutines for concurrency issues?

We audit Kotlin Coroutines by inspecting sharing of mutable state and validating thread-safe access controls on core wallet balances to prevent race conditions during concurrent API requests.

Related reading

Blog: React Native pentesting guide · Reverse engineering Android apps · Flutter security checklist

Services: Mobile penetration testing · Secure architecture review