Client-side root detection is not a security boundary; it is a speed bump. If your application relies on local Java or Native checks to halt execution on a compromised Android device, you are trusting an environment controlled entirely by the user you are trying to restrict.
Static binary patching remains one of the cleanest ways to bypass these checks. While dynamic instrumentation engines like Frida get all the press, statically modifying Smali code creates a permanently modified binary that runs cleanly without needing a specialized runtime environment or hooking framework active at runtime. Here is how to do it step-by-step, along with the engineering reality defenders must accept.
Disassembling the Target Binary
To modify an Android application, you first need to disassemble its compiled DEX files into an editable intermediate representation. Java source code compiles to bytecode inside .dex files, which standard decompilers like jadx render back into readable Java. However, to rebuild the application reliably without source code, we patch at the byte-code representation level: Smali.
Download apktool and unpack your target binary:
apktool d target_app.apk -o decompiled_app
This generates a project folder containing the application manifest, raw resources, and one or more smali directories (smali, smali_classes2, etc.).
Locating the Root Check Logic
Developers generally implement root checks using one of three methods: scanning the filesystem for known binaries (/system/xbin/su, /system/app/Superuser.apk), checking build tags for test-keys, or executing which su via Runtime.getRuntime().exec().
Search the decompiled directory for common detection strings or method signatures:
grep -rn "Superuser" decompiled_app/smali*
grep -rn "test-keys" decompiled_app/smali*
grep -rn "/system/xbin/su" decompiled_app/smali*
If the string searches return hits, open the corresponding .smali file in a text editor. If the code is obfuscated, look for methods returning a boolean (Z) that call methods checking system properties or file existence. You will often find a structure similar to this:
.method public static isRooted()Z
.registers 2
# [File checks or command execution logic here]
:cond_0
const/4 v0, 0x1
return v0
:cond_1
const/4 v0, 0x0
return v0
.end method
In Smali, const/4 v0, 0x1 assigns the boolean value true to register v0, while const/4 v0, 0x0 assigns false.
Flipping the Logic in Smali
Once you have located the target method, you do not need to understand every instruction in the check routine. You simply need to force the method to always return false (0x0) immediately upon invocation.
Edit the .method block directly:
.method public static isRooted()Z
.registers 1
const/4 v0, 0x0
return v0
.end method
By removing the intermediate file checks and returning 0x0 on the first line, the app instantly receives a clean bill of health whenever it asks if the device is rooted. Save the file.
Rebuilding, Aligning, and Signing
An unpacked and edited directory cannot be installed directly; it must be recompiled into an APK, aligned for RAM efficiency, and signed with a valid cryptographic key.
First, recompile the directory with apktool:
apktool b decompiled_app -o patched_unaligned.apk
Next, use zipalign to align uncompressed data on 4-byte boundaries. Android’s package manager requires this step for optimized memory mapping:
zipalign -v -p 4 patched_unaligned.apk patched_aligned.apk
Finally, sign the app. Android requires all APKs to be signed, even if it is just with a self-signed key. Generate a throwaway keystore if you do not already have one:
keytool -genkey -v -keystore dev_key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias patchkey
Sign the aligned package using apksigner (found in your Android SDK build-tools directory):
apksigner sign --ks dev_key.jks --ks-key-alias patchkey --out patched_final.apk patched_aligned.apk
You can now push patched_final.apk onto a rooted device or emulator using adb install patched_final.apk. The application will launch, run its “root check,” and proceed normally without alerting the user or exiting.
What This Means for Defenders
If your security architecture relies on an APK executing logic on an untrusted client to verify its own integrity, your security model is broken.
Static patching is the absolute baseline of reverse engineering. When defenders add heavy obfuscation (like DexGuard) or split checks into compiled C/C++ shared libraries (.so files) via the NDK, they merely raise the required skill floor from “basic Smali editing” to “Ghidra/IDA Pro binary patching.” The fundamental flaw remains unchanged: the client hardware is under the adversary’s physical control.
If your application handles high-value transactions, sensitive financial data, or critical infrastructure, stop relying on client-side boolean checks. Instead:
- Use Hardware-Backed Integrity APIs: Pivot to server-validated attestations like Google’s Play Integrity API. These frameworks use hardware-backed key stores (TEE/StrongBox) to pass signed, encrypted payloads to your backend server, proving the binary identity and device state off-device.
- Shift Logic to the API: Do not execute business logic locally and trust the output. Require backend authorization tokens that are only issued after successfully validating device attestation server-side.
- Assume an Attacker Has Source-Level Control: Write code under the assumption that the user running it has full read, write, and execute access to every memory register, instruction, and key stored on the local disk. Because they do.
Related content
Android Pentesting Lab Setup: System CAs, Writable Storage, and Frida
ResearchBeyond Exported Flags: Securing Android Deep Links and Component Boundaries
ResearchDefeating Android Certificate Pinning: From Java Hooks to Native BoringSSL Bypass
Security NewsInside the Splintered Underground Market of the BTMOB Android RAT
Want a second set of eyes on your security posture?
Let's talk about where your real exposure is.
Book an advisory call