//Freeda — Not Root
Challenge summary
-
Category: Android / Java + obfuscated logic (medium)
-
Goal: find the password to open the vault (flag format:
^Hero{\S+}$). -
Files provided:
app-release1.apk(decompiled asapktool_out_1/).
Short summary of approach
-
This app moved more of its logic into an obfuscated
Vaultclass, but the algorithm is still accessible via smali. -
I identified the Base64-encoded payload and a deterministic
seed()(a constant-return helper in this APK), reproduced the byte-transforms (shuffle+rotate+xor) and decoded the final ASCII flag.
What I looked for first
-
Search for
get_flag,Vault, orCheckFlagin the smali. Foundcom.heroctf.freeda2.utils.VaultandCheckFlagforwarding calls. -
Vaultcontains functionsE()(builds/decodes a base64 blob),K()(returns a constant seed),P()(permutation),B()(split seed into bytes).get_flag()stitches everything.
Static analysis notes (high level)
-
E()concatenates static strings and Base64-decodes to a byte array. -
K()returns the constant0x5f9d7bc3(seed). -
P(len, seed)builds indices by doing an xorshift-style RNG seeded withseed ^ 0xA5A5A5A5then performing a Fisher–Yates-like shuffle. -
get_flag()then usesrot = (seed >> 27) & 7, subtracts the position, rotates, XORs with the seed bytes and builds the string.
Local reproduction (commands)
apktool d -f app-release1.apk -o apktool_out_1
less apktool_out_1/smali/com/heroctf/freeda2/utils/Vault.smali
Look for E(), K() and get_flag() in Vault.smali.
Solver code (Python) — what I used locally
# solver_not_root.py
import base64
MASK = 0xFFFFFFFF
def to_uint32(x):
return x & MASK
def xorshift(v):
v ^= (v << 13) & MASK
v &= MASK
v ^= (v >> 17)
v &= MASK
v ^= (v << 5) & MASK
v &= MASK
return v
# Fisher-Yates-like shuffle driven by xorshift PRNG (seeded with seed ^ 0xA5A5A5A5)
def shuffle(n, seed):
arr = list(range(n))
v = to_uint32(seed ^ 0xA5A5A5A5)
for i in range(n, 0, -1):
v = xorshift(v)
j = v % i
arr[i-1], arr[j] = arr[j], arr[i-1]
return arr
# These values came out of the smali: base64 blob and K() seed
seed = 0x5f9d7bc3
encoded = 'fH6Da4rCaxDW/lvs32vwcvJcmy9TgPQaLHfJuw=='
raw = list(base64.b64decode(encoded))
perm = shuffle(len(raw), seed)
key = [(seed >> (8*k)) & 0xFF for k in range(4)]
rot = (seed >> 27) & 7
out = []
for i in range(len(raw)):
val = raw[perm[i]]
val = (val - (i & 0xFF)) & 0xFF
# rotation direction is determined by inspecting smali — this variant matches the output
if rot:
val = ((val >> rot) | ((val << (8 - rot)) & 0xFF)) & 0xFF
val ^= key[i & 3]
out.append(val)
print(bytes(out).decode())
Local success
- Running the above script printed:
HERO{D1D_Y0U_U53_0BJ3C71ON?}
Notes / tips
-
When a smali file contains multiple helper functions (E/P/B/K) implement and call them exactly as the Java code would; names and constants are the key.
-
If you see a small constant-return helper (like
K()), dump it — often seed values are constants.