A personal device audit of a Eonon aftermarket Android car stereo (Topway TS18 platform), run like a mini pentest engagement — recon → static analysis → sandboxed dynamic verification → reporting. Real engagement, real finding: the unit is carrying a dropper tied to an active malware campaign (BADBOX / MoYu Group). Written up here because the whole arc — “I don’t trust this Chinese box” turning into a confirmed live C2 chain — is exactly the kind of finding a pentest report exists to capture, and the techniques (static Android RE, obfuscated-payload decoding, passive OSINT instead of live contact, sandbox construction) generalize past this one device.

Scope and authorization

Device owned by the operator (self-engagement, no third-party authorization needed — this methodology is for your own hardware or an explicit engagement, never someone else’s device). Scope was read-only analysis: dump accessible files, decompile apps, verify firmware hashes, and identify indicators of compromise. Explicitly out of scope: contacting C2 infrastructure with the real device identity, running any decoded payload, flashing or modifying the unit. The rule followed throughout: never run an unknown sample on your real machine or network.

Target profile

Field Value
Platform Unisoc UIS8581A (8× Cortex-A55 @ 1.6 GHz), PowerVR GE8322 GPU
OS Android 10, kernel 4.14.133
Vendor stack Topway TS18 (uis8581a2h10_Automotive-user), rebadged as Eonon
Installed build TS18.2.2_20240402.213649, MCU Ts18.1.1-100-8F-A5489D-240424
FOTA server iotapi.abupdate.com (plain HTTP download host: iotdown-jd.mayitek.com)
App store DoFun market, osappstore.cdn.cardoor.cn

Methodology

  1. Recon (passive): read on-device “About” screens, pulled version/build strings from photos, searched XDA/vendor mirrors for matching firmware — no device contact needed for this phase.
  2. Firmware acquisition and hash verification: downloaded a same-build firmware package from a third-party mirror, compared its SHA-256 against the value the unit’s own FOTA server reported for that version (mismatch, by design — the official package was a delta update, not a full image; documented rather than assumed malicious).
  3. Static triage of a full file-system dump (USB-accessible folders pulled by the operator) — file, strings, size/entropy sampling, looking for installed-package lists, cached APKs, and app logs rather than reading every file blind.
  4. APK decompilation: jadx + apktool against every cached/pushed APK, focused on manifests (permissions, exported components, shared UID) before diving into code.
  5. Obfuscated payload extraction: the suspicious app’s embedded second stage was XOR-encoded across split byte arrays reassembled at runtime — wrote a small Python script to statically replicate that reassembly (same XOR key schedule, same byte-array layout) without ever executing the app..
  6. Firmware image unpack: lpunpack/simg2img on the super.img dynamic-partition blob, debugfs to list system/vendor partitions read-only, searched for the dropper’s strings to establish whether it ships in the image or arrives only via push (it’s push-only — not in the factory image).
  7. Passive OSINT on IOCs: dig/whois against the C2 domains (resolution + registration dates only, no HTTP requests), then cross-referenced against public threat-intel write-ups. This is the step that turned “a weird obfuscated loader” into “a documented campaign”.
  8. Sandbox construction for anything requiring execution-adjacent tooling (decompilers, not the sample itself): bwrap --unshare-all with no network, no $HOME, read-only sample mounts, dedicated output dir.

Findings

🔴 Critical — Active malware dropper installed (com.tw.jar1)

What: com.tw.jar1 v1.11 is present in the unit’s installed-package list. Static decompilation showed an obfuscated loader (XOR-encoded strings, class names scrambled) that decrypts an embedded jar at runtime and loads it via reflection — a deliberate change from the app’s own v1.8, which called the same ad-SDK openly (ADMOYU.start(...)). The decoded second stage (package com.xgw.f) phones home to task.mymoyu.shop/cpc/api/client/active, polls four rotating domains (a1.ty4523.space, a1.vrr8345.site, a1.ty54fgd435.my, a1.ue886578433.online) for a sdk.jar third stage, falls back to an Alibaba OSS config bucket, and class-loads whatever it downloads.

Confirmed via third-party intel: this exact chain is documented. Nokia Deepfield’s report describes the identical host.dex (package com.xgw.f, sha256 prefix ccce124a… — matching the hash of the payload extracted here) hitting the same endpoint and domains. Kaspersky/Securelist and VicOne independently name com.tw.jar1 (“JarService”) as the first-stage dropper in a campaign targeting DoFun/Topway head units specifically, attributed with high confidence to MoYu Group, linked to the BADBOX botnet (per HUMAN Security’s BADBOX 2.0 disclosure, over 1M compromised off-brand Android devices). The third stage (not recovered in this engagement) is reported elsewhere as a clicker/residential-proxy module (“zhima”) — ad fraud plus using the device as an exit node for someone else’s traffic.

Delivery vector: not preinstalled in the factory firmware image (confirmed absent from both system.img and vendor.img after unpacking the official-channel firmware). Delivered post-sale by com.tw.core (TWCore), the OEM’s own update/device-management app — which runs as sharedUserId="android.uid.system" and holds INSTALL_PACKAGES/DELETE_PACKAGES/FORCE_STOP_PACKAGES. Per Kaspersky, the mechanism is a malicious MQTT upgrade command sent to TWCore’s broker — the legitimate OEM update channel gets hijacked to push the dropper, no user interaction and no separate exploit required. This is the textbook supply-chain/update-channel trust failure: the thing with install authority is the thing that got abused.

Impact: CVSS-style reasoning — network-adjacent attacker (owns the C2) achieves persistent code execution on a privileged automotive IVI system with phone-state and location permissions, chained to a stage-3 payload capable of arbitrary JS execution (web command per Kaspersky’s IOC list) and further code download (loadlib2). Practically: ad fraud, device used as a reverse proxy for other people’s traffic, and an open door for whatever the operators push next — MQTT commands observed elsewhere in this family include silent app uninstall, HOME launcher replacement, and disabling a competing SIM.

Remediation: block all listed IOCs at the network edge (below). Uninstall com.tw.jar1 if the unit’s package manager allows it — expect TWCore to attempt reinstall, since it still has install authority. Full remediation requires either a firmware rebuild without TWCore/the DoFun push stack, or keeping the unit permanently network-isolated.

🟠 High — OTA channel integrity is weak

FOTA client (com.abupdate.fota_demo_iot) downloads update packages over plain HTTP (iotdown-jd.mayitek.com), with integrity checked only by a SHA-256 the same HTTP-served JSON response supplies — an on-path attacker who can MITM the check call can also just serve a different hash alongside a different package. The server’s checkVersion response additionally carries free-form commands with "method":"write" that the client appears to execute — remote configuration write capability baked into an unauthenticated-looking channel. This is the same class of failure that let the stage-1 dropper in via TWCore/MQTT, just on a different OEM service.

🟠 High — Decoded payload trusts all TLS certificates

The decoded second-stage loader’s HTTPS client (com.xgw.f.k) installs a custom TrustManager/HostnameVerifier that accepts any certificate. Combined with loading and executing whatever sdk.jar it receives with zero signature or hash check, this means anyone who can MITM the unit’s traffic — not just the campaign operators — can supply arbitrary code to it. Ironic given the payload’s own job is to compromise the device; it’s sloppy malware, but the flaw is equally present against a legitimate-looking MITM, which matters for a defender deciding whether network isolation alone is sufficient (it is, if done correctly — this flaw doesn’t create a second path in, it just means a blocked-domain unit isn’t magically safer against an on-path attacker on an untrusted network).

🟡 Medium — SELinux effectively permissive

Crash-dump logs show seccomp disabled by setenforce 0 on every app launch — SELinux is not enforcing on this build. Lowers the bar for any future code execution (including the stage-3 payload, if it lands) to escalate beyond its own app sandbox.

🟡 Medium — Unguarded exported system components

com.tw.carinfoservice (the CAN-bus/vehicle-integration service manager) runs as sharedUserId="android.uid.system" and exports CarService and PermissionService with no permission enforcement beyond a normal-level custom permission — meaning any app installed on the unit, dropper or otherwise, can bind to a system-privileged service that controls vehicle-side integrations. Classic over-broad exported="true" finding, the same class of over-broad exported-component finding, just on the IPC surface instead of the network surface.

🔵 Low — Shared vendor signing key across unrelated apps

Multiple Topway-origin packages (com.tw.carinfoservice, TWCore, others) share one platform signing certificate (CN=John, O=TW, Shenzhen, valid to 2040, SHA1withRSA — itself a weak/disabled algorithm). A single compromised key compromises the trust boundary for the entire app set at once. Worth knowing as a vendor-practice smell even though it wasn’t the entry point here (the dropper was signed by a different, unrelated key — CN=Zheng, O=jk, with a fake 457 “country” — itself a tell, since legitimate platform apps shouldn’t be signed by random third-party certs).

⚪ Informational — Red herring, documented and dismissed

A configuration.txt found in the same file dump was a stock NiceHash OS mining config (BTC address, worker name). Traced back to its actual origin — a USB drive previously used to boot NiceHash OS, unrelated to the head unit — before being ruled out. Worth noting as process, not just outcome: a pentest report should show the dead ends that got checked and cleared, not just the live findings; a USB stick’s provenance mattering is the same “verify before escalating” discipline as not assuming a hash mismatch means a malicious firmware mirror (that mismatch above turned out to be a delta-vs-full-image difference, not tampering).

Indicators of Compromise

Domains (all confirmed live via passive DNS as of this engagement — not dead infrastructure):

Domain Role Resolves to Registered
task.mymoyu.shop activation/heartbeat 10 rotating IPs (Leaseweb, others) —
a1.ty4523.space loader 47.76.118.67 (Alibaba Cloud) 2026-09-02
a1.vrr8345.site loader 47.76.118.67 (same IP as above) 2026-09-02
a1.ty54fgd435.my loader 157.90.33.73/74 2026-08-10
a1.ue886578433.online loader 193.166.255.171 2026-08-21
terrtest.oss-cn-hangzhou.aliyuncs.com config fallback Alibaba OSS (returned AccessDenied) —

Package/file hashes:

Item Hash
com.tw.jar1 v1.11 APK SHA-256 0776f04a36539bfc5e5760c29dbb19f4dc950826b224b27ffdab70c208dbb0ce
com.tw.jar1 v1.8 APK SHA-256 0c9cc75b5ababdff37477723148956990115d8be52107f4b89f98d13f722cc9d
Decoded stage-2 jar (host.dex equiv.) SHA-256 ccce124a28b1c181c0f18ae8bd1b47ea1904e6f108b8d10664838c37262d4640 — matches Deepfield’s published host.dex hash prefix

Local behavior: payload queries a loopback service on 127.0.0.1:12101 for a device UID — confirms an on-device companion/service component beyond the app itself.

Remediation summary

  1. Block all domains above at the router/firewall level — the single most effective mitigation available without touching the unit itself.
  2. Remove the SIM / disable cellular if present; keep the unit off any network you’d mind being used as an exit node.
  3. Attempt removal of com.tw.jar1; assume TWCore will try to reinstall it and monitor for recurrence.
  4. Treat com.tw.core as the actual trust boundary that failed — any future clean-firmware project should omit it and the DoFun push stack entirely, not just remove the dropper after the fact.
  5. Report the sample/IOCs to the vendor and to Kaspersky/relevant threat-intel trackers — this device is currently live, which the published reports’ “domains now dead” status didn’t capture.

Follow-up: native REA/Ghidra sweep of the companion casting app

A separate app on the same unit (com.zjinnova.zlink, “T-Link5” — the head-unit-side casting receiver for Android Auto Wireless/HiCar/CarLife/AirPlay) was audited with REA’s Ghidra provider, decompiling its bundled native libraries directly rather than relying on strings output alone.

Follow-up: sdk.jar (stage 3) — confirmed unavailable via any passive source

Checked whether the undocumented third stage could be obtained without contacting the campaign’s live infrastructure: general web search, the original Deepfield/Kaspersky/VicOne reporting, and MalwareBazaar directly. No public copy exists anywhere. Deepfield’s own researchers never recovered it either — their loader domains were dead at analysis time, so even the primary published source describes sdk.jar’s behavior only by inference from what the first-stage loader does with it, never from its own bytes. Live retrieval from the (now-reactivated) loader domains remains the only way to see it, and remains explicitly out of scope for this engagement.

Follow-up: building a clean firmware image

With the findings confirmed, work began on a debloated firmware image built from the official-channel TS18.2.2_20240904 package (not the third-party mirror used for analysis) — unpacked via lpunpack, edited in place with debugfs -w (no loop-mount/root needed for simple file removal), to be repacked with lpmake once the removal list is finalized and tested.

Key constraint that shaped the approach: TWCore and com.tw.carinfoservice both declare sharedUserId="android.uid.system", co-signed with the vendor’s own platform certificate (not available to us). This rules out patching those apps’ manifests and repacking — a signature mismatch on a sharedUserId app risks a boot failure. Deletion sidesteps the problem entirely: removing a file from /system/priv-app needs no signature validation at all, Android just sees one fewer preinstalled app. Where modification was viable (com.zjinnova.zlink, confirmed to use no sharedUserId), stripping its two telemetry libraries and re-signing with a self-owned key remains an option — a smaller, separately-testable change, not bundled into the main image build.

Removed from the working image (13 APKs, verified via debugfs ls before and after): the malware delivery vector (TWCore), the FOTA client, three DoFun-ecosystem apps, and eight Google/Play-services packages (GmsCore, GooglePartnerSetup, GoogleServicesFramework, Phonesky, Velvet, AndroidAutoStub, ConfigUpdater, GmsSampleIntegration) — a deliberate de-Googling pass, replacing the Play ecosystem with F-Droid (sideloaded from a copy the operator already had) and, later, Aurora Store pulled through F-Droid’s own catalog rather than fetched and trusted blind.

Deliberately kept: the vehicle-integration stack (CAN bus/AC/radio/voice — not implicated in any finding), WebView/Chrome (AOSP-licensed, not Play-Services-gated, assumed present by other system surfaces), and carrier config (closer to AOSP telephony plumbing than Google tracking surface).

Open question carried forward: removing AndroidAutoStub assumes the casting app’s own protocol stack doesn’t depend on it for Android Auto Wireless specifically. Unconfirmed until first boot — the kind of assumption a changelog should surface explicitly rather than discover silently.

Follow-up: auditing the cleaned build found a shared signing key

A permission-reference sweep (/system/etc/permissions, /product/etc/permissions, sysconfig XMLs) of the rebuilt image turned up a finding worth its own entry:

Remediation taken: all three theme APKs stripped of their original signature and re-signed with a newly generated, self-owned key (jarsigner, SHA256withRSA — no apksigner/v2 scheme available in this environment, but none of the three use sharedUserId, so legacy v1 JAR signing was sufficient and needed no vendor key). Verified post-repack by extracting the rebuilt image and re-checking the signer in place, not just the pre-repack file.

Follow-up: v2 build — theme re-signing, a GMC boot logo, and a documented gap

A second repack pass added: the three re-signed theme APKs (above), F-Droid (/system/app/FDroid/FDroid.apk, sideloaded from a copy the operator already had — Aurora Store deliberately left for post-flash install via F-Droid’s own catalog rather than baking in an unverified binary), and a generated 1024×600 24-bit boot-splash BMP built from the operator’s own GMC artwork — this last one stays a separate userdata-level file (.Tcfg/bootlogo.bmp), not part of the flashable package, since the local-updater package format here has no userdata-partition slot to embed it into.

Every untouched file in the package was byte-size-diffed against the original before and after both repack passes — the discipline of proving “nothing else changed” rather than assuming it. A README-CLEAN-BUILD.txt ships inside the package itself, documenting exactly what was removed, restored, re-signed, and — critically — what flashing this package does not fix (the /data-resident apps above), so the gap travels with the artifact instead of living only in chat history.

Follow-up: the data wipe is a separate vendor package, not part of the OTA

Since the firmware-only flash can’t touch /data (above), checked whether the vendor provides a dedicated wipe mechanism through the same local-updater channel rather than relying on the device’s own Settings menu. It does. The same firmware host that served the base build also hosts TS18.2.2_Format+File.zip, explicitly tagged (WIPE), flashed through the identical USB/Key-1 local-updater path as regular firmware — the vendor deliberately keeps data wipes out of the normal OTA package (so routine updates don’t destroy user data by accident) rather than making wiping impossible through that channel.

Verified by extraction before trusting a third-party host for it, same discipline as every other download this engagement: the package contains exactly one file, a 32 MiB blob that turned out to be a freshly formatted, genuinely empty ext4 filesystem labeled "data" — 33,547,704 of its 33,554,432 bytes are literal zero, with only filesystem metadata and an empty lost+found as content. This is close to the ideal case for trusting an unverified download: a structurally blank filesystem image has no space to hide a payload in, which is a stronger safety argument than hash-checking alone would give for an executable or an app.

Minor forensic bonus: the wipe package’s file UUID shares a middle segment (9cbf-22ea) with three of the four files in this engagement’s own (unrelated, newer) firmware package — suggesting that segment is a stable vendor-assigned partition-type tag, not a per-build random value. Weak but free corroborating evidence that a 2020-dated wipe package is still structurally compatible with 2024-era firmware on this platform.

Follow-up: why a full system-image test can’t be done in Waydroid

Before committing to the “do we have a safe way to test this before flashing real hardware” question, checked whether Waydroid (or a similar Android container) could run the modified system.img/vendor.img pair. It can’t, and the reasons are structural, not a tooling gap:

What’s actually available as pre-flight validation is what this engagement has been doing throughout: structural verification (partition metadata matching via lpdump, extracting the rebuilt image and confirming file-level content rather than trusting the build process), not behavioral/boot testing. The one thing a tool like Waydroid could legitimately help with is testing an individual modified APK’s installability and signature validity in isolation (e.g. the re-signed theme packages) — a much narrower, app-layer-only check that says nothing about whether the image as a whole boots on real hardware.

Follow-up: the backdoor confirmed, full root achieved, and the project’s real ending

Everything from here on happened in a single extended session, on live hardware, after run-as was confirmed to require shell or root UID before even inspecting its argument — which meant Termux’s own app-sandboxed attempt failing earlier was expected, not a dead end. The actual path to shell:

  1. Wireless ADB, built from nothing but property writes. ro.secure=1/ro.debuggable=0 (a real user build) normally makes this impossible without a prior USB-authorized ADB session. Permissive SELinux let it happen anyway: setprop service.adb.tcp.port 5555 then setprop ctl.start adbd from an unprivileged Termux shell started adbd in TCP mode regardless — ctl.* properties are gated purely by SELinux property_contexts on modern Android, and permissive mode doesn’t enforce that gate. adb connect 127.0.0.1:5555 from Termux’s own (initially broken, later working) adb client then produced a genuine shell (uid 2000) session.
  2. From shell, the backdoor fired for real: run-as "john@tw-desktop.UIS8581A" id → uid=0(root). Confirmed, not inferred — the exact vendor-build-identity-shaped string found weeks earlier in a decompiled toolbox applet (xupdate_main) turned out to be a genuine, functioning privilege-escalation backdoor, reachable the moment any process reaches shell UID by any means. This is a finding independent of anything else in this engagement — any process that can reach ADB shell on this platform has root, full stop.

Full partition map obtained as root (/dev/block/by-name/, real eMMC device mmcblk0), revealing something the whole prior engagement never knew: this device implements AVB (vbmeta/vbmeta_system/vbmeta_vendor partitions). ro.boot.verifiedbootstate=orange (unlocked) + ro.boot.veritymode=enforcing meant the bootloader itself wouldn’t hard-block custom content, but runtime dm-verity on the mounted partitions still needed neutralizing — standard custom-ROM technique: avbtool make_vbmeta_image --set_hashtree_disabled_flag --set_verification_disabled_flag, written directly to all three vbmeta partitions before anything else.

This fully superseded the entire local-updater/recovery-signature investigation. Every finding about RecoverySystem.installPackage(), the otacerts.zip/”Zhantang” certificate, and the unidentified recovery-partition validation logic — real findings, fully documented above — turned out to be beside the point once root was achieved: direct dd to the real block devices (super at mmcblk0p31, userdata at mmcblk0p44) bypasses that entire verification chain, because nothing in it runs before raw bytes land on the chip. Lesson for the methodology: a software-signature wall that looks unbreakable from inside the OS can become irrelevant the moment a lower-privilege boundary (here, run-as’s own backdoor) is crossed — worth checking what’s actually enforcing a wall before assuming it has to be defeated on its own terms.

Write executed and verified, byte-count-exact on every target (vbmeta×3, super — 2,936,012,800 bytes, zero dropped records — userdata), using the RootSU community build’s system.img (independently verified clean of every project IOC, decompiled root mechanism confirmed attributed/non-obfuscated) combined with this engagement’s own verified-stock vendor.img, substituted in place of a corrupted download. The device boots successfully — confirmed by Russian-language UI (the RootSU build’s actual content), working su in Termux, and a pre-installed Root Explorer, all independently confirming genuine success despite the About screen’s version string staying unchanged (the RootSU author never edited that string — a red herring the operator initially read as failure, resolved by checking three independent content signals instead of one string).

With real root, the original goal — stripping TWCore/DoFun bloat — stopped needing the whole rebuild-and-flash pipeline. Direct live removal (mount -o rw,remount /, delete the APKs, pm uninstall --user 0) replaced hours of offline image surgery. Also closed off cleanly: Magisk was considered and correctly rejected for this specific goal — it solves a problem (no root) this engagement no longer had, at the cost of patching an unexamined boot partition for zero actual benefit toward debloating.

The touch-inversion mystery — the one thread that ran the entire second half of this engagement — had nothing to do with any of it. Ruled out, in order, with real evidence at each step: not firmware content (persisted across stock, custom, and fully-wiped RootSU flashes), not Android’s rotation/window-manager state (dumpsys window showed a completely clean ROTATION_0, display matching physical panel dimensions exactly), not hardware (unit never opened). Confirmed via pointer_location as a genuine raw-coordinate 180° mirror, present before Android’s input pipeline ever sees it — at which point the actual fix turned out to be a standard factory-service gesture baked into the touch controller itself: holding five fingers on the screen simultaneously re-triggers calibration. No file, partition, or line of code in this entire engagement needed to change. The lesson generalizes past this one device: when a symptom survives every software variable you can control changing, and the hardware is confirmed untouched, check for a vendor self-service mechanism before assuming the fix has to be built — especially on cheap consumer hardware, where “undocumented factory gesture” is a genuinely common design pattern, not an edge case.

Lessons for the methodology itself