Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
AndroidMedium

Freeda — Not Root

CTF writeup for Freeda — Not Root from heroCTF

//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 as apktool_out_1/).

Short summary of approach

  • This app moved more of its logic into an obfuscated Vault class, 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, or CheckFlag in the smali. Found com.heroctf.freeda2.utils.Vault and CheckFlag forwarding calls.

  • Vault contains functions E() (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 constant 0x5f9d7bc3 (seed).

  • P(len, seed) builds indices by doing an xorshift-style RNG seeded with seed ^ 0xA5A5A5A5 then performing a Fisher–Yates-like shuffle.

  • get_flag() then uses rot = (seed >> 27) & 7, subtracts the position, rotates, XORs with the seed bytes and builds the string.

Local reproduction (commands)

bash

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

python

# 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.