Skip to main content

04 - Reverse Engineering and Tampering

[!IMPORTANT] Security Baseline (OWASP MASVS-RESILIENCE): Local client-side anti-tampering heuristics (such as root checks or debugger detectors) can always be hooked and bypassed by an attacker using Frida. Production mobile applications must combine binary obfuscation with hardware-backed server-side remote attestation (Google Play Integrity API and Apple App Attest).


The Reverse Engineering Lifecycle​

Reverse engineering is the process of decompiling, disassembling, and analyzing compiled mobile binaries to discover vulnerabilities, bypass security controls, or extract proprietary business logic.

1. Static Analysis Techniques​

  • Android DEX Decompilation: APK archives contain .dex (Dalvik Executable) files. Tools like JADX decompile .dex files into near-original Kotlin/Java source code.
  • Android Smali Analysis: APKTool disassembles .dex into Smali (an assembly-like representation of Dalvik bytecode), allowing attackers to edit instructions and rebuild signed APKs.
  • iOS Binary Decryption & Disassembly: Apps downloaded from the App Store are encrypted using Apple FairPlay DRM. Attackers execute the app on a jailbroken device, dump decrypted memory using tools like frida-ios-dump, and disassemble the resulting Mach-O binary in Ghidra or Hopper.

2. Dynamic Analysis & Instrumentation​

  • Function Hooking (Frida): Injecting JavaScript runtime hooks into memory addresses or class methods to intercept input parameters, modify return values, or bypass control flow statements.
  • Debugger Attachment: Attaching lldb or gdb using OS debugging interfaces (ptrace on Linux/Android, sysctl on iOS) to step through memory execution instructions.

Defensive Countermeasures​

1. Code Obfuscation & Shrinking​

Code obfuscation transforms human-readable class names, methods, and variables into non-descriptive identifiers, flattens control flow, and encrypts hardcoded strings.

Android ProGuard / R8 Configuration​

R8 is Android's default compiler for code shrinking and obfuscation. Add strict rules to proguard-rules.pro:

# Enable aggressive optimizations and obfuscation
-repackageclasses 'a'
-allowaccessmodification
-flattenpackagehierarchy 'a'

# Strip debug attributes and source file attributes
-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable

# Obfuscate all custom application packages
-keep class com.example.app.security.** { *; }

# Encrypt strings (Requires commercial DexGuard / GuardSquare extension)

2. Root & Jailbreak Detection Heuristics​

While client-side heuristics can be bypassed by Frida, implementing multi-layered checks raises the bar for automated attack scripts.

Common Android Root Indicators​

  • Presence of su binaries under /system/bin/, /system/xbin/, /sbin/, or /vendor/bin/.
  • Presence of root management packages (e.g., com.topjohnwu.magisk, eu.chainfire.supersu).
  • Test-keys build tags (android.os.Build.TAGS.contains("test-keys")).
  • Ability to write files to system directories outside the app sandbox.

Common iOS Jailbreak Indicators​

  • Presence of Cydia, Sileo, Zebra, or Installer package paths (/Applications/Cydia.app, /var/binpack).
  • Presence of dynamic hooking frameworks (/Library/MobileSubstrate/MobileSubstrate.dylib, ElleKit).
  • Ability to open restricted URL schemes (cydia://, sileo://).
  • Ability to write files outside the sandbox (/private/jailbreak_test.txt).

Hardware-Backed Remote Attestation​

To achieve true resilience against root/jailbreak and dynamic tampering, mobile applications must defer integrity verdicts to hardware attestation services provided by Google and Apple.


Android Play Integrity API Implementation​

The Google Play Integrity API generates a cryptographically signed token containing evaluation verdicts for app licensing, device integrity, and application identity.

Client-Side Request (Kotlin)​

import android.content.Context
import com.google.android.gms.tasks.Task
import com.google.android.play.core.integrity.IntegrityManagerFactory
import com.google.android.play.core.integrity.IntegrityTokenRequest
import com.google.android.play.core.integrity.IntegrityTokenResponse

fun requestPlayIntegrityToken(
context: Context,
serverNonce: String,
onSuccess: (String) -> Unit,
onError: (Exception) -> Unit
) {
// Step 1: Initialize Integrity Manager
val integrityManager = IntegrityManagerFactory.create(context)

// Step 2: Build Token Request using server-provided Nonce
val request = IntegrityTokenRequest.builder()
.setNonce(serverNonce) // Must be a cryptographically random, fresh nonce from backend
.build()

// Step 3: Request Integrity Token
val task: Task<IntegrityTokenResponse> = integrityManager.requestIntegrityToken(request)
task.addOnSuccessListener { response ->
val token = response.token()
onSuccess(token)
}.addOnFailureListener { exception ->
onError(exception)
}
}

Backend Token Verification (Python)​

from google.oauth2 import service_account
from googleapiclient.discovery import build
import json

def verify_play_integrity_token(integrity_token: str, expected_package: str, expected_nonce: str) -> bool:
"""
Verifies Play Integrity Token via Google Play Developer API.
"""
SCOPES = ['https://www.googleapis.com/auth/playintegrity']
SERVICE_ACCOUNT_FILE = 'service_account_credentials.json'

# Authenticate with Google Play API
creds = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
service = build('playintegrity', 'v1', credentials=creds)

# Call decodeIntegrityToken
body = {'integrityToken': integrity_token}
request = service.v1().decodeIntegrityToken(packageName=expected_package, body=body)
response = request.execute()

token_payload = response.get('tokenPayloadExternal', {})
request_details = token_payload.get('requestDetails', {})
app_integrity = token_payload.get('appIntegrity', {})
device_integrity = token_payload.get('deviceIntegrity', {})

# 1. Validate Nonce freshness
if request_details.get('nonce') != expected_nonce:
print("[!] Nonce mismatch!")
return False

# 2. Validate App Package Name and Licensing
if app_integrity.get('appRecognitionVerdict') != 'PLAY_RECOGNIZED':
print("[!] Unrecognized app binary!")
return False

# 3. Validate Device Integrity Verdict
device_verdicts = device_integrity.get('deviceRecognitionVerdict', [])
if 'MEETS_DEVICE_INTEGRITY' not in device_verdicts:
print("[!] Device failed integrity check (Rooted/Emulated/Compromised)")
return False

return True

iOS DeviceCheck & App Attest API Implementation​

Apple's App Attest API creates an asymmetric keypair inside the Secure Enclave and registers the public key with Apple servers to attest to app authenticity.

Client-Side App Attest Key Generation (Swift)​

import DeviceCheck
import Foundation

class AppAttestManager {

let service = DCAppAttestService.shared

func attestDevice(serverChallenge: Data, completion: @escaping (Result<(String, Data), Error>) -> Void) {
guard service.isSupported else {
completion(.failure(NSError(domain: "AppAttest", code: -1, userInfo: [NSLocalizedDescriptionKey: "AppAttest not supported"])))
return
}

// Step 1: Generate Keypair in Secure Enclave
service.generateKey { keyId, error in
if let error = error {
completion(.failure(error))
return
}

guard let keyId = keyId else { return }

// Step 2: Attest Key against Server Challenge
let challengeHash = Data(SHA256.hash(data: serverChallenge))
self.service.attestKey(keyId, clientDataHash: challengeHash) { attestationObject, error in
if let error = error {
completion(.failure(error))
return
}

if let attestationObject = attestationObject {
completion(.success((keyId, attestationObject)))
}
}
}
}
}

Runtime Application Self-Protection (RASP)​

RASP represents native defense-in-depth security embedded directly into the application binary. RASP solutions continuously audit runtime behavior:

  1. Memory Integrity Checks: Periodically validating memory checksums of native code segments to detect runtime bytecode patches.
  2. Ptrace Attachment Block: Issuing ptrace(PTRACE_TRACEME, 0, 1, 0) on startup. Since only one debugger can attach to a process at a time, this prevents attackers from attaching gdb or lldb.
  3. Frida Named Pipe Detection: Scanning active file descriptors and TCP sockets for standard Frida instrumentation ports (27042) and named pipes (/data/local/tmp/frida-*).
  4. Active Termination: If a runtime compromise is detected, the RASP engine immediately clears cryptographic key material from memory and cleanly terminates the process.