HTB: Pedometer Challenge
Pedometer - HackTheBox Challenge Writeup
Challenge Information
| Field | Value |
|---|---|
| Challenge Name | Pedometer |
| Category | Reverse Engineering (Mobile/Misc) |
| Difficulty | Hard |
| Author | d3vn0mi |
Description
I’ve been using this pedometer app for weeks, and I am convinced it’s using me as a power supply for some hidden machine. I bet it holds the key or a map to some sort of treasure. If only I could figure out what it’s doing…
The challenge ships as an Android APK for a package called com.rloura.pedometer. Nothing about the flavor text obviously screams “reverse an APK” — the description is a riddle pointing at the app’s real behavior: it doesn’t just count steps, it uses each detected step as the clock tick for a hidden, self-modifying virtual machine buried in native code.
Solution Overview
Unpacking the APK revealed a native library implementing a small stack-based VM (function u1.c, internally labeled "StepReader"). A 143-byte bytecode blob (assets/a) is the VM’s program. Instead of running to completion immediately, the VM executes exactly one opcode per pedometer step event — read() ^ d, where d is a running XOR key. That key is itself mutated at runtime by dedicated XOR/ENC/DEC opcodes, which decode the rest of the bytecode as execution proceeds (a self-modifying decode chain, presumably meant to defeat naive static disassembly).
Before it ever reaches the interesting logic, the program runs an anti-tamper / environment gate: it pushes an expected 3-bit environment vector, then uses three custom opcodes — CHRG (charging state), AIRPLN (airplane-mode state), and INTRNT (internet connectivity) — to push the actual live device state, compares the two with EQ + conditional IF, and hits STOP (no flag) on any mismatch. This lines up with the challenge’s flavor text about the app “using me as a power supply” — the real device has to be plugged in (and in airplane mode, with internet somehow still up) for the VM to proceed. If the gate passes, execution reaches a FLAG opcode that pops 21 characters off the stack and reveals the answer.
Rather than reverse-engineer the exact anti-tamper semantics by hand, I re-implemented the entire VM faithfully in Python and brute-forced the three environment booleans, since the search space is trivially small (2³ = 8 combinations).
Key Steps
Step 1: Unpack the APK and locate the interesting artifacts
# Extract the APK (it's just a zip)mkdir -p work/mobile_pedometer && cd work/mobile_pedometerunzip -o -P hackthebox pedometer.apk -d ext
# Confirm file type and inspect contentsfile pedometer.apkls -la ext/Inside the extracted APK: a native library exposing a function referred to as u1.c / "StepReader", and a 143-byte payload at assets/a — the VM bytecode.
Step 2: Set up tooling for static analysis
The environment had no jadx/apktool/dex2jar preinstalled, so I fell back to androguard for DEX/native inspection:
pip install --break-system-packages -q androguardpython3 -c "from androguard.misc import AnalyzeAPK; print('ok')"This was enough to confirm the package name (com.rloura.pedometer), locate the sensor-event callback wiring the pedometer’s step counter to the VM’s dispatch() function, and pull the raw assets/a bytecode blob for offline analysis.
Step 3: Reverse-engineer the VM’s instruction set
Static analysis of the dispatcher recovered the following opcode set:
STOP PUSH POP ADD SUB MUL DIV MODEQ LT GT NOT XOR IF JMPCHRG AIRPLN INTRNT # device-state pushes (anti-tamper)ENC DEC # mutate the running XOR keyFLAG (0xff) # pop 21 bytes, emit as the flagKey VM mechanics:
- Each opcode is decoded as
read() ^ d, so the effective program only becomes readable oncedreaches the right value at the right offset — a lightweight self-modifying decoder rather than a static disassembly target. dis updated in place byXOR,ENC, andDECas the program runs, meaning the “real” bytecode past the gate can’t be dumped statically — it has to be executed (or emulated) step by step.- The environment-gate opcodes (
CHRG,AIRPLN,INTRNT) push live device booleans that get compared against an expected vector baked into the program viaEQ/IF; failing the check routes execution straight toSTOP.
Step 4: Re-implement the VM in Python and brute-force the environment
# Faithful re-implementation of the StepReader VM's fetch-decode-execute loop.# `d` is the running XOR key, mutated by XOR/ENC/DEC as execution proceeds.def run_vm(bytecode, charging, airplane, internet): stack = [] pc = 0 d = 0 # initial XOR key while pc < len(bytecode): op = bytecode[pc] ^ d pc += 1 # ... dispatch on op: PUSH/POP/ADD/SUB/MUL/DIV/MOD/EQ/LT/GT/NOT/XOR/IF/JMP ... if op == OP_CHRG: stack.append(int(charging)) elif op == OP_AIRPLN: stack.append(int(airplane)) elif op == OP_INTRNT: stack.append(int(internet)) elif op == OP_STOP: return None # gate failed, no flag elif op == OP_FLAG: return bytes(stack[-21:]) # pop 21 chars, this is the flag # ENC/DEC opcodes mutate `d` here, decoding subsequent bytes return None
data = open("assets/a", "rb").read()
# Only 8 possible environment states — brute force all of themfor charging in (0, 1): for airplane in (0, 1): for internet in (0, 1): flag = run_vm(data, charging, airplane, internet) if flag: print(charging, airplane, internet, flag)Only charging=1, airplane_mode=1, internet=1 passed the environment gate and reached the FLAG opcode, yielding the flag bytes directly off the VM stack.
Step 5: Capture the flag
printf 'HTB{REDACTED}\n' > flag.txtTools Used
| Tool | Purpose |
|---|---|
unzip | Extract the APK (zip) contents and assets/a bytecode |
androguard (Python) | Static analysis of the DEX/native code without a full jadx/apktool setup |
| Custom Python VM emulator | Faithful re-implementation of the stack-based StepReader VM to safely execute the self-modifying bytecode offline |
| Brute force (environment booleans) | Bypassing the CHRG/AIRPLN/INTRNT anti-tamper gate without needing a real Android device in that exact state |
Key Learnings
- Step-driven execution as an obfuscation primitive: tying VM opcode execution to a real-world sensor event (a detected pedometer step) is an unusual throttling/obfuscation technique — it makes naive dynamic tracing painful since the “clock” is physical motion, not CPU cycles. Emulating the VM directly sidesteps this entirely.
- Self-modifying XOR-keyed bytecode defeats static disassembly of the tail of a program, but is trivial to unwind once you understand the fetch-decode step (
read() ^ d) and simply execute it in an emulator that tracksdfaithfully. - Small boolean search spaces should be brute-forced, not reasoned about. Rather than fully reverse the anti-tamper comparison logic, treating the 3-bit environment vector as an 8-way brute-force target was far faster and less error-prone than manually tracing
EQ/IFsemantics. - Flavor text often encodes the actual mechanism. “Using me as a power supply for some hidden machine” was a direct (if indirect) hint at the
CHRG(charging) check gating the hidden VM’s flag path — worth re-reading the description after initial static analysis surfaces something odd.
Flag
HTB{REDACTED}