Een Android TV-box van $25 uit elkaar halen: Een firmware-analyse van supply-chain malware

Inhoudsopgave

  • Hoofdstuk 0: De Hook
  • Hoofdstuk 1: Eerste Contact
  • Hoofdstuk 2: De Behuizing Openbreken
  • Hoofdstuk 3: De ESP32-doodlopende weg
  • Hoofdstuk 4: Een echte adapter en een bootloader die terugvecht
  • Hoofdstuk 5: Het slagveld in kaart brengen
  • Hoofdstuk 6: Drie manieren om 1GB van een chip te trekken, waarvan er twee falen
  • Hoofdstuk 7: De Autopsie
  • Hoofdstuk 8: Volledige Aanvalsketen
  • Hoofdstuk 9: Indicators of Compromise
  • Hoofdstuk 10: Inperking en Opschoning
  • Hoofdstuk 11: Waarom dit niet kan worden opgelost met een fabrieksreset
  • Hoofdstuk 12: Conclusies en volgende stappen
  • Bijlage A: Stuklijst (Bill of Materials)
  • Bijlage B: Commandoreferentie

---

Hoofdstuk 0: De Hook

Dit project begon als een gesprek op DEFCON over goedkope Android TV-boxen die al vanuit de fabriek met malware worden geleverd: de BADBOX en BADBOX 2.0 campagnes waar de FBI openbare waarschuwingen over heeft uitgegeven, het Vo1d-botnet dat door Kaspersky op miljoenen apparaten is gevolgd, en een groeiende hoeveelheid onderzoek van Human Security, Bitsight en de EFF die hetzelfde patroon herhaaldelijk documenteren. Onmerkbare streaming-boxen, verkocht voor $20 tot $40 op grote marktplaatsen, draaiend op Android-builds ondertekend met openbare testkeys, met malware die direct in de firmware is gecompileerd voordat het apparaat zelfs maar is ingepakt.

Ik had erover gehoord. Dus ik kocht er een.

Ik kocht een apparaat op eBay dat werd vermeld als een "MXQ Pro 4K", een van de modellen die herhaaldelijk worden genoemd in Badbox-gerelateerd onderzoek, draaiend op een Rockchip/HiSilicon-klasse SoC en verkocht door een externe wederverkoper zonder merkverantwoordelijkheid. Bij die prijs en dat soort verkopers zou een volledig schone box de eigenlijke verrassing zijn geweest.

Het plan was simpel: root-toegang verkrijgen, hoe dat ook moest gebeuren, en precies uitzoeken wat er binnenin leefde.

Hoofdstuk 1: Eerste Contact

De box kwam aan met vier USB-poorten, één Ethernet-poort en HDMI-uitgang. Voordat ik iets aan de binnenkant deed, sloot ik hem aan op een monitor om de standaardinterface te bekijken. Geen netwerkverbinding, nog geen beslissing over antennes, alleen stroom en HDMI.

Het apparaat bleef tijdens het gehele onderzoek losgekoppeld van elk live-netwerk.

De vooraf geïnstalleerde app-lijst was de eerste rode vlag. Naast verwachte apps zoals Netflix en YouTube stonden er apps op die geen reden hadden om op een consumenten-streamingbox te staan: Xuper, Emota Store, zowel Firefox als Google Chrome die naast elkaar waren geïnstalleerd, en iets genaamd Happycast dat onverklaarbaar zowel de Google Play Store als een aparte "GetApps"-store nodig had om te functioneren. Twee volledige browsers en twee concurrerende app-stores in een box van $25 is niet wat een legitieme fabrikant verzendt.

Ik ging naar de Instellingen om de build-informatie te controleren en te zien of er een weg was naar ADB.

De build-details toonden iets dat bij een legitiem apparaat niet klopt: de kernel was zo recent als april van dit jaar opnieuw gebouwd, terwijl het Android-beveiligingspatchniveau meer dan een jaar oud was. Iemand was actief deze firmware aan het onderhouden en opnieuw compileren, maar niet om de redenen die een normale leverancier zou hebben.

Ik klikte herhaaldelijk op het veld 'Build Number' om de Ontwikkelaarsopties te ontgrendelen, wat werkte en meldde "u bent nu een ontwikkelaar". Ik schakelde USB-debugging in en verbond de box met mijn laptop via een USB-A male-to-male kabel. Er gebeurde niets; de box werd niet herkend. Ik verbond zowel de TV-box als mijn laptop ook met een geïsoleerde router zonder WAN-uplink om ADB-via-netwerk op poort 5555 te testen. Dat werkte ook niet.

Softwarematige toegang was afgesloten. Tijd om de behuizing te openen.

Hoofdstuk 2: De Behuizing Openbreken

Het verwijderen van de plastic behuizing kostte een paar minuten werk met een gitaarplect als geïmproviseerde spudger. Er waren geen schroeven toegankelijk aan de buitenkant, dus het was puur een kwestie van de clips lossnijden zonder de behuizing te kraken.

Binnenin zat een enkele groene PCB (gesilkscreened als 3800-2ATHE06) met een secundaire USB-dochterkaart verbonden via een lintkabel voor de drie "host"-poorten; de vierde poort was apart aangesloten op de hoofdkaart. Het doel was nu om de UART-header te vinden, de seriële debuginterface op laag niveau die op goedkope OEM-kaarten bijna altijd wordt achtergelaten als onbevolkte doorvoergaatjes.

Ik vond vier kandidaat-pads naast de hoofd-SoC, zonder labels. Ik koos ervoor om hiermee te communiceren met behulp van een pogo pin testklem in plaats van solderen, omdat klemmen op de pads een soldeerloze, omkeerbare verbinding geeft en elk risico op het lostrekken van een pad of het beschadigen van de printplaat door hitte vermijdt.

Op dit punt in het project bezat ik nog geen speciale USB-naar-TTL-adapter. In plaats van te wachten, besloot ik een ESP32-S3 te hergebruiken als een provisorische seriële bridge.

Hoofdstuk 3: De ESP32-doodlopende weg

Dit is het deel van het verslag dat niet werkte, en het is de moeite waard om dit volledig te documenteren omdat het tijd kostte en een belangrijke les leerde: het improviseren van een seriële bridge uit een algemene microcontroller is niet hetzelfde als een dedicated adapter.

Het plan was om een eenvoudige Arduino-passthrough-sketch op de ESP32-S3 te flashen: lees alles wat binnenkomt op Serial1 (verbonden met de TX/RX van de pogo-klem) en echo dit uit via Serial (de USB-C verbinding naar de laptop), en vice versa. Dit maakt van de printplaat een software-gedefinieerde USB-naar-UART-bridge.

Ik maakte de verbindingen als volgt:

  • ESP32 G → Clamp GND
  • ESP32 TX → Clamp RX
  • ESP32 RX → Clamp TX

Met de sketch geflasht en de klem op de UART-pads geplaatst, opende ik een seriële sessie op de Parrot OS-laptop: screen /dev/ttyUSB0 115200

Het scherm bleef volledig leeg. Geen boot-tekst, geen willekeurige tekens, niets. Ik startte de printplaat meerdere keren opnieuw op, plaatste de klem opnieuw en controleerde de bedrading. Geen verandering.

Ik heb hier geen definitieve oorzaak voor, en ik wil daar eerlijk over zijn. De meest waarschijnlijke kandidaten, gebaseerd op hoe ESP32-S3-kaarten native USB afhandelen, zijn:

  1. Timing- of bufferinstabiliteit in een software-gestuurde passthrough vergeleken met een dedicated UART-bridge IC (zoals CP2102/CH340/FTDI), die de elektrische en timing-laag in hardware afhandelt in plaats van in de hoofdloop van een microcontroller.
  2. Native USB-complicaties: in tegenstelling tot oudere ESP32-kaarten die een aparte fysieke USB-naar-seriële chip gebruiken, is de USB-poort van de ESP32-S3 direct verbonden met de processor, wat beïnvloedt hoe zuiver een passthrough-sketch zich gedraagt onder belasting.
  3. Mogelijke imperfecte pogo-klemcontacten, hoewel dit later werd uitgesloten toen dezelfde fysieke opstelling probleemloos werkte met een dedicated adapter.

Na een redelijke hoeveelheid troubleshooting besloot ik te stoppen met het debuggen van het ESP32-pad en in plaats daarvan een dedicated USB-naar-TTL-adapter te bestellen. Dit was de juiste beslissing. Soms is de oplossing voor een hardwareprobleem niet meer debuggen, maar het gebruiken van de tool die specifiek voor die taak is gebouwd.

Hoofdstuk 4: Een echte adapter en een bootloader die terugvecht

Een paar dagen later arriveerde de USB-naar-TTL-adapter, een unit gebaseerd op de CP2102. Voordat ik deze op de printplaat plaatste, pakte ik iets aan dat me sinds het openmaken van de behuizing stoorde: het apparaat had een Wi-Fi/Bluetooth-antenne die was aangesloten via een U.FL-connector. Ik heb deze fysiek losgekoppeld.

Dit was geen optionele voorzorgsmaatregel, maar een bewuste stap om te garanderen dat zelfs als het Android-OS somehow zou laden tijdens een mislukte extractiepoging, het apparaat geen zinvol RF-bereik had om verbinding te maken. Pas nadat de antenne eraf was, ging het testen verder.

De bedrading van de CP2102 volgde dezelfde regel als voorheen, en de belangrijkste regel bij UART-werk: verbind nooit VCC. De printplaat wordt gevoed via zijn eigen DC-jack; het introduceren van een tweede spanningsbron op dezelfde rail is de manier waarop je een printplaat van $25 (en mogelijk je adapter) doorbrandt.

CP2102 AdapterMXQ Pro Board PadOpmerking
GNDGNDGemeenschappelijke aarde vereist
RXTXAdapter luistert naar verzending van board
TXRXAdapter spreekt tegen ontvangst van board
VCC/3V3/5VNiet verbondenBoard heeft eigen stroom

Er deden zich twee problemen kort achter elkaar voor.

Probleem 1: nog steeds geen output. Hetzelfde screen /dev/ttyUSB0 115200 commando dat met de ESP32 was mislukt, produceerde aanvankelijk ook niets met de CP2102. Ik verbond de laptop kortstondig met een persoonlijke hotspot om picocom als alternatief terminalprogramma te installeren en voerde uit: sudo picocom -b 115200 /dev/ttyUSB0 --flow n

Dit werkte onmiddellijk, en de boot-tekst begon over het terminal te streamen. Wat de exacte oorzaak ook was, picocom slaagde waar screen dat niet deed op deze specifieke adapter/kernel-combinatie.

Probleem 2: ik kon het board lezen, maar het board kon mij niet horen. Boot-logs streamden perfect, maar het indrukken van toetsen deed niets; de terminal accepteerde geen input. Ik startte opnieuw op met hetzelfde resultaat. Na wat troubleshooting probeerde ik de RX- en TX-draden op de klem om te wisselen, een klassieke UART-valkuil waarbij "TX" en "RX" labels kunnen betekenen "deze pin verzendt" of "verbind dit met de verzendpin". Na de wissel werden toetsaanslagen geregistreerd. TX was eindelijk actief.

Nu begon de eigenlijke strijd om de bootloader. U-Boot op dit board geeft slechts een zeer kort venster tijdens het inschakelen om de automatische boot naar Android te onderbreken, en de fabrikant had dat venster agressief kort ingesteld. Ik probeerde de standaard interrupt-toetsen, Spatie en Enter, herhaaldelijk over meerdere power-cycles. Geen van beide werkte. Het board zeilde elke keer rechtstreeks voorbij de prompt naar de Android-bootsequentie.

Uiteindelijk probeerde ik herhaaldelijk Ctrl+C in te drukken tijdens het inschakelvenster, en dat brak door: fastboot#

Het bereiken van deze prompt is belangrijker dan het lijkt. Op fastboot# zitten betekent dat de Linux-kernel en de Android-userspace nooit zijn geladen. Elke component die later in dit verslag wordt ontdekt — de malware-launcher, de Zygote-hooks, de netwerkscanners — was volledig latent. Het systeem was bevroren in een pre-boot status, volledig onder mijn controle, zonder dat er iets draaide.

Tijd om te zien wat er werkelijk op de chip stond.

Hoofdstuk 5: Het slagveld in kaart brengen

Het eerste commando dat je uitvoert bij een onbekende bootloader-prompt is degene die vertelt wat er beschikbaar is: help

De fabrikant had een extreem rijke, volledig ontgrendelde U-Boot-omgeving achtergelaten: omgevingsvariabelen lezen/schrijven, MMC block-level lezen/schrijven, USB, TFTP-netwerking, en zelfs OTP (one-time-programmable memory) commando's. Niets was ingekort voor beveiliging. Vanuit het oogpunt van een aanvaller is dit een geschenk.

Stap 1: Een permanente weg terug veiligstellen. De standaard boot-vertraging was agressief kort. Ik heb dat eerst aangepast zodat ik niet bij elke power-cycle opnieuw hoefde te vechten met het timing-venster: setenv bootdelay 5 saveenv

Stap 2: De boot-omgeving dumpen om de eigenlijke partitie-indeling te zien die de fabrikant gebruikte: printenv

Begraven in de bootargs/blkdevparts output stond de volledige fysieke indeling van de eMMC-chip in platte tekst: blkdevparts=mmcblk0: 1M(fastboot) 20M(recovery) 60M(boot) 1000M(system)rw ← core Android OS 1100M(vendor)rw ← manufacturer apps/drivers ...

De 1000M(system) partitie was het overduidelijke doel, aangezien hier de Android-build van de fabrikant, en alles wat erin is gebakken, zich bevindt.

Stap 3: De hardwarematige limiet bevestigen. Twee snelle read-only queries: bdinfo # bevestigt RAM mmcinfo # bevestigt flash-chip

bdinfo toonde een RAM-grootte van 0x40000000, precies 1GB. mmcinfo rapporteerde een 8GB Samsung/HiMCI eMMC-chip (himci v300, 8GTF4, MMC 5.1, capaciteit 7818182656 bytes).

Die 1GB RAM is het getal dat alles vanaf dit punt vormgaf: de doelsystemapartitie is zelf 1000MB. Je kunt een partitie van die grootte niet laden in een apparaat dat slechts 1GB RAM in totaal heeft zonder dat het systeem halverwege het kopiëren uit het geheugen loopt, aangezien U-Boot geen virtueel geheugen of swap heeft. Elk extractieplan moest vanaf het begin rekening houden met dit plafond.

Hoofdstuk 6: Drie manieren om 1GB van een chip te trekken, waarvan er twee falen

  • [Poging 1] Fastboot via USB OTGMISLUKT (gadget-driver ontbreekt)
  • [Poging 2] TFTP via directe EthernetMISLUKT (32MB protocol-plafond)
  • [Poging 3] FAT32 USB, chunked readsSUCCES

Poging 1: Fastboot via de OTG-poort

Het board heeft vier USB-poorten. Drie zijn bedraad als standaard "host"-poorten; alleen poort 4, naast de SD-kaartslot, is bedraad als een OTG "slave"-poort die in staat is om de opslag van het board over te dragen aan een aangesloten computer. Ik verbond een USB-A male-to-male kabel van poort 4 naar de laptop en voerde vanuit de fastboot# prompt uit: fastboot usb 0

Als dit correct werkt, zou U-Boot moeten wachten en luisteren naar een host-side fastboot-client. In plaats daarvan keerde hij onmiddellijk terug naar de fastboot# prompt zonder enige luisterstatus.

Oorzaak: De fabrikant heeft de USB-gadget/slave-drivers uit het gecompileerde U-Boot-image verwijderd. Het commando bestaat in de binary en de poort is fysiek bedraad voor dit gebruik, maar de onderliggende driver-ondersteuning is nooit ingebouwd. Dit is een bewuste (of in ieder geval handige) anti-extractiemaatregel.

Poging 2: TFTP via een directe Ethernet-verbinding

Met USB-fastboot uitgesloten, was de volgende snelste optie TFTP over een directe Ethernet-kabel tussen de box en de laptop.

Host-side setup (Parrot OS):

ip -br a                                            # identificeer de interface, bijv. enp0s31f6
sudo ip addr add 192.168.99.10/24 dev enp0s31f6
sudo ip link set enp0s31f6 up
sudo apt update && sudo apt install tftpd-hpa -y
sudo nano /etc/default/tftpd-hpa                     # stel TFTP_OPTIONS="--secure --create" in
sudo mkdir -p /srv/tftp
sudo chmod -R 777 /srv/tftp
sudo chown -R tftp:tftp /srv/tftp
sudo systemctl restart tftpd-hpa

Target-side setup (U-Boot):

setenv ipaddr 192.168.99.20
setenv serverip 192.168.99.10
ping 192.168.99.10

De eerste ping-poging mislukte. Dit bleek twee gestapelde problemen te zijn:

  1. Het "luie Ethernet"-gedrag van U-Boot. Om stroom te besparen, houdt U-Boot de Ethernet PHY volledig uitgeschakeld totdat er daadwerkelijk een netwerkcommando wordt gegeven. Dit betekende dat de interface aan de laptopzijde "no carrier" zag totdat de ping-commando op het board vuurde.
  2. De laptop liet het statische IP vallen. De korte link-down/link-up gebeurtenis tijdens het wakker worden van de PHY was genoeg om de netwerkstack van Parrot het handmatig toegewezen statische adres te laten verwijderen.

Na het flushen van de firewall en het opnieuw vastzetten van het IP werkte de ping.

Proof-of-concept dump (64MB): mmc read 0 0x10000000 0 0x20000 tftpput 0x10000000 0x4000000 mxqbasedump.bin

Ook dit liep vast: de TFTP-server weigerde een nieuw bestand aan te maken. De workaround was het vooraf aanmaken van een dummy-bestand op de server. Daarna begon de transfer, maar hij stopte abrupt bij ongeveer 31-33MB en herstartte vervolgens in een oneindige loop.

Oorzaak: De oorspronkelijke TFTP-specificatie gebruikt een 16-bit block-counter. Bij de standaard 512 bytes per block maximeert deze counter bij $65.535 \times 512$ bytes $\approx 33,5\text{MB}$. Daarna loopt de counter over en start de TFTP-client van U-Boot de transfer opnieuw. Een volledige 1000MB partitie trekken zou ongeveer 32 separate handmatige transfers vereisen.

Ik probeerde ook mmc part, maar dit gaf Unknown partition table type 0. De fabrikant had geen standaard MBR- of GPT-tabel geschreven; de partitie-indeling bestond alleen als de platte tekst blkdevparts string. Offsets moesten handmatig worden berekend.

Poging 3: FAT32 USB-stick met handmatige block-address slicing (de methode die werkte)

De resterende optie in de U-Boot command set was de FAT32-bestandssysteemdriver in combinatie met raw MMC block reads.

De hex-berekening. Omdat er geen partitietabel was, moest de start-offset van de systemapartitie worden afgeleid door elke partitie vóór hem in de blkdevparts string op te tellen: $1\text{M} + 0.5\text{M} + 0.5\text{M} + 20\text{M} + 2\text{M} + 8\text{M} + 8\text{M} + 2\text{M} + 10\text{M} + 10\text{M} + 20\text{M} + 20\text{M} + 60\text{M} + 20\text{M} + 20\text{M} = 202\text{MB}$

Omzetting naar 512-byte block adressering:

  • Start offset: $202\text{MB} \rightarrow \text{block } 0\text{x}65000$
  • Systeemgrootte: $1000\text{MB} \rightarrow 0\text{x}1\text{F}4000\text{ blocks totaal}$
  • Chunk-grootte (250MB): $0\text{x}7\text{D}000\text{ blocks} = 0\text{x}\text{FA}00000\text{ bytes}$

Omdat het 1GB RAM-plafond het onmogelijk maakte de hele partitie in één keer te laden, werd de partitie gesplitst in vier chunks van 250MB. Elke chunk werd in het RAM gelezen op hetzelfde adres en onmiddellijk naar de USB-stick geschreven voordat de volgende chunk deze overschreef.

ChunkStart BlockBlock CountBestemmingsbestand
1$0\text{x}65000$$0\text{x}7\text{D}000$system_chunk1.bin
2$0\text{x}\text{E}2000$$0\text{x}7\text{D}000$system_chunk2.bin
3$0\text{x}15\text{F}000$$0\text{x}7\text{D}000$system_chunk3.bin
4$0\text{x}1\text{DC}000$$0\text{x}7\text{D}000$system_chunk4.bin

De drive voorbereiden (Parrot OS laptop): sudo mkfs.vfat -F 32 /dev/sdb1 (U-Boot spreekt alleen FAT32).

De drive mounten op het doelapparaat: usb start fatls usb 0

Extractie per chunk (U-Boot): (Voorbeeld voor Chunk 1) mmc read 0 0x10000000 0x65000 0x7D000 fatwrite usb 0 0x10000000 system_chunk1.bin 0xFA00000 (Dit proces werd herhaald voor alle vier de chunks)

Reconstructie op de laptop: cat systemchunk1.bin systemchunk2.bin systemchunk3.bin systemchunk4.bin > systemfull.bin ls -lh systemfull.binsystem_full.bin 1.0G

De volledige 1GB systemapartitie was nu aanwezig op de Parrot OS laptop, volledig offline, zonder dat de malware ooit was uitgevoerd.

Hoofdstuk 7: De Autopsie

Met system_full.bin was de analyse een kwestie van binaire string searching (strings + grep) tegen het image. Elke bevinding hieronder is gebaseerd op een letterlijke string uit de binary.

Bevinding 1: Apparaatidentiteit & Build Server Leak

Bewijs: #line 1 "device/hisilicon/bigfish/system/sepolicy/system/private/property_contexts" file:///home/wwwroot/newsite/www.easyicon.net/cdn-img.easyicon.cn/src/11706/1170636.png=

Wat dit bewijst: Het apparaat draait op het "Bigfish"-platform van HiSilicon. Het pad device/hisilicon/bigfish/ is de interne Android-sourcetree, wat bevestigt dat deze firmware direct is gecompileerd vanuit de SDK van HiSilicon. De file:///home/wwwroot/newsite/ string is een lokaal bestandspad van de build-machine dat is gelekt in de binary, wat wijst op de interne omgeving waar het image is samengesteld.

Bevinding 2: Primair Malware-pakket

Bewijs: com.google.appdown appdown.apk bigfish-setup /(vendor|system/vendor)/etc/init.bigfish.sh u:objectr:bigfish-setupexec:s0 start bigfish-setup

Wat dit bewijst: com.google.appdown is een gecompileerd systeemcomponent, geen sideloaded app. De init.bigfish.sh regel heeft een SELinux-context (bigfish-setup_exec) die het markeert als een executable met shell-privileges, en start bigfish-setup is de instructie die dit bij boot activeert.

Bevinding 3: Manipulatie van Silent Install-rechten

Bewijs: definstallnonmarketapps unknownsourcesdefaultreversed "SILENTENROLLMENTSTARTONINSTALL" isCallerAllowedToSilentlyUninstall 7googlehome://appdownload?androidpackage_name=com.sling

Wat dit bewijst: Sideloading van onbekende bronnen is standaard ingeschakeld. unknownsourcesdefaultreversed draait de normale Android-beveiliging volledig om. SILENTENROLLMENTSTARTON_INSTALL bevestigt infrastructuur voor stille installaties. De 7googlehome:// URI is een spoofed Google-deep-link om stilletjes Sling TV te installeren zonder interactie van de gebruiker.

Bevinding 4: Command-and-Control (C2) Infrastructuur

Bewijs: http://bngt.itv.cmvideo.cn:8095 http://ips.itv.cmvideo.cn http://tview.itv.cmvideo.cn:80/ACSServlet http://appcontrol.itv.cmvideo.cn:8095/aics-authorize/authorize https://ctserver.cnnic.cn/

Wat dit bewijst: Vier hardcoded China Mobile (cmvideo.cn) endpoints voor botnet-registratie, IP/geolocation logging, TR-069 ACS-verkeersmonitoring en remote app-installatie autorisatie. ctserver.cnnic.cn wordt beheerd door de Chinese internettoezichthouder (CNNIC).

Bevinding 5: Triada / Zygote Injectie

Bewijs: dalvik.system.ZygoteHooks ZN3artL25ZygoteHooksnativePreForkEP7JNIEnvP7jclass ZN3artL31ZygoteHooksnativePostForkChildEP7JNIEnvP7jclassxihhP8_jstring

Wat dit bewijst: Dit zijn C++ symbolen van de Android ART runtime. nativePreFork vuurt vlak voordat Zygote (het master-proces van Android) een nieuwe app-proces start; nativePostForkChild vuurt direct daarna. Door beide te hooken, wordt kwaadaardige code gekloond in elke app die op het apparaat wordt gestart (bank-apps, browsers, e-mail). Dit is het tekstboek-mechanisme van Triada.

Bevinding 6: Vo1d Malwarefamilie

Bewijs: service flash_recovery /system/bin/install-recovery.sh VO1d ak.alizandro.smartaudiobookplayer,com.apple.android.music,com.spotify.music,deezer.android.app...

Wat dit bewijst: VO1d is een directe match met de Indicators of Compromise (IoC) van Kaspersky. Het kapen van install-recovery.sh om een daemon te lanceren bij boot komt exact overeen met de gedocumenteerde Vo1d-methode. De lijst met audio-apps (Spotify, Deezer, etc.) bevestigt de focus op audio ad-fraud.

Bevinding 7: Peachpit-stijl Ad Fraud

Bewijs: TimerThrottlingForHiddenFrames getRealMetrics ADCLICKED ONDEVICEAD_EVENTS

Wat dit bewijst: TimerThrottlingForHiddenFrames is een WebView-vlag die CPU-throttling voor onzichtbare browser-vensters uitschakelt. getRealMetrics omzeilt sandboxing om fysieke schermafmetingen te verkrijgen in een onzichtbaar venster. Dit alles dient om advertenties te renderen en interacties te simuleren in onzichtbare vensters.

Bevinding 8: OpenRTB Programmatische Ad Bidder

Bewijs: com.google.openrtb.ContentCategory contentads_bidder.openrtb.ContentCategory

Wat dit bewijst: OpenRTB is de standaard voor real-time advertentie-veilingen. contentads_bidder is een custom bidder-implementatie. Het apparaat consumeert niet alleen frauduleuze advertenties, maar participeert actief als een fake publisher in live veilingen.

Bevinding 9: Root Backdoor

Bewijs: /system/xbin/su /system/xbin/su u:objectr:suexec:s0

Wat dit bewijst: /system/xbin/su is het pad naar de root-escalatie binary, met een SELinux-label dat root-executierechten verleent. Dit is een permanente, niet-geauthentiseerde root-backdoor in de systemapartitie.

Bevinding 10: BusyBox Toolkit

Bewijs: /system/bin/busybox ip -6 route add default via $replyServer dev $interface metric 118

Wat dit bewijst: BusyBox is aanwezig en wordt gebruikt voor IPv6 route-manipulatie en connectiviteitstests, typisch voor botnet-clients die routes mappen voor traffic proxying.

Bevinding 11: Shell Executie-paden

Bewijs: /system/bin/sh -c read /(vendor|system/vendor)/bin/sh u:objectr:vendorshell_exec:s0

Wat dit bewijst: Er zijn drie onafhankelijke shell-executiepaden met eigen SELinux-labels. De /system/bin/sh -c read regel toont een actief geconstrueerd shell-commando, wat is hoe malware programmatisch willekeurige commando's uitvoert.

Bevinding 12: SMS Interceptie Capaciteit

Bewijs: READSMS RECEIVESMS READICCSMS <permission name="android.permission.RECEIVE_SMS" />

Wat dit bewijst: De volledige SMS-permissiestack is aanwezig, inclusief READICCSMS voor berichten op een fysieke SIM-kaart. De manifest-tag bevestigt dat dit actief wordt aangevraagd.

Bevinding 13: Database voor Credential Harvesting

Bewijs: create table samba (id integer primary key autoincrement,serverip varchar(100),nickname varchar(100), servername varchar(100), work_path varchar(300),account varchar(100),password varchar(100))

Wat dit bewijst: Een volledige SQLite-schema speciaal gebouwd voor het oogsten van Windows-netwerkshares (SMB) credentials, opgeslagen in plaintext.

Bevinding 14: Data Exfiltratie Infrastructuur

Bewijs: CONFIRMEDUPLOADSAFEUSERDATA N4grpc8internal16RpcMethodHandlerIN9assistant3api20VisionContextService7ServiceENS3_22StartDataUploadRequestE...

Wat dit bewijst: CONFIRMEDUPLOADSAFEUSERDATA bevestigt dat gebruikersdata is klaargezet voor upload. De gRPC-method handlers wijzen op server-grade uploadcode die niet normaal op een client-apparaat staat.

Bevinding 15: Persistentie Mechanismen

Bewijs: Landroid/app/IAlarmManager$Stub; Landroid/app/job/IJobScheduler$Stub;

Wat dit bewijst: AlarmManager en JobScheduler zijn Android-systemen voor terugkerende taken. Hun aanwezigheid als gecompileerde IPC-interfaces bevestigt dat de malware zichzelf automatisch opnieuw start na elke reboot.

Bevinding 16: Chinese Staats- & Commerciële Infrastructuur

Bewijs: com.cantv.otaservice com.cibn.tv com.cmcc.media com.hivendor.baidu.pcs

Wat dit bewijst: com.cantv.otaservice (CANTV) en com.cibn.tv zijn gelieerd aan de Chinese staat. com.cmcc.media is China Mobile. Dit geeft deze entiteiten theoretisch de mogelijkheid om remote firmware-updates uit te voeren zonder dat de gebruiker dit merkt.

Bevinding 17: Standaard Wachtwoord & PPPoE Credential Handling

Bewijs: DEFAULT_PASSWORD busybox setsid pppd pty "pppoe -p /data/ppp/pppoe.pid -I $IFACE -T 80 -U -m 1412" debug logfd 1

Wat dit bewijst: De aanwezigheid van DEFAULT_PASSWORD suggereert een hardcoded fallback-credential, een klassiek IoT-backdoor patroon. De PPPoE-regel toont live handling van credentials via shell-commando's.

Bevinding 18: Lokale Netwerk Reconnaissance

De MAC-adressen van de router en analyse-laptop werden gecontroleerd tegen de binary. Geen van beide kwam voor in de data-secties van system_full.bin. De identiteit van het analysenetwerk is dus nooit gefingerprint, omdat het apparaat tijdens de extractie nooit live netwerktoegang had. De inperking bleef succesvol.

Hoofdstuk 8: Volledige Aanvalsketen

(Gereconstrueerd uit binaire analyse)

Fabriek → Firmware gewijzigd vóór verpakking → Verzending naar consument $\downarrow$ Inschakelenbigfish-setup.sh draait → Sideloading stilletjes ingeschakeld $\downarrow$ appdown.apk start vanuit /system/priv-app/ (vooraf geïnstalleerd, verhoogde privileges) $\downarrow$ ZygoteHooks injecteren in elke gestarte app $\downarrow$ C2 Check-in sequentie:

  1. bngt.itv.cmvideo.cn:8095: Botnet-registratie, device fingerprint verzonden
  2. ips.itv.cmvideo.cn: Home IP/ISP/geolocation gelogd
  3. tview.itv.cmvideo.cn/ACSServlet: TR-069 verkeersmonitoring kanaal geopend
  4. appcontrol.itv.cmvideo.cn/authorize: Remote install/update autorisatie verleend

$\downarrow$ install-recovery.sh gekaapt door Vo1d + /system/xbin/su root-backdoor beschikbaar $\downarrow$ CONTINU, GELIJKTIDIG DRAAIEND:

  • Silent pay-per-install fraud (spoofed 7googlehome:// deep-links)
  • Invisible WebView ad-click fraud (Peachpit-stijl)
  • OpenRTB fake-publisher participatie in veilingen
  • Audio ad-injectie gericht op Spotify/Pandora/iHeartRadio etc. (Vo1d)
  • Home IP verkocht/gebruikt als residentiële proxy exit-node
  • Lokaal netwerk gescand op Samba (SMB) shares; credentials geoogst naar SQLite
  • SMS/OTP interceptie capaciteit aanwezig
  • App-inventaris geëxfiltreerd voor fraudetargeting

$\downarrow$ OVERLEEFT:

  • Fabrieksreset (wist alleen /data/, niet de read-only /system/ partitie)
  • Verwijderen van individuele apps (ingebakken in system image)
  • Netwerkonderbreking (herstelt bij opnieuw verbinden)
  • Verwijdering van één malwarefamilie (vier onafhankelijke paden backen elkaar op)

Waarom één fabrieksreset dit niet oplost

  1. De malware leeft in /system/, een read-only partitie waar normale gebruikers niet bij kunnen.
  2. Een fabrieksreset wist alleen /data/; de systemapartitie blijft volledig intact.
  3. Er bestaat geen schoon, geverifieerd fabrieks-firmware image in openbare repositories voor dit specifieke board.
  4. Er zijn vier verschillende malwarefamilies aanwezig met onafhankelijke persistentiemethoden.

De enige echte oplossing is het fysiek herflashen van de eMMC-chip met een geverifieerd schoon image. Voor alle praktische doeleinden is dit apparaat vanaf het moment dat het de fabriek verlaat gecompromitteerd voor zijn gehele levensduur.

Hoofdstuk 9: Indicators of Compromise (IoC's)

Netwerkinfrastructuur:

  • bngt.itv.cmvideo.cn:8095
  • ips.itv.cmvideo.cn
  • tview.itv.cmvideo.cn:80/ACSServlet
  • appcontrol.itv.cmvideo.cn:8095/aics-authorize/authorize
  • ctserver.cnnic.cn

Bestandssysteem artefacten:

  • /system/priv-app/appdown.apk
  • /system/bin/bigfish-setup.sh (of /vendor/etc/init.bigfish.sh)
  • /system/bin/preinstall.sh
  • /system/bin/install-recovery.sh (gewijzigd door Vo1d)
  • /system/xbin/su (niet-geauthentiseerde root backdoor)
  • /system/bin/busybox

Pakketnamen (Chinese staats-/commerciële infrastructuur):

  • com.cantv.otaservice
  • com.cantv.otaservice.launcher
  • com.cibn.tv
  • com.cmcc.media
  • com.hivendor.baidu.pcs
  • com.hivendor.voice.recoginition
  • com.qzx.gamebox
  • com.qzx.tv.launcher
  • com.qzx.writesn
  • com.youku.taitan.tv
  • com.youku.tv.appstore.all

Gedragsmarkers:

  • 7googlehome://appdownload?androidpackagename=com.sling
  • create table samba (server_ip, account, password) [plaintext credential store]

Hoofdstuk 10: Inperking en Opschoning

De volgende OPSEC-beslissingen zijn strikt gehanteerd:

  1. De Wi-Fi/Bluetooth-antenne werd fysiek losgekoppeld bij de U.FL-connector.
  2. Al het hardware-werk werd uitgevoerd terwijl het apparaat bevroren was in de U-Boot bootloader.
  3. Netwerktests (ADB-over-LAN, TFTP) gebruikten een geïsoleerde router zonder WAN-uplink.
  4. De Parrot OS analyse-laptop werd aan het einde van het project volledig opnieuw geïnstalleerd.
  5. De USB-stick voor extractie werd volledig overschreven met nullen (zero-written) en opnieuw geformatteerd.
  6. De malware-binaries werden uitsluitend als inerte data geanalyseerd en nooit uitgevoerd.

Hoofdstuk 11: Waarom dit niet kan worden opgelost met een fabrieksreset

Ter herhaling: er is geen door de gebruiker toegankelijke oplossing. De infectie leeft onder de laag waar een consument bij kan. Een fabrieksreset raakt /data/. Deze firmware leeft in /system/. Dit zijn verschillende partities.

Hoofdstuk 12: Conclusies en volgende stappen

Dit project bevestigde op firmware-niveau dat goedkope, niet-gecertificeerde Android streaming-boxen een actueel supply-chain beveiligingsprobleem zijn. Een aankoop van $25 en een pogo-pin klem waren genoeg om vier malwarefamilies en een root-backdoor te vinden.

Belangrijkste lessen:

  • Vertrouw nooit een goedkoop, onmerkbaar IoT-apparaat op een thuisnetwerk. De economie van een box van $25 werkt alleen als er aan de achterkant iets anders wordt gemonetiseerd.
  • Infectie vereist geen gebruikersfout; dit apparaat werd pre-infected geleverd.
  • Deze apparaten worden op grote schaal verkocht via Amazon, eBay en Facebook Marketplace onder talloze inwisselbare merknamen.

Volgende stappen:

  1. binwalk -e uitvoeren op system_full.bin om de .apk bestanden en het bigfish-setup.sh script te extraheren voor volledige statische decompilatie met jadx.
  2. appdown.apk reverse-engineeren om het volledige C2-protocol in kaart te brengen.
  3. De cmvideo.cn subdomeinen indienen bij openbare threat intelligence feeds (zoals AlienVault OTX, abuse.ch).

---

Bijlage A: Stuklijst (Bill of Materials)

ItemDoel
MXQ Pro 4K Android TV-box (eBay)Doelapparaat
Burner laptop, Parrot OSGeïsoleerde analyse-omgeving
CP2102 USB-naar-TTL/UART adapterSeriële verbinding met bootloader
ESP32-S3 microcontrollerPoging tot seriële bridge (mislukt)
6-pin, 2.54mm pogo pin testklemSoldeerloze UART-verbinding
Female-to-female jumper wiresBedrading klem naar adapter
USB Type-A male-to-male kabelPoging tot fastboot/OTG verbinding
Ethernet patchkabelPoging tot directe TFTP link
USB-flashdrive (64GB, FAT32)Medium voor succesvolle extractie
Oude router, WAN losgekoppeldGeïsoleerd netwerksegment
GitaarplectSpudger voor het openen van de behuizing
Een gezonde dosis paranoiaNiet-onderhandelbaar

Bijlage B: Commandoreferentie

U-Boot / bootloader:

  • help — Lijst beschikbare commando's
  • setenv bootdelay 5 && saveenv — Permanent verlengen van het boot-interrupt venster
  • printenv — Dump omgevingsvariabelen / partitie-indeling
  • bdinfo — Bevestig RAM-grootte
  • mmcinfo — Bevestig eMMC chip identiteit/capaciteit
  • mmc part — Poging partitietabel te lezen
  • mmc read <dev> <ramaddr> <startblk> <blk_count>
  • fatwrite usb 0 <ramaddr> <filename> <bytecount>
  • usb start
  • fatls usb 0
  • fastboot usb 0 — Mislukt op dit apparaat (driver ontbreekt)
  • setenv ipaddr / setenv serverip / ping / tftpput — TFTP pad

Parrot OS (host):

  • sudo picocom -b 115200 /dev/ttyUSB0 --flow n — Seriële terminal
  • lsblk
  • sudo mkfs.vfat -F 32 /dev/sdb1
  • sudo apt install tftpd-hpa -y
  • sudo systemctl restart tftpd-hpa
  • cat systemchunk1.bin systemchunk2.bin systemchunk3.bin systemchunk4.bin > system_full.bin
  • strings system_full.bin | grep -i "cmvideo\|appdown\|bigfish\|http"