<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://mizfitxtm.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mizfitxtm.dev/" rel="alternate" type="text/html" /><updated>2026-10-25T05:04:05+00:00</updated><id>https://mizfitxtm.dev/feed.xml</id><title type="html">MizfitXTM</title><subtitle>Cybersecurity | Penetration Testing | Homelab</subtitle><author><name>Joshua Duane Penland</name></author><entry><title type="html">Aftermarket Android Head Unit Security Audit</title><link href="https://mizfitxtm.dev/writeups/aftermarket-android-head-unit-security-audit/" rel="alternate" type="text/html" title="Aftermarket Android Head Unit Security Audit" /><published>2026-10-24T00:00:00+00:00</published><updated>2026-10-24T00:00:00+00:00</updated><id>https://mizfitxtm.dev/writeups/aftermarket-android-head-unit-security-audit</id><content type="html" xml:base="https://mizfitxtm.dev/writeups/aftermarket-android-head-unit-security-audit/"><![CDATA[<p>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 (<strong>BADBOX / MoYu Group</strong>). 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.</p>

<h2 id="scope-and-authorization">Scope and authorization</h2>

<p>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 <strong>out of scope</strong>: 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.</p>

<h2 id="target-profile">Target profile</h2>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Platform</td>
      <td>Unisoc UIS8581A (8× Cortex-A55 @ 1.6 GHz), PowerVR GE8322 GPU</td>
    </tr>
    <tr>
      <td>OS</td>
      <td>Android 10, kernel 4.14.133</td>
    </tr>
    <tr>
      <td>Vendor stack</td>
      <td>Topway TS18 (<code class="language-plaintext highlighter-rouge">uis8581a2h10_Automotive-user</code>), rebadged as Eonon</td>
    </tr>
    <tr>
      <td>Installed build</td>
      <td><code class="language-plaintext highlighter-rouge">TS18.2.2_20240402.213649</code>, MCU <code class="language-plaintext highlighter-rouge">Ts18.1.1-100-8F-A5489D-240424</code></td>
    </tr>
    <tr>
      <td>FOTA server</td>
      <td><code class="language-plaintext highlighter-rouge">iotapi.abupdate.com</code> (plain HTTP download host: <code class="language-plaintext highlighter-rouge">iotdown-jd.mayitek.com</code>)</td>
    </tr>
    <tr>
      <td>App store</td>
      <td>DoFun market, <code class="language-plaintext highlighter-rouge">osappstore.cdn.cardoor.cn</code></td>
    </tr>
  </tbody>
</table>

<h2 id="methodology">Methodology</h2>

<ol>
  <li><strong>Recon (passive):</strong> 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.</li>
  <li><strong>Firmware acquisition and hash verification:</strong> 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).</li>
  <li><strong>Static triage of a full file-system dump</strong> (USB-accessible folders pulled by the operator) — <code class="language-plaintext highlighter-rouge">file</code>, <code class="language-plaintext highlighter-rouge">strings</code>, size/entropy sampling, looking for installed-package lists, cached APKs, and app logs rather than reading every file blind.</li>
  <li><strong>APK decompilation:</strong> <code class="language-plaintext highlighter-rouge">jadx</code> + <code class="language-plaintext highlighter-rouge">apktool</code> against every cached/pushed APK, focused on manifests (permissions, exported components, shared UID) before diving into code.</li>
  <li><strong>Obfuscated payload extraction:</strong> 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..</li>
  <li><strong>Firmware image unpack:</strong> <code class="language-plaintext highlighter-rouge">lpunpack</code>/<code class="language-plaintext highlighter-rouge">simg2img</code> on the <code class="language-plaintext highlighter-rouge">super.img</code> dynamic-partition blob, <code class="language-plaintext highlighter-rouge">debugfs</code> to list <code class="language-plaintext highlighter-rouge">system</code>/<code class="language-plaintext highlighter-rouge">vendor</code> 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).</li>
  <li><strong>Passive OSINT on IOCs:</strong> <code class="language-plaintext highlighter-rouge">dig</code>/<code class="language-plaintext highlighter-rouge">whois</code> 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”.</li>
  <li><strong>Sandbox construction for anything requiring execution-adjacent tooling</strong> (decompilers, not the sample itself): <code class="language-plaintext highlighter-rouge">bwrap --unshare-all</code> with no network, no <code class="language-plaintext highlighter-rouge">$HOME</code>, read-only sample mounts, dedicated output dir.</li>
</ol>

<h2 id="findings">Findings</h2>

<h3 id="-critical--active-malware-dropper-installed-comtwjar1">🔴 Critical — Active malware dropper installed (<code class="language-plaintext highlighter-rouge">com.tw.jar1</code>)</h3>

<p><strong>What:</strong> <code class="language-plaintext highlighter-rouge">com.tw.jar1</code> 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 (<code class="language-plaintext highlighter-rouge">ADMOYU.start(...)</code>). The decoded second stage (package <code class="language-plaintext highlighter-rouge">com.xgw.f</code>) phones home to <code class="language-plaintext highlighter-rouge">task.mymoyu.shop/cpc/api/client/active</code>, polls four rotating domains (<code class="language-plaintext highlighter-rouge">a1.ty4523.space</code>, <code class="language-plaintext highlighter-rouge">a1.vrr8345.site</code>, <code class="language-plaintext highlighter-rouge">a1.ty54fgd435.my</code>, <code class="language-plaintext highlighter-rouge">a1.ue886578433.online</code>) for a <code class="language-plaintext highlighter-rouge">sdk.jar</code> third stage, falls back to an Alibaba OSS config bucket, and class-loads whatever it downloads.</p>

<p><strong>Confirmed via third-party intel:</strong> this exact chain is documented. <a href="https://github.com/deepfield/public-research/blob/main/ipmoyu/report.md">Nokia Deepfield’s report</a> describes the identical <code class="language-plaintext highlighter-rouge">host.dex</code> (package <code class="language-plaintext highlighter-rouge">com.xgw.f</code>, sha256 prefix <code class="language-plaintext highlighter-rouge">ccce124a…</code> — matching the hash of the payload extracted here) hitting the same endpoint and domains. <a href="https://securelist.com/android-head-unit-malware/121106/">Kaspersky/Securelist</a> and <a href="https://vicone.com/blog/how-badbox-linked-malware-turned-automotive-head-units-into-proxy-nodes/">VicOne</a> independently name <code class="language-plaintext highlighter-rouge">com.tw.jar1</code> (“JarService”) as the first-stage dropper in a campaign targeting DoFun/Topway head units specifically, attributed with high confidence to <strong>MoYu Group</strong>, linked to the <strong>BADBOX</strong> botnet (per <a href="https://www.humansecurity.com/learn/blog/satori-threat-intelligence-disruption-badbox-2-0/">HUMAN Security’s BADBOX 2.0 disclosure</a>, 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.</p>

<p><strong>Delivery vector:</strong> not preinstalled in the factory firmware image (confirmed absent from both <code class="language-plaintext highlighter-rouge">system.img</code> and <code class="language-plaintext highlighter-rouge">vendor.img</code> after unpacking the official-channel firmware). Delivered post-sale by <code class="language-plaintext highlighter-rouge">com.tw.core</code> (TWCore), the OEM’s own update/device-management app — which runs as <code class="language-plaintext highlighter-rouge">sharedUserId="android.uid.system"</code> and holds <code class="language-plaintext highlighter-rouge">INSTALL_PACKAGES</code>/<code class="language-plaintext highlighter-rouge">DELETE_PACKAGES</code>/<code class="language-plaintext highlighter-rouge">FORCE_STOP_PACKAGES</code>. 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. <strong>This is the textbook supply-chain/update-channel trust failure</strong>: the thing with install authority is the thing that got abused.</p>

<p><strong>Impact:</strong> 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 (<code class="language-plaintext highlighter-rouge">web</code> command per Kaspersky’s IOC list) and further code download (<code class="language-plaintext highlighter-rouge">loadlib2</code>). 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.</p>

<p><strong>Remediation:</strong> block all listed IOCs at the network edge (below). Uninstall <code class="language-plaintext highlighter-rouge">com.tw.jar1</code> 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.</p>

<h3 id="-high--ota-channel-integrity-is-weak">🟠 High — OTA channel integrity is weak</h3>

<p>FOTA client (<code class="language-plaintext highlighter-rouge">com.abupdate.fota_demo_iot</code>) downloads update packages over <strong>plain HTTP</strong> (<code class="language-plaintext highlighter-rouge">iotdown-jd.mayitek.com</code>), with integrity checked only by a SHA-256 the <em>same</em> 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 <code class="language-plaintext highlighter-rouge">checkVersion</code> response additionally carries free-form <code class="language-plaintext highlighter-rouge">commands</code> with <code class="language-plaintext highlighter-rouge">"method":"write"</code> 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.</p>

<h3 id="-high--decoded-payload-trusts-all-tls-certificates">🟠 High — Decoded payload trusts all TLS certificates</h3>

<p>The decoded second-stage loader’s HTTPS client (<code class="language-plaintext highlighter-rouge">com.xgw.f.k</code>) installs a custom <code class="language-plaintext highlighter-rouge">TrustManager</code>/<code class="language-plaintext highlighter-rouge">HostnameVerifier</code> that accepts any certificate. Combined with loading and executing whatever <code class="language-plaintext highlighter-rouge">sdk.jar</code> it receives with zero signature or hash check, this means <strong>anyone who can MITM the unit’s traffic</strong> — 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).</p>

<h3 id="-medium--selinux-effectively-permissive">🟡 Medium — SELinux effectively permissive</h3>

<p>Crash-dump logs show <code class="language-plaintext highlighter-rouge">seccomp disabled by setenforce 0</code> 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.</p>

<h3 id="-medium--unguarded-exported-system-components">🟡 Medium — Unguarded exported system components</h3>

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

<h3 id="-low--shared-vendor-signing-key-across-unrelated-apps">🔵 Low — Shared vendor signing key across unrelated apps</h3>

<p>Multiple Topway-origin packages (<code class="language-plaintext highlighter-rouge">com.tw.carinfoservice</code>, <code class="language-plaintext highlighter-rouge">TWCore</code>, others) share one platform signing certificate (<code class="language-plaintext highlighter-rouge">CN=John, O=TW, Shenzhen</code>, 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 <em>different</em>, unrelated key — <code class="language-plaintext highlighter-rouge">CN=Zheng, O=jk</code>, with a fake <code class="language-plaintext highlighter-rouge">457</code> “country” — itself a tell, since legitimate platform apps shouldn’t be signed by random third-party certs).</p>

<h3 id="-informational--red-herring-documented-and-dismissed">⚪ Informational — Red herring, documented and dismissed</h3>

<p>A <code class="language-plaintext highlighter-rouge">configuration.txt</code> 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. <strong>Worth noting as process, not just outcome:</strong> 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).</p>

<h2 id="indicators-of-compromise">Indicators of Compromise</h2>

<p><strong>Domains (all confirmed live via passive DNS as of this engagement — not dead infrastructure):</strong></p>

<table>
  <thead>
    <tr>
      <th>Domain</th>
      <th>Role</th>
      <th>Resolves to</th>
      <th>Registered</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">task.mymoyu.shop</code></td>
      <td>activation/heartbeat</td>
      <td>10 rotating IPs (Leaseweb, others)</td>
      <td>—</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">a1.ty4523.space</code></td>
      <td>loader</td>
      <td>47.76.118.67 (Alibaba Cloud)</td>
      <td>2026-09-02</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">a1.vrr8345.site</code></td>
      <td>loader</td>
      <td>47.76.118.67 (same IP as above)</td>
      <td>2026-09-02</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">a1.ty54fgd435.my</code></td>
      <td>loader</td>
      <td>157.90.33.73/74</td>
      <td>2026-08-10</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">a1.ue886578433.online</code></td>
      <td>loader</td>
      <td>193.166.255.171</td>
      <td>2026-08-21</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">terrtest.oss-cn-hangzhou.aliyuncs.com</code></td>
      <td>config fallback</td>
      <td>Alibaba OSS (returned <code class="language-plaintext highlighter-rouge">AccessDenied</code>)</td>
      <td>—</td>
    </tr>
  </tbody>
</table>

<p><strong>Package/file hashes:</strong></p>

<table>
  <thead>
    <tr>
      <th>Item</th>
      <th>Hash</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">com.tw.jar1</code> v1.11 APK</td>
      <td>SHA-256 <code class="language-plaintext highlighter-rouge">0776f04a36539bfc5e5760c29dbb19f4dc950826b224b27ffdab70c208dbb0ce</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">com.tw.jar1</code> v1.8 APK</td>
      <td>SHA-256 <code class="language-plaintext highlighter-rouge">0c9cc75b5ababdff37477723148956990115d8be52107f4b89f98d13f722cc9d</code></td>
    </tr>
    <tr>
      <td>Decoded stage-2 jar (<code class="language-plaintext highlighter-rouge">host.dex</code> equiv.)</td>
      <td>SHA-256 <code class="language-plaintext highlighter-rouge">ccce124a28b1c181c0f18ae8bd1b47ea1904e6f108b8d10664838c37262d4640</code> — matches Deepfield’s published <code class="language-plaintext highlighter-rouge">host.dex</code> hash prefix</td>
    </tr>
  </tbody>
</table>

<p><strong>Local behavior:</strong> payload queries a loopback service on <code class="language-plaintext highlighter-rouge">127.0.0.1:12101</code> for a device UID — confirms an on-device companion/service component beyond the app itself.</p>

<h2 id="remediation-summary">Remediation summary</h2>

<ol>
  <li>Block all domains above at the router/firewall level — the single most effective mitigation available without touching the unit itself.</li>
  <li>Remove the SIM / disable cellular if present; keep the unit off any network you’d mind being used as an exit node.</li>
  <li>Attempt removal of <code class="language-plaintext highlighter-rouge">com.tw.jar1</code>; assume TWCore will try to reinstall it and monitor for recurrence.</li>
  <li>Treat <code class="language-plaintext highlighter-rouge">com.tw.core</code> 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.</li>
  <li>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.</li>
</ol>

<h2 id="follow-up-native-reaghidra-sweep-of-the-companion-casting-app">Follow-up: native REA/Ghidra sweep of the companion casting app</h2>

<p>A separate app on the same unit (<code class="language-plaintext highlighter-rouge">com.zjinnova.zlink</code>, “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 <code class="language-plaintext highlighter-rouge">strings</code> output alone.</p>

<ul>
  <li><strong>Commercially shielded, not custom-obfuscated:</strong> the APK’s own DEX is a thin loader (<code class="language-plaintext highlighter-rouge">MyWrapperProxyApplication</code>) that decrypts and <code class="language-plaintext highlighter-rouge">System.load()</code>s the real logic from an encrypted asset at runtime via a named commercial packer (<code class="language-plaintext highlighter-rouge">libshellx-super.2019.so</code>, confirmed by string xrefs in the unpacker stub). This is a known class of Chinese app-shielding product, not evidence of malice by itself — but it means the real application logic is unreachable by static analysis, full stop, regardless of tooling.</li>
  <li><strong>No <code class="language-plaintext highlighter-rouge">com.tw.jar1</code>-style C2 pattern found</strong> in the 9 of ~50 bundled <code class="language-plaintext highlighter-rouge">.so</code> libraries checked (prioritized by risk: telemetry, location/camera, auth, casting-protocol libraries). No rotating domains, no hardcoded IPs, no runtime code-fetch-and-load.</li>
  <li><strong>Two legitimate but undisclosed telemetry SDKs confirmed via decompiled, cross-referenced strings</strong> (not just floating text): Tencent Bugly (<code class="language-plaintext highlighter-rouge">libBugly.so</code>, <code class="language-plaintext highlighter-rouge">bugly.qq.com</code>) and <strong>Tencent MTA</strong> (<code class="language-plaintext highlighter-rouge">libMtaNativeCrash_v2.so</code>, <code class="language-plaintext highlighter-rouge">com.tencent.stat.StatNativeCrashReport</code>) — a second analytics SDK not previously flagged.</li>
  <li><strong>Real privacy-relevant capability, no evidence of active use:</strong> <code class="language-plaintext highlighter-rouge">libdmsdpdvgps.so</code>, part of Huawei’s DMSDP distributed-device framework, exports <code class="language-plaintext highlighter-rouge">GpsReceiveLocationData</code>/<code class="language-plaintext highlighter-rouge">GpsReceiveDrivingData</code>/<code class="language-plaintext highlighter-rouge">GpsReceiveCarBaseInfo</code> — a module whose entire purpose is relaying location and driving-behavior data across linked devices. No external hostname appears in its strings (DMSDP uses its own service-mesh transport, not fixed URLs), so this is a documented capability, not a confirmed exfil channel.</li>
  <li><strong>Methodology note:</strong> <code class="language-plaintext highlighter-rouge">search_strings</code> returning cross-referenced, provider-verified string <em>objects</em> (tied to real code via Ghidra’s analysis) is meaningfully stronger evidence than a raw <code class="language-plaintext highlighter-rouge">strings</code> dump of the same binary — worth defaulting to REA/Ghidra over shell <code class="language-plaintext highlighter-rouge">strings</code> when a finding needs to hold up in a report, not just inform triage.</li>
</ul>

<h2 id="follow-up-sdkjar-stage-3--confirmed-unavailable-via-any-passive-source">Follow-up: <code class="language-plaintext highlighter-rouge">sdk.jar</code> (stage 3) — confirmed unavailable via any passive source</h2>

<p>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. <strong>No public copy exists anywhere.</strong> Deepfield’s own researchers never recovered it either — their loader domains were dead at analysis time, so even the primary published source describes <code class="language-plaintext highlighter-rouge">sdk.jar</code>’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.</p>

<h2 id="follow-up-building-a-clean-firmware-image">Follow-up: building a clean firmware image</h2>

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

<p><strong>Key constraint that shaped the approach:</strong> <code class="language-plaintext highlighter-rouge">TWCore</code> and <code class="language-plaintext highlighter-rouge">com.tw.carinfoservice</code> both declare <code class="language-plaintext highlighter-rouge">sharedUserId="android.uid.system"</code>, co-signed with the vendor’s own platform certificate (not available to us). This rules out <em>patching</em> those apps’ manifests and repacking — a signature mismatch on a <code class="language-plaintext highlighter-rouge">sharedUserId</code> app risks a boot failure. <strong>Deletion sidesteps the problem entirely</strong>: removing a file from <code class="language-plaintext highlighter-rouge">/system/priv-app</code> needs no signature validation at all, Android just sees one fewer preinstalled app. Where modification <em>was</em> viable (<code class="language-plaintext highlighter-rouge">com.zjinnova.zlink</code>, confirmed to use no <code class="language-plaintext highlighter-rouge">sharedUserId</code>), 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.</p>

<p><strong>Removed from the working image (13 APKs, verified via <code class="language-plaintext highlighter-rouge">debugfs ls</code> before and after):</strong> the malware delivery vector (<code class="language-plaintext highlighter-rouge">TWCore</code>), the FOTA client, three DoFun-ecosystem apps, and eight Google/Play-services packages (<code class="language-plaintext highlighter-rouge">GmsCore</code>, <code class="language-plaintext highlighter-rouge">GooglePartnerSetup</code>, <code class="language-plaintext highlighter-rouge">GoogleServicesFramework</code>, <code class="language-plaintext highlighter-rouge">Phonesky</code>, <code class="language-plaintext highlighter-rouge">Velvet</code>, <code class="language-plaintext highlighter-rouge">AndroidAutoStub</code>, <code class="language-plaintext highlighter-rouge">ConfigUpdater</code>, <code class="language-plaintext highlighter-rouge">GmsSampleIntegration</code>) — 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.</p>

<p><strong>Deliberately kept:</strong> 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).</p>

<p><strong>Open question carried forward:</strong> removing <code class="language-plaintext highlighter-rouge">AndroidAutoStub</code> 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.</p>

<h2 id="follow-up-auditing-the-cleaned-build-found-a-shared-signing-key">Follow-up: auditing the cleaned build found a shared signing key</h2>

<p>A permission-reference sweep (<code class="language-plaintext highlighter-rouge">/system/etc/permissions</code>, <code class="language-plaintext highlighter-rouge">/product/etc/permissions</code>, sysconfig XMLs) of the rebuilt image turned up a finding worth its own entry:</p>

<ul>
  <li><strong>Dangling references to removed packages are harmless</strong> — <code class="language-plaintext highlighter-rouge">SystemConfig</code>/<code class="language-plaintext highlighter-rouge">PackageManager</code> read privapp-permission allow-lists as grants-if-present; a listed-but-absent package is a normal boot-time no-op, not a crash risk. Confirmed by checking what kind of file each reference lived in before concluding this.</li>
  <li><strong>Real gap found:</strong> <code class="language-plaintext highlighter-rouge">com.dofun.market</code>, <code class="language-plaintext highlighter-rouge">com.dofun.variety</code>, and <code class="language-plaintext highlighter-rouge">com.dofun.carassistant.car</code> (ad-supported app store, an ad-laden widget app, and a GPS/trip-logging app) are <strong>not part of the factory firmware at all</strong> — they exist only in <code class="language-plaintext highlighter-rouge">/data</code> on the live unit, installed post-sale. The same is true of <code class="language-plaintext highlighter-rouge">com.tw.jar1</code> itself. <strong>A system-only firmware flash does not remove any of them</strong> — this only became visible by checking whether each suspect package existed as a file in the image, not just by trusting the original removal list was complete. A factory data reset is a required second step, not optional. <code class="language-plaintext highlighter-rouge">com.dofun.carsetting</code> <em>is</em> part of the factory image (it’s the real Settings app) and was correctly left alone.</li>
  <li><strong>Headline finding:</strong> the OS’s default theme engine (<code class="language-plaintext highlighter-rouge">/system/etc/theme/theme.apk</code> and two selectable packs in <code class="language-plaintext highlighter-rouge">/system/etc/theme_package/</code>) is signed with the <strong>exact same certificate fingerprint</strong> as the <code class="language-plaintext highlighter-rouge">com.tw.jar1</code> dropper (<code class="language-plaintext highlighter-rouge">CN=Zheng, OU=jkkj, O=jk, L=gd, ST=sz, C=457</code>, SHA-256 <code class="language-plaintext highlighter-rouge">5B:88:18:77:16:D7:4B:CA:64:1B:35:DA:26:1C:FA:52:F8:64:67:DD:DA:0A:C6:09:90:83:18:4D:F8:42:99:9D</code>). Decompiling all three theme APKs found them individually clean — no exported components, no obfuscation pattern matching the dropper, no embedded hosts, no <code class="language-plaintext highlighter-rouge">sharedUserId</code>. <strong>The significance isn’t “this file is malware” — static analysis didn’t show that — it’s that the signing key had write access to a component baked into the official factory firmware, not just post-sale push infrastructure.</strong> That’s a supply-chain-trust finding distinct from an active-compromise finding, and the right response is to stop trusting the <em>key</em> going forward (treat any future artifact bearing it as deserving the same scrutiny the dropper got), independent of what any single signed file currently contains.</li>
</ul>

<p><strong>Remediation taken:</strong> all three theme APKs stripped of their original signature and re-signed with a newly generated, self-owned key (<code class="language-plaintext highlighter-rouge">jarsigner</code>, SHA256withRSA — no <code class="language-plaintext highlighter-rouge">apksigner</code>/v2 scheme available in this environment, but none of the three use <code class="language-plaintext highlighter-rouge">sharedUserId</code>, 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.</p>

<h2 id="follow-up-v2-build--theme-re-signing-a-gmc-boot-logo-and-a-documented-gap">Follow-up: v2 build — theme re-signing, a GMC boot logo, and a documented gap</h2>

<p>A second repack pass added: the three re-signed theme APKs (above), F-Droid (<code class="language-plaintext highlighter-rouge">/system/app/FDroid/FDroid.apk</code>, 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 <em>separate</em> userdata-level file (<code class="language-plaintext highlighter-rouge">.Tcfg/bootlogo.bmp</code>), not part of the flashable package, since the local-updater package format here has no userdata-partition slot to embed it into.</p>

<p>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 <code class="language-plaintext highlighter-rouge">README-CLEAN-BUILD.txt</code> ships inside the package itself, documenting exactly what was removed, restored, re-signed, and — critically — what flashing this package does <em>not</em> fix (the <code class="language-plaintext highlighter-rouge">/data</code>-resident apps above), so the gap travels with the artifact instead of living only in chat history.</p>

<h2 id="follow-up-the-data-wipe-is-a-separate-vendor-package-not-part-of-the-ota">Follow-up: the data wipe is a separate vendor package, not part of the OTA</h2>

<p>Since the firmware-only flash can’t touch <code class="language-plaintext highlighter-rouge">/data</code> (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. <strong>It does.</strong> The same firmware host that served the base build also hosts <code class="language-plaintext highlighter-rouge">TS18.2.2_Format+File.zip</code>, explicitly tagged <code class="language-plaintext highlighter-rouge">(WIPE)</code>, 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.</p>

<p><strong>Verified by extraction before trusting a third-party host for it, same discipline as every other download this engagement:</strong> the package contains exactly one file, a 32 MiB blob that turned out to be a freshly formatted, genuinely empty ext4 filesystem labeled <code class="language-plaintext highlighter-rouge">"data"</code> — 33,547,704 of its 33,554,432 bytes are literal zero, with only filesystem metadata and an empty <code class="language-plaintext highlighter-rouge">lost+found</code> as content. <strong>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</strong>, which is a stronger safety argument than hash-checking alone would give for an executable or an app.</p>

<p><strong>Minor forensic bonus:</strong> the wipe package’s file UUID shares a middle segment (<code class="language-plaintext highlighter-rouge">9cbf-22ea</code>) 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.</p>

<h2 id="follow-up-why-a-full-system-image-test-cant-be-done-in-waydroid">Follow-up: why a full system-image test can’t be done in Waydroid</h2>

<p>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 <code class="language-plaintext highlighter-rouge">system.img</code>/<code class="language-plaintext highlighter-rouge">vendor.img</code> pair. <strong>It can’t, and the reasons are structural, not a tooling gap:</strong></p>
<ul>
  <li>Waydroid runs Android as a container on the <em>host’s own Linux kernel</em> via binder/ashmem, using generic LineageOS-based system images built for that approach. It does not boot an arbitrary OEM <code class="language-plaintext highlighter-rouge">system.img</code>+<code class="language-plaintext highlighter-rouge">vendor.img</code> pair the way real hardware or a full device emulator would.</li>
  <li>Even if it could, the vendor kernel modules bundled in <code class="language-plaintext highlighter-rouge">vendor.img</code> (<code class="language-plaintext highlighter-rouge">pvrsrvkm.ko</code>, <code class="language-plaintext highlighter-rouge">sprd_camera.ko</code>, etc.) are version-locked to kernel <code class="language-plaintext highlighter-rouge">4.14.133</code> via <code class="language-plaintext highlighter-rouge">vermagic</code> (confirmed earlier in this engagement) — no desktop Linux host runs that kernel, so those modules couldn’t load regardless.</li>
  <li>The vendor HALs (PowerVR GPU, Unisoc audio/camera/radio, the CAN-bus integration) have no equivalent on a desktop host at all; a generic container has nothing to substitute them with.</li>
</ul>

<p><strong>What’s actually available as pre-flight validation</strong> is what this engagement has been doing throughout: structural verification (partition metadata matching via <code class="language-plaintext highlighter-rouge">lpdump</code>, 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 <em>could</em> 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.</p>

<h2 id="follow-up-the-backdoor-confirmed-full-root-achieved-and-the-projects-real-ending">Follow-up: the backdoor confirmed, full root achieved, and the project’s real ending</h2>

<p>Everything from here on happened in a single extended session, on live hardware, after <code class="language-plaintext highlighter-rouge">run-as</code> was confirmed to require <code class="language-plaintext highlighter-rouge">shell</code> or <code class="language-plaintext highlighter-rouge">root</code> UID <em>before</em> even inspecting its argument — which meant Termux’s own app-sandboxed attempt failing earlier was expected, not a dead end. The actual path to <code class="language-plaintext highlighter-rouge">shell</code>:</p>

<ol>
  <li><strong>Wireless ADB, built from nothing but property writes.</strong> <code class="language-plaintext highlighter-rouge">ro.secure=1</code>/<code class="language-plaintext highlighter-rouge">ro.debuggable=0</code> (a real <code class="language-plaintext highlighter-rouge">user</code> build) normally makes this impossible without a prior USB-authorized ADB session. Permissive SELinux let it happen anyway: <code class="language-plaintext highlighter-rouge">setprop service.adb.tcp.port 5555</code> then <code class="language-plaintext highlighter-rouge">setprop ctl.start adbd</code> from an <em>unprivileged</em> Termux shell started <code class="language-plaintext highlighter-rouge">adbd</code> in TCP mode regardless — <code class="language-plaintext highlighter-rouge">ctl.*</code> properties are gated purely by SELinux <code class="language-plaintext highlighter-rouge">property_contexts</code> on modern Android, and permissive mode doesn’t enforce that gate. <code class="language-plaintext highlighter-rouge">adb connect 127.0.0.1:5555</code> from Termux’s own (initially broken, later working) <code class="language-plaintext highlighter-rouge">adb</code> client then produced a genuine <code class="language-plaintext highlighter-rouge">shell</code> (uid 2000) session.</li>
  <li><strong>From <code class="language-plaintext highlighter-rouge">shell</code>, the backdoor fired for real:</strong> <code class="language-plaintext highlighter-rouge">run-as "john@tw-desktop.UIS8581A" id</code> → <code class="language-plaintext highlighter-rouge">uid=0(root)</code>. Confirmed, not inferred — the exact vendor-build-identity-shaped string found weeks earlier in a decompiled <code class="language-plaintext highlighter-rouge">toolbox</code> applet (<code class="language-plaintext highlighter-rouge">xupdate_main</code>) turned out to be a genuine, functioning privilege-escalation backdoor, reachable the moment any process reaches <code class="language-plaintext highlighter-rouge">shell</code> 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.</li>
</ol>

<p><strong>Full partition map obtained as root</strong> (<code class="language-plaintext highlighter-rouge">/dev/block/by-name/</code>, real eMMC device <code class="language-plaintext highlighter-rouge">mmcblk0</code>), revealing something the whole prior engagement never knew: <strong>this device implements AVB</strong> (<code class="language-plaintext highlighter-rouge">vbmeta</code>/<code class="language-plaintext highlighter-rouge">vbmeta_system</code>/<code class="language-plaintext highlighter-rouge">vbmeta_vendor</code> partitions). <code class="language-plaintext highlighter-rouge">ro.boot.verifiedbootstate=orange</code> (unlocked) + <code class="language-plaintext highlighter-rouge">ro.boot.veritymode=enforcing</code> 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: <code class="language-plaintext highlighter-rouge">avbtool make_vbmeta_image --set_hashtree_disabled_flag --set_verification_disabled_flag</code>, written directly to all three vbmeta partitions before anything else.</p>

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

<p><strong>Write executed and verified</strong>, byte-count-exact on every target (<code class="language-plaintext highlighter-rouge">vbmeta</code>×3, <code class="language-plaintext highlighter-rouge">super</code> — 2,936,012,800 bytes, zero dropped records — <code class="language-plaintext highlighter-rouge">userdata</code>), using the RootSU community build’s <code class="language-plaintext highlighter-rouge">system.img</code> (independently verified clean of every project IOC, decompiled root mechanism confirmed attributed/non-obfuscated) combined with this engagement’s own verified-stock <code class="language-plaintext highlighter-rouge">vendor.img</code>, substituted in place of a corrupted download. <strong>The device boots successfully</strong> — confirmed by Russian-language UI (the RootSU build’s actual content), working <code class="language-plaintext highlighter-rouge">su</code> 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).</p>

<p><strong>With real root, the original goal — stripping TWCore/DoFun bloat — stopped needing the whole rebuild-and-flash pipeline.</strong> Direct live removal (<code class="language-plaintext highlighter-rouge">mount -o rw,remount /</code>, delete the APKs, <code class="language-plaintext highlighter-rouge">pm uninstall --user 0</code>) 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 <code class="language-plaintext highlighter-rouge">boot</code> partition for zero actual benefit toward debloating.</p>

<p><strong>The touch-inversion mystery — the one thread that ran the entire second half of this engagement — had nothing to do with any of it.</strong> 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 (<code class="language-plaintext highlighter-rouge">dumpsys window</code> showed a completely clean <code class="language-plaintext highlighter-rouge">ROTATION_0</code>, display matching physical panel dimensions exactly), not hardware (unit never opened). Confirmed via <code class="language-plaintext highlighter-rouge">pointer_location</code> 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: <strong>holding five fingers on the screen simultaneously re-triggers calibration.</strong> 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.</p>

<h2 id="lessons-for-the-methodology-itself">Lessons for the methodology itself</h2>

<ul>
  <li><strong>An obfuscated loader that changes behavior between point versions</strong> (v1.8 calling the ad SDK openly vs. v1.11 hiding the same call behind reflection + decryption) is itself a signal worth flagging before you even know what the payload does — see Malware Persistence and Evasion Techniques.</li>
  <li><strong>Passive OSINT before active contact</strong> is what turned this from “suspicious decompiled code” into “confirmed live campaign, here’s the vendor’s internal naming (JarService), here’s the attribution (MoYu/BADBOX), here’s what stage 3 actually does” — all without a single packet sent to the C2. That ordering (recon → static analysis → OSINT correlation → <em>then</em> decide whether live contact is justified and authorized) is the general lesson, not just an Android-specific one.</li>
  <li><strong>A full no-network <code class="language-plaintext highlighter-rouge">bwrap</code> sandbox is cheap enough to build per-engagement</strong> that there’s no excuse for decompiling an unknown sample directly on a connected machine, even for “just reading the code.”</li>
</ul>]]></content><author><name>Joshua Duane Penland</name></author><category term="writeups" /><category term="android" /><category term="malware-analysis" /><category term="reverse-engineering" /><category term="pentest" /><summary type="html"><![CDATA[Static and dynamic analysis of an aftermarket Android car stereo that turned up a live BADBOX/MoYu Group malware dropper, weak OTA integrity, and a privilege-escalation backdoor.]]></summary></entry></feed>