← back to posts

6 - DeadDrop: Emulating an MQTT Bot to Recover a Flag

Overview

DeadDrop presented an Android malware sample that used MQTT as its C2 channel, with RC4 encryption on every message. The challenge was to reverse the network protocol, recover the broker credentials and encryption keys from the APK, and then write a bot emulator that could register a fake device and trigger flag delivery.

Decoding the Config

The APK’s Cfg.java class stored all configuration values as Base64-encoded strings. This is a minimal evasion technique — strings won’t appear in a naive grep for IP addresses or credentials — but a single Base64 decode reveals everything:

1BROKER_URL = base64.b64decode("dGNwOi8vNjguMTgzLjIxOC41OToxODgz").decode()
2# → tcp://68.183.218.59:1883
3
4USERNAME = base64.b64decode("Ym90Y29uZl9jbGllbnQ=").decode()
5# → botconf_client
6
7PASSWORD = base64.b64decode("TTBiMWwzX0IwdF8yMDI2").decode()
8# → M0b1l3_B0t_2026

The broker ran on the same IP as the C2 server from the earlier challenges — the workshop deliberately reused infrastructure.

Recovering the RC4 Key

The RC4 implementation lived in RC4.java. The static key was stored as a byte array _K[] that was XOR-obfuscated with 0x55:

1static final byte[] _K = { 7, 22, 97, 10, 24, 4, 1, 1, 10, 30, 102, 12, 116 };

Applying XOR 0x55 to each byte:

1_RAW_K = bytes([7, 22, 97, 10, 24, 4, 1, 1, 10, 30, 102, 12, 116])
2STATIC_KEY = bytes(b ^ 0x55 for b in _RAW_K)
3# → b"RC4_MQTT_K3Y!"

The RC4 implementation itself was a standard KSA + PRGA — nothing unusual. Messages were RC4-encrypted, then Base64-encoded for transit.

The Protocol

Connecting to the broker and subscribing to the relevant topics revealed a two-key architecture:

Static key (RC4_MQTT_K3Y!) encrypted three channels:

  • /info — session key delivery: SESSION:<hex_key>
  • /commands — operator commands: CMD:PING, CMD:STOP, CMD:CHECK, CMD:CONFIG
  • /heartbeat — acknowledgments from the client to the server

Session key (delivered via /info, rotated per session) encrypted:

  • /data — telemetry JSON: battery, sensor readings, process lists, etc.
  • /flag — flag delivery, sent as {"type":"flag","data":"FLAG{...}"}

The session key was the pivot. Once received via /info, the client was expected to acknowledge it and then send a device registration payload to /data using the session key. That registration — a JSON object containing device metadata — was what prompted the server to emit the flag on /flag.

The Bot Emulator

c2_client.py implemented the complete protocol flow using paho-mqtt:

 1def _handle_info(client, raw):
 2    plain = dec(raw)                         # decrypt with static key
 3    if not plain.startswith("SESSION:"):
 4        return
 5    hex_key = plain[8:].strip()
 6    session_key = bytes.fromhex(hex_key)
 7
 8    # 1. Acknowledge the session key
 9    ts = int(time.time())
10    client.publish("/heartbeat", enc(f"KEY_ACK:{hex_key[:8]}:{ts}"))
11
12    # 2. Publish device info — this triggers the flag
13    _publish_device_registration(client)

The device registration payload mirrored what DeviceCollector.buildInfoPayload() would produce on a real Android device:

 1info = json.dumps({
 2    "type": "info",
 3    "android_id": ANDROID_ID,
 4    "manufacturer": "Google",
 5    "model": "Pixel 6",
 6    "sdk": 33,
 7    "release": "13",
 8    "fingerprint": "google/oriole/oriole:13/TQ3A.230805.001/...",
 9    "locale": "en_US",
10    ...
11})
12client.publish("/data", enc(info, session_key))

The server validated the payload and, on receiving a well-formed device info message encrypted with the correct session key, published the flag on /flag.

What the connection.out Showed

Running the emulator and capturing output to connection.out showed the full session lifecycle: receiving the session key, acknowledging it, publishing device info, and watching the server stream telemetry back. The server continuously pushed {"type":"process"}, {"type":"telemetry"}, {"type":"sensor"} messages to /data — all decryptable with the session key.

The server also issued CMD:CHECK version=1.2.1 via /commands, which the emulator acknowledged with ACK:CHECK:version=1.2.1.

The flag arrived on /flag after the device info was accepted.

Why MQTT

Using MQTT as a C2 transport is an underused technique in real malware. The reasons it works well:

  • MQTT traffic on port 1883 blends in with legitimate IoT device communication
  • Publish/subscribe decouples the C2 operator from the victim — the operator never connects directly to the infected device
  • The session key architecture means the APK can be fully analyzed without ever obtaining the flag — the server controls key issuance
  • Broker authentication (username/password) provides a minimal access barrier against casual connection attempts

The workshop used MQTT here specifically to illustrate how C2 channels don’t have to look like HTTP REST APIs, and how the message format analysis changes when the transport is a message broker rather than a web server.

Key Techniques

TechniqueWhere
Base64-encoded configBroker URL and credentials hidden from naive string search
XOR-obfuscated RC4 key_K[] XOR 0x55"RC4_MQTT_K3Y!"
Two-key protocolStatic key for control plane; session key for data plane
Session key gatingServer issues session key only to connected clients; flag requires it
Device registration triggerServer delivers flag only after receiving valid session-key-encrypted device info
Full protocol emulatorc2_client.py implements KSA+PRGA, subscribe, handle, publish loop
series
BotConf 2026 Android Workshop