AF_UNIX MSG_OOB UAF & SKB-based kernel primitives

Tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする

TL;DR

  • Linux >=6.9 introduced a flawed manage_oob() refactor (5aa57d9f2d53) for AF_UNIX MSG_OOB handling. Stacked zero-length SKBs bypassed the logic that clears u->oob_skb, so a normal recv() could free the out-of-band SKB while the pointer remained live, leading to CVE-2025-38236.
  • Re-triggering recv(..., MSG_OOB) dereferences the dangling struct sk_buff. With MSG_PEEK, the path unix_stream_recv_urg() -> __skb_datagram_iter() -> copy_to_user() becomes a stable 1-byte arbitrary kernel read; without MSG_PEEK the primitive increments UNIXCB(oob_skb).consumed at offset 0x44, i.e., adds +4 GiB to the upper dword of any 64-bit value placed at offset 0x40 inside the reallocated object.
  • By draining order-0/1 unmovable pages (page-table spray), force-freeing an SKB slab page into the buddy allocator, and reusing the physical page as a pipe buffer, the exploit forges SKB metadata in controlled memory to identify the dangling page and pivot the read primitive into .data, vmemmap, per-CPU, and page-table regions despite usercopy hardening.
  • The same page can later be recycled as the top kernel-stack page of a freshly cloned thread. CONFIG_RANDOMIZE_KSTACK_OFFSET becomes an oracle: by probing the stack layout while pipe_write() blocks, the attacker waits until the spilled copy_page_from_iter() length (R14) lands at offset 0x40, then fires the +4 GiB increment to corrupt the stack value.
  • A self-looping skb_shinfo()->frag_list keeps the UAF syscall spinning in kernel space until a cooperating thread stalls copy_from_iter() (via mprotect() over a VMA containing a single MADV_DONTNEED hole). Breaking the loop releases the increment exactly when the stack target is live, inflating the bytes argument so copy_page_from_iter() writes past the pipe buffer page into the next physical page.
  • By monitoring pipe-buffer PFNs and page tables with the read primitive, the attacker ensures the following page is a PTE page, converts the OOB copy into arbitrary PTE writes, and obtains unrestricted kernel read/write/execute. Chrome mitigated reachability by blocking MSG_OOB from renderers (6711812), and Linux fixed the logic flaw in 32ca245464e1 plus introduced CONFIG_AF_UNIX_OOB to make the feature optional.

Root cause: manage_oob() assumes only one zero-length SKB

unix_stream_read_generic() expects every SKB returned by manage_oob() to have unix_skb_len() > 0. After 93c99f21db36, manage_oob() skipped the skb == u->oob_skb cleanup path whenever it first removed a zero-length SKB left behind by recv(MSG_OOB). The subsequent fix (5aa57d9f2d53) still advanced from the first zero-length SKB to skb_peek_next() without re-checking the length. With two consecutive zero-length SKBs, the function returned the second empty SKB; unix_stream_read_generic() then skipped it without calling manage_oob() again, so the true OOB SKB was dequeued and freed while u->oob_skb still pointed to it.

最小トリガーシーケンス

char byte;
int socks[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, socks);
for (int i = 0; i < 2; ++i) {
send(socks[1], "A", 1, MSG_OOB);
recv(socks[0], &byte, 1, MSG_OOB);
}
send(socks[1], "A", 1, MSG_OOB);   // SKB3, u->oob_skb = SKB3
recv(socks[0], &byte, 1, 0);         // normal recv frees SKB3
recv(socks[0], &byte, 1, MSG_OOB);   // dangling u->oob_skb

unix_stream_recv_urg() によって露呈するプリミティブ

  1. 1-byte arbitrary read (repeatable): state->recv_actor() は最終的に copy_to_user(user, skb_sourced_addr, 1) を呼びます。ダングリングした SKB が攻撃者制御下のメモリ(または pipe page のような制御されたエイリアス)に再割り当てされると、各 recv(MSG_OOB | MSG_PEEK)__check_object_size() が許可する任意のカーネルアドレスから 1 バイトをユーザ空間にコピーし、クラッシュさせずに取得できます。MSG_PEEK を維持するとダングリングポインタが保持され、無制限に読み出せます。
  2. Constrained write: MSG_PEEK がクリアされていると、UNIXCB(oob_skb).consumed += 1 はオフセット 0x44 の 32 ビットフィールドをインクリメントします。0x100 境界でアロケートされた SKB ではこれは 8 バイト整列の語より 4 バイト上に位置し、このプリミティブはオフセット 0x40 にある語を +4 GiB インクリメントする操作に変換されます。これをカーネル書き込みに変えるには、そのオフセットに敏感な 64 ビット値を配置する必要があります。

Reallocating the SKB page for arbitrary read

  1. Drain order-0/1 unmovable freelists: 巨大な read-only anonymous VMA をマップし、すべてのページでフォルトさせて page-table を割り当てさせます(order-0 unmovable)。ページテーブルで RAM の約 10% を埋めると、その後の skbuff_head_cache の割り当てが order-0 リストが枯渇した際に新しい buddy ページを引くようになります。
  2. Spray SKBs and isolate a slab page: 何十組もの stream socketpair を使い、各ソケットに数百個の小さなメッセージ(SKB あたり約 0x100 バイト)をキューして skbuff_head_cache を埋めます。選択した SKB を free してターゲットの slab ページを完全に攻撃者制御下に置き、出現した read プリミティブでその struct page の refcount を監視します。
  3. Return the slab page to the buddy allocator: ページ上のすべてのオブジェクトを free し、さらに十分な追加割り当て/解放を行って SLUB の per-CPU partial lists と per-CPU page lists からページを押し出し、buddy freelist 上で order-1 ページにします。
  4. Reallocate as pipe buffer: 数百個の pipe を作成します。各 pipe は少なくとも 2 つの 0x1000 バイトのデータページを予約します(PIPE_MIN_DEF_BUFFERS)。buddy allocator が order-1 ページを分割すると、その半分が解放した SKB ページを再利用します。どの pipe・どのオフセットが oob_skb とエイリアスしているかを特定するため、pipe ページ全体にフェイク SKB を格納してユニークなマーカーバイトを書き込み、マーカーが返されるまで recv(MSG_OOB | MSG_PEEK) を繰り返します。
  5. Forge a stable SKB layout: エイリアスした pipe ページを、data/head ポインタや skb_shared_info 構造が任意のカーネルアドレスを指すフェイク struct sk_buff で埋めます。x86_64 の copy_to_user() 内では SMAP が無効になるため、カーネルポインタが判明するまでユーザモードアドレスをステージングバッファとして使えます。
  6. Respect usercopy hardening: コピーは .data/.bss、vmemmap エントリ、per-CPU vmalloc 範囲、他スレッドのカーネルスタック、そして高次の folio 境界をまたがない direct-map ページに対して成功します。.text__check_heap_object() によって拒否される特殊なキャッシュに対する読み出しは、プロセスを殺さずに単に -EFAULT を返します。

Introspecting allocators with the read primitive

  • Break KASLR: 固定マッピング CPU_ENTRY_AREA_RO_IDT_VADDR (0xfffffe0000000000) から任意の IDT ディスクリプタを読み、既知のハンドラオフセットを引くことでカーネルベースを復元できます。
  • SLUB/buddy state: グローバルな .data シンボルは kmem_cache ベースを明かし、vmemmap エントリは各ページの type フラグ、freelist ポインタ、所属 cache を露出します。per-CPU vmalloc セグメントを走査すると struct kmem_cache_cpu インスタンスが見つかり、主要なキャッシュ(例: skbuff_head_cache, kmalloc-cg-192)の次の割り当てアドレスを予測可能にします。
  • Page tables: mm_struct を直接読む代わりに、グローバルな pgd_list (struct ptdesc) を辿り、cpu_tlbstate.loaded_mm を使って現在の mm_struct と照合します。ルート pgd が分かれば、このプリミティブで全ページテーブルを辿って pipe バッファ、ページテーブル、カーネルスタックの PFN をマッピングできます。

Recycling the SKB page as the top kernel-stack page

  1. 制御下にある pipe ページを再度 free し、vmemmap でその refcount が 0 に戻ることを確認します。
  2. 直ちに 4 枚の補助 pipe ページを割り当て、逆順に free して buddy allocator の LIFO 挙動を決定論化します。
  3. clone() を呼んでヘルパースレッドを生成します。x86_64 のスタックは 4 ページなので、直近に free された 4 つのページがそのスタックになり、最後に free されたページ(元の SKB ページ)が高位アドレスになります。
  4. ページテーブルウォークでヘルパースレッドの top stack PFN が再利用した SKB PFN と等しいことを確認します。
  5. arbitrary read を使ってスタックレイアウトを観察しつつスレッドを pipe_write() に誘導します。CONFIG_RANDOMIZE_KSTACK_OFFSET により syscall ごとに RSP からランダムな 0x0–0x3f0(整列済み)が引かれるため、別スレッドからの poll()/read() と繰り返しの write を組み合わせることで、writer が望むオフセットでブロックする瞬間を検出します。運が良ければ、spill された copy_page_from_iter()bytes 引数(R14)は再利用ページ内のオフセット 0x40 に位置します。

Placing fake SKB metadata on the stack

  • AF_UNIX datagram socket に対して sendmsg() を使うと、カーネルはユーザの sockaddr_un をスタック上の sockaddr_storage(最大 108 バイト)にコピーし、ancillary data を別のスタック上バッファにコピーしてから syscall がキュー空きを待ってブロックします。これによりスタック上に精密なフェイク SKB 構造を植え付けることができます。
  • コピーが完了した時点を検出するには、未マップのユーザページにある 1 バイトの control message を与えます。____sys_sendmsg() はそれをフォルトインさせるので、mincore() をポーリングするヘルパースレッドが宛先ページが存在するかを知れます。
  • CONFIG_INIT_STACK_ALL_ZERO によるゼロ初期化パディングが未使用フィールドを埋め、追加書き込みなしで有効な SKB ヘッダを完成させます。

Timing the +4 GiB increment with a self-looping frag list

  • skb_shinfo(fakeskb)->frag_list を第二のフェイク SKB(攻撃者制御のユーザメモリに格納)を指すように偽造し、その SKB が len = 0next = &self を持つようにします。skb_walk_frags()__skb_datagram_iter() 内でこのリストを反復すると、イテレータが NULL に到達しないため実行は無限ループし、コピーループは進みません。
  • recv syscall をカーネル内で走らせ続けるために第二のフェイク SKB をセルフループさせておき、インクリメントを発射するタイミングで単にその二番目 SKB の next ポインタをユーザ空間から NULL に変更します。ループが抜けると unix_stream_recv_urg() は直ちに UNIXCB(oob_skb).consumed += 1 を一度だけ実行し、再利用されたスタックページのオフセット 0x40 にある現在のオブジェクトに影響を与えます。

Stalling copy_from_iter() without userfaultfd

  • 巨大な anonymous RW VMA をマップし、完全にフォルトさせます。
  • madvise(MADV_DONTNEED, hole, PAGE_SIZE) で単一ページを穴にし、そのアドレスを write(pipefd, user_buf, 0x3000) に使う iov_iter に入れます。
  • 並行して別スレッドで VMA 全体に対して mprotect() を呼びます。syscall は mmap write lock を取りすべての PTE を走査します。pipe writer が穴に到達すると page fault ハンドラが mprotect() が保持する mmap lock を待つため、copy_from_iter() が決定論的なポイントで停止し、spill された bytes 値が再利用した SKB ページ上のスタックセグメントにあるままになります。

Turning the increment into arbitrary PTE writes

  1. Fire the increment: copy_from_iter() がスタールしている間に frag ループを解放して +4 GiB インクリメントを bytes 変数に当てます。
  2. Overflow the copy: フォールトが再開されると、copy_page_from_iter() は現在の pipe ページに >4 GiB をコピーできると信じます。正当な 0x2000 バイト(2 つの pipe バッファ)を埋めた後、さらにもう一回のイテレーションを行い、残りのユーザデータを pipe buffer PFN の後に続く物理ページに書き込みます。
  3. Arrange adjacency: アロケータのテレメトリを使い、buddy allocator が対象の pipe buffer ページの直後にプロセス所有の PTE ページを配置するように仕向けます(例えば pipe ページの割り当てと新しい仮想領域へのアクセスを交互に行い、PFN が同じ 2 MiB pageblock 内で揃うまで page-table 割り当てを誘発します)。
  4. Overwrite page tables: ユーザデータの余分な 0x1000 バイトに望む PTE エントリをエンコードし、OOB copy_from_iter() が隣接ページを攻撃者指定のエントリで埋めて、カーネル物理メモリの RW/RWX ユーザマッピングを付与するか既存エントリを書き換えて SMEP/SMAP を無効化します。

Mitigations / hardening ideas

  • Kernel: 32ca245464e1479bfea8592b9db227fdc1641705 を適用して SKB を適切に再検証する(manage_oob の修正)ことを検討し、CONFIG_AF_UNIX_OOB を厳密に必要でない限り無効化する案を検討します(5155cbcdbf03)。manage_oob() を追加の健全性チェック(例: unix_skb_len() > 0 になるまでループ)で強化し、他のソケットプロトコルに対しても同様の仮定がないか監査します。
  • Sandboxing: seccomp プロファイルや上位ブローカー API で MSG_OOB/MSG_PEEK フラグをフィルタリングします(Chrome の変更 6711812 はレンダラ側での MSG_OOB をブロックするようになりました)。
  • Allocator defenses: SLUB freelist のランダム化強化や per-cache page coloring の強制は決定論的ページ再利用を難しくします。pipe buffer 数の制限も再割り当ての信頼性を低下させます。
  • Monitoring: 高頻度の page-table 割り当てや異常な pipe 使用量をテレメトリで検出します — このエクスプロイトは大量の page table と pipe バッファを消費します。

References

Tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする