In the previous blog, we bypassed root detection by forcing each method to return false immediately. But what if we don’t touch the return statement at all — and instead, manipulate the loop itself?
In this blog, we are using OWASP UnCrackable Level 1 — an intentionally vulnerable Android application designed for security testing and research.
What is Loop skipping
Instead of modifying the return value, we manipulate the loop counter itself — so the loop never executes in the first place.
If this feels unfamiliar, don’t worry — everything will make sense by the end of this walkthrough.
I will not waste your time by explaining and giving all pre requisite because i had already explained in my previous blog, if you are not read yet, Please do read, visit this url: https://cybersecasad.com/blogs/bypass-root-with-smali-1/
Walkthrough
Look this C class in jadx gui, and we can clear see that we have three method in which method a() and C () is a loop where as method b() is not a loop.
Lets directly jump into the Smali code where we can see that v2 is set to false by default and v3 is set to 0.
What is the Loop Doing?
`array-length v1, v0 # v1 = total number of PATH entries const/4 v2, 0x0 # v2 = 0 (default return value = false) const/4 v3, 0x0 # v3 = 0 (loop counter — starts from 0)
:goto_0 if-ge v3, v1, :cond_1 # if v3 >= v1 → exit loop`
What is v2?
v2 is the loop’s default return value. If the loop completes fully and the su binary is not found in any PATH entry, the method returns v2 — which is false. Think of it as the “nothing found” flag.
Important Note:
v2never changes throughout the entire loop — it always stays0x0(false).
What is v3?
v3 is the loop counter — it starts at 0 and increases by 1 after every round.
The loop’s job is simple — check every PATH entry one by one and look for the su binary.
How the Loop Works — Step by Step
if-ge v3, v1, :cond_1 # if v3 >= v1 → jump to cond_1
This is the line of code which runs the code into the loop.
On our emulator, the PATH has 9 entries. So the loop runs like this
v1 = 9 (total PATH entries)
v3 = 0 (loop counter starts at 0)
Round 1: v3=0, v1=9 → 0 >= 9? NO → loop continues
Round 2: v3=1, v1=9 → 1 >= 9? NO → loop continues
Round 3: v3=2, v1=9 → 2 >= 9? NO → loop continues
Round 4: v3=3, v1=9 → 3 >= 9? NO → loop continues
Round 5: v3=4, v1=9 → 4 >= 9? NO → loop continues
Round 6: v3=5, v1=9 → 5 >= 9? NO → loop continues
Round 7: v3=6, v1=9 → 6 >= 9? NO → loop continues
Round 8: v3=7, v1=9 → 7 >= 9? NO → loop continues
Round 9: v3=8, v1=9 → 8 >= 9? NO → loop continues
Round 10: v3=9, v1=9 → 9 >= 9? YES → cond_1 → return false!
All 9 paths checked — su not found anywhere — returns false.
What Does Our Patch Do?
# Original const/4 v3, 0x0 # v3 = 0 (start from beginning)
Patched
move v3, v1 # v3 = v1 (copy v1's value into v3)
move v3, v1 means:
“Copy whatever value is stored in v1 — and put it into v3“
So now with v3 = 9 and v1 = 9:
if-ge v3, v1, :cond_1 # if v3 >= v1 → jump to cond_1
Round 1: v3=9, v1=9 → 9 >= 9? YES → cond_1 → return false!
The loop exits on the very first check — not a single PATH entry was inspected! ✅
This is exactly why our patch works — as soon as the loop exits on the first check, it returns v2 which is already false!
Real Life Analogy
Think of it like a security guard assigned to check 9 rooms for an intruder:
Original behavior:
- Start from Room 1 → check Room 2 → check Room 3 → … → check Room 9 → “All clear!”
Patched behavior (move v3, v1):
- The guard walks straight to Room 9 → stands outside the door → writes in the report “All rooms checked — nothing found!”
Not a single room was actually inspected. But as far as the system is concerned — the job is done!
Summary
| Code | Value | Loop Executed? |
|---|---|---|
Original const/4 v3, 0x0 | v3 = 0 | Yes — all 9 paths checked |
Patched move v3, v1 | v3 = v1 = 9 | No — exited on first check! |
If you look closely at the three methods, you will notice something important — a() and c() use a loop, but b() does not.
This is a critical observation because our Loop Skip technique only works on methods that have a loop. Since b() has no loop, we need a different approach.
Why b() is Different?
Before applying the patch, let’s understand why we cannot use the Loop Skip technique on b().
Take a look at b() in JADX-GUI:
public static boolean b() {
String str = Build.TAGS;
return str != null && str.contains("test-keys");
}
Unlike a() and c(), method b() does not use a loop at all. It simply checks two conditions:
- Is
Build.TAGSnull? - Does
Build.TAGScontain the string"test-keys"?
There is no loop counter, no v3, no goto — nothing to manipulate.
Key Insight: The Loop Skip technique works by manipulating the loop counter
v3. Sinceb()has no loop and nov3, this technique simply cannot be applied here.
This is an important lesson in Smali patching — always analyze the code structure before choosing a technique. A patch that works perfectly on one method may be completely useless on another.
For b(), we fall back to our Technique 1 — Early Return from the previous blog:
const/4 v0, 0x0 return v0
In method c(), we apply the exact same patch as a() — replace const/4 v3, 0x0 with move v3, v1. Save the file once done.
Let’s recompile the patched APK
Command: apktool b .\\UnCrackable-Level1\\ -o .\\UnCrackable-Level1-R2.apk
Time to sign the APK
Note: here i had created a shortcut of uber-apk-signer jar file.
Finally, install the patched APK
As soon as we open the app — no root detection dialog! The app launches normally on our rooted emulator. Root detection successfully bypassed using the Loop Skip technique!
Key Takeaways
- Smali is powerful — you don’t need to be a Frida expert to bypass security mechanisms. Understanding bytecode gives you direct control over app behavior.
- Root detection is not foolproof — with just 2 lines of Smali code, we completely disabled all three root checks.
- Early Return is the simplest technique — force the method to return
falsebefore any check even runs. - APKTool + Uber APK Signer is a powerful combo — decompile, patch, recompile and sign in minutes.
What’s Next?
This was Technique 2 — Loop Skip. We manipulated the loop counter v3 to make the loop exit immediately on the very first check — without touching the return statement at all.
But we are not done yet. In the next blog, we will take an even more surgical approach — a single line change, targeting the jump condition itself. Instead of manipulating what the loop does, we will control where it goes.
Same result. One line. Different strategy!
Stay tuned! 🔐