HTB: Joker Challenge

Joker - HackTheBox Challenge Writeup

Challenge Information

FieldValue
Challenge NameJoker
CategoryMobile / Reverse Engineering
DifficultyHard
Authord3vn0mi

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:

  1. A malicious ContentProvider (JokerBr) is the entry point and calls into a2.a.b().
  2. 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.
  3. a2.a.b() performs a decoy liveness check: a GET to https://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.
  4. 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).
  5. 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.
  6. XOR-decoding that asset with the recovered key produced a second, hidden DEX file smuggled inside the assets folder.
  7. That hidden DEX contains a small Payload2 class whose main() simply builds and prints the flag by string-concatenating a static field named host — which is set to the C2 domain string itself — between the HTB{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.

Terminal window
# 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 -40

Step 2: Statically analyze the DEX with androguard

Terminal window
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()
EOF

Step 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.joker

Step 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 hashlib
from 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

Terminal window
pip3 install --break-system-packages -q capstone pyelftools
python3 - <<'EOF'
from elftools.elf.elffile import ELFFile
from 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).
EOF

Step 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

Terminal window
strings -n 3 hidden.dex
from 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 string
v0_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

ToolPurpose
unzipExtract the password-protected challenge bundle and APK contents
androguardParse/decompile the APK’s classes.dex and the hidden second DEX
pyelftoolsParse the decrypted ARM64 .so ELF headers
capstoneDisassemble the ARM64 native payload to locate its decode routine
Python (hashlib, PyCryptodome)Reimplement the app’s XOR string-deobfuscator and AES-CBC asset decryption
stringsQuick 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 .so forces 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}