A few months back I was reviewing the remote-update service in an Android app I help maintain for a client — the kind of background component that pings a broker, checks for new bundles, and quietly installs whatever comes down the pipe so the user never has to think about it. Then I read Kaspersky's Securelist report on malware found in Android-based car head unit firmware, and the pattern in it looked uncomfortably close to a decision I'd made in that exact service six months earlier.
The short version, as Kaspersky researcher Dmitry Kalinin lays it out: a legitimate system app called TWCore, built to handle analytics and firmware updates for DoFun-brand automotive head units, was used to silently push a multi-stage Android dropper onto real vehicles. No phishing link, no sideloaded APK from a shady forum — the malware rode in through the head unit's own official update channel. Kaspersky attributes the campaign, with high confidence, to MoYu Group, an actor tied to the BADBOX botnet.
I don't build car infotainment software, but I do build the exact category of thing that made this attack possible: a background service that decides, on a device it doesn't fully control, whether to trust a message telling it to install something. That's what I want to pick apart here — not as a car story, but as a lesson in how ordinary-looking update-pipeline decisions become attack surface.
How TWCore's Update Channel Was Supposed to Work
TWCore's job, according to Kaspersky's analysis, is unglamorous: collect analytics and keep the head unit's software current. The mechanism behind that is an MQTT message broker hosted on a subdomain of cardoor[.]cn, which pushes messages describing which APK files the device should download and install. That's a completely standard pattern for fleet-managed embedded devices — you don't want to ship a firmware update to every car by hand, so you push instructions over a lightweight pub/sub protocol and let the device pull the payload.
The part that turns a convenience feature into a weapon is a single field in that MQTT message: a boolean called installNotExists. According to Kaspersky's writeup, when that flag is set to true, TWCore skips checking whether the app is already installed and just installs it anyway. In other words, the "check for updates" function and the "install anything you're told to, whether it's on the device or not" function are the same code path, gated by one flag that the broker controls.
Kaspersky's telemetry showed exactly what you'd expect from that design: previously unknown malware turning up at the file path TWCore uses for downloads, <TWCore external cache dir>/push/apk/, and in every observed case the installing app was com.tw.core — TWCore itself, doing precisely what it was built to do. The update mechanism wasn't exploited through a bug. It was used as designed.
Anatomy of the Multi-Stage Payload
What TWCore actually pushed, per Kaspersky's teardown, is a dropper the researchers named JarService. It has no user interface at all, which was the detail that first caught the analysts' attention — a normal app tries to look legitimate, and JarService didn't even bother. Its job is to decrypt data stored as blocks inside its own code, each block XOR-encrypted with a single-byte key that shifts linearly from block to block, then deserialize the result into a payload version and entry point for the next stage.
I haven't seen JarService's actual bytecode, obviously, but the general shape of that decoding routine — a rolling single-byte XOR key applied block by block — is a pattern you can reconstruct just from the description, and it's worth seeing in code because it's such a cheap way to defeat naive static string-scanning:
fun decodeRollingXor(blocks: List<ByteArray>, startKey: Int): ByteArray {
val output = mutableListOf<Byte>()
var key = startKey and 0xFF
for (block in blocks) {
for (b in block) {
output.add((b.toInt() xor key).toByte())
}
key = (key + 1) and 0xFF // key shifts linearly from block to block
}
return output.toByteArray()
}
That's the whole trick: nothing in the compiled dropper looks like a URL or a class name until it runs, because the "real" strings only exist after this loop executes at runtime. In JarService's case, the decrypted payload points to a class called com.c.j.qbh and an entry method named wa, which hands off to a stage-2 loader. That loader beacons to a command-and-control server with basic app fingerprinting data, gets back a download URL for a stage-3 payload, and decrypts it using the same rolling-XOR idea plus a floating-point key. Kaspersky found seven distinct versions of that stage-3 payload just by guessing at version numbers in the download URL, which tells you this pipeline gets iterated on fairly often.
The Command Surface: From Ad Clicks to a Reverse Proxy
The stage-3 payload is where the malware stops being a dropper and starts being a remote-controlled agent. It phones home every 90 minutes by default with device fingerprinting data — screen resolution, device model, connected Wi-Fi SSID, MAC address — and receives either a configuration update or a set of numeric command identifiers the attackers call productId. Kaspersky documented nine distinct command types implemented in the code, mapped by a field called tagName, ranging from reading clipboard values to pinging hosts over ICMP.
The one that stopped me mid-scroll was called web: it opens a URL inside a WebView and executes arbitrary JavaScript inside it, either from a remote resource or from base64-encoded JS shipped directly in the command payload.
In practice, Kaspersky observed the attackers using only a couple of these commands at the time of publication: http, for generic beaconing and reporting, and loadlib2, which downloads and executes arbitrary compiled code via reflection. The payload it fetched was a reverse-proxy module the researchers named "zhima" — and notably, researchers from the Nokia Deepfield Emergency Response Team independently found the same module running on infected TV set-top boxes around the same time, which is strong confirmation that the endgame here is building a residential proxy botnet, not just serving ads.
The Three Stages, Side by Side
It's easy to lose the forest for the trees once you're a few paragraphs into decryption routines and reflection calls, so it's worth laying the three stages out next to each other before moving on to attribution.
| Stage | Component | Entry point | Primary role |
|---|---|---|---|
| 1 | JarService dropper | wa() in com.c.j.qbh | No-UI installer; decrypts and launches stage 2 |
| 2 | Loader | init() in com.ast.sdk.BillingMain | Beacons to C2, fetches and decrypts stage 3 |
| 3 | Clicker / proxy loader | Command dispatcher keyed by tagName | Ad fraud, remote commands, deploys the zhima proxy module |
What stands out in that table, at least to me, is how little each individual stage actually does on its own. JarService doesn't defraud anyone. The loader doesn't run ads or proxy traffic. Each stage is small, disposable, and replaceable — which is exactly why Kaspersky found seven versions of stage 3 and eight versions of the zhima module just by trying different version numbers in the same URL pattern.
Attribution: Why This Points Back to MoYu Group and BADBOX
Kaspersky's attribution case is built on naming coincidences that turned out not to be coincidences. The stage-2 loader spawns a thread named mosdk-host-loader, and tracing what mosdk referred to led researchers to a separate malicious app installed on Android TV set-top boxes, with a component literally named AdmoyuService. Put "moyu" next to the thread name and the pattern points at MoYu Group, an actor already linked to the BADBOX botnet platform previously documented by researchers at HUMAN Security.
The infrastructure overlap makes the case stronger than naming alone would. Kaspersky traced a domain used by the zhima module's C2 infrastructure to an admin panel that let anyone register as an operator given a valid invite code, and found that panel's terms of service pointed to two vendors selling residential proxy services. Independently, the Nokia Deepfield Emergency Response Team identified overlapping MoYu Group infrastructure around the same time, which is the kind of cross-confirmation that turns a plausible theory into a high-confidence attribution.
None of this means every Android-based embedded device is compromised, and it's worth being precise about what was and wasn't found: this is one vendor's update pipeline, on one family of head units, fixed after Kaspersky's disclosure. But it's also the first documented case of malware built specifically around a car head unit's infection chain, and that milestone matters more than the specific vendor involved.
What I'd Change If I Owned an Update Pipeline Like This
Reading this report made me go back and re-audit the remote-update service I mentioned at the start, and I didn't love what I found. It had its own version of installNotExists — not named that, but functionally identical — and the only thing standing between "push a config update" and "push an arbitrary APK" was a code review from eight months ago that nobody had revisited since.
The failure mode here isn't really a coding bug you can unit-test your way out of. It's a design decision that quietly expands scope over time: someone adds a flag for a legitimate edge case, that flag never gets revisited, and eventually it's the exact hole an attacker needs. That's a slower, more organizational kind of vulnerability, and it's the reason I now treat any "install if missing" style flag as something that needs its own threat model, not just a code comment.
Concretely, here's the checklist I ended up running against my own update service after reading Kaspersky's report:
- Replace any open-ended "install if not present" flag with a curated allowlist of package names and signing certificates the update channel is ever permitted to reference — the server should not get to name arbitrary new packages on a whim.
- Verify APK signatures against a pinned certificate on-device, not just trust that a payload arrived from your own broker — a compromised subdomain or MQTT topic looks identical to a legitimate one from the client's perspective.
- Log and rate-limit every silent install path so an abuse pattern shows up in telemetry before it shows up in a security researcher's report.
- Never expose an equivalent of "download and execute arbitrary code via reflection" on an unauthenticated topic or endpoint, even for legitimate dynamic-module use cases — scope it to a specific, versioned, signed module list.
- Keep the channel that installs new packages structurally separate from the one that flips remote feature flags; collapsing both into one message schema is how a single compromised broker becomes full code execution.
None of that is exotic advice. It's the same discipline you'd apply to any privileged remote-execution surface. The only reason it's easy to skip is that update pipelines don't feel like remote-execution surfaces until someone points out that, functionally, that's exactly what they are.
Conclusion
What sticks with me about this report isn't the car angle, even though that's the headline-grabbing part. It's that BADBOX-linked actors keep finding new categories of "boring" system software to ride in on — a TV box's IPTV app, a phone's pre-installed backdoor, and now a car head unit's own update service — because those components are trusted by default and rarely audited with the same scrutiny as a user-facing app. If you maintain any background service that has the authority to install or update software without a fresh user prompt each time, this report is worth reading end to end, not for the car-specific details, but for the shape of the mistake. I'd rather find my own equivalent of installNotExists in a code review than in someone else's incident writeup.
Sources: Dmitry Kalinin, Securelist — "The invisible passenger in your car", Nokia Deepfield Emergency Response Team — ipmoyu report, HUMAN Security — Satori Threat Intelligence: BADBOX 2.0

Comments
No comments yet — be the first to share your thoughts.