FlawOpen

flawopen.com/Teardowns/cve-2024-0582-linux-io-uring-privilege-escalation

● CVE-2024-0582 · CVSS 7.8 · 높음
보안 연구 · FlawOpen

CVE-2024-0582: Linux io_uring 매핑된 버퍼 링의 사용 후 해제

CVE-2024-0582(CVSS 7.8, 높음): IORING_REGISTER_PBUF_RING과 IOU_PBUF_RING_MMAP로 등록한 버퍼 링이, 사용자 공간이 아직 mmap()으로 매핑하고 있는 동안 해제 과정에서 반환되어, 커널이 재사용한 뒤에도 공격자가 읽고 쓸 수 있는 페이지가 남아 로컬 권한 상승으로 이어졌습니다.

💡 알기 쉬운 설명 (ELI5)

도서관은 전용 열람실을 등록하게 해 주고 드나들 수 있는 열쇠를 줍니다. 열람실 등록을 취소하면 사서는 곧바로 그 방을 대여 목록에 돌려놓지만, 당신의 열쇠는 결코 회수하지 않습니다. 그 뒤 도서관이 같은 방을 다른 사람에게 빌려주면, 당신은 옛 열쇠로 여전히 들어가 그 사람이 책상에 둔 것을 고쳐 쓸 수 있습니다. 해결책은 그런 방을 계속 예약된 상태로 두고, 폐관 때 모든 열쇠가 반납된 뒤에 딱 한 번만 반환하는 것입니다.

핵심 개념 및 용어

io_uring
Linux의 비동기 I/O 인터페이스입니다. 애플리케이션과 커널이 제출 링과 완료 링을 공유해, 작업마다 시스템 호출을 하지 않아도 됩니다.
제공 버퍼 링(IORING_REGISTER_PBUF_RING)
커널이 작업마다 하나씩 고르도록 애플리케이션이 등록하는 버퍼 링입니다. IOU_PBUF_RING_MMAP를 쓰면 커널이 링을 할당하고, 애플리케이션은 자기 메모리를 주는 대신 mmap()으로 매핑합니다.
사용 후 해제(UAF)
해제된 뒤의 메모리를 사용하는 것입니다. 커널이 그 메모리를 다른 객체에 재할당하면, 남은 참조가 그것을 읽거나 손상시킬 수 있습니다.
remap_pfn_range / folio_put
링은 remap_pfn_range()로 사용자 공간에 매핑되므로 일반 munmap 회계로는 해제되지 않습니다. 해제 시 folio_put()을 호출하면 매핑이 아직 살아 있는데도 페이지를 반환했습니다.

프레임워크 매핑

  • 이 가이드에서 설명하는 약점의 CWE 참조: CWE-416

근본 원인 분석 (Root Cause)

IOU_PBUF_RING_MMAP로 만든 버퍼 링에서는 io_alloc_pbuf_ring()이 페이지를 할당하고, 사용자 공간이 remap_pfn_range()를 통해 mmap()으로 그것을 매핑합니다. 이 경로는 일반 munmap/release 회계를 건너뛰기 때문에, __io_remove_buffers()가 folio_put()을 호출하면 사용자 매핑이 아직 존재할 수 있는데도 페이지를 해제했습니다. 사용 후 해제(CWE-416)입니다. 사용자 공간은 커널이 할당기에 돌려주고 재할당할 수 있는 페이지에 대한 유효한 매핑을 계속 쥐고 있었습니다. Jann Horn이 보고했습니다. 커밋 c392cbecd8ec는 mmap으로 할당한 각 링을 새로운 io_buf_list에 기록하고, 매핑이 사라진 뒤 ->release()의 io_kbuf_mmap_list_free()에서만 해제합니다.

단계별 공격 실행 흐름

단계 1

mmap 기반 링 등록

권한 없는 프로세스가 IORING_REGISTER_PBUF_RING과 IOU_PBUF_RING_MMAP 플래그로 io_uring_register()를 호출하면 커널이 버퍼 링을 할당합니다.

단계 2

사용자 공간에 매핑

프로세스가 링을 mmap()합니다. 커널은 remap_pfn_range()로 페이지를 매핑하고, 프로세스는 매핑을 유지합니다.

단계 3

매핑 중에 링 해제

버퍼 등록 해제(또는 링 해체)가 __io_remove_buffers()에 도달하고, 그 folio_put()이 사용자 매핑이 아직 살아 있는데도 페이지를 해제합니다.

단계 4

남은 매핑으로 쓰기

커널이 해제된 페이지를 다른 객체에 재할당하고, 매핑을 계속 쥔 프로세스가 그것을 읽고 덮어써 커널 상태를 손상시키며 root로 상승합니다.

소스 코드 비교: 취약한 구현 vs 보안 패치

취약한 구현
/* io_uring/kbuf.c 수정 전 (Linux 6.6.4) */
static int io_alloc_pbuf_ring(struct io_uring_buf_reg *reg,
			      struct io_buffer_list *bl)
{
	gfp_t gfp = GFP_KERNEL_ACCOUNT | __GFP_ZERO | __GFP_NOWARN | __GFP_COMP;
	size_t ring_size;
	void *ptr;

	ring_size = reg->ring_entries * sizeof(struct io_uring_buf_ring);
	ptr = (void *) __get_free_pages(gfp, get_order(ring_size));
	if (!ptr)
		return -ENOMEM;

	bl->buf_ring = ptr;
	bl->is_mapped = 1;
	bl->is_mmap = 1;
	return 0;
}

static int __io_remove_buffers(struct io_ring_ctx *ctx,
			       struct io_buffer_list *bl, unsigned nbufs)
{
	if (bl->is_mapped) {
		i = bl->buf_ring->tail - bl->head;
		if (bl->is_mmap) {
			/* 버그: ring은 mmap()과
			 * remap_pfn_range()를 통해 사용자 공간에 전달되었습니다. 여기서 folio_put()이 페이지를 해제하면
			 * 매핑이 아직 존재할 수 있어 사용자 공간이 해제 후 재할당된
			 * 커널 페이지에 계속 쓸 수 있습니다. */
			folio_put(virt_to_folio(bl->buf_ring));
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		}
	}
	/* ... */
}
보안 강화 패치
/* io_uring/kbuf.c, commit c392cbecd8ec 이후 (Linux 6.6.5 / 6.7) */
struct io_buf_free {
	struct hlist_node		list;
	void				*mem;
};

static int io_alloc_pbuf_ring(struct io_ring_ctx *ctx,
			      struct io_uring_buf_reg *reg,
			      struct io_buffer_list *bl)
{
	struct io_buf_free *ibf;
	size_t ring_size;
	void *ptr;

	ring_size = reg->ring_entries * sizeof(struct io_uring_buf_ring);
	ptr = io_mem_alloc(ring_size);
	if (!ptr)
		return -ENOMEM;

	/* 수정: mmap 가능한 각 ring을 기록해 해당 페이지를
	 * 매핑이 사라진 뒤 ->release() 시점에만 해제하고 teardown 중에는 해제하지 않습니다. */
	ibf = kmalloc(sizeof(*ibf), GFP_KERNEL_ACCOUNT);
	if (!ibf) {
		io_mem_free(ptr);
		return -ENOMEM;
	}
	ibf->mem = ptr;
	hlist_add_head(&ibf->list, &ctx->io_buf_list);

	bl->buf_ring = ptr;
	bl->is_mapped = 1;
	bl->is_mmap = 1;
	return 0;
}

static int __io_remove_buffers(struct io_ring_ctx *ctx,
			       struct io_buffer_list *bl, unsigned nbufs)
{
	if (bl->is_mapped) {
		i = bl->buf_ring->tail - bl->head;
		if (bl->is_mmap) {
			/* io_kbuf_mmap_list_free()는 ->release() 시점에 페이지를 해제하므로
			 * 아직 매핑되었을 수 있는 동안 여기서 페이지를 해제하지 않습니다. */
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		}
	}
	/* ... */
}

/* ->release() 시점 또는 그 이후에 호출: 이제 mmap된 ring을 안전하게 해제할 수 있습니다. */
void io_kbuf_mmap_list_free(struct io_ring_ctx *ctx)
{
	struct io_buf_free *ibf;
	struct hlist_node *tmp;

	hlist_for_each_entry_safe(ibf, tmp, &ctx->io_buf_list, list) {
		hlist_del(&ibf->list);
		io_mem_free(ibf->mem);
		kfree(ibf);
	}
}

엔지니어링 및 시스템 보안 강화 체크리스트

출처