Lego Clicker
Lego Clicker
Platform: Umasscybersec | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-07 | Status: Solved Techniques: apk_decompilation, jni_reversing, decoy_flag_elimination, xor_string_recovery, cross_architecture_validation
Summary
Task: an Android clicker game hides its reward flow behind Java and JNI checks, while shipping several obvious fake native flags. Solution: decompile the APK, notice Java insists on UMASS format, then recover the real body from a cross-architecture XOR-obfuscated native string and rebuild the intended flag.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
umasscybersec| ID:20260407_umasscybersec_lego_clicker - Tags: xor, android, apk, jadx, native_library, jni, fake_flag, mobile, apktool
- Indicators: Android APK loads a JNI library named legocore, Java-side validation explicitly requires a UMASS{...} prefix, native code exposes obvious fake flags with the wrong BHREV prefix, the same suspicious byte sequence appears at fixed offsets across multiple architecture builds, a single-byte XOR mask reveals a clean flag body from hidden native data
- Source:
20260407_umasscybersec_lego_clicker.md
Foothold
Vulnerability / Misconfiguration
- Apk_decompilation
- Jni_reversing
- Decoy_flag_elimination
- Xor_string_recovery
- Cross_architecture_validation
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- apk_decompilation
- jni_reversing
- decoy_flag_elimination
- xor_string_recovery
- cross_architecture_validation
- Tags: xor, android, apk, jadx, native_library, jni, fake_flag, mobile, apktool
Original Writeup
<details><summary>Click to expand original content</summary>Description
Hackers have taken over and corrupted your beloved Lego Clicker game, can you reclaim the top of the leaderboard? Note: There are fake flags throughout the challenge which should be obvious to tell based on contents
English summary: the challenge provides an Android APK for a fake clicker game. Reaching the top of the leaderboard triggers reward logic, but the real solution requires separating deliberate fake flags from the actual flag reconstruction path.
Challenge Summary
This was a mobile reversing challenge with a strong native component. The APK decompiles cleanly enough to expose the app structure, but the interesting flag logic sits behind JNI calls in liblegocore.so.
The key to the solve is not trusting the first flag-looking strings you see. The APK contains multiple decoys, and the Java layer gives the decisive constraint: the final accepted answer must look like UMASS{...}.
Recon and Decompilation
After unpacking the APK with apktool and jadx, the app structure immediately showed that the package name is com.example.LegoClicker and that the important native library is legocore.
Useful files and classes:
com.example.LegoClicker.RA— leaderboard activitycom.example.LegoClicker.SessionValidator— JNI entry points and library loadingcom.example.LegoClicker.FCA— native-backed dialog path with extra decoy behaviordefpackage/n0.java— result handling and final format checks
SessionValidator.java is the first important hint:
- it loads the native library with
System.loadLibrary(...) - it exposes JNI methods such as
refreshTileMap,syncBrickCache, andvalidateBrickToken - helper
a(long j, long j2)resolves and invokes a native method by reflection
RA.java shows the intended user flow. If the player reaches the top of the leaderboard, the app calls native validation and then displays a reward string. That makes the leaderboard path look like the intended flag trigger.
The decisive Java-side condition appears in defpackage/n0.java:
if (strA != null && !strA.isEmpty() && strA.startsWith("UMASS{") && strA.endsWith("}") && strA.length() - 7 >= 3) {
That single check is enough to reject the obvious fake flags later found in native code. Even if native code returns something flag-shaped, the Java layer only accepts a UMASS{...} result.
Fake-Flag Analysis
During native reversing, two decoy flags appear as obvious traps:
BHREV{fAk3_flAG_wr0ng_s3ss10n}
BHREV{f4k3_fl4g_n1c3_try_d3bugger}
Both are intentionally wrong for two reasons:
- The prefix is
BHREV{...}, notUMASS{...} - The challenge description explicitly warns that fake flags exist
So these strings are useful only as confirmation that the binary is designed to waste time if you trust surface-level string extraction.
Native Analysis Summary
The JNI side is intentionally messy. The obvious native paths are corrupted or decoyed, and following them directly does not yield a valid final answer. Looking at only one architecture can also be misleading, because the same library is shipped for several ABIs:
arm64-v8aarmeabi-v7ax86x86_64
The better approach is to compare all native builds and search for data that repeats across architectures. Repeated hidden data in every build is much more likely to be intentional challenge material than a single noisy disassembly branch.
Decisive XOR Discovery
The important finding is that all relevant native libraries contain the same hidden byte sequence at architecture-specific offsets. XORing that data with 0x4c decodes to the same plaintext every time.
Offsets:
apktool/lib/arm64-v8a/liblegocore.soat0x141d5apktool/lib/armeabi-v7a/liblegocore.soat0xee50apktool/lib/x86_64/liblegocore.soat0x13df0
Applying byte ^ 0x4c to 39 bytes at each location yields:
CTF{REDACTED}
This is the real flag body hidden in the native data, but it still uses the wrong prefix for the app's Java-side acceptance logic.
Final Flag Reasoning
At this point there are two facts that must be reconciled:
- Native hidden data cleanly decodes to
CTF{REDACTED} - Java validation clearly insists the returned result must match
UMASS{...}
That means the intended answer is not the raw decoded string verbatim. The hidden native string gives the flag body, while the Java layer gives the correct wrapper format.
So the intended final flag is:
UMASS{REDACTED}
Solution
- Decompile the APK with
apktoolandjadx. - Identify the important Java classes:
RA.java,SessionValidator.java,FCA.java, anddefpackage/n0.java. - Follow the leaderboard reward flow into JNI.
- Notice the fake flag strings in native logic, but reject them because they do not satisfy the Java-side
UMASS{...}requirement. - Compare the shipped
liblegocore.sofiles across architectures. - Extract the repeated hidden byte sequence at the noted offsets.
- XOR each byte with
0x4c. - Recover
CTF{REDACTED}. - Replace the wrapper with the format required by the Java validator to obtain the intended final flag.
#!/usr/bin/env python3
from pathlib import Path
TARGETS = [
(Path("tasks/umasscybersec/Lego Clicker/apktool/lib/arm64-v8a/liblegocore.so"), 0x141D5),
(Path("tasks/umasscybersec/Lego Clicker/apktool/lib/armeabi-v7a/liblegocore.so"), 0x0EE50),
(Path("tasks/umasscybersec/Lego Clicker/apktool/lib/x86_64/liblegocore.so"), 0x13DF0),
]
XOR_KEY = 0x4C
HIDDEN_LEN = 39
def decode_at_offset(path: Path, offset: int) -> str:
blob = path.read_bytes()
enc = blob[offset:offset + HIDDEN_LEN]
return bytes(b ^ XOR_KEY for b in enc).decode()
def main() -> None:
decoded = []
for path, offset in TARGETS:
value = decode_at_offset(path, offset)
decoded.append(value)
print(f"{path}: {value}")
assert len(set(decoded)) == 1, "architectures disagree"
native_flag = decoded[0]
body = native_flag.removeprefix("CTF{").removesuffix("}")
final_flag = f"UMASS{{{body}}}"
print(f"native hidden string: {native_flag}")
print(f"final flag: {final_flag}")
if __name__ == "__main__":
main()
</details>
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR