AF_UNIX MSG_OOB UAF & SKB 기반 커널 프리미티브

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에서 AF_UNIX MSG_OOB 처리를 위한 manage_oob() 리팩터(5aa57d9f2d53)에 결함이 도입되었습니다. 연속된 제로-길이 SKB가 u->oob_skb를 초기화하는 로직을 우회하여, 일반적인 recv()가 포인터가 여전히 살아있는 동안 OOB SKB를 free할 수 있었고 그 결과 CVE-2025-38236이 발생했습니다.
  • recv(..., MSG_OOB)를 재실행하면 dangling struct sk_buff를 역참조합니다. MSG_PEEK가 있을 경우, 경로 unix_stream_recv_urg() -> __skb_datagram_iter() -> copy_to_user()가 안정적인 1바이트 임의 커널 읽기가 됩니다; MSG_PEEK가 없으면 이 프리미티브는 재할당된 객체의 offset 0x40에 놓인 64비트 값의 상위 dword에 +4 GiB를 더하는, offset 0x44UNIXCB(oob_skb).consumed를 증가시킵니다.
  • order-0/1 unmovable pages를 소진(page-table spray)하고 SKB slab 페이지를 buddy allocator로 강제 free한 뒤 물리 페이지를 pipe buffer로 재사용함으로써, 익스플로잇은 제어된 메모리에 SKB 메타데이터를 위조해 dangling 페이지를 식별하고 read 프리미티브를 .data, vmemmap, per-CPU, 페이지-테이블 영역으로 피벗시킵니다 — usercopy hardening에도 불구하고.
  • 동일한 페이지는 이후 새로 클론된 스레드의 최상위 커널 스택 페이지로 재활용될 수 있습니다. CONFIG_RANDOMIZE_KSTACK_OFFSET는 오라클이 됩니다: pipe_write()가 블록되는 동안 스택 레이아웃을 탐지하여 공격자는 spilled copy_page_from_iter() 길이(R14)가 offset 0x40에 위치할 때까지 기다린 후 +4 GiB 증가를 발사해 스택 값을 손상시킵니다.
  • 자체 루프를 도는 skb_shinfo()->frag_list는 협력 스레드가 단일 MADV_DONTNEED 홀이 있는 VMA에 대해 mprotect()copy_from_iter()를 멈출 때까지 UAF syscall을 커널 공간에서 반복시키게 합니다. 루프를 깨면 증가가 스택 타깃이 활성화된 정확한 시점에 해제되어 bytes 인자를 부풀리고 copy_page_from_iter()가 pipe buffer 페이지를 넘어 다음 물리 페이지에 쓰게 만듭니다.
  • read 프리미티브로 pipe-buffer PFN과 페이지 테이블을 모니터링함으로써, 공격자는 다음 페이지가 PTE 페이지임을 확인하고 OOB 복사를 임의의 PTE 쓰기로 변환하여 무제한 커널 R/W/X를 획득합니다. Chrome은 렌더러에서 MSG_OOB를 차단하여 접근 가능성을 완화했으며(6711812), Linux는 로직 결함을 32ca245464e1에서 수정하고 CONFIG_AF_UNIX_OOB를 도입해 해당 기능을 선택형으로 만들었습니다.

근본 원인: manage_oob()는 하나의 제로-길이 SKB만 가정한다

unix_stream_read_generic()manage_oob()가 반환하는 모든 SKB가 unix_skb_len() > 0임을 기대합니다. 93c99f21db36 이후 manage_oob()recv(MSG_OOB)가 남긴 제로-길이 SKB를 처음 제거할 때마다 skb == u->oob_skb 정리 경로를 건너뛰었습니다. 이후의 수정(5aa57d9f2d53)은 여전히 첫 번째 제로-길이 SKB에서 skb_peek_next()로 넘어가면서 길이를 재확인하지 않았습니다. 두 개의 연속된 제로-길이 SKB가 있으면 함수는 두 번째 빈 SKB를 반환했고, unix_stream_read_generic()는 다시 manage_oob()를 호출하지 않고 이를 건너뛰었기 때문에 실제 OOB SKB가 dequeue되어 free되었는데 u->oob_skb는 여전히 이를 가리키고 있었습니다.

최소 트리거 시퀀스

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)을 수행합니다. dangling SKB가 공격자가 제어하는 메모리(또는 pipe 페이지 같은 제어 가능한 alias)로 재할당되면, 매번 recv(MSG_OOB | MSG_PEEK)__check_object_size()로 허용되는 임의의 커널 주소에서 1바이트를 유저 공간으로 복사합니다(프로세스가 죽지 않음). MSG_PEEK을 유지하면 dangling 포인터를 보존하여 무제한으로 읽을 수 있습니다.
  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: 거대한 읽기 전용 anonymous VMA를 매핑하고 모든 페이지를 fault 시켜 페이지 테이블 할당을 강제합니다 (order-0 unmovable). 대략 RAM의 10% 정도를 페이지 테이블로 채우면 이후 skbuff_head_cache 할당이 order-0 리스트가 고갈된 후 신규 buddy 페이지를 가져오게 됩니다.
  2. Spray SKBs and isolate a slab page: 여러 개의 stream socketpair를 사용하고 소켓당 수백 개의 작은 메시지(~0x100 바이트 per SKB)를 큐잉하여 skbuff_head_cache를 채웁니다. 선택한 SKB들을 해제해 목표 슬랩 페이지를 공격자 제어 하에 완전히 놓고, 읽기 프리미티브로 struct page refcount를 모니터링합니다.
  3. Return the slab page to the buddy allocator: 페이지의 모든 객체를 해제한 뒤 추가적인 할당/해제를 충분히 수행해 SLUB per-CPU partial 리스트와 per-CPU 페이지 리스트에서 페이지가 밀려나도록 하여 buddy freelist의 order-1 페이지가 되게 합니다.
  4. Reallocate as pipe buffer: 수백 개의 pipe를 생성합니다; 각 pipe는 최소 두 개의 0x1000-바이트 데이터 페이지(PIPE_MIN_DEF_BUFFERS)를 예약합니다. buddy allocator가 order-1 페이지를 분할할 때, 한 쪽 절반이 해제된 SKB 페이지를 재사용합니다. 어떤 pipe와 어떤 오프셋이 oob_skb와 alias 되는지 찾기 위해 파이프 페이지 전역에 가짜 SKB에 고유 마커 바이트를 써넣고 반복적으로 recv(MSG_OOB | MSG_PEEK)을 호출해 마커가 반환될 때까지 확인합니다.
  5. Forge a stable SKB layout: alias된 pipe 페이지를 가짜 struct sk_buff로 채워 그 data/head 포인터와 skb_shared_info 구조가 관심 있는 임의의 커널 주소를 가리키도록 합니다. 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 항목은 각 페이지의 타입 플래그, freelist 포인터 및 소유 캐시를 노출합니다. per-CPU vmalloc 세그먼트를 스캔하면 struct kmem_cache_cpu 인스턴스를 찾아 주요 캐시(skbuff_head_cache, kmalloc-cg-192 등)의 다음 할당 주소를 예측할 수 있습니다.
  • Page tables: mm_struct를 직접 읽는 대신(유저카피로 차단됨) 글로벌 pgd_list(struct ptdesc)를 따라 현재 mm_structcpu_tlbstate.loaded_mm으로 매치합니다. 루트 pgd를 알게 되면, 이 프리미티브로 모든 페이지 테이블을 순회하여 pipe 버퍼, 페이지 테이블, 커널 스택의 PFN을 매핑할 수 있습니다.

Recycling the SKB page as the top kernel-stack page

  1. 제어하던 pipe 페이지를 다시 해제하고 vmemmap으로 그 refcount가 0으로 돌아오는지 확인합니다.
  2. 즉시 네 개의 헬퍼 파이프 페이지를 할당한 뒤 역순으로 해제해 buddy allocator의 LIFO 동작을 결정론적으로 만듭니다.
  3. clone()으로 헬퍼 스레드를 생성합니다; x86_64에서 스택은 네 페이지이므로 최근에 해제된 네 페이지가 그 스레드의 스택이 되며, 마지막으로 해제된 페이지(이전의 SKB 페이지)는 높은 주소에 위치합니다.
  4. 페이지 테이블 워크로 헬퍼 스레드의 최상위 스택 PFN이 재활용된 SKB PFN과 같은지 확인합니다.
  5. arbitrary read로 스택 레이아웃을 관찰하면서 스레드를 pipe_write()로 유도합니다. CONFIG_RANDOMIZE_KSTACK_OFFSET는 syscall마다 RSP에서 정렬된 0x0–0x3f0 범위의 랜덤 값을 빼므로, 다른 스레드의 poll()/read()와 결합한 반복적인 쓰기로 작성자가 원하는 오프셋에서 블록될 때를 찾아냅니다. 운이 좋으면, 흘러나온 copy_page_from_iter()bytes 인자(R14)가 재활용된 페이지 내 오프셋 0x40에 위치합니다.

Placing fake SKB metadata on the stack

  • AF_UNIX datagram 소켓에서 sendmsg()를 사용하면: 커널은 유저의 sockaddr_un을 최대 108바이트까지 스택 상의 sockaddr_storage로 복사하고, 부수 데이터(ancillary data)를 syscall이 큐 공간을 기다리며 블록되기 전에 또 다른 온-스택 버퍼로 복사합니다. 이를 통해 스택 메모리 안에 정밀한 가짜 SKB 구조를 심을 수 있습니다.
  • 복사가 끝난 시점을 감지하려면 unmapped 유저 페이지에 위치한 1바이트 컨트롤 메시지를 제공하세요; ____sys_sendmsg()가 이를 fault 시키므로 그 주소에 대해 mincore()를 폴링하는 헬퍼 스레드는 목적지 페이지가 준비됐을 때를 알 수 있습니다.
  • CONFIG_INIT_STACK_ALL_ZERO로 인한 0으로 초기화된 패딩은 사용되지 않은 필드를 채워 추가 쓰기 없이도 유효한 SKB 헤더를 완성시켜 줍니다.

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

  • skb_shinfo(fakeskb)->frag_list를 두 번째 가짜 SKB(공격자 제어 유저 메모리에 저장)로 가리키도록 위조하고, 그 SKB가 len = 0next = &self가 되게 합니다. __skb_datagram_iter() 내부에서 skb_walk_frags()가 이 리스트를 순회할 때, 이터레이터가 NULL에 도달하지 않으므로 실행은 영원히 회전하고 복사 루프는 진행하지 않습니다.
  • 두 번째 가짜 SKB가 self-loop하는 한 recv syscall을 커널 안에서 지속시킵니다. 증가를 발사할 시점에는 두 번째 SKB의 next 포인터를 유저 공간에서 NULL로 바꾸면 됩니다. 루프가 종료되고 unix_stream_recv_urg()는 즉시 UNIXCB(oob_skb).consumed += 1을 실행하여 재활용된 스택 페이지의 오프셋 0x40에 현재 배치된 객체에 영향을 줍니다.

Stalling copy_from_iter() without userfaultfd

  • 거대한 anonymous RW VMA를 매핑하고 전부 fault 시킵니다.
  • madvise(MADV_DONTNEED, hole, PAGE_SIZE)로 단일 페이지 홀을 만들고 그 주소를 write(pipefd, user_buf, 0x3000)에 사용되는 iov_iter에 넣습니다.
  • 병렬로 다른 스레드에서 VMA 전체에 대해 mprotect()를 호출하세요. 해당 syscall은 mmap 쓰기 락을 잡고 모든 PTE를 순회합니다. 파이프 작성자가 홀에 도달하면 페이지 폴트 핸들러는 mprotect()가 잡고 있는 mmap 락을 기다리며 차단되므로 copy_from_iter()는 결정론적인 시점에서 멈추고 흘러나온 bytes 값은 재활용된 SKB 페이지가 호스팅하는 스택 세그먼트에 남습니다.

Turning the increment into arbitrary PTE writes

  1. Fire the increment: frag 루프를 해제하여 copy_from_iter()가 멈춘 동안 +4 GiB 증가가 bytes 변수에 적용되게 합니다.
  2. Overflow the copy: 결함이 재개되면 copy_page_from_iter()는 현재 파이프 페이지로 >4 GiB를 복사할 수 있다고 판단합니다. 합법적인 0x2000 바이트(두 파이프 버퍼)를 채운 뒤 추가 반복을 실행해 남은 유저 데이터를 파이프 버퍼 PFN 다음에 오는 물리 페이지에 씁니다.
  3. Arrange adjacency: 할당자 텔레메트리를 이용해 buddy allocator가 프로세스 소유의 PTE 페이지를 대상 파이프 버퍼 페이지 바로 다음에 놓도록 강제합니다(예: 파이프 페이지를 번갈아 할당하고 새 가상 범위를 터치해 페이지-테이블 할당을 트리거해 PFN들이 같은 2 MiB 페이지블록에 정렬되도록 함).
  4. Overwrite page tables: OOB copy_from_iter()가 이웃 페이지를 공격자가 선택한 엔트리로 채우게 하려면 추가 0x1000 바이트의 유저 데이터에 원하는 PTE 엔트리들을 인코딩하세요. 이를 통해 커널 물리 메모리에 대한 RW/RWX 유저 매핑을 부여하거나 기존 엔트리를 덮어써 SMEP/SMAP을 비활성화할 수 있습니다.

Mitigations / hardening ideas

  • Kernel: 32ca245464e1479bfea8592b9db227fdc1641705 적용( SKB를 올바르게 재검증) 및 CONFIG_AF_UNIX_OOB(5155cbcdbf03)를 통해 필요하지 않다면 AF_UNIX OOB를 비활성화하는 것을 고려하세요. manage_oob()를 추가적인 무결성 검사(예: unix_skb_len() > 0가 될 때까지 루프)로 강화하고 유사한 가정이 존재하는 다른 소켓 프로토콜을 감사하세요.
  • Sandboxing: seccomp 프로필이나 상위 브로커 API에서 MSG_OOB/MSG_PEEK 플래그를 필터링하세요(Chrome 변경 6711812는 렌더러 측 MSG_OOB를 차단합니다).
  • Allocator defenses: SLUB freelist 무작위화 강화나 캐시별 페이지 컬러링 강제는 결정론적 페이지 재활용을 복잡하게 합니다; 파이프 버퍼 수를 제한하는 것도 재할당 신뢰도를 줄입니다.
  • Monitoring: 높은 비율의 페이지-테이블 할당이나 비정상적인 파이프 사용을 텔레메트리로 노출하세요—이 익스플로잇은 대량의 페이지 테이블과 파이프 버퍼를 소모합니다.

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 지원하기