
Detector de rootkit Linux baseado em eBPF usando análise multi-canal de visão cruzada (sched_switch, NMI, /proc) para detectar DKOM, adulteração de tracepoint e ocultação de processos com verificação de integridade em nível de hardware.
Integridade de Processos do Sistema e Análise de Visão Cruzada
"Vou cantar, então brilhe intensamente, SPiCa..."
SPiCa é um detector de rootkit Linux baseado em eBPF escrito em Rust. O nome vem da música de Hatsune Miku SPiCa e da estrela que ela referencia — Spica (Alpha Virginis), o ponto mais brilhante em Virgem. O que parece uma única estrela a olho nu é na verdade uma binária espectroscópica: duas estrelas em órbita mútua, indistinguíveis como objetos separados sem medir seus espectros. SPiCa aplica o mesmo princípio à observação do kernel: múltiplos canais independentes medem o mesmo estado do kernel a partir de mecanismos fisicamente distintos, e um rootkit que suprime um é exposto pelos outros.
Aviso: Partes significativas deste código foram geradas ou refatoradas com auxílio de GLM. Testes rigorosos e design iterativo foram aplicados, mas revise o código quanto à segurança e desempenho antes do uso em produção.
SPiCa foi projetado para derrotar o adversário restrito por eBPF — um atacante com privilégios elevados (CAP_BPF ou CAP_SYS_ADMIN) que carrega um programa eBPF privilegiado no kernel. Esse adversário é fundamentalmente mais fraco que um rootkit LKM porque o verificador BPF impõe restrições rígidas:
Com LKMs bloqueados pela porta LSM após a inicialização, esse adversário restrito é a ameaça realista restante. A maquinaria anti-evasão do SPiCa é calibrada para essa ameaça — toda defesa é honesta sobre o que cobre e o que não cobre.
init_module. SPiCa eleva o nível ao bloquear o vetor mais fácil de LKM, mas não limita o adversário superior.SPiCa é a camada de último recurso em uma pilha de defesa em profundidade, não um substituto para as camadas acima.
SPiCa executa quatro programas eBPF anexados a hooks do kernel, além de um motor de detecção em userspace que correlaciona suas saídas com a visão que o sistema tem de si mesmo (/proc).
A principal propriedade arquitetural: um rootkit não pode suprimir todos os três canais simultaneamente sem que a supressão em si se torne detectável ou desestabilizadora. Suprimir NMI requer patchear o IDT (Tabela de Descritores de Interrupção), o que causa pânico na maioria dos kernels. Esta é a "bomba viva" — o único caminho do atacante para a cegueira total é aquele que provavelmente trava o sistema.
sched sees it, /proc doesn't → DKOM NMI finds sched's heartbeat stopped → TAMPER sched goes globally silent, /proc != empty → SILENT
Cada classe de detecção é um veredito diferencial: uma discrepância entre dois ou mais canais. O motor de detecção é uma função pura sobre o registro + snapshot do /proc + timestamps dos canais — sem E/S, sem efeitos colaterais, totalmente testável unitariamente.
### A reformulação do NMI: da observação à integridade
No design original, o NMI era um segundo canal de observação de processos que amostrava a CPU e reportava qual tarefa estava em execução. Isso era redundante: o sched\_switch já observa o escalonamento, e o NMI amostrava os mesmos dados por meio de um mecanismo diferente. A redundância custava ~1000+ eventos/s/CPU no ring-buffer de dados de processos que 99,999% das vezes confirmavam "sim, o escalonador está fazendo o que o escalonador faz."
Na arquitetura reformulada, **o NMI é redirecionado da observação de processos para a verificação de integridade de tracepoints.** Ele não reporta mais qual processo está na CPU. Em vez disso, ele verifica se `sched_switch` está realmente em execução lendo um heartbeat compartilhado `.bss`. Isso:
1. Elimina ~99% do tráfego do ring-buffer do NMI (eventos de estado estacionário próximos de zero)
2. Detecta diretamente desanexação de tracepoint, supressão e falhas de BTF/attach (o bug original do BTF — veja [§11](#11-the-btf-bug-incident))
3. Executa a partir de uma interrupção de hardware, fora do caminho de despacho do tracepoint — imune a `bpf_override_return`, interceptação de kprobe e manipulação do array de funções
4. Lê globais `.bss` via acesso direto à memória, não helpers do BPF — imune a `fmod_ret` em funções helper
---
## 3. O Canal de Observação sched\_switch
Um programa eBPF anexado ao tracepoint `sched_switch` é disparado toda vez que o kernel escala um processo para uma CPU. Ele lê o PID e o comm da tarefa de entrada diretamente dos argumentos do tracepoint usando leituras tradicionais de deslocamento fixo (não-BTF):```
ctx.read_at::<u32>(56) → next_pid
ctx.read_at::<[u8;16]>(40) → next_comm
Deliberadamente não BTF/CO-RE. A disposição dos argumentos do tracepoint é estável entre versões do kernel (faz parte da ABI do tracepoint). Usar offsets fixos evita a fragilidade de versão do kernel da navegação de structs resolvida por BTF. Esta é uma escolha de design deliberada documentada em §11.
Em cada invocação, o programa:
next_pid e next_comm do contexto do tracepointProcessInfo com BASE_KEYsc_schedbpf_ktime_get_ns() no .bss global SCHED_HEARTBEAT — o heartbeat que o verificador de integridade NMI monitoraO programa evita deliberadamente bpf_get_current_pid_tgid() neste contexto. No momento da sched_switch, "current" é a tarefa que está saindo, não a que está entrando. Os argumentos do tracepoint fornecem a identidade correta (entrada) do processo.
O motor de detecção usa uma única base de tempo monotônica: nanossegundos desde a inicialização do processo SPiCa (Instant::now() desde a entrada de run_detection()). Quando um evento sched chega, o manipulador de eventos armazena o now fornecido pelo chamador (nanos locais do processo), não o valor eBPF bpf_ktime_get_ns() embutido no evento. Isso evita a incompatibilidade de base de tempo que ocorreria se o tempo de boot do kernel fosse misturado com o tempo local do processo — um bug que estava presente em versões anteriores e fazia com que todos os predicados de atividade falhassem silenciosamente.
O programa NMI está anexado a um contador de perf de hardware (HW_CPU_CYCLES) que dispara no nível de interrupção de hardware. NMIs não podem ser desabilitados com cli — suprimi-los requer reprogramar a PMU ou hookar a IDT, ambos os quais são barulhentos e desestabilizadores.
O mecanismo de verificação de integridade é extremamente simples:``` .bss (shared within SPiCa's ELF object — all programs see the same memory): SCHED_HEARTBEAT ← written by sched_switch on every invocation NMI_LAST_HB ← NMI's record of the last heartbeat it observed NMI_FIRST_TICK ← first NMI invocation timestamp (grace period reference) NMI_LAST_EMIT ← throttle: last event emission timestamp
sched_switch runs → SCHED_HEARTBEAT = bpf_ktime_get_ns()
NMI fires (once per second, throttled): if SCHED_HEARTBEAT == NMI_LAST_HB: if past grace period (5 seconds since first NMI tick): → sched_switch stopped running → emit event_type=1 (TAMPER) else: → sched_switch is alive → emit event_type=0 (heartbeat OK) NMI_LAST_HB = SCHED_HEARTBEAT
### Por que isso detecta todos os ataques no escopo
| Ataque | O que acontece | O que o NMI vê |
|---|---|---|
| Tracepoint removido do array de funcs | sched\_switch para de executar | `SCHED_HEARTBEAT` congela → TAMPER |
| Erro de BTF / falha de attach (silenciosa) | sched\_switch nunca executa | `SCHED_HEARTBEAT` permanece 0 → TAMPER (após carência) |
| `bpf_override_return` em uma função do escalonador | Se impedir o tracepoint de disparar | `SCHED_HEARTBEAT` congela → TAMPER |
| Bytecode corrigido in-place | Requer escrita arbitrária no kernel (nível LKM) | Fora do modelo de ameaça do eBPF |
| Ponteiro do consumidor do ring buffer manipulado | Eventos de sched não chegam ao espaço do usuário | `SCHED_HEARTBEAT` ainda avança (o programa executa) → sem TAMPER falso; espaço do usuário detecta via `max(sched_last)` obsoleto → SILENT |
### Por que `.bss` especificamente
Variáveis globais `.bss` são armazenadas na seção de dados interna do programa BPF, apoiadas por um mapa de array interno que o carregador gerencia. Elas são:
- **Não podem ser fixadas separadamente** — não aparecem como mapas nomeados em `/sys/fs/bpf/`
- **Não interceptáveis via hooks `bpf_map_update_elem`** — escritas em `.bss` são armazenamentos diretos na memória, não chamadas de sistema de atualização de mapa. O antigo mecanismo `sc_canary` (comparar uma cópia `.bss` com uma cópia de mapa nomeado para detectar interceptação de `bpf_map_update_elem`) não é mais necessário.
- **Compartilhadas entre programas no mesmo objeto ELF** — sched\_switch e NMI se comunicam através de `.bss` sem nenhuma interface externa
### Imunidade à interceptação baseada em BPF
O verificador de integridade do NMI é estruturalmente imune a ataques de substituição do BPF devido a uma propriedade fundamental: `bpf_override_return` intercepta **chamadas** de função, mas o verificador do NMI não *chama* as coisas que verifica — ele *lê a memória `.bss` diretamente*. Você não pode substituir o valor de retorno de uma leitura de memória porque uma leitura de memória não é uma chamada de função.
Além disso:
- `bpf_probe_read_kernel` (usado para leituras de structs do kernel em designs alternativos) é um helper seguro contra falhas que aceita qualquer endereço — mas o design de heartbeat `.bss` do SPiCa nem precisa dele. O verificador lê globais `.bss` via instruções de carga direta.
- O programa NMI é executado em contexto NMI, onde kprobes são estruturalmente não confiáveis (o kernel os adia ou suprime). Um ataque baseado em kprobe contra a execução do verificador luta contra o hardware.
### Semântica de eventos do NMI
O ring buffer do NMI (`sc_nmi`) carrega eventos leves:
| `event_type` | Significado | Ação do espaço do usuário |
|---|---|---|
| 0 | Heartbeat — NMI ativo, sched\_switch ativo | Atualizar timestamp `last_nmi_heartbeat` |
| 1 | TAMPER — NMI ativo, heartbeat de sched\_switch congelado | Imprimir `[TAMPER]` imediatamente |
Eventos são emitidos no máximo uma vez por segundo (limitados por `NMI_LAST_EMIT`). Se o ring buffer do NMI ficar silencioso por mais de 5 segundos, o espaço do usuário dispara `[SILENT]` — o próprio canal NMI está morto.
---
## 5. Lógica de Detecção```mermaid
graph TD
subgraph RING0["Kernel Space: Four eBPF Programs"]
direction TB
SCHED_P["TracePoint, sched_switch<br/>read next_pid, next_comm<br/>write SCHED_HEARTBEAT (.bss)<br/>XOR obfuscate → sc_sched"]
NMI_P["PerfEvent hardware NMI<br/>read SCHED_HEARTBEAT (.bss)<br/>compare to NMI_LAST_HB<br/>frozen → event_type=1 (TAMPER)<br/>alive → event_type=0 (heartbeat)<br/>→ sc_nmi"]
LSM_P["BPF LSM, kernel_read_file<br/>READING_MODULE<br/>gate=0: allow + log<br/>gate=1: EPERM + log → sc_lsm"]
WATCH_P["TracePoint, sched_process_exit<br/>current == SPICA_PID (.bss)? → sc_wd flag"]
end
subgraph RING3["User Space: Differential Engine"]
ENGINE["SPiCa (Tokio async)"] -->|XOR deobfuscate| RB_S[(sc_sched RingBuf)]
ENGINE -->|event_type=0: heartbeat<br/>event_type=1: TAMPER| RB_N[(sc_nmi RingBuf)]
ENGINE -->|raw read| RB_L[(sc_lsm RingBuf)]
ENGINE -->|read_dir| PROC[" /proc"]
RB_S --> FSM{Detection FSM}
PROC --> FSM
FSM -->|scheduled, absent /proc, >2s| D1["[DKOM]"]
FSM -->|in /proc, never seen by sched, >5s| D2["[GHOST]"]
RB_N -->|event_type = 1| D3["[TAMPER]"]
FSM -->|sched channel silent, /proc ≠∅| D4["[SILENT]"]
FSM -->|raw getdents64 ≠ libc readdir| D9["[HOOK]"]
RB_S -->|same TGID, different start_time_ns| D5["[DUPE]"]
RB_L -->|allowed = 1| D7["[LKM-ALLOW]"]
RB_L -->|allowed = 0| D8["[LKM-DENY]"]
WATCH_P -.->|pin exists at startup| D6["[WATCHDOG]"]
end
classDef kernbox fill:#fdf2f8,stroke:#9333ea,stroke-width:2px,color:#581c87;
classDef userbox fill:#eff6ff,stroke:#2563eb,stroke-width:2px,color:#1e3a8a;
classDef alertbox fill:#fff7ed,stroke:#ea580c,stroke-width:1px,color:#7c2d12;
classDef storage fill:#f8fafc,stroke:#64748b,stroke-width:1px;
class RING0 kernbox;
class RING3 userbox;
class D1,D2,D3,D4,D5,D6,D7,D8,D9 alertbox;
class RB_S,RB_N,RB_L,PROC storage;
SILENT é um veredito no nível de canal, não por processo. Ele dispara quando:
canal sched morto: max(sched_last) em todos os registros está obsoleto além de CHANNEL_DEAD_NANOS enquanto /proc não está vazio. A guarda de /proc não vazio é a garantia de atividade: em qualquer sistema Linux em execução, /proc não está vazio (init, threads do kernel, kworkers), e o escalonador nunca está verdadeiramente ocioso (ticks de timer, espera de I/O, callbacks RCU). Um max(sched_last) obsoleto em um sistema não vazio significa que o canal morreu — não que o sistema está ocioso.
canal NMI morto: Nenhum evento de heartbeat (event_type=0) chegou do buffer circular NMI por >5 segundos. Isso significa que o evento perf NMI parou de disparar — ou o evento perf foi desconectado ou o PMU foi reprogramado.
Ambas as verificações possuem um período de espera de 30 segundos para evitar inundação de alertas.
[HOOK])SPiCa lê /proc via dois mecanismos independentes a cada tick:
Chamada de sistema getdents64 bruta (read_tgids()) — O std::fs::read_dir do Rust chama isso diretamente via libc::syscall(SYS_getdents64, ...), ignorando todos os hooks de funções libc. Rootkits LD_PRELOAD não podem interceptar este caminho.
libc readdir (read_tgids_libc()) — chama libc::opendir / libc::readdir via FFI, que passa pelo linker dinâmico e é totalmente interceptável por hooks de /etc/ld.so.preload.
Se getdents64 vê PIDs que a libc readdir não vê, um rootkit de espaço do usuário está fazendo hook do readdir para ocultar processos. SPiCa dispara [HOOK]:```
[HOOK] 3 PID(s) hidden from libc readdir — LD_PRELOAD rootkit suspected
Esta técnica transforma o privilégio de nível de kernel do SPiCa em uma detecção ativa de rootkits do espaço do usuário. O caminho da chamada de sistema bruta é a verdade fundamental — nenhum hook do espaço do usuário pode cegá-la. O caminho da libc é a visão "percebida" que uma ferramenta normal (ps, ls) veria. A discrepância é uma prova inequívoca de interceptação de biblioteca.
**Rootkits detectados por `[HOOK]`:**
| Rootkit | Mecanismo de ocultação | Detectado |
|---------|-----------------|----------|
| Symbiote | LD_PRELOAD parasítico, hook `readdir` | Sim (quando ocultando ativamente) |
| JynxKit | LD_PRELOAD, ocultação via `MAGIC_GID` via `readdir` | Sim |
| Azazel | LD_PRELOAD, hooks `readdir` + `stat` | Sim |
| Medusa/OrBit | LD_PRELOAD, hook `readdir` + roubo de credenciais | Sim |
**Testado contra o Symbiote real:** Uma amostra ao vivo do Symbiote (SHA256 `f55af21f...`, MalwareBazaar) foi implantada via `/etc/ld.so.preload` em uma VM Ubuntu 24.04. A amostra intercepta `readdir`, `readdir64`, `stat`, `fstatat`, `pam_authenticate`, `pcap_loop`, `recvmsg`, `fopen`, `read`, `execve`. Quando o hook LD_PRELOAD oculta ativamente um PID do `readdir`, o SPiCa dispara `[HOOK]` dentro de um ciclo de tick (<1 s). Quando o Symbiote está carregado, mas passivo (não ocultando ativamente), o SPiCa produz zero falsos positivos.
### Grace window
Uma janela de graça de 50 ms evita falsos positivos em processos recém-forkados. As verificações SILENT em nível de canal têm um período de graça de 5 segundos (a referência `NMI_FIRST_TICK` do programa NMI) para que a latência de inicialização não produza alertas TAMPER falsos.
---
## 6. Verifier-Bounded Address Secrecy
### Definição
**Verifier-bounded address secrecy** é uma propriedade de segurança computacional que é válida sob o modelo de adversário restrito a eBPF. Ela afirma:
> Dado um adversário que pode carregar programas eBPF (CAP\_BPF) mas é limitado pelo verificador BPF (loops limitados, helpers restritos, sem escrita arbitrária no kernel), o endereço do kernel da seção de dados `.bss` de um programa BPF é computacionalmente inviável de descobrir.
### Os quatro mecanismos
A propriedade é uma composição de quatro barreiras independentes:
1. **kASLR** — Kernel Address Space Layout Randomization randomiza a região do heap do kernel onde a memória dos mapas BPF (incluindo os mapas de apoio `.bss`) é alocada. O endereço exato muda a cada inicialização.
2. **Limite de instruções do Verificador** — O verificador BPF limita o total de instruções por programa a ~1 milhão e exige loops que terminam comprovadamente. Um programa de varredura de um adversário pode sondar no máximo N ≈ 10^6 endereços por carga via `bpf_probe_read_kernel`. Dado um espaço de busca randomizado por kASLR, a probabilidade de sucesso por programa é desprezível.
3. **Portão de acesso a mapas LSM** (veja [§7](#7-lsm-map-access-gate)) — Sem o portão, um adversário poderia enumerar mapas BPF por ID (`BPF_MAP_GET_NEXT_ID` → `BPF_MAP_GET_FD_BY_ID` → `BPF_MAP_LOOKUP_ELEM`) e ler o conteúdo de `.bss` diretamente. O portão remove esse caminho fácil, forçando o adversário ao caminho de varredura de memória limitada pelo verificador.
4. **Posicionamento de `.bss`** — As globais `.bss` não aparecem em interfaces de enumeração de mapas nomeados com nomes legíveis por humanos. O mapa de apoio `.bss` interno é nomeado `.bss` por libbpf/aya, mas seu conteúdo (as variáveis globais reais) é acessível apenas se você tiver o fd do mapa. O fd do mapa requer a tabela de fd do processo de carregamento ou o ID do mapa (bloqueado pelo portão).
### Estrutura do argumento formal```
Adversary's goal: locate SPiCa's .bss in kernel virtual memory
Search space: kernel heap region, kASLR-randomized (~2^30 entropy)
Path A — map enumeration (blocked):
BPF_MAP_GET_NEXT_ID → BPF_MAP_GET_FD_BY_ID → BPF_MAP_LOOKUP_ELEM
→ blocked by LSM gate (§7) for non-SPiCa processes
Path B — memory scan (bounded):
bpf_probe_read_kernel(addr, buf, len) — fault-safe, accepts any addr
Per-program capacity: N ≈ 10^6 probes (instruction limit)
Success probability per load: N / |search space| ≈ 10^6 / 2^30 ≈ 10^-24
Can reload programs, but: each load is a syscall (observable), and
the target address doesn't change between loads (same boot session)
Path C — timing side channel (theoretical):
bpf_probe_read_kernel timing varies on fault vs success
Binary search over the heap could reduce scan complexity to O(log |A|)
→ O(30) probes, well within instruction budget
LIMITATION: must be acknowledged. Mitigated by rate-limiting BPF_PROG_LOAD
via the LSM gate (future work).
A maior parte da pesquisa de segurança em BPF trata o verificador como uma superfície de ataque (bugs do verificador, inconsistência, JIT spray). Este trabalho inverte o enquadramento: as restrições do verificador são uma primitiva defensiva que limita o espaço de busca do adversário. O sigilo não é criptográfico (não é AES); é computacional — limitado pelo que o verificador permite que o adversário compute.
bpf_probe_read_kernel é observável em termos de temporização (falha vs. sucesso leva ciclos diferentes). Uma busca binária baseada em temporização sobre o heap é teoricamente mais eficiente que uma varredura linear. O limite de instruções restringe as sondagens por programa, mas a busca binária poderia reduzir o número de sondagens para O(log |A|) ≈ 30. Esta é uma limitação real. Mitigação: limitar a taxa de BPF_PROG_LOAD através do gate LSM.Globais .bss são suportados por um mapa BPF interno. Qualquer processo com CAP_BPF pode enumerar todos os mapas no sistema:```
bpf(BPF_MAP_GET_NEXT_ID, …) → iterate all map IDs
bpf(BPF_MAP_GET_FD_BY_ID, id) → get fd to any map
bpf(BPF_MAP_LOOKUP_ELEM, fd, key, value) → read map contents
Contra o adversário eBPF (que não pode chamar syscalls), `.bss` está oculto. Contra o root do userspace, é um mapa padrão — totalmente legível. O portão LSM fecha essa lacuna.
### A defesa```
hook = "bpf"
read cmd (arg 0)
if cmd == BPF_MAP_GET_FD_BY_ID:
read map_id from userspace bpf_attr (bpf_probe_read_user)
if map_id matches any of SPiCa's stored IDs (.bss):
if caller_tgid != SPICA_PID:
return -EPERM
Os IDs dos mapas são escritos no .bss pelo carregador do espaço do usuário imediatamente após o carregamento do programa, antes que o hook do LSM seja armado (mesmo padrão de temporização do sc_gate existente para bloqueio de LKM). Nenhum BTF é necessário — cmd e map_id vêm dos argumentos da chamada de sistema, não de estruturas do kernel.
Cirúrgico: bloqueia apenas o acesso aos IDs de mapa específicos do SPiCa. Outras ferramentas BPF de outros processos (tcpdump, bpftrace, bcc) acessam seus próprios mapas e não são afetadas.
/proc/pid/mem — Outro processo root poderia ler a memória do processo do SPiCa diretamente via ptrace, contornando completamente a chamada de sistema BPF. Esta é uma limitação fundamental: você não pode se proteger contra um processo com o mesmo nível de privilégio lendo sua memória via /proc/pid/mem. Defesa: envio de logs para fora do host (se o atacante consegue ler a memória local, ele também pode suprimir alertas locais — apenas o registro remoto ajuda).Em hosts equipados com TPM, a chave de ofuscação XOR de 64 bits é obtida do TPM2_GetRandom através da crate tss-esapi (chamada direta à biblioteca libtss2-esys). A chave não transita por pipe, linha de comando de subprocesso ou disco. Ela existe apenas no hardware do TPM e na memória volátil do kernel (mapa de suporte .bss) durante a duração do processo do SPiCa.
Em hosts sem TPM (VMs sem vTPM, contêineres, máquinas sem chip TPM), o SPiCa recai para uma chave de tempo de compilação por build gerada a partir de /dev/urandom pelo script de build. A queda é visivelmente ruidosa na inicialização com um banner de aviso.
.bssOs seguintes valores residem na seção .bss do programa eBPF, escrita pelo espaço do usuário via set_global() antes que qualquer programa seja carregado:
Mapas BPF nomeados são enumeráveis através da interface de iteração de mapas da chamada de sistema bpf() (bloqueada pela porta do LSM — veja §7); globais .bss são acessados através do mesmo mapa interno, mas não são separadamente nomeados ou fixáveis.
Todos os campos de ProcessInfo são dobrados por XOR com a BASE_KEY de 64 bits antes de serem escritos nos buffers de anel. O campo event_type é intencionalmente deixado sem ofuscação para que o espaço do usuário possa lê-lo como um sentinela antes de desofuscar o restante.
Isso é ofuscação contra vazamento por leitura, não criptografia contra um adversário capaz. XOR com uma chave repetitiva de 8 bytes é estruturalmente vulnerável a texto claro conhecido: valores previsíveis de comm ("bash", "systemd", "kthreadd") aplicados XOR ao texto cifrado recuperam bytes da chave diretamente. A defesa é apropriada para o adversário do eBPF (que não pode ler facilmente o buffer de anel — requer a mesma lacuna de capacidade que protege .bss), não para um adversário com acesso binário + interceptação de buffer de anel.
Status: Fase de design. Ainda não implementado. Esta seção documenta a arquitetura alvo para o artigo de pesquisa.
A selagem TPM vinculada a PCR move o limite de distribuição de chaves da camada de software para o silício. A chave é selada no momento da instalação contra valores esperados dos Registradores de Configuração de Plataforma (PCRs), e só pode ser desselada se o estado de inicialização do sistema corresponder às medições esperadas.
Durante a inicialização, o firmware, o bootloader e o kernel medem componentes críticos nos PCRs:
Nota: A alocação de PCR é dependente da cadeia de inicialização. O GRUB mede o kernel no PCR 4; o systemd-stub mede no PCR 4 e a linha de comando no PCR 8. A política de selagem deve corresponder à cadeia de inicialização alvo.
Selar (momento da instalação): A chave de 64 bits é selada contra valores esperados de PCR usando TPM2_Create com uma sessão TPM2_PolicyPCR. O blob selado é armazenado em disco. Ele é criptografado com a chave interna do TPM e só pode ser descriptografado quando os PCRs especificados corresponderem.
Desselar (inicialização precoce, estágio initramfs): Antes que qualquer código de espaço do usuário não confiável execute, o hook initramfs do SPiCa solicita TPM2_Unseal. Se os valores atuais de PCR corresponderem à política de selagem, o TPM libera a chave.
Injetar: A chave é escrita no .bss via set_global() antes que os programas sejam carregados.
A armadilha: Se um atacante modificou o kernel (incompatibilidade PCR 4), trocou o initramfs (incompatibilidade PCR 9) ou alterou a política de Secure Boot (incompatibilidade PCR 7), os hashes do PCR divergem. O TPM se recusa a desselar, e o SPiCa falha de forma segura — ele se recusa a iniciar em vez de executar cego com uma chave comprometida.
Para o artigo de pesquisa, apresente a política forte e discuta a troca do resselamento nas limitações.
Os dois mecanismos são complementares:
.bss onde a chave reside.Nenhum é suficiente sozinho. A selagem PCR não ajuda se a chave for comprometida em tempo de execução (enumeração de mapas). O sigilo de endereço não ajuda se o sistema foi comprometido antes do SPiCa iniciar (initramfs hostil).
spica_lsm_modblock, a porta de acesso ao mapa) são carregados em tempo de execução e NÃO são medidos em nenhum PCR. Um rootkit que os desanexa após a inicialização não é detectado pela selagem PCR. O que detecta isso é o heartbeat NMI (detectando que o próprio sistema de detecção parou de executar).O SPiCa é a última camada de aplicação. Ele complementa um sistema configurado adequadamente, não substitui as camadas acima.```mermaid
flowchart TD
SB["UEFI Secure Boot
─────────────────────────────
Verifies bootloader signature against
the UEFI db certificate store
Measured to PCR 7"]
MS["Kernel Module Signing
─────────────────────────────
CONFIG_MODULE_SIG_FORCE=y
Kernel rejects unsigned .ko at load time"]
IMA["IMA, Integrity Measurement Architecture
─────────────────────────────
Measures file hashes to TPM PCR10
Appraise policy blocks non-matching signatures"]
SPICA["SPiCa
─────────────────────────────
LSM gate: blocks LKM loads + map enumeration
sched_switch + NMI integrity channels
Differential detection: DKOM · GHOST · TAMPER · SILENT · DUPE
PCR-bound key sealing (design phase)"]
SB -->|"boot chain verified"| MS
MS -->|"signed modules only"| IMA
IMA -->|"measured + appraised"| SPICA
classDef firmware fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f
classDef kernel fill:#fdf2f8,stroke:#9333ea,stroke-width:2px,color:#581c87
classDef spica fill:#eff6ff,stroke:#2563eb,stroke-width:2px,color:#1e3a8a
class SB firmware
class MS,IMA kernel
class SPICA spica
SPiCa verifica todas as quatro camadas na inicialização e exibe seu status.
---
## 11. O Incidente do Bug BTF
### O que aconteceu
Durante os testes no Ubuntu (kernel mais recente), uma incompatibilidade BTF fez com que o programa de tracepoint `sched_switch` anexasse com sucesso, mas **nunca disparasse um único evento.** A chamada de sistema `attach()` retornou `Ok(())`, então o SPiCa prosseguiu normalmente — mas o ring buffer de sched permaneceu vazio. Nenhum alerta de detecção foi acionado porque o motor de detecção não tinha dados de escalonamento. **SPiCa funcionou às cegas sem qualquer indicação de falha.**
Este é o pior modo de falha para uma ferramenta de segurança: cegueira silenciosa.
### Por que não foi detectado
O motor de detecção original raciocinava **por processo**: cada registro de processo tinha timestamps `sched_last` e `nmi_last`, e a vivacidade era calculada por registro. Quando sched\_switch morreu globalmente:
1. `sched_live` tornou-se falso para cada registro (sem novos eventos sched → todos os valores `sched_last` envelhecem)
2. O predicado `TAMPER` por processo (`in_proc && nmi_live && !sched_live`) poderia disparar, mas exigia que `nmi_live` fosse mantido *continuamente* por 2 segundos. As amostras NMI são esparsas (período de 10M ciclos), então `suspect_since` era continuamente redefinido devido à variação da amostragem e nunca amadurecia.
3. O predicado `SILENT` por processo exigia `sched_live`, que agora era falso para tudo — o predicado invertia exatamente na condição que deveria detectar.
4. **Não havia verificação de vivacidade em nível de canal** — nenhum mecanismo do tipo "sched JÁ disparou?" ou "max(sched_last) está obsoleto?".
Além disso, foi descoberta uma incompatibilidade de base temporal: `sched_last` armazenava `bpf_ktime_get_ns()` (nanossegundos de inicialização do kernel) enquanto `evaluate()` comparava com `nanos_since_startup()` (nanossegundos locais do processo). O `wrapping_sub` dessas bases temporais diferentes produzia valores enormes, tornando todos os predicados de vivacidade trivialmente falsos. A lógica de detecção nunca havia funcionado corretamente em produção — o bug BTF apenas a mascarou ao impedir a chegada de eventos.
### Como o redesign corrige
| Problema | Correção |
|---|---|
| Nenhuma verificação de vivacidade em nível de canal | `evaluate()` agora calcula `max(sched_last)` e dispara `[SILENT]` se estiver obsoleto enquanto `/proc` não está vazio |
| TAMPER por processo nunca amadurecia (reset por jitter) | TAMPER agora é um sinal direto do programa NMI (comparação de heartbeat `.bss`), não uma FSM por processo com limites |
| SILENT por processo invertia quando sched morria | SILENT agora é em nível de canal, calculado a partir de agregados, não de predicados por registro |
| Incompatibilidade de base temporal | `sched_last` agora armazena tempo local do processo (parâmetro `now`), não `bpf_ktime_get_ns()` |
| NMI era observação redundante | NMI agora é um verificador de integridade — detecta morte de sched diretamente via heartbeat `.bss` |
---
## 12. Limitações Conhecidas e Superfície de Ataque
### Dentro do modelo de ameaça eBPF
| Ataque | Status | Notas |
|---|---|---|
| **Manipulação do ponteiro consumidor do ring buffer** | **Detectado** (SILENT) | `max(sched_last)` fica obsoleto → SILENT. `SCHED_HEARTBEAT` ainda avança (programa executa) → sem falso TAMPER. |
| **DKOM da struct perf event** (zerar campo `state`) | **Detectado** (SILENT) | NMI para de disparar → sem eventos de heartbeat → SILENT no canal NMI |
| **Spoofing de campo `task_struct`** (sobrescrever pid/tgid) | **Parcial** (DUPE) | Se o TGID spoofado corresponder a um registro existente com `start_time_ns` diferente, DUPE dispara. Se `start_time_ns` também for spoofado, isso passa despercebido. |
| **Desanexação do programa eBPF** (remover do array funcs) | **Detectado** (TAMPER) | Heartbeat NMI congela → TAMPER |
| **Bytecode patchado in-place** | **Não detectado** | Requer escrita arbitrária no kernel (nível LKM) — fora do modelo de ameaça eBPF |
| **Canal lateral de temporização no `bpf_probe_read_kernel`** | **Risco teórico** | Busca binária na heap poderia encontrar `.bss` em ~30 sondas. Veja limitações em [§6](#6-verifier-bounded-address-secrecy). |
### Fora do modelo de ameaça eBPF (reconhecido, não resolvido)
| Ataque | Status | Notas |
|---|---|---|
| **Hook de IDT/manipulador NMI** | **A bomba viva** | Burlamento arquiteturalmente completo. Quase certeza de pânico no kernel. |
| **Supressão de canal duplo** | **A bomba viva** | Suprimir simultaneamente sched + NMI. Herda instabilidade do hooking NMI. |
| **Exploração do Verificador** | **Não é objetivo** | Escapada para execução arbitrária de código do kernel. Classe de ameaça separada. |
| **ptrace / `/proc/pid/mem`** | **Limitação fundamental** | Leitura de memória com mesmo privilégio. Apenas envio de logs fora do host ajuda. |
| **Patch de ponteiro consumidor + heartbeat** | **Nível LKM** | Se o atacante tiver escrita arbitrária no kernel, pode avançar o ponteiro consumidor E escrever heartbeats falsos. Mas escrita arbitrária no kernel = nível LKM = fora do modelo de ameaça. |
---
## 13. Compilar & Executar
### Pré-requisitos
- Kernel Linux >= 5.15 com `CONFIG_DEBUG_INFO_BTF=y` (apenas para o hook LSM)
- Para bloqueio de módulos: `CONFIG_BPF_LSM=y` e `lsm=bpf` na linha de comando do kernel
- Chip TPM 2.0 + biblioteca `tpm2-tss` (opcional; recai com aviso visível)
- Cadeia de ferramentas Rust nightly
> **Nota:** A etapa `generate-vmlinux` **não é mais necessária**. Os programas eBPF usam offsets de tracepoint tradicionais e globais `.bss` — nenhuma navegação de struct CO-RE/BTF é necessária. O comando xtask `generate-vmlinux` é mantido para uso futuro, mas não faz parte do pipeline de compilação.
Verifique se BPF LSM está ativo: `cat /sys/kernel/security/lsm` deve conter `bpf`.
### Setup```shell
make install-deps # system packages + nightly Rust (pacman/apt/dnf auto-detected)
make install-tools # bpf-linker
make build # compiles eBPF + userspace (no vmlinux generation needed)
make run # sudo ./target/release/spica
### Instalar no initramfs (proteção de inicialização antecipada)```shell
sudo make install # spica install — auto-detects Debian (initramfs-tools) or Fedora (dracut)
sudo insmod some_module.ko # should fail with EPERM (gate locked) cat /sys/kernel/security/lsm # should contain 'bpf' ls /sys/fs/bpf/spica_watchdog # exists if previous run was killed ungracefully
### Desenvolvimento (macOS ou Linux)
Os programas eBPF não podem ser executados no macOS. `cargo check` e testes unitários para a lógica de detecção funcionam; a verificação completa em tempo de execução requer Linux.```shell
make check # cargo check for workspace
make check-ebpf # cargo check for the eBPF crate (bpfel-unknown-none)
make test # unit tests for detection FSM, key derivation, obfuscation
GetRandom apenas no TPM por uma raiz de confiança completa em hardware. Consulte §9.bpf bloqueando a enumeração de mapas do estado interno do SPiCa por processos não SPiCa. Consulte §7.BPF_PROG_LOAD — Estender o portão LSM para limitar a taxa de carregamento de programas BPF a partir de processos não SPiCa, mitigando o risco de canal lateral de temporização na descoberta de .bss. Consulte §6 limitações.siphasher já está na árvore para o token de integridade).Licença do SPiCa Engine: MIT OU Apache-2.0 (workspace). O programa eBPF exporta uma licença GPL através do static _license — este é um requisito do kernel para programas eBPF que usam helpers licenciados sob GPL, e se aplica apenas ao bytecode eBPF carregado, não ao binário do espaço do usuário.
Atribuição de Personagem: "Hatsune Miku" e obras de arte de personagem associadas são propriedades protegidas por direitos autorais da Crypton Future Media, INC. (www.piapro.net). Este projeto é uma ferramenta de pesquisa independente, não comercial, não afiliada à Crypton Future Media. Personagem usado sob a Piapro Character License (PCL).
O nome do projeto SPiCa é inspirado pela música original de Toku-P.
| Restrição | Efeito no adversário |
|---|
| Laços limitados (~1M instruções por programa) | Não pode escanear toda a memória do kernel |
| Segurança de ponteiros (desreferência direta requer alvo comprovadamente válido e limitado) | Não pode seguir ponteiros arbitrários |
| Helpers restritos (lista de permissão por tipo de programa) | Não pode chamar kmalloc, vmalloc ou qualquer função de alocação do kernel |
Nenhuma escrita arbitrária no kernel (bpf_probe_write_kernel fortemente restrita) | Não pode modificar task_struct, arrays de funções de tracepoint ou bytecode de programas BPF |
| Acesso ao mapa limitado às referências carregadas | Não pode acessar mapas pertencentes a outros programas (a menos enumerados por ID — veja §7) |
| Canal | Mecanismo | O que o suprime | Custo da supressão |
|---|
/proc | Enumeração de diretórios procfs | DKOM (desvincular processo da lista de tarefas) | Baixo — técnica de rootkit bem compreendida |
sched_switch | Tracepoint eBPF no escalonador | Desanexar programa do array de funções; filtrar contexto | Médio — requer escrita no kernel no array de funções |
| NMI | Interrupção de contador de performance por hardware → evento de perf eBPF | Hook no IDT/manipulador NMI | Quase certeza de pânico no kernel — interrupção de hardware, não mascarável |
| Alerta | Condição | O que detecta |
|---|
[DKOM] | TGID agendado pelo kernel ausente de /proc por >2 s | Manipulação Direta de Objetos do Kernel — processo ocultado da enumeração do espaço do usuário |
[GHOST] | TGID presente em /proc mas nunca visto por sched_switch por >5 s | Falsificação de /proc — entrada falsa inserida para passar nas verificações DKOM |
[TAMPER] | NMI detecta heartbeat do sched_switch congelado (event_type=1) | Tracepoint desconectado, suprimido ou falha de BTF/attach |
[SILENT] | max(sched_last) obsoleto em todo o registro enquanto /proc não está vazio; ou heartbeat NMI ausente por >5 s | Perda de observação no nível de canal — canal sched ou NMI está morto |
[DUPE] | Mesmo TGID, diferentes start_time_ns entre eventos | Falsificação de campo task_struct — rootkit modifica tgid para se passar por um processo legítimo |
[HOOK] | PIDs visíveis via getdents64 bruto, mas ausentes de libc readdir | Rootkit LD_PRELOAD — interceptação de biblioteca no espaço do usuário ocultando processos de ps, ls e outras ferramentas (Symbiote, JynxKit, Azazel, Medusa/OrBit) |
[WATCHDOG] | O pino /sys/fs/bpf/spica_watchdog existe na inicialização | Instância anterior foi encerrada abruptamente (SIGKILL, OOM, crash) |
[LKM-ALLOW] | READING_MODULE interceptado enquanto a porta está aberta (janela de inicialização) | Registro de auditoria: módulo carregado antes da porta ser bloqueada |
[LKM-DENY] | READING_MODULE interceptado enquanto a porta está bloqueada | insmod/modprobe bloqueados pós-inicialização |
| Global | Propósito | Escrito por |
|---|
BASE_KEY | Chave de ofuscação XOR | espaço do usuário no momento do carregamento |
SPICA_PID | TGID próprio do SPiCa (watchdog) | espaço do usuário no momento do carregamento |
SCHED_HEARTBEAT | Timestamp de atividade do sched_switch | programa sched_switch em cada invocação |
NMI_LAST_HB | Registro do NMI do último heartbeat do sched | programa NMI em cada verificação |
NMI_FIRST_TICK | Primeiro ktime de invocação do NMI (período de graça) | programa NMI na primeira invocação |
NMI_LAST_EMIT | Throttle: último ktime de emissão de evento | programa NMI em cada emissão |
| PCR | O que mede | Estabilidade |
|---|
| PCR 4 | Código do bootloader + imagem do kernel (GRUB mede ambos) | Muda na atualização do kernel |
| PCR 5 | Tabela de partição GPT/MBR, configuração de inicialização | Estável entre atualizações |
| PCR 7 | Política de Secure Boot (política SI, MOK, db/dbx) | Estável entre atualizações de kernel |
| PCR 8 | Linha de comando do kernel (medições do systemd-stub) | Estável a menos que cmdline mude |
| PCR 9 | Initramfs (GRUB mede initrd aqui) | Muda na atualização do initramfs |
| PCR 10 | Lista de medição IMA | Muda conforme executáveis são medidos |
| Política | Selado contra | Força | Custo operacional |
|---|
| Forte | PCR 4 + 7 + 9 + 10 | Detecta alterações no kernel, initramfs, Secure Boot e IMA | Resselar após cada atualização de kernel/initramfs |
| Equilibrada | PCR 7 + 10 | Detecta alterações no Secure Boot e avaliação IMA; estável entre atualizações de kernel | Resselar apenas em mudanças na política de Secure Boot ou IMA |
| Mínima | PCR 7 apenas | Detecta apenas alterações no estado do Secure Boot | Muito estável; vínculo mais fraco |
| Termo | Definição |
|---|
| BPF | Berkeley Packet Filter — motor de execução no kernel para programas em sandbox. O BPF moderno (eBPF) estende-se além de pacotes para rastreamento, segurança e rede. |
| BTF | BPF Type Format — informação de depuração do kernel que permite que programas CO-RE (Compile Once, Run Everywhere) naveguem por estruturas do kernel de forma portátil. |
| CO-RE | Compile Once, Run Everywhere — técnica BPF que usa BTF para escrever programas portáteis que se adaptam a diferentes versões do kernel. |
| DKOM | Direct Kernel Object Manipulation — técnica de rootkit que remove um processo da lista encadeada do kernel para ocultá-lo de /proc. |
| fmod_ret | Tipo de programa BPF que modifica o valor de retorno de uma função do kernel através do trampolim BPF. |
| freplace | Extensão de programa BPF — anexa a uma (sub)função específica de outro programa BPF, interceptando sua execução. |
| funcs array | O array de ponteiros de função em uma estrutura de tracepoint do kernel que mantém funções de retorno de chamada (incluindo programas BPF) a serem invocadas quando o tracepoint dispara. |
| IDT | Interrupt Descriptor Table — estrutura da CPU que mapeia vetores de interrupção para funções manipuladoras. Enganchar a entrada NMI requer correção da IDT. |
| kASLR | Kernel Address Space Layout Randomization — randomiza endereços de código/dados do kernel a cada inicialização para dificultar a exploração. |
| NMI | Non-Maskable Interrupt — interrupção de hardware que não pode ser desabilitada por software (cli). Usada por contadores de desempenho para observação em nível de hardware. |
| PCR | Platform Configuration Register — registro do TPM que acumula medições (hashes) de componentes de inicialização. Não pode ser redefinido (exceto reinicialização), apenas estendido. |
| PMU | Performance Monitoring Unit — contadores de hardware na CPU que contam eventos (ciclos, cache misses, etc.) e podem disparar interrupções (NMIs) em limites. |
| TPM | Trusted Platform Module — coprocessador criptográfico que fornece armazenamento de chaves com raiz em hardware, geração de números aleatórios e atestação de medições. |
| Verifier | O verificador BPF — componente do kernel que analisa estaticamente programas BPF antes do carregamento para garantir que terminem e não acessem memória insegura. |