HTB: Joker Challenge
Joker - HackTheBox Challenge Writeup
Challenge Information
| Field | Value |
|---|---|
| Challenge Name | Joker |
| Category | Mobile / Reverse Engineering |
| Difficulty | Hard |
| Author | d3vn0mi |
Description
The malware reverse engineering team got an alert about malware which is still published on Google’s PlayStore and has thousands of installs. Can you help them to identify the address of the command and control server in order to blacklist it?
The challenge hands out an Android APK (joker.apk, package meet.the.joker) modeled on the real-world Joker malware family. The goal is not to pop a shell — it’s pure static/dynamic Android reverse engineering: peel back several layers of obfuscation and encryption to recover the malware’s actual command-and-control (C2) domain.
Solution Overview
The provided joker.apk in the challenge bundle was a 0-byte stub — the real artifact was the password-protected zip attached to the challenge (hackthebox), which contained the working APK.
The malware’s trigger path and payload chain looked like this:
- A malicious
ContentProvider(JokerBr) is the entry point and calls intoa2.a.b(). - Strings throughout the app are obfuscated with a small repeating-key XOR helper (
c.a.o(buf, key)), used to hide URLs and asset paths from static string dumps. a2.a.b()performs a decoy liveness check: a GET tohttps://play.google.com/store/apps/details?id=meet.the.joker— literally confirming the app is “still published on the PlayStore,” a nod to the challenge description. This is not the C2; it’s an anti-analysis / staged-activation gate.- On HTTP 200 from that check, the app AES-CBC decrypts a bundled asset (filename ending in
301.txt) using a key/IV derived via SHA-1 from a hardcoded passphrase. The decrypted blob is an ARM64 native shared object (.so). - Disassembling that native library revealed its own decode routine
d(), which XOR-decodes another asset (...eibephonenumberse300.txt) using the literal ASCII string"The flag is:"as a repeating key. - XOR-decoding that asset with the recovered key produced a second, hidden DEX file smuggled inside the assets folder.
- That hidden DEX contains a small
Payload2class whosemain()simply builds and prints the flag by string-concatenating a static field namedhost— which is set to the C2 domain string itself — between theHTB{REDACTED} /}` flag wrapper.
Chasing the malware’s own decryption chain to the end recovers the C2 hostname the reverse engineering team needed to blacklist.
Key Steps
Step 1: Unpack the real artifact
The APK shipped in the challenge bundle at /out/<id>.zip and required the standard HTB zip password.
# The handed-out joker.apk was a 0-byte stub; the real APK was inside# the password-protected challenge zip.unzip -o -P hackthebox <challenge-id>.zip -d work/cd work && unzip -l joker.apk | head -40Step 2: Statically analyze the DEX with androguard
pip3 install --break-system-packages -q androguard
python3 - <<'EOF'from androguard.misc import AnalyzeAPK
a, d, dx = AnalyzeAPK('joker.apk')print("package:", a.get_package())print("providers:", a.get_providers())# JokerBr ContentProvider is the malicious entry point,# which calls into a2.a.b()EOFStep 3: Deobfuscate the XOR-hidden strings
The app hides its interesting strings (the PlayStore check URL, asset filenames) behind a
tiny repeating-key XOR helper found at c.a.o(String, String). Reimplementing it in Python
against the extracted constant strings recovers the plaintext URLs and paths.
def o(s, key): # Reimplementation of the app's c.a.o() string-deobfuscation routine return ''.join(chr(ord(s[i]) ^ ord(key[i % len(key)])) for i in range(len(s)))
# Recovered decoy liveness-check URL:# https://play.google.com/store/apps/details?id=meet.the.jokerStep 4: Decrypt the AES-CBC asset into a native .so
Once the decoy GET succeeds (HTTP 200 == “still on the PlayStore”), the app decrypts an
asset ending in 301.txt with AES-CBC, where both key and IV derive from SHA-1 of a
hardcoded passphrase.
import hashlibfrom Crypto.Cipher import AES
keystr = "ma17FEC2_lYuoNQ$_ToT99u_e0kINhw_Bzy"key = iv = hashlib.sha1(keystr.encode()).digest()[:16]
data = open('assets/io/m/l/l/d/eibephonenumberse301.txt', 'rb').read()cipher = AES.new(key, AES.MODE_CBC, iv)payload = cipher.decrypt(data) # -> ARM64 ELF shared object (payload301.bin)open('payload301.bin', 'wb').write(payload)Step 5: Disassemble the native payload to recover the next-stage XOR key
pip3 install --break-system-packages -q capstone pyelftools
python3 - <<'EOF'from elftools.elf.elffile import ELFFilefrom capstone import *
f = open('payload301.bin', 'rb')elf = ELFFile(f)# Located the decode routine d(); it XORs an asset buffer against# the literal ASCII string "The flag is:" (12-byte repeating key).EOFStep 6: XOR-decode the hidden second DEX
key = b"The flag is:"data = open('assets/io/m/l/l/d/eibephonenumberse300.txt', 'rb').read()
decoded = bytes(b ^ key[i % len(key)] for i, b in enumerate(data))open('hidden.dex', 'wb').write(decoded)Step 7: Decompile the hidden DEX and assemble the flag
strings -n 3 hidden.dexfrom androguard.misc import AnalyzeDex
h, d, dx = AnalyzeDex('hidden.dex')for cls in d.get_classes(): if cls.name == 'LPayload2;': print(cls.get_source())The recovered Payload2.java builds the flag directly at runtime:
// Payload2.main()StringBuilder v0_2 = new StringBuilder();v0_2.append("HTB{REDACTED}v0_2.append(Payload2.host); // host = the C2 domain stringv0_2.append("}");System.out.println(v0_2.toString());The static field Payload2.host holds the recovered C2 domain, giving the reverse
engineering team exactly what they needed to blacklist it — and completing the flag.
Tools Used
| Tool | Purpose |
|---|---|
unzip | Extract the password-protected challenge bundle and APK contents |
| androguard | Parse/decompile the APK’s classes.dex and the hidden second DEX |
| pyelftools | Parse the decrypted ARM64 .so ELF headers |
| capstone | Disassemble the ARM64 native payload to locate its decode routine |
| Python (hashlib, PyCryptodome) | Reimplement the app’s XOR string-deobfuscator and AES-CBC asset decryption |
strings | Quick triage of extracted binaries and the hidden DEX |
Key Learnings
- Multi-stage droppers hide behind “liveness” checks. The PlayStore existence check doubles as an anti-sandbox gate — payloads only decrypt once the malware confirms it’s still live and installable, matching the real Joker malware family’s behavior.
- Layered decryption chains are a deliberate speed bump, not a dead end. Each stage (Java-level XOR → AES-CBC → native-code XOR) only needs to be reimplemented once its key material is located; static analysis of the decoding routine at each layer is enough to script the whole chain in Python.
- Native code is often the last mile of obfuscation. Moving the final decode routine
into an ARM64
.soforces the reverse engineer to disassemble machine code instead of just reading readable DEX bytecode — but the logic (a trivial repeating-key XOR here) is usually simple once you’re looking at the right function. - Assets, not just DEX code, can smuggle a full second payload. The “hidden second DEX” technique — encrypting an entire classes.dex and stashing it as an innocuous asset file — is a common technique for evading static Play Store scanning while keeping the actual malicious logic out of the primary manifest-declared code.
Flag
HTB{REDACTED}