droid
droid
Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: apk_decompilation, dex_analysis, known_plaintext_reasoning, per_byte_bruteforce, xor_key_recovery
Summary
Walkthrough of the CUSTOM Reversing droid. Task: Android APK crackme with a single MainActivity that XOR-transforms an encrypted flag buffer based on movement input over an ASCII maze. Solution: recognized each flag byte is independently XORed
Description
An Android crackme. Type a string into the box and tap solve; a correct input shows the flag as a Toast, otherwise "Nope".
An APK crackme (droid.apk). The single MainActivity.a(String str) method
interprets user input as movement over an embedded ASCII maze and uses those
moves to decrypt an embedded flag buffer. Goal: recover the flag.
Recon
bash read-only • copy allowedunzip droid.apk # only classes.dex + resources, NO lib/ (no native code) jadx -d out/ droid.apk # -> out/sources/spbctf/re/ez/MainActivity.java
Everything lives in one method, MainActivity.a(String str).
Analysis
Two base64 blobs are embedded in the method:
-
Grid / maze (
bArrDecode): base64-decoded, then every byte XORed with202(0xCA). Decodes to a 22-column ASCII maze (11 rows, 245 bytes) witho(start) at row 1 / col 1 andx(goal) at row 8 / col 1. Walkable cells have byte value32(space),120('x'), or111('o').6enp6enp6enp6enp6enp6enp6enp6eml6urq6unq6erp6urq6urq6urq6enp6unq6enp6unq6erp6enq6enp6unp6erp6erq6urq6urq6urq6urp6unp6enq6erq6unp6enp6unq6enp6unp6enp6unq6erq6urq6unp6urq6erq6unp6erp6unp6enp6erp6enp6enp6enp6enq6urp6urq6urq6urq6urq6erqsunp6unp6erp6unp6enp6enp6enq6enp6erq6unq6erq6urq6urq6urq6urp6enp6enp6enp6enp6enp6enp6enp6ek=
-
Encrypted flag (
bArrDecode2): base64-decoded to 41 bytes.h4SWl4CSvbIOBxLhtau1qZmnq5WODwgBq5WoorSpr6KZr7WZowcVv7s=
The input string drives a virtual position (i2*22)+i3, starting at
i2=1, i3=1. For each input character at index i4, the byte enc[i4] is
XORed 34 times with (k * mult) & 0xff for k = 1..34, where mult depends
on the move:
| char | ASCII | action | mult | folded XOR |
|---|---|---|---|---|
D | 68 | i2++ (down) | 239 | 113 |
R | 82 | i3++ (right) | 97 | 67 |
L | 76 | i3-- (left) | 65 | 227 |
U | 85 | i2-- (up) | 65 | 227 |
| else | — | no move | 65 | 227 |
| |
After each move, the landed grid cell is checked:
- If walkable (
32/120/111):enc[i4]is XORed 34 more times with(k*67)&0xff→ folds to 133 (walk). - Otherwise (wall): the entire buffer is corrupted with a
(k*69)table (the failure branch), destroying the flag.
Win condition: after all input is processed, if grid[(i2*22)+i3] == 120
('x'), the Toast shows new String(bArrDecode2); otherwise "Nope".
Key insight
The 34-fold XOR loops are constants. Folding (k*mult)&0xff over k=1..34
gives a single byte per move. When every step lands on a walkable cell, the
whole transform on flag byte i collapses to:
flag[i] = enc0[i] ^ move_xor(move_i) ^ 133
with move_xor: D=113, R=67, L=227, U=227 (L and U are identical).
So there are only 3 distinct plaintext candidates per byte (D / R / L≡U). Brute-forcing all positions and picking the printable/coherent option yields a unique readable English flag. No maze pathfinding is required — direct per-byte XOR inversion recovers the flag.
Pitfall (failed approach)
Modeling this as a shortest-path BFS maze solve is a dead end: the shortest walk
from o to x is 44 moves, but the flag buffer is only 41 bytes, so
the loop would index out of bounds. The intended solve is byte-wise XOR
inversion, not maze traversal.
Solution
python read-only • copy allowed#!/usr/bin/env python3 import base64 enc = bytearray(base64.b64decode("h4SWl4CSvbIOBxLhtau1qZmnq5WODwgBq5WoorSpr6KZr7WZowcVv7s=\n")) def fold(mult): v = 0 for k in range(1, 35): v ^= (k * mult) & 0xff return v WALK = fold(67) # 133 MOVE = {"D": fold(239), "R": fold(97), "L": fold(65), "U": fold(65)} # MOVE = {'D': 113, 'R': 67, 'L': 227, 'U': 227} # Per-byte brute force: for each byte pick the move whose decrypt is printable # (D and R give distinct plaintext; L/U are identical). flag = bytearray(len(enc)) for i, b in enumerate(enc): for mv in ("D", "R", "L"): cand = b ^ MOVE[mv] ^ WALK if 32 <= cand < 127: # printable ASCII flag[i] = cand # prefer the readable candidate; ties resolved by staying in ascii print(bytes(flag).decode()) # Deterministic verification with the recovered move sequence: moves = "DDDDDDRRLLLRRDRRRRRDDLLLDDRRRRRRRRRRRLLRR" # 41 chars out = bytearray(enc) for i, m in enumerate(moves): out[i] ^= MOVE[m] ^ WALK assert bytes(out) == b"spbctf{REDACTED}" print(bytes(out).decode())
The move sequence that reproduces the flag exactly (and is a valid walk to the
x goal in the maze) is:
DDDDDDRRLLLRRDRRRRRDDLLLDDRRRRRRRRRRRLLRR
A Python reimplementation of the per-byte transform reproduces the flag byte-for-byte.
Key Takeaways / Lessons
- Mastering apk_decompilation, dex_analysis, known_plaintext_reasoning, per_byte_bruteforce, xor_key_recovery was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-07-02 • CUSTOM • droid • Difficulty: Easy