Also available in English: Taking apart a compromised TV box.
Essa é a metade técnica. A parte um é a versão em português claro, para quem só quer saber se precisa se preocupar com a box da sala. Essa aqui é a tarde como ela foi: os comandos, as saídas, as duas vezes que eu matei o meu próprio shell, e a briga de política do SELinux que eu acabei perdendo de propósito.
Tudo abaixo aconteceu em 2026-11-04. Horários em GMT-3. A box não é minha; ela foi trazida para eu olhar, e a investigação inteira rodou aqui em casa com a box numa VLAN isolada, sem rota para o resto da LAN. A saída para a internet eu deixei aberta de propósito: eu queria ver o que o app transmitia de verdade e como a botnet se comportava, e nenhuma das duas coisas dá para observar numa box sem para onde ir. Eu não vou nomear o app de IPTV, por motivos que eu explico mais para o final; a família de malware é pública e bem documentada, então essa parte tem nome. Os endereços da minha própria rede foram trocados por endereços inventados. Todo endereço externo, domínio e hash é real, porque é justamente essa parte que você pode precisar comparar.
| TV box | Streaming stick | |
|---|---|---|
| Modelo | MX9, Rockchip RK3229 (4× Cortex-A7, 32 bits) | Xiaomi Mi TV Stick |
| Android | 7.1.2, build NHG47K, userdebug/test-keys, de 2018-08-06 |
14, user/release-keys |
| Root | SuperSU, /system/xbin/su + daemonsu, sem app gerenciador |
nenhum |
| RAM declarada | 2.097.152 kB | n/a |
| RAM real | 995 MB | n/a |
| Armazenamento | NAND cru (rknand_*) |
n/a |
Conseguindo um shell Link to heading
Não tem adb na minha máquina. Eu rodo Bazzite e prefiro não empilhar um SDK num sistema baseado em imagem por causa de uma tarde, então eu montei um container descartável para isso. Segue completo, se você quiser fazer o mesmo:
# Containerfile
FROM registry.fedoraproject.org/fedora-minimal:latest
RUN microdnf install -y android-tools \
&& microdnf clean all
# Mantém o container vivo para o servidor adb sobreviver entre os exec.
CMD ["sleep", "infinity"]
podman build -t adb-tools -f Containerfile .
podman run -d --name adb --network host \
-v adbkeys:/root/.android \
adb-tools
podman exec adb adb connect 192.168.50.10:5555
podman exec adb adb -s 192.168.50.10:5555 shell
Isso te dá o adb do pacote android-tools do Fedora (1.0.41 / 37.0.0p1 quando eu escrevi isso) sem colocar SDK no host, e você joga tudo fora com um podman rmi quando terminar.
O --network host importa, porque o adb connect precisa alcançar a LAN direto. E o volume nomeado para /root/.android importa mais do que parece: é ali que fica o par de chaves do ADB, e se você perder ele, vai reaprovar o prompt RSA numa televisão, com controle remoto, todas as vezes.
A primeira tentativa foi recusada. Um scan TCP completo na box achou só 8008/8009 (Google Cast) e umas portas de DLNA anunciando Server: Linux/3.10.104 eHomeMediaCenter/1.0. Android 7 não tem tela de “depuração sem fio”, e as configurações desse firmware não tinham nenhuma opção de ADB pela rede.
O erro: eu tentei USB. USB-C no PC para USB-A na box, o que não funciona, porque as duas pontas são host. A box nunca apareceu no barramento USB do PC, e o PC ficou visivelmente lento enquanto estava plugado: dois hosts USB ligados um no outro, possivelmente com a box devolvendo energia. Desplugei. Essas boxes precisam de USB-A para USB-A na única porta OTG delas, não de um cabo C para A.
O que funcionou foi entrar pela porta da frente. Instalei um emulador de terminal da Play Store na própria box e rodei, como root:
su
setprop service.adb.tcp.port 5555
stop adbd
start adbd
O netstat então mostrou :::5555, um socket IPv6, mas dual-stack, então IPv4 conecta normal. Dentro, como product:rk322x_box model:MX9.
Vale parar um segundo nisso: eu digitei su e recebi root. Sem prompt, sem diálogo, sem nada para aprovar. O SuperSU estava instalado sem app gerenciador, então não tinha nada no caminho para perguntar a um humano. Todo app daquele aparelho tinha o mesmo acordo disponível.
A box estava ocupada fazendo nada Link to heading
load average: 5.2 / 6.9 / 9.3
Nada tocando. Dois processos nativos rodando como root, filhos do init:
/system/bin/supervisord -c /system/bin/s.conf (pid 1443, pai init)
/system/bin/pandoraspearrk (pid 1453, filho do supervisord)
supervisord é um supervisor de processos legítimo e conhecido, aqui o binário único em Go, não o de Python. Nada numa TV box precisa de supervisor de processos. A config dele mantinha o pandoraspearrk vivo com autorestart=true e logava em /data/sp.log.
A persistência no boot era o hook de inicialização do próprio fabricante. O /init.rk30board.rc tem:
service preinstall /system/bin/preinstall.sh
…e a última linha desse script tinha sido alterada para subir o supervisord. Nada criou serviço novo de init, que é justamente o que você procuraria. Foi acrescentada uma linha num script que o fabricante já rodava como root a cada boot.
Depois as tabelas de socket, lidas direto do /proc:
192.168.50.10:47705 -> 173.255.220.200:9999 ESTABLISHED (root)
Porta 9999, mantida aberta por um processo root, para um endereço que não tem nada a fazer numa sala brasileira. É o C2.
O arquivo hosts, e por que pastebin.com é a pista
Link to heading
O /system/etc/hosts tinha sido reescrito com cerca de 60 entradas apontando para IPs do atacante. pandorabackup.*, *.pdonno.com, *.panddna.com, um monte de domínio pcdn*. Reescrito a cada boot; o mtime era 14:00:01, segundos depois do boot que eu estava olhando.
E essa entrada:
23.12.198.15 pastebin.com
Essa é a denúncia, e vale explicar porque é genuinamente esperto. Botnets dessa família usam o pastebin como canal de reserva para configuração e endereços de C2: se o servidor principal cai, o bot busca um paste para descobrir para onde ir. O que significa que o pastebin é também onde um pesquisador, ou um operador concorrente, poderia publicar algo para redirecionar os bots. Fixando o pastebin.com no IP deles em cada aparelho infectado, o operador mantém controle exclusivo desse canal de reserva. Não é bloquear o pastebin para o usuário. É garantir que, quando o bot fizer uma pergunta ao pastebin, quem responde é o operador.
Tem uma ironia boa aqui. Os provedores brasileiros já bloquearam o pastebin no DNS deles, pelo que se relata justamente porque sites de paste carregam lista de IPTV e config de botnet. Só que uma entrada de hosts como a de cima significa que esse aparelho não pergunta a resolvedor nenhum, e o app em cima dele usa DNS-over-HTTPS. O bloqueio nacional alcança o cliente; ele não alcança o bot que está na prateleira daquele cliente. Eu entrei nesse assunto direito na parte um.
Também no /data: a.sh (um atualizador), supervisord.tar.gz (o pacote do payload), cópias de pandoraspearrk, supervisord, busybox, curl, arp, tar, um blob criptografado .ms, e um arquivo chamado txt com a string Need Upgraded!.
O atualizador, a.sh, é a peça que mostra que isso é um produto e não uma brincadeira. Ele checa o modelo do aparelho contra uma lista específica de modelos de set-top box (o hardware da própria marca do operador de IPTV) e, fora dessa lista, baixa cpcdn.sh / pd.sh / rku.sh de um host de atualização. Alguém mantém isso. Existe caminho de código por modelo.
Atribuição Link to heading
Essa é a parte em que eu fui mais cuidadoso, porque “um app instalou malware” é uma afirmação séria e a resposta honesta tem camadas. Em ordem de força:
1. O instalador fica no diretório privado do app. O /data/data/<o app de IPTV>/files/ continha .tmp.sh. No Android, esse diretório só é gravável pelo UID daquele app. Nada mais no aparelho consegue colocar arquivo ali. O script remonta o /system como leitura e escrita, baixa o pacote, copia binários para /system/bin/, cria links dos applets do busybox em /system/xbin, e se engancha em qualquer serviço de boot que encontrar. Ele também libera espaço no /system apagando apps embarcados quando a partição está com ≥95% de uso, que é por que o Kodi que vinha nesse firmware simplesmente não existia mais.
2. Propriedade dos arquivos. O /data/curl pertence ao u0_a52. Cruzando com o /data/system/packages.list, u0_a52 é o app de IPTV.
3. Os timestamps batem no segundo.
| Hora (GMT-3) | Arquivo | O quê |
|---|---|---|
| 2026-08-23 11:23:26 | <app>/files/gomediad.so |
app solta o motor de P2P |
| 2026-08-23 11:23:59 | <app>/files/classes.dex |
app solta um dex extra |
| 2026-08-23 11:24:24 | <app>/files/busybox |
app solta o busybox |
| 2026-08-23 11:24:39 | /data/supervisord.tar.gz |
pacote da botnet baixado |
| 2026-08-23 11:24:41 | /system/bin/pandoraspearrk |
botnet instalada no /system |
| 2026-08-28 13:05:16 | <app>/files/curl, .tmp.sh |
instalador atualizado |
| 2026-08-28 13:10:15 | base.apk do app |
app se atualiza, 5 min depois |
| 2026-10-11 22:41:13 | /data/a.sh, /system/bin/s.conf |
atualizador substituído |
O que isso comprova, e o que não comprova. Comprova que o dropper executou de dentro do armazenamento privado daquele app, com os privilégios daquele app, e que a botnet caiu no /system dezessete segundos depois. É mais ou menos o máximo de clareza que análise forense de sistema de arquivos dá num aparelho em produção.
O que isso não comprova é intenção, nem quem na cadeia é o responsável. Um APK empacotado com payload ofuscado pode ser o fabricante embarcando botnet com conhecimento; ou um SDK de terceiro comprometido; ou uma pipeline de build que pertence a outra pessoa. Eu não consigo distinguir esses casos de fora, e quem disser que consegue com essa evidência está vendendo mais do que tem.
Essa distinção é também o motivo de eu não nomear o app. O que eu posso apontar é o relatório da Dr.Web de setembro de 2023 sobre essa família, que nomeia várias marcas de streaming pirata como vetores de distribuição e descreve exatamente esse comportamento [1]. Se o seu app está na lista deles, isso é uma conclusão publicada por eles, não uma acusação minha.
A família em si: Pandoraspear, derivada do Mirai. O XLab da Qianxin atribui a operação a um grupo que eles chamam de Bigpanzi: cerca de 170 mil bots ativos por dia no pico, mais de 1,3 milhão de IPs distintos, maioria absoluta no Brasil e concentrada em São Paulo [2]. A numeração de versão nas amostras sugere desenvolvimento contínuo desde mais ou menos 2015.
Traga o seu próprio userland Link to heading
A coisa em que eu fico pensando é o tanto de sistema operacional próprio que esse malware carrega.
Para o /system/bin/: pandoraspearrk, supervisord, s.conf, e os próprios curl, arp e awk dele. Para o /system/xbin/: busybox mais links para arp, awk, tar, xz, todos datados de 2026-08-23 11:24.
Duas coisas decorrem disso, e a segunda é a que passa batido.
A óbvia é portabilidade. Se você não depende do que o firmware oferece, você funciona igual em centenas de boxes baratas com situações de toybox/busybox completamente diferentes. É engenharia competente, aplicada a um fim ruim.
A sutil é que ele substituiu parte do toolchain do próprio sistema operacional. A partir do momento em que /system/xbin/awk é um link para o busybox do atacante, todo script do aparelho que chama awk está rodando código fornecido pelo atacante. Inclusive os scripts de boot do fabricante. Você não tem só malware na box; você tem malware embaixo do encanamento de shell da própria box.
E são binários ARM estaticamente ligados, sem auditoria, de procedência desconhecida, rodando como root, num aparelho que nunca vai receber atualização. Mesmo deixando de lado backdoor deliberado, aquele curl embarcado tem as CVEs com que foi compilado, para sempre. É o argumento de cadeia de suprimentos para por que aparelho de consumo com root é má ideia, só que você não precisa argumentar em hipótese; está ali no ls -la.
Eu descobri isso do jeito chato durante a limpeza. Apaguei os links maliciosos, e o meu one-liner seguinte quebrou, porque eu tinha removido o awk que o shell ia usar.
O APK Link to heading
Puxei os dois APKs para análise de strings offline. O app é empacotado: um protetor comercial estilo DexHelper/Bangcle, então a lógica real é criptografada e desempacotada em tempo de execução. O que é visível no classes.dex é um stub de carregamento mais algumas strings que sobreviveram:
- uma checagem de
/system/xbin/su, o teste de root, feito como simples existência de arquivo busyboxpcdn(7 ocorrências)luna(14 ocorrências; o diretório de cache do P2P éapp_luna)
Ele embarca gomediad.so, um motor de P2P/PCDN de 15 MB, e um helper empacotado pequeno. A box tinha a 4.20.1; o stick tem a 4.22.0. Mesmo empacotador, mesmas strings de checagem de root nos dois.
O empacotamento importa por um motivo além de esconder lógica: o código visível não é o que roda, então análise estática do dex embarcado não te diz quase nada. E a checagem de root ser um stat() simples em vez de um exec é por que ela não deixa rastro num aparelho onde falha: nada é logado, porque nada foi tentado.
Higiene com amostras, já que sempre perguntam. Os binários ARM foram hasheados, escaneados como texto e apagados do PC. Eles não executam em x86 e nunca foram rodados. Depois eu guardei alguns APKs para um possível sandbox, transmitidos direto do aparelho para um 7z com AES e nomes de arquivo criptografados, de forma que nenhum APK em texto claro tocou o meu disco:
adb exec-out cat /caminho/do/app.apk | 7z a -si -p -mhe=on quarentena.7z
Uma pegadinha: adb exec-out su -c cat binario > saida corrompe binários. O su aloca um pty, que converte LF em CRLF, e o seu arquivo cresce. Copie para o /sdcard primeiro e faça adb pull, ou não use su.
E uma lição de verdade, aprendida tarde: pegue a amostra antes de desinstalar. A versão do dropper estava na box e em nenhum outro lugar. Desinstalar destruiu a última cópia existente a que eu tinha acesso. Hoje eu tenho só o sha256.
Limpeza Link to heading
A ordem importa, e os dois primeiros passos são os que as pessoas pulam.
Backup primeiro. /data/local/backup-20261104/system-originals.tgz na box: preinstall.sh, rootsudaemon.sh, su, daemonsu, supolicy, hosts, build.prop, /data/adb.
Confirme que você tem outro caminho de entrada antes de remover o que você está usando. ro.debuggable=1 nesse build userdebug significa que adb root funciona. Eu verifiquei isso antes de apagar o su, porque remover root enquanto o seu único root é o su é como se brica a própria tarde.
Depois:
1. Matar a botnet. Matar o supervisord levou o pandoraspearrk junto. Depois apaguei /system/bin/{pandoraspearrk,supervisord,s.conf,curl,arp,awk}, os links de busybox do malware no /system/xbin, e o conjunto de payload do /data.
Pegadinha:
busybox pkill -f daemonsumatou o meu próprio shell adb, porque o-fcasa com a linha de comando completa e a linha do meu shell continha o padrão. Isso aconteceu comigo duas vezes numa tarde, a segunda compkill -f "7z a ..."durante a quarentena. Se você usapkill -f, o seu próprio processo é candidato.
2. Remover root dos apps. Matei todo daemonsu, apaguei /system/xbin/su, /system/xbin/daemonsu, /system/bin/rootsudaemon.sh, /data/adb. Acesso administrativo agora é só adb root, que exige aprovação física.
3. Exigir aprovação no ADB. No /system/build.prop: ro.adb.secure=0 → 1. Depois do reboot, o adb mostrou unauthorized até eu aceitar o prompt RSA na TV. (Eu perdi o primeiro prompt; adb disconnect e depois connect mostra de novo.) Antes dessa mudança, qualquer pessoa na LAN tinha um shell root na porta 5555 por pedir.
4. Assumir o hook de boot que o malware estava abusando. O mesmo preinstall.sh que o dropper sequestrou, agora meu:
- restaura o
/system/etc/hostsde uma cópia endurecida se mudou (os 61 domínios do malware →0.0.0.0) - monta uma chain iptables
ioc_blocknoOUTPUT, rejeitando o C2 mais 31 endereços que o dropper tinha fixado (eu excluí os IPs anycast da Cloudflare; bloquear aqueles quebra metade da internet e não te diz nada) - reativa o ADB de rede para mim
- opcionalmente aplica uma política de SELinux, que é uma história à parte mais abaixo
Verificação pelo log do boot seguinte: supolicy Success, hosts byte a byte idêntico à cópia endurecida, ioc_block com 32 regras REJECT em primeiro lugar no OUTPUT, sem su, sem daemonsu, sem supervisord, sem pandoraspearrk, /system montado somente leitura.
Zero acertos nas 32 regras de reject pelo resto da vida da box. Nada tentou ligar para casa de novo.
A história do SELinux, incluindo o teste que mentiu para mim Link to heading
A box veio Permissive. O firmware embarca o supolicy do SuperSU, que consegue aplicar patch na política ao vivo, então eu tentei construir uma jaula com a ferramenta do próprio fabricante.
O objetivo: impedir que apps de terceiros executem binários que eles baixaram, que é o mecanismo inteiro do dropper.
deny untrusted_app app_data_file file execute_no_trans
Eu mantive o execute (mmap) permitido, porque dex/oat empacotado e carregamento de .so nativo precisam dele: negue aquilo e todo app do aparelho quebra, inclusive os que você quer.
Aí eu testei:
runcon u:r:untrusted_app:s0 sh
Falhou. Regra funciona. Seguindo.
Só que aquele teste não valia nada. O runcon para dentro de untrusted_app falha com qualquer binário, porque untrusted_app não tem permissão de entrypoint neles, e é uma negação diferente da que eu estava testando. Eu tinha escrito um teste que passa com ou sem a minha regra, que é o pior tipo de teste.
A checagem real era abrir um app de verdade e procurar um filho shell. O emulador de terminal subiu sh numa boa. A regra não fez nada.
O motivo é que a permissão vinha do atributo appdomain, não do untrusted_app diretamente, e regras allow do SELinux são união, negar um tipo não faz nada enquanto um atributo mais amplo ainda permite. A correção:
deny appdomain shell_exec file execute_no_trans
allow <cada tipo de sistema> shell_exec file execute_no_trans
Reautorizando system_server, nfc, platform_app, bluetooth, recovery, priv_app, init, radio, shared_relro, system_app, su e shell. Efeito final: só untrusted_app e isolated_app não conseguem iniciar shell. Verificado corretamente dessa vez: app de terminal abre, sem filho sh.
Aí o modo Enforcing quebrou duas coisas, e nenhuma foi culpa minha:
Áudio. O audioserver levou negação de read no próprio netlink_kobject_uevent_socket (o socket que ele usa para saber de hotplug de HDMI e áudio) umas 1.460 vezes por minuto, queimando ~20% de CPU só nas negações. Correção:
allow audioserver audioserver netlink_kobject_uevent_socket { read create bind setopt getopt }
Vídeo. Áudio tocava, imagem não. Nenhuma negação na janela, nenhum erro de codec, nada óbvio. Demorou para achar: a política de fábrica nega ao mediacodec acesso a
/sys/firmware/devicetree/base/vpu_service@20020000/iommu_enabled
então o decodificador de vídeo em hardware não consegue inicializar. O áudio continua porque está no caminho de software. Isso é um bug na política do próprio fabricante, não nas minhas regras: qualquer boot em Enforcing nesse firmware quebra a decodificação de vídeo em hardware. O que é quase certamente por que ele veio Permissive. O fabricante bateu nisso e, em vez de corrigir quatro linhas de política, desligou o modelo de segurança de todo aparelho que vendeu.
Eu adicionei a correção:
allow mediacodec sysfs file { read open getattr }
…e desliguei a jaula de qualquer forma, atrás de um arquivo de flag para poder religar. Os motivos honestos: a pessoa que usa essa box queria assistir televisão, eu já tinha removido o root e deixado o /system somente leitura, e a jaula não bloqueava a coisa com que eu realmente me importava. O que nos leva aos limites.
O que a jaula não conseguiu tocar Link to heading
Eu reinstalei uma 4.22.0 limpa, sem root disponível, e observei. Em Enforcing, as únicas negações do app foram leituras em sysfs de /sys/class/net/*/duplex por uma thread chamada ranger. Nenhuma tentativa de exec. Nenhum shell. Nenhum .tmp.sh.
A jaula parou a identificação de hardware e mais nada, porque:
O motor de P2P roda dentro do processo do app. ranger / libranger-jni.so é uma biblioteca JNI carregada no próprio app, não um binário separado. Não existe exec para negar. Qualquer política que bloqueie isso também bloqueia o app de carregar o código nativo dele, o que quebra a reprodução. O estado dele fica em app_luna/ (.p2p_host_cache, .upnp_cache).
Então, o que o app limpo ainda fazia, lido de /proc/net/tcp{,6} e mapeado de volta para processos pelos inodes de socket em /proc/*/fd. O netstat dá segfault no toybox desse firmware, então você lê as tabelas do kernel direto:
- Escutando em todas as interfaces: 4 portas TCP (39900, 49462, 46245, e 47287 em IPv6) mais ~13 UDP. Portas de peer. A box é um servidor.
- DoH para quatro resolvedores na 443:
8.8.8.8,1.1.1.1,9.9.9.10(Quad9),223.5.5.5(AliDNS). É por isso que o arquivo hosts era a camada errada e o firewall por IP era a certa. - Hosts atrás da Cloudflare na 80/443, indistinguíveis de tráfego web normal.
- Um VPS numa porta não padrão:
192.3.185.237:31521(faixa da ColoCrossing/RackNerd) mais um peer IPv6 na mesma porta. Parece tracker ou relay. - Um cache de UPnP, ou seja, ele pede ao roteador para abrir portas para peers de entrada.
app_luna/block_cache/test.txt: um arquivo de 204.800 bytes marcado como executável, zerado no começo. É uma sonda de velocidade de disco para o cache de blocos do P2P: ele está medindo o seu armazenamento para decidir o quanto usar.
E o busybox escondido. Às 17:05:35, uid=10063 busybox fez chamadas ioctl num socket UDP: 0x8927 (SIOCGIFHWADDR, o endereço MAC), mais 0x891d e 0x8970. Uma thread com nome codificado em hex também leu /sys/class/net/*/address.
E então:
# find / -name "*busybox*"
#
Nada. Nem em files/, nem em .cache/, nem em lib/. O app solta, roda e apaga, ou roda da memória. A jaula bloqueou as leituras de MAC, mas não o exec, o que me diz que não veio de app_data_file. Eu nunca achei de onde veio.
Você não detecta isso procurando arquivo. Entre uma execução e outra, não existe arquivo. O único artefato é uma linha de log de negação, e num aparelho Permissive não existe nem isso.
O controle: o mesmo app num aparelho travado Link to heading
Eu tinha um Mi TV Stick na sala (comprado para fazer uma LG smart antiga voltar a ser utilizável, o que ele faz bem), então instalei o mesmo APK nele e fui observar. Android 14, release-keys, sem root, bootloader travado, verified boot verde. Um ambiente genuinamente limpo para o mesmo software.
Nenhuma tentativa de root. Em cerca de 2.000 negações logadas para o app não tem execute, não tem shell_exec, não tem su, não tem xbin, não tem busybox, não tem remount. O caminho do dropper fica dormente: ele dá stat() em /system/xbin/su, que não é logado, não acha nada, e não tenta.
O que o Android 14 bloqueou, tudo untrusted_app_32, permissive=0:
| qtd | leitura negada de | para quê |
|---|---|---|
| 584 | /proc/diskstats |
estatística de I/O de disco |
| 424 | /proc/net/sockstat |
contagem de sockets do sistema |
| 420 | /proc/net/snmp |
contadores TCP/UDP do sistema |
| 314 | /proc/loadavg |
carga de CPU |
| 311 | /proc/net/dev |
contadores de tráfego por interface |
| 183 | busca em /mnt/vendor |
sondagem da partição vendor |
| 4 | serviço binder SwlNativeService |
procurando um serviço do fabricante |
| 1 | vendor_xiaomi_prop |
propriedades do aparelho Xiaomi |
A thread fazendo tudo isso é o ranger de novo, o motor de P2P, consultando mais ou menos uma vez por segundo. Leia essa tabela como um conjunto e fica óbvio para que serve: throughput de disco, carga do sistema e contadores de tráfego por interface são exatamente o que você amostraria para decidir quanto de upload dá para tirar desse aparelho antes do dono perceber. Na box, em Permissive, todas essas leituras simplesmente funcionavam.
Esse é o comportamento de “ficar fora do radar”, e é uma decisão de projeto. Um cliente P2P que só saturasse o seu link seria desinstalado. Um que mede a sua folga primeiro, não.
E o segundo processo. O Android logou PhantomProcessRecord {4239:3271:<app>}, um filho nativo do app. Depois de um relaunch era pid 6316, ppid 4689, com nome idêntico ao do pai, 2 threads, 65 MB de RSS, mesmo domínio SELinux, sem tracer anexado. Não é um exec de sh ou su; é um fork() do próprio app.
Essa é a coisa mais evasiva de todo o texto, e é evasiva justamente porque é sem graça. Nada novo na lista de apps. Nenhum shell criado. Nenhum domínio SELinux novo. Monitoramento em nível de processo vê um app usando um pouco mais de memória. Típico do processo guardião do empacotador ou de um worker de P2P, e eu não consegui estreitar mais, porque /proc/<pid>/exe e maps exigem root e o stick corretamente não me deu.
Eu tentei: adb root → adbd cannot run as root in production builds. ro.build.type=user, ro.debuggable=0, ro.secure=1, ro.boot.flash.locked=1, verifiedbootstate=green. Os únicos caminhos são desbloqueio de bootloader (apaga o aparelho, e normalmente não é oferecido nesses sticks) ou um exploit. Nenhum dos dois aconteceu.
Tem uma ironia real nisso. Depois da limpeza, a box era o laboratório perfeito: adb root para mim, sem su para apps. O stick travado é mais seguro para o dono e completamente opaco para mim. A mesma tranca que protege o usuário esconde o app do pesquisador. Isso não é argumento contra aparelho travado; é argumento sobre por que análise independente dessa categoria é tão escassa.
Pecados de fábrica, de brinde Link to heading
Nenhum desses é o malware. Todos estavam ali desde a fábrica.
RAM falsa. O MemTotal é exatamente 2.097.152 kB, suspeitosamente 2 GiB exatos. O dumpsys meminfo relata Lost RAM 1,193,507K. O free -m diz que o total real é 995 MB. O “2 GB” da caixa é uma mentira contada na linha de comando do kernel.
Load average falso. Load 7 parado, CPU 90% ociosa. A carga é de threads de kernel presas em estado D: rknand e quatro threads ssv6xxx_encry do Wi-Fi. Elas contam para a carga e não consomem CPU. A box parece estar morrendo e está apenas mal feita.
Swap quebrado, em todo boot desde 2018. O fstab.rk30board define zram0 como swap, o init.box.rc escreve lz4 em comp_algorithm, e o kernel não tem lz4:
zram: Cannot initialise lz4 compressing backend
fs_mgr: swapon failed
Um aparelho de 1 GB saiu de fábrica com zero swap por causa de uma palavra divergente que ninguém nunca testou. Eu corrigi no meu hook de boot (reset, lzo, 256 MB, mkswap, swapon); em minutos uns 40 MB já tinham ido para swap.
Um app de teste de estresse que não morre. O com.cghs.stresstest é um priv-app persistent com UID de sistema e RECEIVE_BOOT_COMPLETED. pm disable não para. pm disable-user não para, porque o Android 7 inicia apps persistentes de qualquer forma. pm hide finalmente resolveu.
O Wi-Fi é um iComm SSV6051, mal suportado, e o motivo de a maioria das imagens Armbian/LibreELEC de RK3229 acabar sem rede sem fio. Ele caiu da rede por alguns minutos depois de um reboot e voltou.
O teclado digitava em chinês. O IME padrão era o Google Pinyin, com o locale do sistema em pt-BR. Isso também ninguém testou.
Juntando tudo: o firmware é feito para que sideload “simplesmente funcione”, e é exatamente isso que torna trivial tomar o aparelho. O malware não explorou nada. Ele usou um root que foi oferecido, escreveu num /system que remonta sem resistência, e acrescentou uma linha num script de boot que roda como root. Toda porta que ele usou foi instalada pelo fabricante.
O resto da rede foi atingido? Link to heading
O Pandoraspear é derivado do Mirai, e o Mirai se espalha por força bruta em telnet e SSH de outros aparelhos. Então eu checei o PC (Bazzite, x86_64):
- sem servidor telnet;
sshdinativo - nada escutando na interface da LAN além de Steam (27036), KDE Connect (1716), mDNS/LLMNR
- nenhuma conexão para endereço de IOC, nem para as portas 9999/23/2323
- toda conexão de saída mapeada para uma aplicação conhecida
- nenhum processo rodando de
/tmp,/dev/shmou binário deletado (os/dev/shm/u1000-Shm_*são dosteamwebhelper) /etc/hostslimpo; nenhuma unit do systemd alterada desde agosto além das que eu instalei- o experimento com USB-C nunca estabeleceu conexão de dados, então nada passou por ali
Os outros aparelhos da LAN que falavam com a box eram celulares, o PC e o Home Assistant via Cast (8008/8009), que é descoberta normal de Cast.
Uma ponta solta que eu vou admitir: eu tentei enumerar os mapeamentos UPnP do roteador a partir do PC com um M-SEARCH SSDP para InternetGatewayDevice e GetGenericPortMappingEntry, e nenhum IGD respondeu. Só que esse resultado não diz nada sobre o roteador. Eu uso OPNsense, multicast SSDP não atravessa ele sem uma regra que eu teria que criar de propósito, e eu não ia abrir isso por causa de uma box que ficou comigo por uma tarde. O scan não tinha como funcionar aqui. O app mantinha um .upnp_cache, então ele certamente estava tentando. Olhe a página do seu roteador na mão em vez de confiar num scan como eu fiz.
Indicadores de comprometimento Link to heading
Se você quer checar uma box, é com isso que você compara. Publicado porque conteúdo de detecção é para defensor; a mecânica do dropper não está aqui.
Arquivos
/system/bin/pandoraspearrk
/system/bin/supervisord
/system/bin/s.conf
/system/bin/curl
/data/a.sh
/data/s.conf
/data/sp.log
/data/supervisord.tar.gz
/data/.ms
/data/txt
/data/data/<app de iptv>/files/.tmp.sh
/sdcard/ipfull (JSON de geolocalização da vítima, regravado no boot)
/sdcard/.config, /sdcard/.properties (IDs de rastreio que sobrevivem à reinstalação)
SHA-256 dos componentes instalados
Esses são os arquivos que o dropper colocou no aparelho, não o app que carregou ele:
| sha256 | arquivo |
|---|---|
6cd689de76459a92d0a7ee9700860578c6906829b2839d94b46c26b288971707 |
pandoraspearrk, o bot em si |
a6c7c7636fd3929bcdb0bb2104302fcb49792164abe0cd743b06676bd0259172 |
supervisord, mantém o bot vivo |
a16fe5d6fb56fae12e4b00c58121145d209e4a38aa51a75bfb20c912058a95b9 |
supervisord.tar.gz, o pacote do payload como baixado |
9e78365c8cbe5994f932166250be98e185e14877845bf9deb90431597486b0a7 |
/system/bin/s.conf, config do supervisor |
d7597c8b4a241d57a540ed9a66a6b3526a745d1314fd6248e96e9eecd06fa18a |
/data/s.conf, segunda cópia |
3a6e939c4b4cc2697bbdf81585af8f8ea1616a2038833475326d5fc1533e5597 |
/data/a.sh, o atualizador |
adb9598f640102daee72581813fdb0997b857edb92833c317f04698a9d8a02b7 |
.tmp.sh, o dropper, do diretório privado do app |
f55d2bd57db658ab61ab2cc1405bf9e239b95a2602c8ceb767051079ad0d5735 |
o /system/etc/hosts sequestrado (61 entradas) |
722744c07404c35986a082d06e4363ffa7852329690ec90b72c0551bc3206430 |
preinstall.sh depois de o malware acrescentar a linha dele |
Os quatro primeiros são os que valem comparar: são os binários e a config da própria botnet. Os dois últimos são específicos do aparelho: o arquivo hosts e o script de boot do fabricante variam entre firmwares, então trate esses como referência e não como assinatura.
Rede
C2: 173.255.220.200:9999
Hosts de update: nisge.eylcqofxg.com
fadfa.gdalieyw.com:8080
fadfatest.pneydn.com:8080
pandorabackup.lftpdo.com
Escuta P2P: 39900/tcp (mesma porta nos dois aparelhos)
Peer/relay: 192.3.185.237:31521, 38.145.199.56:48985
Triagem rápida, numa box em que você consegue um shell
ps -A | grep -E "supervisord|pandora|daemonsu"
cat /system/etc/hosts # qualquer coisa além de localhost é sinal vermelho
grep -c . /system/etc/hosts # ~60 entradas significa que foi reescrito
cat /proc/net/tcp # procure :270F (9999) e endereços desconhecidos
ls -la /data/data/<app de iptv>/files/ # .tmp.sh, busybox, curl
getprop ro.adb.secure # 0 significa que qualquer um na sua LAN toma a box
getprop ro.debuggable
ls -la /system/xbin/su /system/xbin/daemonsu
getenforce
Se ro.adb.secure é 0 e existe su sem app gerenciador, a box já está pré-comprometida, tenha chegado alguma coisa ou não.
Como a box ficou no fim Link to heading
| Botnet | removida, 0 acertos no firewall depois |
| Root para apps | removido (su/daemonsu apagados) |
| ADB | aprovação RSA obrigatória; root só via adb root |
| SELinux | Permissive por escolha; jaula disponível atrás de flag, com as correções de áudio e vídeo |
| Rede | sinkhole de 61 domínios restaurado no boot, ioc_block com 32 endereços |
| Memória | zram 256 MB lzo corrigido, ~30 apps desativados ou ocultos |
| Janela de exposição | 73 dias (2026-08-23 11:24 → 2026-11-04 ~15:15) |
E então eu desliguei para sempre.
A limpeza funcionou, e eu me diverti fazendo. Mas um aparelho cujo fabricante desligou o modelo de segurança para contornar um bug de política dele mesmo, embarcou root silencioso, embarcou acesso remoto sem autenticação, e nunca vai ser atualizado, não é um aparelho que você protege. É um aparelho que você para de usar. O próximo app encontra as mesmas portas abertas.
Antes de sair de casa: tudo que ela conseguia ler é tratado como vazado. Senha do Wi-Fi trocada. O armazenamento dela ainda tinha a geolocalização da casa, os tokens de rastreio do app e o wpa_supplicant.conf, então reset de fábrica pelo recovery, ou melhor, regravar o firmware, antes que qualquer box dessas vá para um dono novo.
A única coisa genuinamente útil que eu diria para quem for repetir isso: pegue a amostra primeiro. Eu desinstalei a versão do dropper antes de arquivar, e destruí a única cópia que eu teria. O hash é tudo que sobrou.
Referências
1 - Pandora’s box is now open: the well-known Mirai trojan arrives in a new disguise to Android-based TV sets and TV boxes, Dr.Web, setembro de 2023
2 - Bigpanzi botnet infects 170,000 Android TV boxes with malware, BleepingComputer sobre o relatório do XLab/Qianxin, janeiro de 2024
3 - Anatel conclui que TV box pirata pode roubar dados e fazer ataques DDoS, Tecnoblog, julho de 2022
4 - Armbian para TV boxes rk322x, se você quiser regravar em vez de limpar. NAND cru limita as opções; o Multitool do jock2 faz backup do firmware original primeiro
5 - CSC Armbian for rk322x TV box boards, thread do fórum, incluindo quais placas perdem Wi-Fi