u32에서 루트까지: io_uring ZCRX 프리리스트 취약점 분석

Linux 커널의 io_uring 서브시스템은 그 방대한 복잡성과 강력한 기능 때문에 보안 연구자들의 오랜 관심 대상이었습니다. 최근 Zero‑Copy Receive (ZCRX) 서브시스템에서 발견된 취약점은 고전적이면서도 파괴적인 메모리 안전 오류, 즉 스택 기반 프리리스트에 대한 경계 검사 누락을 보여줍니다. 이 취약점은 특정 권한을 가진 공격자가 4바이트 크기의 단순 범위 초과(OOB) 쓰기를 전체 루트 권한으로 전환할 수 있게 합니다.

이 글에서는 ZCRX 취약점의 기술적 메커니즘, 이를 무기화하기 위한 힙 그루밍, 그리고 최종적인 로컬 권한 상승(LPE) 경로를 상세히 분석합니다.

취약점: ZCRX의 경계 검사 누락

Linux 6.15에 도입된 ZCRX는 사용자 공간이 등록된 메모리 영역으로 직접 네트워크 패킷을 받아들여 커널‑사용자 복사의 오버헤드를 없앱니다. 이러한 메모리 슬롯을 관리하기 위해 커널은 net_iov 구조체와 대응되는 프리리스트를 사용합니다:

  • freelist[]: 사용 가능한 슬롯 인덱스들의 스택이며, kcalloc(num_niovs, sizeof(u32)) 로 할당됩니다.
  • free_count: 스택의 현재 깊이를 추적하는 정수입니다.

취약점은 io_zcrx_return_niov_freelist 함수에 존재합니다. 네트워크 I/O 벡터(niov)가 풀에 반환될 때, 커널은 인덱스를 freelist에 푸시하고 free_count를 증가시키지만, free_count가 이미 num_niovs에 도달했는지 여부를 확인하지 않습니다.

static void io_zcrx_return_niov_freelist(struct net_iov *niov)
{
    struct io_zcrx_area *area = io_zcrx_iov_to_area(niov);

    spin_lock_bh(&area->freelist_lock);
    area->freelist[area->free_count++] = net_iov_idx(niov);
    spin_unlock_bh(&area->freelist_lock);
}

free_countnum_niovs와 같아지면, 쓰기는 freelist[num_niovs]에 이루어지며, 이는 할당된 배열 끝을 정확히 4바이트 넘어선 위치입니다. 결과적으로 인접한 슬랩 메모리에 4바이트 OOB 쓰기가 발생합니다.

OOB 쓰기 트리거

이 익스플로잇은 두 개의 커널 해제 경로가 동일한 프리리스트에 niov를 반환하는 레이스 컨디션에 의존합니다:

  1. 경로 A (정상 종료): 네트워크 스택이 패킷을 해제하면서 io_pp_zc_release_netmem을 호출하고, 여기서 niov를 프리리스트에 푸시합니다.
  2. 경로 B (페이지 풀 해제): NIC가 내려갈 때 io_pp_zc_destroy가 모든 niov를 순회합니다. 만약 niov에 아직 참조 카운트가 남아 있으면 강제로 프리리스트에 반환됩니다.

ptr_ring 배출(Path A)과 스크럽 루프(Path B)가 원자적이지 않기 때문에, niov가 두 번 카운트될 수 있는 창이 존재합니다. 프리리스트가 거의 가득 찬 상태라면, 이 이중 카운트가 free_count를 배열 경계 밖으로 밀어 OOB 쓰기를 일으킵니다.

사용자 공간에서 이를 트리거하려면 공격자는 CAP_NET_ADMIN 권한을 가지고 SIOCSIFFLAGS를 통해 NIC를 내릴 수 있어야 합니다. 과정은 ZCRX 인터페이스 큐(IFQ)를 등록하고, UDP 패킷으로 폭격해 niov를 할당한 뒤, 일부 패킷이 아직 비행 중일 때 인터페이스를 내리는 것입니다.

작은 정수에서 루트까지: 익스플로잇 체인

작은 정수(niov 인덱스)를 쓰는 것이 사소해 보일 수 있지만, 공격자는 num_niovs 파라미터를 조작해 쓰기의 값과 위치를 제어할 수 있습니다. 이는 kcalloc 할당 크기를 결정하고, 결과적으로 어떤 슬랩 캐시(예: kmalloc-128)가 사용되는지를 좌우합니다.

1. msg_msg 로 힙 그루밍

OOB 쓰기의 목표는 struct msg_msg 객체입니다. msgsnd()msg_msg 객체들을 스프레이하면, 공격자는 kmalloc-128 슬랩 내에서 freelist 바로 뒤에 msg_msg가 배치되도록 할 수 있습니다.

OOB 쓰기는 인접한 msg_msg의 첫 4바이트를 덮어쓰며, 이는 m_list.next 포인터의 하위 32비트에 해당합니다. x86‑64에서는 상위 32비트를 유지하면서 포인터가 커널 physmap 범위 내에 남아 있게 됩니다.

2. KASLR 깨기

손상된 m_list.next 포인터를 이용해 msgrcv()MSG_COPY 플래그를 지정하면 힙을 과다 읽을 수 있습니다. 반환된 데이터에서 알려진 커널 텍스트 포인터를 스캔하면 커널 베이스 주소를 계산해 KASLR을 우회할 수 있습니다. 혹은 /proc/kallsyms 혹은 dmesg에 접근 가능하면 modprobe_path 주소를 직접 얻을 수도 있습니다.

3. modprobe_path 덮어쓰기

modprobe_path는 커널이 모듈을 로드해야 할 때 실행되는 바이너리를 가리키는 전역 변수입니다. 앞서 유출한 KASLR 주소와 CAP_SYS_ADMIN(대부분 CAP_NET_ADMIN과 함께 컨테이너 설정에 부여됨) 권한을 이용해 /proc/sys/kernel/modprobe를 통해 modprobe_path를 악성 스크립트 경로로 덮어쓸 수 있습니다.

마지막으로 알 수 없는 소켓 주소 패밀리(예: socket(AF_CAN, ...))를 트리거하면 커널이 루트 권한으로 악성 스크립트를 실행합니다.

완화 방안 및 커뮤니티 시각

이 취약점은 커밋 770594e에서 해결되었으며, 중요한 경계 검사가 추가되었습니다:

if (WARN_ON_ONCE(area->free_count >= area->nia.num_niovs))
    return;

비판적 분석

기술 체인은 정교하지만, 사전 요구 권한(CAP_NET_ADMINCAP_SYS_ADMIN)이 익스플로잇의 영향을 크게 제한한다는 점이 커뮤니티에서 지적되었습니다. 한 댓글은 다음과 같이 말했습니다:

"modprobe_path를 쓸 수 있다면, 코드를 실행할 방법을 찾는 것이 정말 새로운 소식인가?"

그럼에도 불구하고, io_uring은 방대한 공격 표면과 권한 상승 버그의 빈번한 발생으로 여전히 "보안 악몽"이라고 평가됩니다. 고보안 환경에서는 sysctl -w kernel.io_uring_disabled=2io_uring을 완전히 비활성화하는 것이 가장 안전한 방책이라는 의견이 다수입니다.

요구 사항 요약

요구 사항 상세
커널 버전 6.15 – 6.19 (커밋 770594e 미포함)
구성 CONFIG_IO_URING_ZCRX=y
하드웨어 ZCRX 지원 NIC (예: Mellanox ConnectX-6+, Intel E800)
권한 CAP_NET_ADMIN (그리고 modprobe_path 쓰기를 위한 CAP_SYS_ADMIN)

SUMMARY: Linux 커널의 ZCRX 서브시스템에서 발생한 치명적인 범위 초과 쓰기 취약점을 깊이 파헤쳐, 힙 손상을 통해 로컬 권한 상승으로 루트 권한을 얻는 방법을 설명합니다.

TITLE: From u32 to Root: Analyzing the io_uring ZCRX Freelist Vulnerability


(Note: The title and summary above are provided in Korean as requested, while the original English title is retained for reference.)

Sources