Remco SprootenRuben Groenewoud

Declawing PUMAKIT

O PUMAKIT é um sofisticado rootkit de módulo de kernel carregável (LKM) que usa mecanismos avançados de furtividade para ocultar sua presença e manter a comunicação com servidores de comando e controle.

30 min de leituraAnálise de malware
Declawing PUMAKIT

PUMAKIT em resumo

PUMAKIT é um malware sofisticado, descoberto inicialmente durante a busca de ameaças de rotina no VirusTotal e nomeado em homenagem a sequências de caracteres incorporadas pelo desenvolvedor encontradas em seu binário. Sua arquitetura de vários estágios consiste em um dropper (cron), dois executáveis residentes na memória (/memfd:tgt e /memfd:wpn), um módulo rootkit LKM e um rootkit userland de objeto compartilhado (SO).

O componente rootkit, referenciado pelos autores do malware como “PUMA”, emprega um rastreador de função Linux interno (ftrace) para conectar 18 diferentes syscalls e diversas funções do kernel, permitindo que ele manipule comportamentos do sistema central. Métodos exclusivos são usados para interagir com o PUMA, incluindo o uso da chamada de sistema rmdir() para escalonamento de privilégios e comandos especializados para extrair informações de configuração e tempo de execução. Por meio de sua implantação em etapas, o rootkit LKM garante que ele só seja ativado quando condições específicas, como verificações de inicialização segura ou disponibilidade de símbolos do kernel, forem atendidas. Essas condições são verificadas pela varredura do kernel do Linux, e todos os arquivos necessários são incorporados como binários ELF dentro do dropper.

As principais funcionalidades do módulo do kernel incluem escalonamento de privilégios, ocultação de arquivos e diretórios, ocultação de ferramentas do sistema, medidas antidepuração e estabelecimento de comunicação com servidores de comando e controle (C2).

Principais conclusões

  • Arquitetura multiestágio: O malware combina um dropper, dois executáveis residentes na memória, um rootkit LKM e um rootkit de usuário SO, sendo ativado apenas sob condições específicas.
  • Mecanismos de Stealth Avançados: intercepta 18 syscalls e diversas funções do kernel usando ftrace() para ocultar arquivos, diretórios e o próprio rootkit, enquanto evita tentativas de depuração.
  • Escalonamento de privilégios exclusivo: utiliza métodos de interceptação não convencionais, como a chamada de sistema rmdir() , para escalonar privilégios e interagir com o rootkit.
  • Funcionalidades críticas: Inclui escalonamento de privilégios, comunicação C2, antidepuração e manipulação do sistema para manter persistência e controle.

PUMAKIT Descoberta

Durante a busca de ameaças de rotina no VirusTotal, nos deparamos com um binário intrigante chamado cron. O binário foi carregado pela primeira vez em setembro 4, 2024, com 0 detecções, levantando suspeitas sobre sua potencial furtividade. Após uma análise mais aprofundada, descobrimos outro artefato relacionado, /memfd:wpn (deleted)71cc6a6547b5afda1844792ace7d5437d7e8d6db1ba995e1b2fb760699693f24, carregado no mesmo dia, também com 0 detecções.

O que chamou nossa atenção foram as sequências distintas incorporadas nesses binários, sugerindo uma possível manipulação do pacote do kernel vmlinuz em /boot/. Isso levou a uma análise mais profunda das amostras, levando a descobertas interessantes sobre seu comportamento e propósito.

Análise de código PUMAKIT

O PUMAKIT, nomeado em homenagem ao seu módulo rootkit LKM incorporado (chamado "PUMA" pelos autores do malware) e o Kitsune, o rootkit do SO userland, empregam uma arquitetura de vários estágios, começando com um dropper que inicia uma cadeia de execução. O processo começa com o binário cron , que cria dois executáveis residentes na memória: /memfd:tgt (deleted) e /memfd:wpn (deleted). Enquanto /memfd:tgt serve como um binário Cron benigno, /memfd:wpn atua como um carregador de rootkit. O carregador é responsável por avaliar as condições do sistema, executar um script temporário (/tmp/script.sh) e, por fim, implantar o rootkit LKM. O rootkit LKM contém um arquivo SO incorporado - Kitsune - para interagir com o rootkit do espaço do usuário. Esta cadeia de execução é exibida abaixo.

Este design estruturado permite que o PUMAKIT execute sua carga útil somente quando critérios específicos forem atendidos, garantindo discrição e reduzindo a probabilidade de detecção. Cada estágio do processo é meticulosamente elaborado para ocultar sua presença, aproveitando arquivos residentes na memória e verificações precisas no ambiente de destino.

Nesta seção, vamos nos aprofundar na análise de código para os diferentes estágios, explorando seus componentes e seu papel na ativação desse sofisticado malware multiestágio.

Etapa 1: Visão geral do Cron

O binário cron atua como um conta-gotas. A função abaixo serve como o principal manipulador lógico em um exemplo de malware PUMAKIT. Seus principais objetivos são:

  1. Verifique os argumentos da linha de comando para uma palavra-chave específica ("Huinder").
  2. Se não for encontrado, incorpore e execute cargas ocultas inteiramente da memória, sem soltá-las no sistema de arquivos.
  3. Se encontrado, manipule argumentos específicos de “extração” para despejar seus componentes incorporados no disco e então saia normalmente.

Resumindo, o malware tenta permanecer furtivo. Se executado normalmente (sem um argumento específico), ele executa binários ELF ocultos sem deixar rastros no disco, possivelmente se mascarando como um processo legítimo (como cron).

Se a string Huinder não for encontrada entre os argumentos, o código dentro de if (!argv_) será executado:

writeToMemfd(...): Esta é uma característica da execução sem arquivo. memfd_create permite que o binário exista inteiramente na memória. O malware grava suas cargas úteis incorporadas (tgtElfp e wpnElfp) em descritores de arquivo anônimos em vez de colocá-los no disco.

fork() e execveat(): O malware se bifurca em um processo filho e um processo pai. A criança redireciona sua saída padrão e erro para /dev/null para evitar deixar logs e então executa a carga útil da “arma” (wpnElfp) usando execveat(). O pai espera pelo filho e então executa a carga útil “alvo” (tgtElfp). Ambas as cargas são executadas a partir da memória, não de um arquivo no disco, dificultando a detecção e a análise forense.

A escolha de execveat() é interessante: é uma chamada de sistema mais recente que permite executar um programa referenciado por um descritor de arquivo. Isso reforça ainda mais a natureza sem arquivo da execução desse malware.

Identificamos que o arquivo tgt é um binário cron legítimo. Ele é carregado na memória e executado após o carregador do rootkit (wpn) ser executado.

Após a execução, o binário permanece ativo no host.

> ps aux
root 2138 ./30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f

Abaixo está uma lista dos descritores de arquivo para este processo. Esses descritores de arquivo mostram os arquivos residentes na memória criados pelo dropper.

root@debian11-rg:/tmp# ls -lah /proc/2138/fd
total 0
dr-x------ 2 root root  0 Dec  6 09:57 .
dr-xr-xr-x 9 root root  0 Dec  6 09:57 ..
lr-x------ 1 root root 64 Dec  6 09:57 0 -> /dev/null
l-wx------ 1 root root 64 Dec  6 09:57 1 -> /dev/null
l-wx------ 1 root root 64 Dec  6 09:57 2 -> /dev/null
lrwx------ 1 root root 64 Dec  6 09:57 3 -> '/memfd:tgt (deleted)'
lrwx------ 1 root root 64 Dec  6 09:57 4 -> '/memfd:wpn (deleted)'
lrwx------ 1 root root 64 Dec  6 09:57 5 -> /run/crond.pid
lrwx------ 1 root root 64 Dec  6 09:57 6 -> 'socket:[20433]'

Seguindo as referências podemos ver os binários que estão carregados no exemplo. Podemos simplesmente copiar os bytes em um novo arquivo para análise posterior usando o deslocamento e os tamanhos.

Após a extração, encontramos os dois novos arquivos a seguir:

  • Wpn: cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe
  • Tgt: 934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136

Agora temos os dumps dos dois arquivos de memória.

Etapa 2: Visão geral dos executáveis residentes na memória

Examinando o arquivo ELF /memfd:tgt , fica claro que este é o binário Cron padrão do Ubuntu Linux. Parece não haver modificações no binário.

O arquivo /memfd:wpn é mais interessante, pois é o binário responsável por carregar o rootkit LKM. Este carregador de rootkit tenta se esconder imitando-o como o executável /usr/sbin/sshd . Ele verifica pré-requisitos específicos, como se a inicialização segura está habilitada e se os símbolos necessários estão disponíveis e, se todas as condições forem atendidas, ele carrega o rootkit do módulo do kernel.

Observando a execução no Kibana, podemos ver que o programa verifica se a inicialização segura está habilitada consultando dmesg. Se as condições corretas forem atendidas, um script de shell chamado script.sh é colocado no diretório /tmp e executado.

Este script contém lógica para inspecionar e processar arquivos com base em seus formatos de compactação.

Veja o que ele faz:

  • A função c() inspeciona arquivos usando o comando file para verificar se são binários ELF. Caso contrário, a função retornará um erro.
  • A função d() tenta descompactar um determinado arquivo usando vários utilitários como gunzip, unxz, bunzip2 e outros com base em assinaturas de formatos de compactação suportados. Ele emprega grep e tail para localizar e extrair segmentos compactados específicos.
  • O script tenta localizar e processar um arquivo ($i) em /tmp/vmlinux.

Após a execução de /tmp/script.sh, o arquivo /boot/vmlinuz-5.10.0-33-cloud-amd64 é usado como entrada. O comando tr é empregado para localizar os números mágicos do gzip (\037\213\010). Posteriormente, uma parte do arquivo começando no deslocamento de byte +10957311 é extraída usando tail, descompactada com gunzip e salva como /tmp/vmlinux. O arquivo resultante é então verificado para determinar se é um binário ELF válido.

Esta sequência é repetida várias vezes até que todas as entradas no script tenham sido passadas para a função d().

d '\037\213\010' xy gunzip
d '\3757zXZ\000' abcde unxz
d 'BZh' xy bunzip2
d '\135\0\0\0' xxx unlzma
d '\211\114\132' xy 'lzop -d'
d '\002!L\030' xxx 'lz4 -d'
d '(\265/\375' xxx unzstd

Este processo é mostrado abaixo.

Depois de executar todos os itens do script, os arquivos /tmp/vmlinux e /tmp/script.sh são excluídos.

O objetivo principal do script é verificar se condições específicas foram satisfeitas e, se forem, configurar o ambiente para implantar o rootkit usando um arquivo de objeto do kernel.

Conforme mostrado na imagem acima, o carregador procura os símbolos __ksymtab e __kcrctab no arquivo do kernel do Linux e armazena os deslocamentos.

Várias sequências mostram que os desenvolvedores do rootkit se referem ao seu rootkit como “PUMA” dentro do dropper. Com base nas condições, o programa emite mensagens como:

PUMA %s
[+] PUMA is compatible
[+] PUMA already loaded

Além disso, o arquivo de objeto do kernel contém uma seção chamada .puma-config, reforçando a associação com o rootkit.

Estágio 3: Visão geral do rootkit LKM

Nesta seção, examinaremos mais de perto o módulo do kernel para entender sua funcionalidade subjacente. Especificamente, examinaremos seus recursos de pesquisa de símbolos, mecanismo de conexão e as principais chamadas de sistema que ele modifica para atingir seus objetivos.

Visão geral do rootkit LKM: mecanismo de busca e engate de símbolos

A capacidade do rootkit LKM de manipular o comportamento do sistema começa com o uso da tabela syscall e sua dependência de kallsyms_lookup_name() para resolução de símbolos. Ao contrário dos rootkits modernos direcionados às versões de kernel 5.7 e superiores, o rootkit não usa kprobes, indicando que foi projetado para kernels mais antigos.

Essa escolha é significativa porque, antes da versão 5.7 do kernel, kallsyms_lookup_name() era exportado e podia ser facilmente aproveitado por módulos, mesmo aqueles sem o licenciamento adequado.

Em fevereiro de 2020, os desenvolvedores do kernel debateram a não exportação de kallsyms_lookup_name() para evitar o uso indevido por módulos não autorizados ou maliciosos. Uma tática comum envolvia adicionar uma declaração MODULE_LICENSE("GPL") falsa para contornar as verificações de licenciamento, permitindo que esses módulos acessassem funções do kernel não exportadas. O rootkit LKM demonstra esse comportamento, como fica evidente em suas strings:

name=audit
license=GPL

Esse uso fraudulento da licença GPL garante que o rootkit possa chamar kallsyms_lookup_name() para resolver endereços de funções e manipular componentes internos do kernel.

Além de sua estratégia de resolução de símbolos, o módulo do kernel emprega o mecanismo de gancho ftrace() para estabelecer seus ganchos. Ao aproveitar ftrace(), o rootkit intercepta efetivamente chamadas de sistema e substitui seus manipuladores por ganchos personalizados.

Uma evidência disso é, por exemplo, o uso de unregister_ftrace_function e ftrace_set_filter_ip , conforme mostrado no trecho de código acima.

Visão geral do rootkit LKM: visão geral de chamadas de sistema conectadas

Analisamos o mecanismo de interceptação de syscall do rootkit para entender o escopo da interferência do PUMA na funcionalidade do sistema. A tabela a seguir resume as syscalls capturadas pelo rootkit, as funções capturadas correspondentes e seus possíveis propósitos.

Ao visualizar a função cleanup_module() , podemos ver o mecanismo de captura ftrace() sendo revertido usando a função unregister_ftrace_function() . Isso garante que o retorno de chamada não seja mais chamado. Depois disso, todas as syscalls são retornadas para apontar para a syscall original em vez da syscall conectada. Isso nos dá uma visão geral limpa de todas as chamadas de sistema que foram conectadas.

Nas seções a seguir, examinaremos mais de perto algumas das chamadas de sistema conectadas.

Visão geral do rootkit LKM: rmdir_hook()

O rmdir_hook() no módulo do kernel desempenha um papel crítico na funcionalidade do rootkit, permitindo que ele manipule operações de remoção de diretório para ocultação e controle. Este gancho não se limita a apenas interceptar chamadas de sistema rmdir() , mas estende sua funcionalidade para impor escalonamento de privilégios e recuperar detalhes de configuração armazenados em diretórios específicos.

Este gancho possui diversas verificações em vigor. O gancho espera que os primeiros caracteres da chamada de sistema rmdir() sejam zarya. Se essa condição for atendida, a função conectada verifica o 6º caractere, que é o comando que será executado. Por fim, é verificado o 8º caractere, que pode conter argumentos de processo para o comando que está sendo executado. A estrutura se parece com: zarya[char][command][char][argument]. Qualquer caractere especial (ou nenhum) pode ser colocado entre zarya e os comandos e argumentos.

Até a data da publicação, identificamos os seguintes comandos:

ComandoObjetivo
zarya.c.0Retrieve the config
zarya.t.0Teste o funcionamento
zarya.k.<pid>Ocultar um PID
zarya.v.0Obtenha a versão em execução

Após a inicialização do rootkit, o hook syscall rmdir() é usado para verificar se o rootkit foi carregado com sucesso. Ele faz isso chamando o comando t .

ubuntu-rk:~$ rmdir test
rmdir: failed to remove 'test': No such file or directory
ubuntu-rk:~$ rmdir zarya.t
ubuntu-rk:~$

Ao usar o comando rmdir em um diretório inexistente, uma mensagem de erro “Nenhum arquivo ou diretório” é retornada. Ao usar rmdir em zarya.t, nenhuma saída é retornada, indicando o carregamento bem-sucedido do módulo do kernel.

Um segundo comando é v, que é usado para obter a versão do rootkit em execução.

ubuntu-rk:~$ rmdir zarya.v
rmdir: failed to remove '240513': No such file or directory

Em vez de zarya.v ser adicionado ao erro “falha ao remover ' directory'”, a versão do rootkit 240513 é retornada.

Um terceiro comando é c, que imprime a configuração do rootkit.

ubuntu-rk:~/testing$ ./dump_config "zarya.c"
rmdir: failed to remove '': No such file or directory
Buffer contents (hex dump):
7ffe9ae3a270  00 01 00 00 10 70 69 6e 67 5f 69 6e 74 65 72 76  .....ping_interv
7ffe9ae3a280  61 6c 5f 73 00 2c 01 00 00 10 73 65 73 73 69 6f  al_s.,....sessio
7ffe9ae3a290  6e 5f 74 69 6d 65 6f 75 74 5f 73 00 04 00 00 00  n_timeout_s.....
7ffe9ae3a2a0  10 63 32 5f 74 69 6d 65 6f 75 74 5f 73 00 c0 a8  .c2_timeout_s...
7ffe9ae3a2b0  00 00 02 74 61 67 00 08 00 00 00 67 65 6e 65 72  ...tag.....gener
7ffe9ae3a2c0  69 63 00 02 73 5f 61 30 00 15 00 00 00 72 68 65  ic..s_a0.....rhe
7ffe9ae3a2d0  6c 2e 6f 70 73 65 63 75 72 69 74 79 31 2e 61 72  l.opsecurity1.ar
7ffe9ae3a2e0  74 00 02 73 5f 70 30 00 05 00 00 00 38 34 34 33  t..s_p0.....8443
7ffe9ae3a2f0  00 02 73 5f 63 30 00 04 00 00 00 74 6c 73 00 02  ..s_c0.....tls..
7ffe9ae3a300  73 5f 61 31 00 14 00 00 00 73 65 63 2e 6f 70 73  s_a1.....sec.ops
7ffe9ae3a310  65 63 75 72 69 74 79 31 2e 61 72 74 00 02 73 5f  ecurity1.art..s_
7ffe9ae3a320  70 31 00 05 00 00 00 38 34 34 33 00 02 73 5f 63  p1.....8443..s_c
7ffe9ae3a330  31 00 04 00 00 00 74 6c 73 00 02 73 5f 61 32 00  1.....tls..s_a2.
7ffe9ae3a340  0e 00 00 00 38 39 2e 32 33 2e 31 31 33 2e 32 30  ....89.23.113.20
7ffe9ae3a350  34 00 02 73 5f 70 32 00 05 00 00 00 38 34 34 33  4..s_p2.....8443
7ffe9ae3a360  00 02 73 5f 63 32 00 04 00 00 00 74 6c 73 00 00  ..s_c2.....tls..

Como a carga útil começa com bytes nulos, nenhuma saída é retornada ao executar zarya.c por meio de um comando de shell rmdir . Ao escrever um pequeno programa em C que encapsula a chamada de sistema e imprime a representação hexadecimal/ASCII, podemos ver a configuração do rootkit sendo retornada.

Em vez de usar a chamada de sistema kill() para obter privilégios de root (como a maioria dos rootkits faz), o rootkit utiliza a chamada de sistema rmdir() para esse propósito também. O rootkit usa a função prepare_creds para modificar os IDs relacionados às credenciais para 0 (root) e chama commit_creds nessa estrutura modificada para obter privilégios de root em seu processo atual.

Para acionar esta função, precisamos definir o 6º caractere como 0. A ressalva para esse hook é que ele concede privilégios de root ao processo chamador, mas não os mantém. Ao executar zarya.0, nada acontece. Entretanto, ao chamar esse gancho com um programa C e imprimir os privilégios do processo atual, obtemos um resultado. Um trecho do código wrapper usado é exibido abaixo:

[...]
// Print the current PID, SID, and GID
pid_t pid = getpid();
pid_t sid = getsid(0);  // Passing 0 gets the SID of the calling process
gid_t gid = getgid();

printf("Current PID: %d, SID: %d, GID: %d\n", pid, sid, gid);

// Print all credential-related IDs
uid_t ruid = getuid();    // Real user ID
uid_t euid = geteuid();   // Effective user ID
gid_t rgid = getgid();    // Real group ID
gid_t egid = getegid();   // Effective group ID
uid_t fsuid = setfsuid(-1);  // Filesystem user ID
gid_t fsgid = setfsgid(-1);  // Filesystem group ID

printf("Credentials: UID=%d, EUID=%d, GID=%d, EGID=%d, FSUID=%d, FSGID=%d\n",
    ruid, euid, rgid, egid, fsuid, fsgid);

[...]

Executando a função, podemos obter a seguinte saída:

ubuntu-rk:~/testing$ whoami;id
ruben
uid=1000(ruben) gid=1000(ruben) groups=1000(ruben),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd)

ubuntu-rk:~/testing$ ./rmdir zarya.0
Received data:
zarya.0
Current PID: 41838, SID: 35117, GID: 0
Credentials: UID=0, EUID=0, GID=0, EGID=0, FSUID=0, FSGID=0

Para aproveitar esse gancho, escrevemos um pequeno script wrapper C que executa o comando rmdir zarya.0 e verifica se agora pode acessar o arquivo /etc/shadow .

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>

int main() {
    const char *directory = "zarya.0";

    // Attempt to remove the directory
    if (syscall(SYS_rmdir, directory) == -1) {
        fprintf(stderr, "rmdir: failed to remove '%s': %s\n", directory, strerror(errno));
    } else {
        printf("rmdir: successfully removed '%s'\n", directory);
    }

    // Execute the `id` command
    printf("\n--- Running 'id' command ---\n");
    if (system("id") == -1) {
        perror("Failed to execute 'id'");
        return 1;
    }

    // Display the contents of /etc/shadow
    printf("\n--- Displaying '/etc/shadow' ---\n");
    if (system("cat /etc/shadow") == -1) {
        perror("Failed to execute 'cat /etc/shadow'");
        return 1;
    }

    return 0;
}

Com sucesso.

ubuntu-rk:~/testing$ ./get_root
rmdir: successfully removed 'zarya.0'

--- Running 'id' command ---
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd),1000(ruben)

--- Displaying '/etc/shadow' ---
root:*:19430:0:99999:7:::
[...]

Embora existam mais comandos disponíveis na função rmdir() , por enquanto, passaremos para o próximo e podemos adicioná-los a uma publicação futura.

Visão geral do rootkit LKM: ganchos getdents() e getdents64()

getdents_hook() e getdents64_hook() no rootkit são responsáveis por manipular syscalls de listagem de diretórios para ocultar arquivos e diretórios dos usuários.

As chamadas de sistema getdents() e getdents64() são usadas para ler entradas de diretório. O rootkit intercepta essas funções para filtrar quaisquer entradas que correspondam a critérios específicos. Especificamente, arquivos e diretórios com o prefixo zov_ ficam ocultos de qualquer usuário que tente listar o conteúdo de um diretório.

Por exemplo:

ubuntu-rk:~/getdents_hook$ mkdir zov_hidden_dir

ubuntu-rk:~/getdents_hook$ ls -lah
total 8.0K
drwxrwxr-x  3 ruben ruben 4.0K Dec  9 11:11 .
drwxr-xr-x 11 ruben ruben 4.0K Dec  9 11:11 ..

ubuntu-rk:~/getdents_hook$ echo "this file is now hidden" > zov_hidden_dir/zov_hidden_file

ubuntu-rk:~/getdents_hook$ ls -lah zov_hidden_dir/
total 8.0K
drwxrwxr-x 2 ruben ruben 4.0K Dec  9 11:11 .
drwxrwxr-x 3 ruben ruben 4.0K Dec  9 11:11 ..

ubuntu-rk:~/getdents_hook$ cat zov_hidden_dir/zov_hidden_file
this file is now hidden

Aqui, o arquivo zov_hidden pode ser acessado diretamente usando seu caminho completo. Entretanto, ao executar o comando ls , ele não aparece na listagem de diretórios.

Etapa 4: Visão geral do Kitsune SO

Ao investigar mais profundamente o rootkit, outro arquivo ELF foi identificado dentro do arquivo de objeto do kernel. Depois de extrair esse binário, descobrimos que este é o arquivo /lib64/libs.so . Após análise, encontramos diversas referências a strings como Kitsune PID %ld. Isso sugere que o SO é chamado de Kitsune pelos desenvolvedores. O Kitsune pode ser responsável por certos comportamentos observados no rootkit. Essas referências se alinham com o contexto mais amplo de como o rootkit manipula as interações do espaço do usuário por meio de LD_PRELOAD.

Este arquivo SO desempenha um papel na obtenção dos mecanismos de persistência e furtividade centrais deste rootkit, e sua integração na cadeia de ataque demonstra a sofisticação de seu design. Agora mostraremos como detectar e/ou prevenir cada parte da cadeia de ataque.

Detecção e prevenção da cadeia de execução PUMAKIT

Esta seção exibirá diferentes regras EQL/KQL e assinaturas YARA que podem prevenir e detectar diferentes partes da cadeia de execução do PUMAKIT.

Estágio 1: Cron

Após a execução do dropper, um evento incomum é salvo no syslog. O evento informa que um processo foi iniciado com uma pilha executável. Isso é incomum e interessante de observar:

[  687.108154] process '/home/ruben_groenewoud/30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f' started with executable stack

Podemos pesquisar isso por meio da seguinte consulta:

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message: "started with executable stack"

Esta mensagem é armazenada em /var/log/messages ou /var/log/syslog. Podemos detectar isso lendo o syslog por meio do Filebeat ou da integração do sistema do agente Elastic.

Estágio 2: Executáveis residentes na memória

Podemos ver imediatamente uma execução incomum do descritor de arquivo. Isso pode ser detectado por meio da seguinte consulta EQL:

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.parent.executable like "/dev/fd/*" and not process.parent.command_line == "runc init"

Este descritor de arquivo permanecerá como pai do dropper até que o processo termine, resultando na execução de vários arquivos também por meio deste processo pai:

file where host.os.type == "linux" and event.type == "creation" and process.executable like "/dev/fd/*" and file.path like (
  "/boot/*", "/dev/shm/*", "/etc/cron.*/*", "/etc/init.d/*", "/var/run/*"
  "/etc/update-motd.d/*", "/tmp/*", "/var/log/*", "/var/tmp/*"
)

Depois que /tmp/script.sh é descartado (detectado pelas consultas acima), podemos detectar sua execução consultando a descoberta de atributos de arquivo e a atividade de desarquivamento:

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and 
(process.parent.args like "/boot/*" or process.args like "/boot/*") and (
  (process.name in ("file", "unlzma", "gunzip", "unxz", "bunzip2", "unzstd", "unzip", "tar")) or
  (process.name == "grep" and process.args == "ELF") or
  (process.name in ("lzop", "lz4") and process.args in ("-d", "--decode"))
) and
not process.parent.name == "mkinitramfs"

O script continua buscando a memória da imagem do kernel Linux por meio do comando tail . Isso pode ser detectado, junto com outras ferramentas de busca de memória, por meio da seguinte consulta:

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and
(process.parent.args like "/boot/*" or process.args like "/boot/*") and (
  (process.name == "tail" and (process.args like "-c*" or process.args == "--bytes")) or
  (process.name == "cmp" and process.args == "-i") or
  (process.name in ("hexdump", "xxd") and process.args == "-s") or
  (process.name == "dd" and process.args : ("skip*", "seek*"))
)

Quando /tmp/script.sh terminar de executar, /memfd:tgt (deleted) e /memfd:wpn (deleted) serão criados. O executável tgt , que é o executável Cron benigno, cria um arquivo /run/crond.pid . Isso não é nada malicioso, mas um artefato que pode ser detectado por meio de uma consulta simples.

file where host.os.type == "linux" and event.type == "creation" and file.extension in ("lock", "pid") and
file.path like ("/tmp/*", "/var/tmp/*", "/run/*", "/var/run/*", "/var/lock/*", "/dev/shm/*") and process.executable != null

O executável wpn , se todas as condições forem atendidas, carregará o LKMrootkit.

Estágio 3: Módulo do kernel do rootkit

O carregamento do módulo do kernel é detectável através do Auditd Manager aplicando a seguinte configuração:

-a always,exit -F arch=b64 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules
-a always,exit -F arch=b32 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules

E usando a seguinte consulta:

driver where host.os.type == "linux" and event.action == "loaded-kernel-module" and auditd.data.syscall in ("init_module", "finit_module")

Para obter mais informações sobre como aproveitar o Auditd com o Elastic Security para aprimorar sua experiência em engenharia de detecção do Linux, confira nossa pesquisa sobre engenharia de detecção do Linux com o Auditd publicada no site do Elastic Security Labs.

Na inicialização, o LKM contamina o kernel, pois não é assinado.

audit: module verification failed: signature and/or required key missing - tainting kernel

Podemos detectar esse comportamento por meio da seguinte consulta KQL:

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:"module verification failed: signature and/or required key missing - tainting kernel"

Além disso, o LKM tem um código defeituoso, o que faz com que ele falhe várias vezes. Por exemplo:

Dec  9 13:26:10 ubuntu-rk kernel: [14350.711419] cat[112653]: segfault at 8c ip 00007f70d596b63c sp 00007fff9be81360 error 4
Dec  9 13:26:10 ubuntu-rk kernel: [14350.711422] Code: 83 c4 20 48 89 d0 5b 5d 41 5c c3 48 8d 42 01 48 89 43 08 0f b6 02 41 88 44 2c ff eb c1 8b 7f 78 e9 25 5c 00 00 c3 41 54 55 53 <8b> 87 8c 00 00 00 48 89 fb 85 c0 79 1b e8 d7 00 00 00 48 89 df 89

Isso pode ser detectado por meio de uma consulta KQL simples que procura falhas de segmento no arquivo kern.log .

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:segfault

Depois que o módulo do kernel é carregado, podemos ver rastros da execução do comando através do processo kthreadd . O rootkit cria novos threads de kernel para executar comandos específicos. Por exemplo, o rootkit executa os seguintes comandos em intervalos curtos:

cat /dev/null
truncate -s 0 /usr/share/zov_f/zov_latest

Podemos detectar esses e outros comandos potencialmente suspeitos por meio de uma consulta como a seguinte:

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.parent.name == "kthreadd" and (
  process.executable like ("/tmp/*", "/var/tmp/*", "/dev/shm/*", "/var/www/*", "/bin/*", "/usr/bin/*", "/usr/local/bin/*") or
  process.name in ("bash", "dash", "sh", "tcsh", "csh", "zsh", "ksh", "fish", "whoami", "curl", "wget", "id", "nohup", "setsid") or
  process.command_line like (
    "*/etc/cron*", "*/etc/rc.local*", "*/dev/tcp/*", "*/etc/init.d*", "*/etc/update-motd.d*",
    "*/etc/ld.so*", "*/etc/sudoers*", "*base64 *", "*base32 *", "*base16 *", "*/etc/profile*",
    "*/dev/shm/*", "*/etc/ssh*", "*/home/*/.ssh/*", "*/root/.ssh*" , "*~/.ssh/*", "*autostart*",
    "*xxd *", "*/etc/shadow*"
  )
) and not process.name == "dpkg"

Também podemos detectar o método dos rootkits de elevar privilégios analisando o comando rmdir em busca de alterações incomuns de UID/GID.

process where host.os.type == "linux" and event.type == "change" and event.action in ("uid_change", "guid_change") and process.name == "rmdir"

Várias outras regras comportamentais também podem ser acionadas, dependendo da cadeia de execução.

Uma assinatura YARA para governar todos eles

O Elastic Security criou uma assinatura YARA para identificar o PUMAKIT (o dropper (cron), o rootkit loader (/memfd:wpn), o rootkit LKM e os arquivos de objeto compartilhados do Kitsune. A assinatura é exibida abaixo:

rule Linux_Trojan_Pumakit {
    meta:
        author = "Elastic Security"
        creation_date = "2024-12-09"
        last_modified = "2024-12-09"
        os = "Linux"
        arch = "x86, arm64"
        threat_name = "Linux.Trojan.Pumakit"

    strings:
        $str1 = "PUMA %s"
        $str2 = "Kitsune PID %ld"
        $str3 = "/usr/share/zov_f"
        $str4 = "zarya"
        $str5 = ".puma-config"
        $str6 = "ping_interval_s"
        $str7 = "session_timeout_s"
        $str8 = "c2_timeout_s"
        $str9 = "LD_PRELOAD=/lib64/libs.so"
        $str10 = "kit_so_len"
        $str11 = "opsecurity1.art"
        $str12 = "89.23.113.204"
    
    condition:
        4 of them
}

Observações

Os seguintes observáveis foram discutidos nesta pesquisa.

ObservávelTipoNomeReferência
30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1fSHA256cronConta-gotas PUMAKIT
cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfeSHA256/memfd:wpn (deleted)Carregador PUMAKIT
934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136SHA256/memfd:tgt (deleted)Cron binário
8ef63f9333104ab293eef5f34701669322f1c07c0e44973d688be39c94986e27SHA256libs.soReferência de objeto compartilhado Kitsune
8ad422f5f3d0409747ab1ac6a0919b1fa8d83c3da43564a685ae4044d0a0ea03SHA256some2.elfVariante PUMAKIT
bbf0fd636195d51fb5f21596d406b92f9e3d05cd85f7cd663221d7d3da8af804SHA256some1.soVariante de objeto compartilhado Kitsune
bc9193c2a8ee47801f5f44beae51ab37a652fda02cd32d01f8e88bb793172491SHA256puma.koLKM rootkit
1aab475fb8ad4a7f94a7aa2b17c769d6ae04b977d984c4e842a61fb12ea99f58SHA256kitsune.soKitsune
sec.opsecurity1[.]artnome de domínioServidor PUMAKIT C2
rhel.opsecurity1[.]artnome de domínioServidor PUMAKIT C2
89.23.113[.]204endereço-ipv4Servidor PUMAKIT C2

Declaração Final

PUMAKIT é uma ameaça complexa e furtiva que usa técnicas avançadas como interceptação de chamadas de sistema, execução residente na memória e métodos exclusivos de escalonamento de privilégios. Seu design multiarquitetônico destaca a sofisticação crescente do malware direcionado aos sistemas Linux.

O Elastic Security Labs continuará analisando o PUMAKIT, monitorando seu comportamento e rastreando quaisquer atualizações ou novas variantes. Ao refinar os métodos de detecção e compartilhar insights úteis, pretendemos manter os defensores um passo à frente.

Compartilhe este artigo