Protecting mobile applications from reverse engineering
APK is a zip archive. apktool d app.apk disassembles it in seconds, jadx turns bytecode into readable Java. Without obfuscation, attacker sees class names, method names, logic of license check, API endpoint URLs, hardcoded strings. On iOS the situation is not much better: class-dump recovers Objective-C class headers, Ghidra disassembles native code.
Code obfuscation
Android. R8 (built into Android Gradle Plugin) — minimal entry point. Enabled via minifyEnabled = true in build.gradle and renames classes, methods, fields to a, b, c. But R8 doesn't encrypt strings. Serious string protection requires separate tool.
ProGuard rules must be written manually for each used library — otherwise reflection, Gson serialization, Firebase breaks. Typical pain: @SerializedName on model fields doesn't help if the class itself is renamed to a, and Gson can't create it via reflection. Solution — @Keep or explicit -keep rules for data classes passed through API.
Commercial obfuscators — DexGuard, iXGuard (from Guardsquare) — go further: string encryption, control flow obfuscation (inserting false branches and goto), resource protection, anti-tamper checks. DexGuard integrates into Gradle as plugin, config resembles ProGuard, but capabilities are orders of magnitude greater. Used in banking apps.
iOS. Obfuscating Swift/Objective-C binary is harder — no intermediate bytecode. Options: obfuscator-llvm (LLVM pass for control flow flattening) or commercial iXGuard. Swift function names in public API can't be hidden without ABI consequences, but private logic can be obfuscated.
Flutter. Dart compiles to machine code, which itself provides some protection. reFlutter — tool for Flutter app reverse engineering — can recover names from snapshot if app compiled without strip symbols. Enable in build: flutter build apk --obfuscate --split-debug-info=./symbols. Debug symbols go to separate file, not included in release.
String protection
Hardcoded URLs, keys, server names — first thing strings searches on binary. Minimum — don't store them in code at all (extract to remote config, e.g. Firebase Remote Config). If string must be in app — encrypt and decrypt at runtime via native code (JNI/NDK on Android), so plaintext doesn't show in dex.
Pattern: strings stored as XOR-encrypted byte array, XOR key split into parts in different native functions. Not cryptographically strong, but raises entry threshold.
Anti-debug techniques
ptrace(PTRACE_TRACEME, 0, 0, 0) on Android/Linux — process becomes tracee itself, second ptrace attach from debugger returns error. On iOS equivalent — PT_DENY_ATTACH. Frida bypasses this (not via ptrace, but via task_for_pid), but adds complexity.
Check via /proc/self/status on Android: TracerPid != 0 means debugger attached to process. Framework appdome automatically adds such checks without code changes.
Anti-Frida: Frida injects frida-agent into process via frida-gadget. Detect via checking loaded libraries (/proc/self/maps), presence of frida-server port (27042), filenames in /proc/self/fd. But userspace Frida detection Frida itself bypasses — arms race.
Checking application integrity
On Android verify APK signature at runtime: PackageManager.getPackageInfo() returns signatures list. Hash them and compare with hardcoded value. If signature changed — app is repackaged. Hide the check itself in native code, otherwise trivially patched in smali.
Google Play Integrity API (replacement for SafetyNet Attestation) — more reliable: server receives signed token from Google with verdict MEETS_DEVICE_INTEGRITY, MEETS_STRONG_INTEGRITY. Can't forge without Google servers. Downside — requires network request and API quota.
On iOS — DCAppAttestService (App Attest). Similar mechanism: device gets attestation from Apple, server verifies via Apple API.
Native code and React Native
Business logic that can't be disclosed, move to NDK (Android) or native framework (iOS). Native code is harder to reverse-engineer than Kotlin/Java or JavaScript bundle.
React Native stores all JavaScript in index.android.bundle — regular file inside APK, readable by any JS formatter. Protection: Hermes bytecode (compile via --hermes), bundle encryption via native module that decrypts in memory before loading into engine. Metro Bundler supports custom serializer for this.
Timelines
Basic R8/ProGuard setup with string protection and anti-tamper checks — 3–5 days. DexGuard/iXGuard integration with fine configuration tuning — up to 2 weeks (many iterations to avoid runtime breaking). Native migration of critical logic — separate assessment by scope.







