Introdução
Esta é a primeira parte de uma série de duas partes sobre rootkits para Linux. Nesta primeira parte, vamos nos concentrar na teoria por trás do funcionamento dos rootkits: sua taxonomia, evolução e as técnicas de gancho que eles usam para subverter o kernel. Na segunda parte, passamos para o lado defensivo e nos aprofundamos na engenharia de detecção, abordando abordagens práticas para identificar e responder a essas ameaças em ambientes de produção.
O que são rootkits?
Rootkits são malwares furtivos projetados para ocultar atividades maliciosas, como arquivos, processos, conexões de rede, módulos do kernel ou contas. Seus principais objetivos são a persistência e a evasão, permitindo que os invasores mantenham acesso a longo prazo a alvos de alto valor, como servidores, infraestrutura e sistemas corporativos. Ao contrário de outras formas de malware, os rootkits têm como foco permanecer indetectáveis em vez de perseguir objetivos imediatos.
Como funcionam os rootkits?
Os rootkits manipulam o sistema operacional para alterar a forma como ele apresenta informações aos usuários e às ferramentas de segurança. Eles operam no espaço do usuário ou dentro do kernel. Os rootkits de espaço do usuário modificam processos de nível de usuário usando técnicas como LD_PRELOAD ou sequestro de biblioteca. Os rootkits de espaço do kernel são executados com os privilégios mais elevados, modificando estruturas do kernel, interceptando chamadas de sistema ou carregando módulos maliciosos. Essa profunda integração lhes confere poderosas capacidades de evasão, mas aumenta o risco operacional.
Por que os rootkits são difíceis de detectar?
Os rootkits de espaço do kernel podem manipular funções essenciais do sistema operacional, subvertendo ferramentas de segurança e ocultando artefatos da visibilidade do espaço do usuário. Frequentemente, deixam vestígios mínimos de sua presença no sistema, evitando indicadores óbvios como novos processos ou arquivos, o que dificulta a detecção por métodos tradicionais. A identificação de rootkits geralmente requer análise forense de memória, verificações de integridade do kernel ou telemetria abaixo do nível do sistema operacional.
Por que os rootkits são uma faca de dois gumes para os atacantes
Embora os rootkits ofereçam discrição e controle, eles acarretam riscos operacionais. Os rootkits de kernel devem ser precisamente adaptados às versões e ambientes do kernel. Erros, como o gerenciamento incorreto de memória ou o interceptamento inadequado de chamadas de sistema, podem causar falhas no sistema (pânicos do kernel), expondo imediatamente o atacante. No mínimo, essas falhas atraem atenção indesejada para o sistema — um cenário que o atacante está ativamente tentando evitar para manter o controle da situação.
As atualizações do kernel também apresentam desafios: alterações nas APIs, estruturas de memória ou chamadas de sistema podem quebrar a funcionalidade do rootkit, tornando a persistência vulnerável. A detecção de módulos ou ganchos suspeitos geralmente desencadeia uma investigação forense profunda, já que os rootkits indicam fortemente ataques direcionados e altamente sofisticados. Para os atacantes, os rootkits são ferramentas de alto risco e alto retorno; para os defensores, essa fragilidade oferece oportunidades de detecção por meio de monitoramento de baixo nível.
Rootkits para Windows vs. Rootkits para Linux
O ecossistema de rootkits do Windows
O Windows é o principal foco para o desenvolvimento de rootkits. Os atacantes exploram vulnerabilidades no kernel, drivers e chamadas de sistema não documentadas para ocultar malware, roubar credenciais e garantir persistência. Uma comunidade de pesquisa consolidada e o uso generalizado em ambientes corporativos impulsionam a inovação contínua, incluindo técnicas como DKOM, bypass do PatchGuard e bootkits.
Ferramentas de segurança robustas e os esforços da Microsoft para reforçar a segurança levam os atacantes a adotar métodos cada vez mais sofisticados. O Windows continua sendo atraente devido ao seu domínio em endpoints corporativos e dispositivos de consumo.
O ecossistema de rootkits do Linux
Historicamente, os rootkits para Linux receberam menos atenção. A fragmentação entre distribuições e versões do kernel complica a detecção e o desenvolvimento. Embora existam pesquisas acadêmicas, muitas ferramentas estão desatualizadas e os ambientes Linux de produção frequentemente carecem de monitoramento especializado.
No entanto, o papel do Linux na nuvem, em contêineres, na IoT e na computação de alto desempenho o tornou um alvo cada vez maior. Rootkits Linux reais foram observados em ataques contra provedores de nuvem, empresas de telecomunicações e governos. Os principais desafios para os atacantes incluem:
- A diversidade de kernels dificulta a compatibilidade entre diferentes distribuições.
- Longos períodos de atividade prolongam as incompatibilidades do kernel.
- Recursos de segurança como SELinux, AppArmor e assinatura de módulos aumentam a dificuldade.
As ameaças exclusivas do Linux incluem:
- Contêineres e Kubernetes: novos vetores de persistência por meio da fuga de contêineres.
- Dispositivos IoT: kernels desatualizados com monitoramento mínimo.
- Servidores de produção: sistemas sem interface gráfica, que não requerem interação do usuário, reduzindo a visibilidade.
Com o Linux dominando a infraestrutura moderna, os rootkits representam uma ameaça crescente, porém pouco monitorada. A melhoria da detecção, das ferramentas e da pesquisa em técnicas específicas para Linux é cada vez mais urgente.
Evolução dos Modelos de Implementação de Rootkits no Linux
Nas últimas duas décadas, os rootkits do Linux evoluíram de técnicas básicas de espaço do usuário para implantes avançados residentes no kernel, aproveitando interfaces modernas do kernel como eBPF e io_uring. Cada etapa dessa evolução reflete tanto a inovação do atacante quanto a resposta do defensor, impulsionando os projetos de rootkits em direção a maior furtividade, flexibilidade e resiliência.
Esta seção descreve essa progressão, incluindo características principais, contexto histórico e exemplos do mundo real.
Início dos anos 2000: Rootkits de espaço do usuário de objetos compartilhados (SO)
Os primeiros rootkits para Linux operavam inteiramente no espaço do usuário sem exigir modificação do kernel, dependendo de técnicas como LD_PRELOAD ou da manipulação de perfis de shell para injetar objetos compartilhados maliciosos em binários legítimos. Ao interceptar funções padrão da libc, como opendir, readdir e fopen, esses rootkits poderiam manipular a saída de ferramentas de diagnóstico como ps, ls e netstat. Embora essa abordagem tenha facilitado a implantação, a dependência de mecanismos de acesso no espaço do usuário significava que eles eram limitados em termos de discrição e alcance em comparação com implantes em nível de kernel; eles eram facilmente interrompidos por simples reinicializações ou redefinições de configuração. Exemplos notáveis incluem o rootkit Jynx (2009), que interceptou funções libc para ocultar arquivos e conexões, e Azazel (2013), que combinou injeção de objeto compartilhado com recursos opcionais de modo kernel. As técnicas fundamentais para esse abuso de links dinâmicos foram detalhadas na famosa edição nº 61 da revista Phrack, em 2003.
Meados dos anos 2000 a 2010: Rootkits de Módulo Kernel Carregável (LKM)
À medida que os defensores se tornaram hábeis em detectar manipulações no espaço do usuário, os atacantes migraram para o espaço do kernel por meio de Módulos de Kernel Carregáveis (LKMs). Embora os LKMs sejam extensões legítimas, agentes maliciosos os utilizam para operar com privilégios totais, interceptando o sys_call_table, manipulando ftrace ou alterando listas encadeadas internas para ocultar processos, arquivos, sockets e até mesmo o próprio rootkit. Embora os LKMs ofereçam controle preciso e excelentes capacidades de ocultação, eles enfrentam um escrutínio significativo em ambientes hostis. Eles são detectáveis por meio de estados de kernel contaminados, listagens em /proc/modules ou scanners LKM especializados e são cada vez mais dificultados por defesas modernas como Secure Boot, assinatura de módulo e Linux Security Modules (LSMs). Exemplos clássicos dessa era incluem o Adore-ng (2004+), um LKM que intercepta chamadas de sistema e é capaz de se ocultar; o Diamorphine (2016), um interceptador popular que permanece funcional em muitas distribuições; e o Reptile (2020), uma variante moderna com recursos de backdoor.
Final da década de 2010: Rootkits baseados em eBPF
Para evitar a crescente detecção de ameaças baseadas em LKM, os atacantes começaram a abusar do eBPF, um subsistema originalmente criado para filtragem segura de pacotes e rastreamento do kernel. Desde o Linux 4.8+, o eBPF evoluiu para uma máquina virtual programável no kernel, capaz de anexar código a ganchos de chamadas de sistema, kprobes, pontos de rastreamento ou eventos do Módulo de Segurança do Linux. Esses implantes são executados no espaço do kernel, mas evitam o carregamento de módulos tradicional, permitindo que eles ignorem os scanners LKM padrão, como rkhunter e chkrootkit, bem como as restrições do Secure Boot. Como não aparecem em /proc/modules e são essencialmente invisíveis para os mecanismos típicos de auditoria de módulos, requerem CAP_BPF ou CAP_SYS_ADMIN (ou acesso BPF não privilegiado raro) para serem implantados. Esta era é definida por ferramentas como Triple Cross (2022), uma prova de conceito que injeta programas eBPF para interceptar chamadas de sistema como execve, e Boopkit (2022), que implementa um canal C2 secreto inteiramente via eBPF, juntamente com inúmeras apresentações da Defcon explorando o tópico.
Anos 2025 e além: Rootkits baseados em io_uring (emergentes)
A evolução mais recente aproveita o io_uring, uma interface de E/S assíncrona de alto desempenho introduzida no Linux 5.1 (2019) que permite que os processos agrupem operações do sistema por meio de anéis de memória compartilhada. Embora projetado para reduzir a sobrecarga de chamadas de sistema para melhorar o desempenho, os especialistas em segurança vermelha demonstraram que io_uring pode ser usado indevidamente para criar agentes furtivos de espaço do usuário ou rootkits de contexto do kernel que evitam EDRs baseados em chamadas de sistema. Ao usar io_uring_enter para operações em lote de arquivos, redes e processos, esses rootkits produzem muito menos eventos de chamada de sistema observáveis, frustrando os mecanismos de detecção tradicionais e evitando as restrições impostas aos LKMs e eBPF. Embora ainda experimental, exemplos como RingReaper (2025), que usa io_uring para substituir furtivamente chamadas de sistema comuns como read, write, connect e unlink, e pesquisas da ARMO destacam isso como um vetor altamente promissor para o futuro desenvolvimento de rootkits que é difícil de rastrear sem instrumentação personalizada.
O design do rootkit para Linux tem se adaptado constantemente em resposta a melhores defesas. À medida que o carregamento do LKM se torna mais difícil e a auditoria de chamadas de sistema se torna mais avançada, os atacantes passaram a recorrer a interfaces alternativas, como eBPF e io_uring. Com essa evolução, a batalha não se resume mais à detecção, mas sim à compreensão dos mecanismos que os rootkits utilizam para se infiltrar no núcleo do sistema, começando por suas estratégias de interceptação e arquitetura interna.
##Componentes internos do Rootkit e técnicas de gancho
Compreender a arquitetura dos rootkits do Linux é essencial para a detecção e defesa. A maioria dos rootkits segue um design modular com dois componentes principais:
- Carregador: Instala ou injeta o rootkit e pode estabelecer persistência. Embora não seja estritamente necessário, um componente de carregador separado é frequentemente encontrado em cadeias de infecção por malware que implantam rootkits.
- Carga útil: Executa ações maliciosas, como ocultar arquivos, interceptar chamadas de sistema ou realizar comunicações secretas.
As cargas úteis dependem muito de técnicas de interceptação para alterar o fluxo de execução e alcançar o sigilo.
Componente de carregamento de rootkit
O carregador é o componente responsável por transferir o rootkit para a memória, inicializar sua execução e, em muitos casos, estabelecer persistência ou elevar privilégios. Sua função é preencher a lacuna entre o acesso inicial (por exemplo, por meio de exploração de vulnerabilidades, phishing ou configuração incorreta) e a implantação completa do rootkit.
Dependendo do modelo do rootkit, o carregador pode operar inteiramente no espaço do usuário, interagir com o kernel por meio de interfaces de sistema padrão ou ignorar completamente as proteções do sistema operacional. De forma geral, os carregadores podem ser categorizados em três classes: droppers baseados em malware, inicializadores de rootkits em espaço de usuário e carregadores personalizados em espaço de kernel. Além disso, os rootkits podem ser carregados manualmente por um atacante através de ferramentas de espaço do usuário, como insmod.
Droppers baseados em malware
Os droppers de malware são programas leves, geralmente implantados após o acesso inicial, cujo único propósito é baixar ou descompactar um payload de rootkit e executá-lo. Esses programas de desbloqueio geralmente operam no espaço do usuário, mas elevam privilégios e interagem com recursos do kernel.
As técnicas comuns incluem:
- Injeção de módulo: Escrever um arquivo
.komalicioso no disco e invocarinsmodoumodprobepara carregá-lo como um módulo do kernel. - Wrapper de chamada de sistema: Usando um wrapper em torno de
init_module()oufinit_module()para carregar um LKM diretamente através de chamadas de sistema. - Injeção em memória: Aproveitando interfaces como
ptraceoumemfd_create, muitas vezes evitando artefatos de disco. - Carregamento baseado em BPF: Usando utilitários como
bpftool,tcou chamadas de sistemabpf()diretas para carregar e anexar programas eBPF a pontos de rastreamento do kernel ou ganchos LSM.
Carregadores de espaço do usuário
No caso de rootkits de objetos compartilhados, o carregador pode estar limitado a modificar a configuração do usuário ou as configurações do ambiente:
- Abuso do vinculador dinâmico: Definir
LD_PRELOAD=/path/to/rootkit.sopermite que o objeto compartilhado malicioso substitua as funções da libc quando o binário de destino for executado. - Persistência por meio de modificação de perfil: Inserir configurações de pré-carregamento em
.bashrc,.profileou arquivos globais como/etc/profilegarante a execução contínua entre sessões.
Embora esses carregadores sejam triviais em sua implementação, eles continuam eficazes em ambientes com defesas frágeis ou como parte de cadeias de infecção em múltiplos estágios.
Carregadores de kernel personalizados
Rootkits avançados podem incluir carregadores de kernel personalizados, projetados para ignorar completamente os caminhos de carregamento de módulos padrão. Esses carregadores interagem diretamente com interfaces de baixo nível do kernel ou dispositivos de memória para gravar o rootkit na memória, muitas vezes burlando os registros de auditoria do kernel ou a verificação de assinatura do módulo.
Por exemplo, o Reptile inclui um binário de espaço de usuário como carregador, permitindo que ele carregue o rootkit sem invocar insmod ou modprobe; no entanto, ele ainda depende da chamada de sistema init_mod para carregar o módulo na memória.
Capacidades adicionais da carregadeira
O carregador de malware frequentemente assume um papel expandido além da simples inicialização, tornando-se um componente multifuncional da cadeia de ataque. Uma etapa fundamental para esses carregadores avançados é a Elevação de Privilégios, na qual eles buscam acesso root antes de carregar a carga principal, frequentemente explorando vulnerabilidades locais do kernel, uma tática comum exemplificada pela vulnerabilidade "Dirty Pipe" (CVE-2022-0847). Uma vez assegurados os privilégios, o carregador é então encarregado de apagar os rastros. Isso envolve um processo de eliminação de evidências de execução, limpando entradas de arquivos críticos como bash_history, logs do kernel, logs de auditoria ou o principal do sistema syslog. Finalmente, para garantir a reexecução após a reinicialização do sistema, o carregador garante a persistência instalando mecanismos como unidades systemd , trabalhos cron , regras udev ou modificações nos scripts de inicialização. Esses comportamentos multifuncionais muitas vezes confundem a distinção entre um mero "carregador" e um malware completo, especialmente em infecções complexas e de múltiplos estágios.
Componente de carga útil
A carga útil oferece funcionalidades essenciais: furtividade, controle e persistência. Existem vários métodos principais que um atacante pode usar. Os payloads de espaço do usuário, frequentemente chamados de rootkits SO, operam sequestrando funções da biblioteca C padrão, como readdir ou fopen por meio do vinculador dinâmico. Isso permite que eles manipulem a saída de ferramentas comuns do sistema, como ls, netstat e ps. Embora sejam geralmente mais fáceis de implantar, seu alcance operacional é limitado.
Em contraste, os payloads do espaço do kernel operam com privilégios totais do sistema. Eles podem ocultar arquivos e processos diretamente de /proc, manipular a pilha de rede e modificar estruturas do kernel. Uma abordagem mais moderna envolve rootkits baseados em eBPF, que exploram bytecode no kernel associado a pontos de rastreamento de chamadas de sistema ou ganchos do Módulo de Segurança do Linux (LSM). Esses kits oferecem discrição sem exigir módulos externos à árvore de código, tornando-os particularmente eficazes em ambientes com Inicialização Segura ou políticas de assinatura de módulos. Ferramentas como bpftool simplificam seu carregamento, complicando assim a detecção. Finalmente, as cargas úteis baseadas em io_uringexploram o agrupamento de E/S assíncrona via io_uring_enter (disponível no Linux 5.1 e posterior) para contornar o monitoramento tradicional de chamadas de sistema. Isso permite operações furtivas com arquivos, redes e processos, minimizando a exposição de dados de telemetria.
Rootkits Linux – Técnicas de Hooking
Partindo dessa base essencial, voltamo-nos agora para o cerne da funcionalidade da maioria dos rootkits: o hooking. Em essência, o hooking envolve interceptar e alterar a execução de funções ou chamadas de sistema para ocultar atividades maliciosas ou injetar novos comportamentos. Ao desviar o fluxo normal do código, os rootkits podem ocultar arquivos e processos, filtrar eventos de segurança ou monitorar secretamente o sistema, muitas vezes sem deixar pistas óbvias. A interceptação de sistemas pode ser implementada tanto no espaço do usuário quanto no espaço do kernel e, ao longo dos anos, os atacantes desenvolveram inúmeras técnicas de interceptação, desde métodos antigos até manobras evasivas modernas. Nesta parte, vamos analisar em detalhes as técnicas comuns de hooking usadas por rootkits do Linux, ilustrando cada método com exemplos e amostras reais de rootkits (como Reptile, Diamorphine, PUMAKIT e, mais recentemente, FlipSwitch) para entender como funcionam e como a evolução do kernel os desafiou.
O conceito de fisgar
Em termos gerais, "hooking" é a prática de interceptar a invocação de uma função ou chamada de sistema e redirecioná-la para um código malicioso. Ao fazer isso, um rootkit pode modificar os dados ou o comportamento retornados para ocultar sua presença ou adulterar as operações do sistema. Por exemplo, um rootkit pode interceptar a chamada de sistema que lista os arquivos em um diretório (getdents), fazendo com que ela ignore quaisquer nomes de arquivos que correspondam aos próprios arquivos do rootkit, tornando assim esses arquivos “invisíveis” para comandos do usuário como ls.
A prática de hooking não se limita aos mecanismos internos do kernel; ela também pode ocorrer no espaço do usuário. Os primeiros rootkits do Linux operavam inteiramente no espaço do usuário, injetando objetos compartilhados maliciosos nos processos. Técnicas como o uso da variável de ambiente LD_PRELOAD do vinculador dinâmico permitem que um rootkit substitua funções da biblioteca C padrão (por exemplo, getdents, readdir e fopen) em programas de usuário. Isso significa que quando um usuário executa uma ferramenta como ps ou netstat, o código injetado do rootkit intercepta chamadas para listar processos ou conexões de rede e filtra os maliciosos. Esses ganchos de espaço do usuário não exigem privilégios do kernel e são relativamente simples de implementar.
Exemplos notáveis incluem JynxKit (2012) e Azazel (2014), rootkits de modo de usuário que interceptam dezenas de funções libc para ocultar processos, arquivos, portas de rede e até mesmo habilitar backdoors. No entanto, a interceptação de código no espaço do usuário apresenta limitações significativas: é mais fácil de detectar e remover, e carece do controle profundo que as interceptações em nível de kernel possuem. Como resultado, a maioria dos rootkits Linux modernos e "em uso" migraram para o uso de hooks no espaço do kernel, apesar da maior complexidade e risco, porque os hooks do kernel podem enganar completamente o sistema operacional e as ferramentas de segurança em um nível baixo.
No kernel, "hooking" geralmente significa alterar as estruturas de dados ou o código do kernel de forma que, quando o kernel tenta executar uma operação específica (por exemplo, abrir um arquivo ou fazer uma chamada de sistema), ele invoque o código do rootkit em vez de (ou além de) o código legítimo. Ao longo dos anos, os desenvolvedores do kernel Linux introduziram proteções mais robustas para evitar modificações não autorizadas, mas os atacantes responderam com métodos de interceptação cada vez mais sofisticados. A seguir, examinaremos as principais técnicas de interceptação no espaço do kernel, começando pelos métodos mais antigos (agora em grande parte obsoletos) e progredindo para as técnicas modernas que tentam contornar as defesas contemporâneas do kernel. Cada subseção explicará a técnica, mostrará um exemplo de código simplificado e discutirá seu uso em rootkits conhecidos e suas limitações, considerando as medidas de segurança atuais do Linux.
Técnicas de gancho no kernel
Interrupção da Tabela Descritora de Interrupções (IDT) - Interceptação
Uma das primeiras técnicas de interceptação de código no kernel do Linux consistia em atacar a Tabela de Descritores de Interrupção (IDT). No Linux x86 de 32 bits, as chamadas de sistema costumavam ser invocadas por meio de uma interrupção de software (int 0x80). A IDT é uma tabela que mapeia números de interrupção para endereços de manipuladores. Ao modificar a entrada IDT para 0x80, um rootkit poderia sequestrar o ponto de entrada da chamada do sistema antes que o próprio despachante de chamadas do sistema do kernel assuma o controle. Em outras palavras, quando qualquer programa acionava uma chamada de sistema via int 0x80, a CPU saltaria primeiro para o manipulador personalizado do rootkit, permitindo que o rootkit filtrasse ou redirecionasse as chamadas no nível mais baixo. Abaixo segue um exemplo de código simplificado de interceptação de IDT (para fins ilustrativos):
// Install the IDT hook
static int install_idt_hook(void) {
// Get pointer to IDT table
idt_table = get_idt_table();
// Save original syscall handler (int 0x80 = entry 128)
original_syscall_entry = idt_table[0x80];
// Calculate original handler address
original_syscall_handler = (void*)(
(original_syscall_entry.offset_high << 16) |
original_syscall_entry.offset_low
);
// Install our hook
idt_table[0x80].offset_low = (unsigned long)custom_int80_handler & 0xFFFF;
idt_table[0x80].offset_high =
((unsigned long)custom_int80_handler >> 16)
& 0xFFFF;
// Keep same selector and attributes as original
// idt_table[0x80].selector and type_attr remain unchanged
printk(KERN_INFO "IDT hook installed at 0x80\n");
return 0;
}
O código acima define um novo manipulador para a interrupção 0x80, redirecionando o fluxo de execução para o manipulador do rootkit antes que qualquer tratamento de chamada de sistema ocorra. Isso permite que o rootkit intercepte ou modifique o comportamento das chamadas de sistema completamente abaixo do nível da tabela de chamadas de sistema. O recurso de hooking IDT é utilizado por rootkits educacionais e mais antigos, como o SuckIT.
A técnica de gancho IDT é hoje em grande parte obsoleta. Só funcionava em sistemas Linux mais antigos que usam o mecanismo int 0x80 (kernels x86 de 32 bits anteriores ao Linux 2.6). O Linux moderno de 64 bits usa as instruções sysenter/syscall em vez da interrupção de software, portanto a entrada IDT para 0x80 não é mais usada para chamadas de sistema. Além disso, o recurso de interceptação de IDT é altamente específico para determinada arquitetura (apenas x86) e não é eficaz em kernels modernos com x86_64 ou outras arquiteturas.
Engate de tabela de chamadas de sistema
O gancho da tabela de chamadas de sistema é uma técnica clássica de rootkit que envolve a modificação da tabela de despacho de chamadas de sistema do kernel, conhecida como sys_call_table. Esta tabela é uma matriz de ponteiros de função onde cada entrada corresponde a um número de chamada de sistema específico. Ao sobrescrever um ponteiro nesta tabela, um atacante pode redirecionar uma chamada de sistema legítima, como getdents64, kill ou read, para um manipulador malicioso. Segue abaixo um exemplo.
asmlinkage int (*original_getdents64)(
unsigned int,
struct linux_dirent64 __user *,
unsigned int);
asmlinkage int hacked_getdents64(
unsigned int fd,
struct linux_dirent64 __user *dirp,
unsigned int count)
{
int ret = original_getdents64(fd, dirp, count);
// Filter hidden entries from dirp
return ret;
}
write_cr0(read_cr0() & ~0x10000); // Disable write protection
sys_call_table[__NR_getdents64] = hacked_getdents64;
write_cr0(read_cr0() | 0x10000); // Re-enable write protection
No exemplo, para modificar a tabela, um módulo do kernel precisaria primeiro desativar a proteção contra gravação na página de memória onde a tabela reside. O seguinte código assembly (como visto em Diamorphine) demonstra como o 20º bit (Proteção contra gravação) do registrador de controle CR0 pode ser limpo, mesmo que a função write_cr0 não seja mais exportada para módulos:
static inline void
write_cr0_forced(unsigned long val)
{
unsigned long __force_order;
asm volatile(
"mov %0, %%cr0"
: "+r"(val), "+m"(__force_order));
}
Uma vez que a proteção contra gravação é desativada, o endereço de uma chamada de sistema na tabela pode ser substituído pelo endereço de uma função maliciosa. Após a modificação, a proteção contra gravação é reativada. Exemplos notáveis de rootkits que usaram essa técnica incluem Diamorphine, Knark e Reveng_rtkit. O uso de hooks em tabelas de chamadas de sistema (syscall) apresenta diversas limitações:
- Endurecimento do kernel (desde 2.6.25) oculta
sys_call_table. - As páginas de memória do kernel foram tornadas somente leitura (
CONFIG_STRICT_KERNEL_RWX). - Recursos de segurança como o Secure Boot e o mecanismo de bloqueio do kernel podem dificultar modificações no CR0.
A solução mais definitiva surgiu com o kernel Linux 6.9, que mudou fundamentalmente a forma como as chamadas de sistema são despachadas na arquitetura x86-64. Antes da versão 6.9, o kernel executava chamadas de sistema procurando diretamente o manipulador na matriz sys_call_table :
// Pre-v6.9 Syscall Dispatch
asmlinkage const sys_call_ptr_t sys_call_table[] = {
#include <asm/syscalls_64.h>
};
A partir do kernel 6.9, o número da chamada de sistema é usado em uma instrução switch para encontrar e executar o manipulador apropriado. O sys_call_table ainda existe, mas só é preenchido para compatibilidade com ferramentas de rastreamento e não é mais usado no caminho de execução da chamada de sistema.
// Kernel v6.9+ Syscall Dispatch
long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
{
switch (nr) {
#include <asm/syscalls_64.h>
default: return __x64_sys_ni_syscall(regs);
}
};
Como resultado dessa mudança arquitetônica, sobrescrever ponteiros de função em sys_call_table em kernels 6.9 e mais recentes não afeta a execução de chamadas de sistema, tornando a técnica completamente ineficaz. Embora isso nos tenha levado a supor que a alteração da tabela de chamadas de sistema não fosse mais viável, publicamos recentemente a técnica FlipSwitch , que demonstra que esse método está longe de estar morto. Este método utiliza recursos específicos de manipulação de registradores para desativar momentaneamente os mecanismos de proteção contra gravação do kernel, permitindo que um invasor contorne a "imutabilidade" do caminho de chamadas de sistema moderno e reintroduza ganchos mesmo nesses ambientes reforçados.
Em vez de visar os dados baseados em sys_call_table, o FlipSwitch concentra-se no código de máquina compilado da nova função de despacho de chamadas de sistema do kernel, x64_sys_call. Como o kernel agora usa uma instrução switch-case enorme para executar chamadas de sistema, cada chamada de sistema tem uma instrução call codificada dentro do binário do despachante. O FlipSwitch examina a memória da função x64_sys_call para localizar a "assinatura" específica de uma chamada de sistema alvo, normalmente um opcode 0xe8 (a instrução CALL ) seguido por um deslocamento relativo de 4 bytes que aponta para o manipulador original e legítimo.
Assim que esse local de chamada é identificado no despachante, o rootkit usa dispositivos para limpar o bit de proteção contra gravação (WP) no registrador de controle CR0, concedendo acesso temporário de gravação aos segmentos de código executável do kernel. O deslocamento relativo original é então sobrescrito com um novo deslocamento que aponta para uma função maliciosa controlada pelo adversário. Isso efetivamente "vira a chave" no ponto de despacho, garantindo que, sempre que o kernel tentar executar a chamada de sistema de destino por meio de seu caminho moderno de instrução switch, ele seja redirecionado para o rootkit. Isso possibilita a interceptação confiável e precisa de chamadas de sistema, que persiste apesar do reforço arquitetônico do kernel 6.9.
Interligação em linha / Correção de prólogo de função
O hooking embutido é uma alternativa ao hooking via tabelas de ponteiros. Em vez de modificar um ponteiro em uma tabela, o hooking embutido modifica o código da própria função de destino. O rootkit insere uma instrução de salto no início (prólogo) de uma função do kernel, que desvia a execução para o próprio código do rootkit. Essa técnica é semelhante ao hot-patching de funções ou ao funcionamento dos hooks em modo de usuário no Windows (por exemplo, modificar os primeiros bytes de uma função para saltar para um desvio).
Por exemplo, um rootkit pode ter como alvo uma função do kernel como do_sys_open (que faz parte do tratamento de chamadas de sistema de arquivo aberto). Ao sobrescrever os primeiros bytes de do_sys_open com uma instrução x86 JMP para código malicioso, o rootkit garante que sempre que do_sys_open for chamado, ele pula para a rotina do rootkit. A rotina maliciosa pode então executar o que quiser (por exemplo, verificar se o nome do arquivo a ser aberto está em uma lista oculta e negar o acesso) e, opcionalmente, chamar o do_sys_open original para prosseguir com o comportamento normal para arquivos não ocultos.
unsigned char *target = (unsigned char *)kallsyms_lookup_name("do_sys_open");
unsigned long hook = (unsigned long)&malicious_function;
int offset = (int)(hook - ((unsigned long)target + 5));
unsigned char jmp[5] = {0xE9};
memcpy(&jmp[1], &offset, 4);
// Memory protection omitted for brevity
memcpy(target, jmp, 5);
asmlinkage long malicious_function(
const char __user *filename,
int flags, umode_t mode) {
printk(KERN_INFO "do_sys_open hooked!\n");
return -EPERM;
}
Este código sobrescreve o início de do_sys_open() com uma instrução JMP que redireciona a execução para um código malicioso. O rootkit de código aberto Reptile utiliza amplamente a correção de funções embutidas por meio de uma estrutura personalizada chamada KHOOK (que discutiremos em breve).
Os hooks embutidos do Reptile visam funções como sys_kill e outras, permitindo comandos backdoor (por exemplo, o envio de um sinal específico para um processo faz com que o rootkit eleve seus privilégios ou oculte o processo). Outro exemplo é o Suterusu, que também aplicou patches embutidos em alguns de seus hooks.
O hooking embutido é frágil e de alto risco: sobrescrever o prólogo de uma função é sensível à versão do kernel e às diferenças do compilador (portanto, os hooks geralmente precisam de patches por compilação ou desmontagem em tempo de execução), pode facilmente travar o sistema se as instruções ou a execução concorrente não forem tratadas corretamente e requer o bypass de proteções de memória modernas (W^X, CR0 WP, assinatura/bloqueio de módulo) ou a exploração de vulnerabilidades para tornar o texto do kernel gravável.
Conexão de sistema de arquivos virtual
A camada de Sistema de Arquivos Virtual (VFS) no Linux fornece uma abstração para operações de arquivo. Por exemplo, quando você lê um diretório (como ls /proc), o kernel eventualmente chamará uma função para iterar sobre as entradas do diretório. Os sistemas de arquivos definem suas próprias operações de arquivo com ponteiros de função para ações como iterate_shared (para listar o conteúdo do diretório) ou leitura/gravação para E/S de arquivo. O hooking do VFS envolve a substituição desses ponteiros de função por funções fornecidas pelo rootkit para manipular a forma como o sistema de arquivos apresenta os dados.
Em essência, um rootkit pode se infiltrar no VFS para ocultar arquivos ou diretórios, filtrando-os das listagens de diretórios. Um truque comum: interceptar a função que itera pelas entradas do diretório e fazê-la ignorar quaisquer nomes de arquivo que correspondam a um determinado padrão. A estrutura file_operations para diretórios (particularmente em /proc ou /sys) é um alvo frequente, uma vez que ocultar processos maliciosos muitas vezes envolve ocultar entradas em /proc/<pid>.
Considere este exemplo de gancho para uma função de listagem de diretório:
static iterate_dir_t original_iterate;
static int malicious_filldir(
struct dir_context *ctx,
const char *name, int namelen,
loff_t offset, u64 ino,
unsigned int d_type)
{
if (!strcmp(name, "hidden_file"))
return 0; // Skip hidden_file
return ctx->actor(ctx, name, namelen, offset, ino, d_type);
}
static int malicious_iterate(struct file *file, struct dir_context *ctx)
{
struct dir_context new_ctx = *ctx;
new_ctx.actor = malicious_filldir;
return original_iterate(file, &new_ctx);
}
// Hook installation
file->f_op->iterate = malicious_iterate;
Essa função de substituição filtra arquivos ocultos durante operações de listagem de diretórios. Ao interceptar a comunicação no nível do VFS, o rootkit não precisa adulterar as tabelas de chamadas do sistema ou o código assembly de baixo nível; ele simplesmente se aproveita da interface do sistema de arquivos. O Adore-NG, um rootkit para Linux que já foi popular, utilizava o recurso de hooking do VFS para ocultar arquivos e processos. A alteração corrigiu os ponteiros de função para iteração de diretórios, ocultando entradas para PIDs e nomes de arquivos específicos. Muitos outros rootkits de kernel possuem código semelhante para se ocultarem ou ocultarem seus artefatos por meio de hooks do VFS.
O uso de hooks VFS ainda é bastante difundido, mas apresenta limitações devido a mudanças nos deslocamentos da estrutura do kernel entre versões, o que pode levar à quebra dos hooks.
Hooking baseado em Ftrace
Os kernels Linux modernos incluem uma poderosa estrutura de rastreamento chamada ftrace (rastreador de funções). O Ftrace destina-se à depuração e análise de desempenho, permitindo anexar ganchos (callbacks) a quase qualquer entrada ou saída de função do kernel sem modificar diretamente o código do kernel. Ele funciona modificando dinamicamente o código do kernel em tempo de execução de maneira controlada (geralmente inserindo um "trampolim" leve que chama o manipulador de rastreamento). É importante destacar que o ftrace fornece uma API para que os módulos do kernel registrem manipuladores de rastreamento, desde que certas condições sejam atendidas (como ter o kernel compilado com suporte a ftrace e a interface debugfs disponível).
Os rootkits começaram a abusar do ftrace para implementar hooks de uma forma menos óbvia. Em vez de escrever manualmente um JMP em uma função, um rootkit pode pedir ao mecanismo ftrace do kernel para fazer isso em seu nome; essencialmente "legitimando" o gancho. Isso significa que o rootkit não precisa encontrar o endereço da função nem modificar as proteções da página; ele simplesmente registra um retorno de chamada para o nome da função que deseja interceptar, e o kernel instala o gancho.
Aqui está um exemplo simplificado de como usar o ftrace para interceptar o manipulador de chamadas de sistema mkdir :
static int __init hook_init(void) {
target_addr = kallsyms_lookup_name(SYSCALL_NAME("sys_mkdir"));
if (!target_addr) return -ENOENT;
real_mkdir = (void *)target_addr;
ops.func = ftrace_thunk;
ops.flags = FTRACE_OPS_FL_SAVE_REGS
| FTRACE_OPS_FL_RECURSION_SAFE
| FTRACE_OPS_FL_IPMODIFY;
if (ftrace_set_filter_ip(&ops, target_addr, 0, 0)) return -EINVAL;
return register_ftrace_function(&ops);
}
Este gancho intercepta a função sys_mkdir e a redireciona através de um manipulador malicioso. Rootkits recentes como KoviD, Singularity e Umbra têm utilizado hooks baseados em ftrace. Esses rootkits registram callbacks ftrace em várias funções do kernel (incluindo chamadas de sistema) para monitorá-las ou manipulá-las.
A principal vantagem do uso do ftrace para interceptar código é que ele não deixa rastros óbvios em tabelas globais ou no código modificado. A interceptação é feita através de interfaces legítimas do kernel. Para um olhar destreinado, tudo parece normal; sys_call_table está intacto, os prólogos das funções não são sobrescritos manualmente pelo rootkit (eles são sobrescritos pelo mecanismo ftrace, mas isso é uma ocorrência comum e permitida em um kernel com rastreamento ativado). Além disso, os hooks do ftrace podem ser ativados/desativados dinamicamente e são inerentemente menos intrusivos do que a aplicação de patches manualmente.
Embora o recurso de interceptação do ftrace seja poderoso, ele é limitado pelo ambiente e pelos limites de privilégio (se usado fora do kernel). Isso requer acesso à interface de rastreamento (debugfs) e privilégios CAP_SYS_ADMIN , que podem não estar disponíveis em sistemas reforçados ou conteinerizados onde até mesmo o UID 0 é restrito por namespaces, LSMs ou políticas de bloqueio de Inicialização Segura. O sistema de arquivos Debugfs também pode ser desmontado ou configurado como somente leitura em produção por motivos de segurança. Assim, embora um usuário root com privilégios totais normalmente possa usar o ftrace, as defesas modernas frequentemente desativam ou limitam essas capacidades, reduzindo a praticidade de soluções baseadas em ftrace em ambientes altamente protegidos.
Engate de sondas K
Kprobes é outro recurso do kernel destinado à depuração e instrumentação, que os atacantes têm reaproveitado para realizar hooks de rootkit. As Kprobes permitem interromper dinamicamente quase qualquer rotina do kernel em tempo de execução, registrando um manipulador de sondagem. Quando a instrução especificada está prestes a ser executada, a infraestrutura kprobe salva o estado e transfere o controle para o manipulador personalizado. Após a execução do manipulador (você pode até alterar os registradores ou o ponteiro de instrução), o kernel retoma a execução normal do código original. Em termos mais simples, os kprobes permitem que você anexe um retorno de chamada personalizado a um ponto arbitrário no código do kernel (entrada de função, instrução específica, etc.), algo semelhante a um ponto de interrupção com um manipulador.
O uso de kprobes para interceptação maliciosa geralmente envolve a interceptação de uma função para impedi-la de realizar alguma ação ou para obter informações. Um uso comum em rootkits modernos: como muitos símbolos importantes (como sys_call_table ou kallsyms_lookup_name) não são mais exportados, um rootkit pode implantar um kprobe em uma função que tenha acesso a esse símbolo e roubá-lo. A estrutura e o registro do kprobe são mostrados abaixo.
// Declare a kprobe targeting the symbol "kallsyms_lookup_name"
static struct kprobe kp = {
.symbol_name = "kallsyms_lookup_name"
};
// Function pointer type matching kallsyms_lookup_name
typedef unsigned long
(*kallsyms_lookup_name_t)(const char *name);
// Global pointer to the resolved kallsyms_lookup_name
kallsyms_lookup_name_t kallsyms_lookup_name;
// Register the kprobe; kernel resolves kp.addr
// to the address of the symbol
register_kprobe(&kp);
// Assign resolved address to our function pointer
kallsyms_lookup_name =
(kallsyms_lookup_name_t) kp.addr;
// Unregister the kprobe (only needed it once)
unregister_kprobe(&kp);
Esta sonda é usada para recuperar o nome do símbolo para kallsyms_lookup_name, normalmente um precursor do gancho da tabela de chamadas de sistema. Embora não estivesse presente nos commits iniciais, uma atualização recente do Diamorphine utilizou essa técnica. Ele coloca um kprobe para obter o ponteiro de kallsyms_lookup_name (ou usa um kprobe em uma função conhecida para obter indiretamente o que precisa). Da mesma forma, outros rootkits usam um kprobe temporário para localizar símbolos e, em seguida, cancelam o registro assim que terminam, passando a executar hooks por outros meios. As sondas K também podem ser usadas para interceptar diretamente o comportamento (e não apenas encontrar endereços). Ou um jprobe (um kprobe especializado) pode redirecionar uma função completamente. No entanto, usar kprobes para substituir completamente uma funcionalidade é complicado e não é uma prática comum, pois é mais simples aplicar um patch ou usar ftrace se você quiser sequestrar uma função de forma consistente. As sondas tipo K são frequentemente usadas para engates intermitentes ou auxiliares.
As sondas K são úteis, mas limitadas: adicionam sobrecarga de tempo de execução e podem desestabilizar sistemas se colocadas em funções de baixo nível muito críticas ou restritas (sondas recursivas são suprimidas), portanto, os atacantes devem escolher os pontos de sondagem com cuidado; elas também são auditáveis e podem acionar avisos do kernel ou serem registradas pela auditoria do sistema, e as sondas ativas são visualizáveis em /sys/kernel/debug/kprobes/list (portanto, entradas inesperadas são suspeitas); alguns kernels podem ser construídos sem suporte a kprobe/debug.
Estrutura de gancho do kernel
Como mencionado anteriormente, com o rootkit Reptile, os atacantes às vezes criam estruturas de nível superior para gerenciar seus ganchos. O Kernel Hook (KHOOK) é um desses frameworks (desenvolvido pelo autor do Reptile) que abstrai o trabalho complexo de aplicação de patches em linha e fornece uma interface mais limpa para desenvolvedores de rootkits. Essencialmente, o KHOOK é uma biblioteca que permite especificar uma função para interceptar e sua respectiva substituição, e lida com a modificação do código do kernel, fornecendo um mecanismo intermediário para chamar a função original com segurança. Para ilustrar, aqui está um exemplo de como alguém poderia usar uma macro semelhante a KHOOK (baseada no uso do Reptile) para interceptar a chamada de sistema kill:
// Creates a replacement for sys_kill:
// long sys_kill(long pid, long sig)
KHOOK_EXT(long, sys_kill, long, long);
static long khook_sys_kill(long pid, long sig) {
// Signal 0 is used to check if a process
// exists (without sending a signal)
if (sig == 0) {
// If the target is invisible (hidden by
// a rootkit), pretend it doesn't exist
if (is_proc_invisible(pid)) {
return -ESRCH; // No such process
}
}
// Otherwise, forward the call to the original sys_kill syscall
return KHOOK_ORIGIN(sys_kill, pid, sig);
}
O KHOOK opera através da manipulação de funções embutidas, sobrescrevendo os prólogos das funções com um salto para manipuladores controlados pelo atacante. O exemplo acima ilustra como sys_kill() é redirecionado para um manipulador malicioso se o sinal de eliminação for 0.
Embora o KHOOK simplifique a aplicação de patches em linha, ele ainda herda todas as suas desvantagens: ele modifica o texto do kernel para inserir jump stubs, então proteções como kernel locked, Secure Boot ou W^X podem bloqueá-lo. Eles também dependem da arquitetura e da versão (geralmente limitados a x86 e falham no kernel 5.x+), o que os torna frágeis em diferentes compilações.
Técnicas de Hooking no Espaço do Usuário
O "userspace hooking" é uma técnica que visa a camada libc, ou outras bibliotecas compartilhadas acessadas por meio do vinculador dinâmico, para interceptar chamadas de API comuns usadas por ferramentas do usuário. Exemplos dessas chamadas incluem readdir, getdents, open, fopen, fgets e connect. Ao interpor funções de substituição, um atacante pode manipular ferramentas comuns do espaço do usuário, como ps, ls, lsof e netstat para retornar visualizações alteradas ou "sanitizadas". Isso é usado para ocultar processos, arquivos, sockets ou esconder evidências de código malicioso.
Os métodos comuns para implementar isso refletem a forma como o vinculador dinâmico resolve os símbolos ou envolvem a modificação da memória do processo. Esses métodos incluem o uso da variável de ambiente LD_PRELOAD ou LD_AUDIT para forçar o carregamento antecipado de um arquivo de objeto compartilhado malicioso (.so), modificar entradas ELF DT_* ou caminhos de pesquisa de biblioteca para priorizar uma biblioteca hostil ou realizar sobrescritas de GOT/PLT em tempo de execução dentro de um processo. Sobrescrever o GOT/PLT normalmente envolve alterar as configurações de proteção de memória (mprotect), escrever o novo código (write) e, em seguida, restaurar as configurações originais (restore) após a injeção.
Uma função interceptada geralmente chama o símbolo libc real usando dlsym(RTLD_NEXT, ...) para sua operação normal. Em seguida, filtra ou altera os resultados apenas para os alvos que pretende ocultar. Um exemplo básico de um filtro LD_PRELOAD para a função readdir() é mostrado abaixo.
#define _GNU_SOURCE // GNU extensions (RTLD_NEXT)
#include <dlfcn.h> // dlsym(), RTLD_NEXT
#include <dirent.h> // DIR, struct dirent, readdir()
#include <string.h> // strstr()
// Pointer to the original readdir()
static struct dirent *(*real_readdir)(DIR *d);
struct dirent *readdir(DIR *d) {
if (!real_readdir) // resolve original once
real_readdir =
dlsym(RTLD_NEXT, "readdir");
struct dirent *ent;
// Fetch next dir entry from real readdir
while ((ent = real_readdir(d)) != NULL) {
// If name contains the secret marker,
// skip this entry (hide it)
if (strstr(ent->d_name, ".secret"))
continue;
return ent; // return visible entry
}
return NULL; // no more entries
}
Este exemplo substitui readdir() em processo, fornecendo uma biblioteca resolvida antes do libc real, ocultando efetivamente os nomes de arquivos que correspondem a um filtro. Ferramentas históricas de ocultação em modo de usuário e “rootkits” leves usaram LD_PRELOAD ou patches GOT/PLT para ocultar processos, arquivos e sockets. Os atacantes também injetam objetos compartilhados em serviços específicos para obter furtividade direcionada sem a necessidade de módulos do kernel.
A interposição no espaço do usuário afeta apenas os processos que carregam a biblioteca maliciosa (ou nos quais ela é injetada). É frágil para persistência em todo o sistema (arquivos de serviço/unidade, ambientes sanitizados, binários setuid/estáticos complicam isso). A detecção é simples em comparação com os hooks do kernel: verifique entradas suspeitas LD_PRELOAD/LD_AUDIT , objetos compartilhados mapeados inesperadamente em /proc/<pid>/maps, incompatibilidades entre bibliotecas em disco e importações em memória ou entradas GOT alteradas. Ferramentas de integridade, supervisores de serviço (systemd) e inspeção simples da memória de processos geralmente expõem essa técnica.
Técnicas de gancho usando eBPF
Um modelo de implementação de rootkit mais recente envolve o abuso do eBPF (extended Berkeley Packet Filter). O eBPF é um subsistema do Linux que permite que usuários com privilégios elevados carreguem programas em bytecode no kernel. Embora seja frequentemente descrita como uma "VM em sandbox", sua segurança na verdade depende de um verificador estático que garante que o bytecode seja seguro (sem loops infinitos, sem acesso ilegal à memória) antes de ser compilado JIT em código de máquina nativo para execução com latência próxima de zero.
Em vez de inserir um LKM para modificar o comportamento do kernel, um atacante pode carregar um ou mais programas eBPF que se conectam a eventos sensíveis do kernel. Por exemplo, pode-se escrever um programa eBPF que se conecta à entrada da chamada de sistema para execve (via kprobe ou tracepoint), permitindo monitorar ou manipular a execução do processo. Da mesma forma, o eBPF pode se conectar à camada LSM (como notificações de execução de programas) para impedir certas ações ou ocultá-las. Segue abaixo um exemplo.
// Attach this eBPF program to the tracepoint for sys_enter_execve
SEC("tp/syscalls/sys_enter_execve")
int tp_sys_enter_execve(struct sys_execve_enter_ctx *ctx) {
// Get the current process's PID and TID as a 64-bit value
// Upper 32 bits = PID, Lower 32 bits = TID
__u64 pid_tgid = bpf_get_current_pid_tgid();
// Delegate handling logic to a helper function
return handle_tp_sys_enter_execve(ctx, pid_tgid);
}
Dois exemplos públicos de destaque são TripleCross e Boopkit. A TripleCross demonstrou um rootkit que usava eBPF para interceptar chamadas de sistema como execve para persistência e ocultação. O Boopkit utilizava o eBPF como um canal de comunicação secreto e backdoor, anexando programas eBPF que podiam manipular buffers de socket (permitindo que uma parte remota se comunicasse com o rootkit através de pacotes manipulados). Esses são projetos de prova de conceito, mas comprovaram a viabilidade do eBPF no desenvolvimento de rootkits.
As principais vantagens são que o hooking do eBPF não requer o carregamento de um LKM e é compatível com as proteções modernas do kernel. Para kernels com suporte a eBPF, esta é uma técnica poderosa. Mas, embora sejam poderosos, também são limitados. Eles precisam de privilégios elevados para serem carregados, são limitados pelas verificações de segurança do verificador, são efêmeros entre reinicializações (exigindo persistência separada) e são cada vez mais detectáveis por ferramentas de auditoria/forense. A utilização do eBPF será especialmente visível em sistemas que normalmente não utilizam ferramentas eBPF.
Técnicas de evasão usando io_uring
Embora io_uring não seja usado para hooking, merece uma menção honrosa como uma adição recente às técnicas de evasão de EDR usadas por rootkits. io_uring é uma API de E/S assíncrona baseada em buffer circular que permite que os processos enviem lotes de solicitações de E/S (SQEs) e coletem conclusões (CQEs) com sobrecarga mínima de chamadas de sistema. Não se trata de um framework de hooking, mas seu design altera a superfície de visibilidade/chamadas de sistema e expõe primitivas poderosas voltadas para o kernel (buffers registrados, arquivos fixos, anéis mapeados) que os atacantes podem explorar para E/S furtiva, fluxos de trabalho que evitam chamadas de sistema ou, quando combinado com uma vulnerabilidade, como uma primitiva de exploração que leva à instalação de hooks em uma camada inferior.
Os padrões de ataque se dividem em duas classes: (1) evasão/abuso de desempenho: um processo malicioso usa io_uring para realizar muitas operações de leitura/gravação/metadados em grandes lotes, de modo que os detectores tradicionais por chamada de sistema veem menos eventos ou padrões atípicos; e (2) habilitação de exploração: bugs nas superfícies io_uring (mapeamentos de anel, recursos registrados) têm sido historicamente o vetor para escalonamento de privilégios, após o qual um invasor pode instalar ganchos de kernel por meios mais tradicionais. io_uring também ignora alguns wrappers libc se o código enviar operações diretamente, portanto, o hooking do espaço do usuário que intercepta chamadas libc pode ser contornado. Um fluxo simples de submissão/colheita é ilustrado abaixo:
// Minimal io_uring usage (error handling omitted)
// io_uring context (SQ/CQ rings shared with kernel)
struct io_uring ring;
// Initialize ring with space for 16 SQEs
io_uring_queue_init(16, &ring, 0);
// Grab a free submission entry (or NULL if full)
struct io_uring_sqe *sqe =
io_uring_get_sqe(&ring);
// Prepare SQE as a read(fd, buf, len, offset=0)
io_uring_prep_read(sqe, fd, buf, len, 0);
// Submit pending SQEs to the kernel (non-blocking)
io_uring_submit(&ring);
struct io_uring_cqe *cqe;
// Block until a completion is available
io_uring_wait_cqe(&ring, &cqe);
// Mark the completion as handled (free slot)
io_uring_cqe_seen(&ring, cqe);
O exemplo acima mostra uma fila de submissão alimentando muitas operações de arquivo no kernel com uma única ou algumas chamadas de sistema io_uring_enter , reduzindo a telemetria de chamada de sistema por operação.
Adversários interessados em coleta furtiva de dados ou exfiltração de alto rendimento podem mudar para io_uring para reduzir o ruído de chamadas de sistema. io_uring não instala inerentemente ganchos globais nem altera o comportamento de outros processos; é local do processo, a menos que seja combinado com escalonamento de privilégios. A detecção é possível instrumentando as chamadas de sistema io_uring (io_uring_enter, io_uring_register) e observando padrões anômalos: lotes incomumente grandes, muitos arquivos/buffers registrados ou processos que executam operações pesadas de metadados em lote. As diferenças de versão do kernel também importam: os recursos io_uring evoluem rapidamente, portanto, as técnicas de ataque podem ser dependentes da versão. Finalmente, como io_uring requer um processo malicioso em execução, os defensores podem frequentemente interrompê-lo e inspecionar seus anéis, arquivos registrados e mapeamentos de memória para descobrir o uso indevido.
Conclusão
As técnicas de hooking no Linux evoluíram muito desde a simples sobrescrita de um ponteiro em uma tabela. Atualmente, vemos atacantes explorando frameworks legítimos de instrumentação do kernel (ftrace, kprobes, eBPF) para implantar mecanismos de segurança mais difíceis de detectar. Cada método, desde patches de tabela IDT e syscall até hooks inline e sondas dinâmicas, tem suas próprias vantagens e desvantagens em termos de discrição e estabilidade. Os defensores precisam estar cientes de todos esses vetores possíveis. Na prática, os rootkits modernos frequentemente combinam múltiplas técnicas de gancho para atingir seus objetivos. Por exemplo, o PUMAKIT usa um gancho direto na tabela de chamadas de sistema e ganchos ftrace, e o Diamorphine usa ganchos de chamadas de sistema mais um kprobe para contornar o ocultamento de símbolos. Essa abordagem em camadas significa que as ferramentas de detecção devem verificar muitas facetas do sistema: entradas IDT, tabelas de chamadas de sistema, registros específicos do modelo (para ganchos do sysenter), integridade dos prólogos de função, conteúdo de ponteiros de função críticos em estruturas (VFS, etc.), operações ftrace ativas, kprobes registrados e programas eBPF carregados.
Na segunda parte desta série, passamos da teoria à prática. Munidos da compreensão da taxonomia de rootkits e das técnicas de hooking abordadas aqui, focaremos na engenharia de detecção, construindo e aplicando estratégias práticas de detecção para identificar essas ameaças em ambientes Linux reais.
