HTB: Pedometer Challenge

Pedometer - HackTheBox Challenge Writeup

Challenge Information

FieldValue
Challenge NamePedometer
CategoryReverse Engineering (Mobile/Misc)
DifficultyHard
Authord3vn0mi

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

Terminal window
# Extract the APK (it's just a zip)
mkdir -p work/mobile_pedometer && cd work/mobile_pedometer
unzip -o -P hackthebox pedometer.apk -d ext
# Confirm file type and inspect contents
file pedometer.apk
ls -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:

Terminal window
pip install --break-system-packages -q androguard
python3 -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 MOD
EQ LT GT NOT XOR IF JMP
CHRG AIRPLN INTRNT # device-state pushes (anti-tamper)
ENC DEC # mutate the running XOR key
FLAG (0xff) # pop 21 bytes, emit as the flag

Key VM mechanics:

  • Each opcode is decoded as read() ^ d, so the effective program only becomes readable once d reaches the right value at the right offset — a lightweight self-modifying decoder rather than a static disassembly target.
  • d is updated in place by XOR, ENC, and DEC as 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 via EQ/IF; failing the check routes execution straight to STOP.

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 them
for 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

Terminal window
printf 'HTB{REDACTED}\n' > flag.txt

Tools Used

ToolPurpose
unzipExtract 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 emulatorFaithful 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 tracks d faithfully.
  • 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/IF semantics.
  • 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}