//Freeda — Simple Hook
Challenge summary
-
Category: Android / Java (easy)
-
Goal: find the password to open the vault (flag format:
^Hero{\S+}$). -
Files provided:
app-release.apk(decompiled asapktool_out/in the workspace).
Short summary of approach
-
This challenge kept its vault logic in Java (smali) under
com.heroctf.freeda1.utils.Vault. -
Instead of heavy static reverse engineering, I quickly read the smali, re-implemented the algorithm in Python and ran it to get the flag.
What I looked for first
-
I searched for obvious
get_flag/flag/CheckFlagstrings, and foundcom/heroctf/freeda1/utils/Vault.smaliwhich hasget_flag(). -
CheckFlag.checkFlagreflects/forwards toVault.get_flag()— so recoveringVault.get_flag()is sufficient.
Static analysis notes (high level)
Vaultcontains a static integer array (byte source) and code that:
- computes a 32-bit seed() from two strings
- builds an indices array and does a Fisher–Yates-like shuffle driven by an xorshift PRNG
- transforms bytes by subtracting the index, rotating, and XORing with seed bytes
- finally converts those bytes to a string with charset UTF-8
- The algorithm is straightforward to reimplement in Python; no native code or obfuscation to defeat.
Local reproduction (commands)
Use apktool to decode the APK:
apktool d -f app-release.apk -o apktool_out
Look at the relevant file:
less apktool_out/smali/com/heroctf/freeda1/utils/Vault.smali
Solver code (Python) — this is what I used locally
# solver_simple_hook.py
MASK = 0xFFFFFFFF
def to_uint32(x):
return x & MASK
def java_hash(s):
h = 0
for ch in s:
h = (31 * h + ord(ch)) & MASK
return h
def rotate_left(val, dist):
dist &= 31
val &= MASK
return ((val << dist) | ((val & MASK) >> (32 - dist))) & MASK
# Re-implemented seed() seen in smali
def seed():
s1 = "com.heroctf.freeda1.MainActivity"
s2 = "com.heroctf.freeda1.utils.CheckFlag"
v0 = java_hash(s1)
v0 ^= to_uint32(-0x3f0011be)
v0 &= MASK
v1 = java_hash(s2)
v0 ^= v1
v0 &= MASK
v1 = rotate_left(v0, 7)
v1 = (v1 * to_uint32(-0x61c88647)) & MASK
v0 ^= v1
v0 &= MASK
return v0
vault_a = [
0x34,0x58,0x1b,0x20,0x1b,0xba,0x60,0x6d,0x2d,0xca,
0x2a,0x7d,0x19,0x86,0x9f,0x45,0x2f,0x8e,0xc0,0xb8,
0x0d,0x13,0x8b,0xad,0x3b,0x81,0x00,0x9e,0xa5,0xbc,
0x0d,0x3e,0x4a,0xb8,0x3a,0x4b,0xac,0xca,0x42
]
assert len(vault_a)==0x27
# get_flag reimplementation
def get_flag():
v0 = seed()
n = 0x27
indices = list(range(n))
# The xorshift (rot-l/XOR/shift pattern) from smali
v4 = to_uint32(-0x5a5a5a5b) ^ v0
v4 &= MASK
for i in range(n-1, -1, -1):
v4 ^= (v4 << 13) & MASK
v4 &= MASK
v4 ^= ((v4 & MASK) >> 17)
v4 &= MASK
v4 ^= (v4 << 5) & MASK
v4 &= MASK
j = (v4 & MASK) % (i + 1)
indices[i], indices[j] = indices[j], indices[i]
out = []
rot = ((v0 & MASK) >> 27) & 0x7
for i in range(n):
val = vault_a[indices[i]]
val = (val - i) & 0xFF
if rot:
val = (((val << (8 - rot)) & 0xFF) | (val >> rot)) & 0xFF
shift = (i & 3) * 8
val ^= ((v0 >> shift) & 0xFF)
out.append(val)
return bytes(out).decode('utf-8')
if __name__ == '__main__':
print(get_flag())
Local success
- Running the script in the same folder produced the flag:
Hero{1_H0P3_Y0U_D1DN'T_S7A71C_4N4LYZ3D}
Notes / tips
-
Always search for
get_flag,CheckFlag,Vault, andHero{first — many CTF Androids put the core logic in Java and not in native code. -
Reimplement exactly what the smali shows; be careful about Java int semantics (32-bit wrap, signedness) when translating to Python.