Concha nuclear sobre ICMP

Concha nuclear sobre ICMP

TL, DR: estou escrevendo um módulo do kernel que lerá comandos da carga ICMP e os executará no servidor mesmo se seu SSH travar. Para os mais impacientes, todo o código é github.

Cuidado! Programadores C experientes correm o risco de explodir em lágrimas de sangue! Posso até estar errado na terminologia, mas qualquer crítica é bem vinda. O post é destinado a quem tem uma ideia bem aproximada de programação C e quer dar uma olhada no interior do Linux.

Nos comentários ao meu primeiro статье mencionou o SoftEther VPN, que pode imitar alguns protocolos “regulares”, em particular HTTPS, ICMP e até DNS. Posso imaginar apenas o primeiro deles funcionando, já que estou muito familiarizado com HTTP(S) e tive que aprender a encapsular ICMP e DNS.

Concha nuclear sobre ICMP

Sim, em 2020 aprendi que você pode inserir uma carga arbitrária em pacotes ICMP. Mas antes tarde do que nunca! E já que algo pode ser feito a respeito, então precisa ser feito. Como no meu dia a dia eu uso mais a linha de comando, inclusive via SSH, a ideia de um shell ICMP veio primeiro à minha mente. E para montar um bingo completo, decidi escrevê-lo como um módulo Linux em uma linguagem que só tenho uma ideia aproximada. Esse shell não estará visível na lista de processos, você pode carregá-lo no kernel e não estará no sistema de arquivos, você não verá nada suspeito na lista de portas de escuta. Em termos de suas capacidades, este é um rootkit completo, mas espero melhorá-lo e usá-lo como um shell de último recurso quando a média de carga for muito alta para fazer login via SSH e executar pelo menos echo i > /proc/sysrq-triggerpara restaurar o acesso sem reiniciar.

Levamos um editor de texto, conhecimentos básicos de programação em Python e C, Google e virtual que você não se importa de colocar na faca se tudo quebrar (opcional - VirtualBox/KVM/etc local) e vamos lá!

Lado do cliente

Pareceu-me que para a parte do cliente eu teria que escrever um roteiro com cerca de 80 linhas, mas houve pessoas gentis que fizeram isso por mim todo o trabalho. O código revelou-se inesperadamente simples, cabendo em 10 linhas significativas:

import sys
from scapy.all import sr1, IP, ICMP

if len(sys.argv) < 3:
    print('Usage: {} IP "command"'.format(sys.argv[0]))
    exit(0)

p = sr1(IP(dst=sys.argv[1])/ICMP()/"run:{}".format(sys.argv[2]))
if p:
    p.show()

O script leva dois argumentos, um endereço e uma carga útil. Antes do envio, a carga útil é precedida por uma chave run:, precisaremos dele para excluir pacotes com cargas aleatórias.

O kernel requer privilégios para criar pacotes, então o script terá que ser executado como superusuário. Não se esqueça de conceder permissões de execução e instalar o próprio scapy. O Debian tem um pacote chamado python3-scapy. Agora você pode conferir como tudo funciona.

Executando e gerando o comando
morq@laptop:~/icmpshell$ sudo ./send.py 45.11.26.232 "Hello, world!"
Begin emission:
.Finished sending 1 packets.
*
Received 2 packets, got 1 answers, remaining 0 packets
###[ IP ]###
version = 4
ihl = 5
tos = 0x0
len = 45
id = 17218
flags =
frag = 0
ttl = 58
proto = icmp
chksum = 0x3403
src = 45.11.26.232
dst = 192.168.0.240
options
###[ ICMP ]###
type = echo-reply
code = 0
chksum = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
load = 'run:Hello, world!

Isto é o que parece no farejador
morq@laptop:~/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Running as user "root" and group "root". This could be dangerous.
Capturing on 'wlp1s0'
Frame 1: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 192.168.0.240, Dst: 45.11.26.232
Internet Control Message Protocol
Type: 8 (Echo (ping) request)
Code: 0
Checksum: 0xd603 [correct] [Checksum Status: Good] Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]

Frame 2: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 45.11.26.232, Dst: 192.168.0.240
Internet Control Message Protocol
Type: 0 (Echo (ping) reply)
Code: 0
Checksum: 0xde03 [correct] [Checksum Status: Good] Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
[Request frame: 1] [Response time: 19.094 ms] Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]

^C2 packets captured

A carga no pacote de resposta não muda.

Módulo do kernel

Para construir uma máquina virtual Debian você precisará de pelo menos make и linux-headers-amd64, o resto virá na forma de dependências. Não fornecerei o código inteiro no artigo; você pode cloná-lo no Github.

Configuração do gancho

Para começar, precisamos de duas funções para carregar e descarregar o módulo. A função de descarga não é necessária, mas então rmmod não funcionará, o módulo só será descarregado quando desligado.

#include <linux/module.h>
#include <linux/netfilter_ipv4.h>

static struct nf_hook_ops nfho;

static int __init startup(void)
{
  nfho.hook = icmp_cmd_executor;
  nfho.hooknum = NF_INET_PRE_ROUTING;
  nfho.pf = PF_INET;
  nfho.priority = NF_IP_PRI_FIRST;
  nf_register_net_hook(&init_net, &nfho);
  return 0;
}

static void __exit cleanup(void)
{
  nf_unregister_net_hook(&init_net, &nfho);
}

MODULE_LICENSE("GPL");
module_init(startup);
module_exit(cleanup);

O que está acontecendo aqui:

  1. Dois arquivos de cabeçalho são extraídos para manipular o próprio módulo e o netfilter.
  2. Todas as operações passam por um netfilter, você pode colocar ganchos nele. Para fazer isso, você precisa declarar a estrutura na qual o gancho será configurado. O mais importante é especificar a função que será executada como gancho: nfho.hook = icmp_cmd_executor; Chegarei à função em si mais tarde.
    Então defino o tempo de processamento do pacote: NF_INET_PRE_ROUTING especifica o processamento do pacote quando ele aparece pela primeira vez no kernel. Pode ser usado NF_INET_POST_ROUTING para processar o pacote conforme ele sai do kernel.
    Eu configurei o filtro para IPv4: nfho.pf = PF_INET;.
    Dou ao meu gancho a maior prioridade: nfho.priority = NF_IP_PRI_FIRST;
    E eu registro a estrutura de dados como o gancho real: nf_register_net_hook(&init_net, &nfho);
  3. A função final remove o gancho.
  4. A licença está claramente indicada para que o compilador não reclame.
  5. funções module_init() и module_exit() defina outras funções para inicializar e encerrar o módulo.

Recuperando a carga útil

Agora precisamos extrair a carga útil, esta acabou sendo a tarefa mais difícil. O kernel não possui funções integradas para trabalhar com cargas úteis; você só pode analisar cabeçalhos de protocolos de nível superior.

#include <linux/ip.h>
#include <linux/icmp.h>

#define MAX_CMD_LEN 1976

char cmd_string[MAX_CMD_LEN];

struct work_struct my_work;

DECLARE_WORK(my_work, work_handler);

static unsigned int icmp_cmd_executor(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
  struct iphdr *iph;
  struct icmphdr *icmph;

  unsigned char *user_data;
  unsigned char *tail;
  unsigned char *i;
  int j = 0;

  iph = ip_hdr(skb);
  icmph = icmp_hdr(skb);

  if (iph->protocol != IPPROTO_ICMP) {
    return NF_ACCEPT;
  }
  if (icmph->type != ICMP_ECHO) {
    return NF_ACCEPT;
  }

  user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
  tail = skb_tail_pointer(skb);

  j = 0;
  for (i = user_data; i != tail; ++i) {
    char c = *(char *)i;

    cmd_string[j] = c;

    j++;

    if (c == '')
      break;

    if (j == MAX_CMD_LEN) {
      cmd_string[j] = '';
      break;
    }

  }

  if (strncmp(cmd_string, "run:", 4) != 0) {
    return NF_ACCEPT;
  } else {
    for (j = 0; j <= sizeof(cmd_string)/sizeof(cmd_string[0])-4; j++) {
      cmd_string[j] = cmd_string[j+4];
      if (cmd_string[j] == '')
	break;
    }
  }

  schedule_work(&my_work);

  return NF_ACCEPT;
}

O que acontece:

  1. Tive que incluir arquivos de cabeçalho adicionais, desta vez para manipular cabeçalhos IP e ICMP.
  2. Eu defini o comprimento máximo da linha: #define MAX_CMD_LEN 1976. Por que exatamente isso? Porque o compilador reclama disso! Já me sugeriram que eu preciso entender pilha e heap, algum dia com certeza farei isso e quem sabe até corrigirei o código. Defino imediatamente a linha que conterá o comando: char cmd_string[MAX_CMD_LEN];. Deve estar visível em todas as funções; falarei sobre isso com mais detalhes no parágrafo 9.
  3. Agora precisamos inicializar (struct work_struct my_work;) estrutura e conectá-lo com outra função (DECLARE_WORK(my_work, work_handler);). Também falarei sobre por que isso é necessário no nono parágrafo.
  4. Agora declaro uma função, que será um gancho. O tipo e os argumentos aceitos são ditados pelo netfilter, estamos interessados ​​apenas em skb. Este é um buffer de soquete, uma estrutura de dados fundamental que contém todas as informações disponíveis sobre um pacote.
  5. Para que a função funcione, você precisará de duas estruturas e diversas variáveis, incluindo dois iteradores.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Podemos começar com lógica. Para que o módulo funcione, nenhum pacote além do ICMP Echo é necessário, então analisamos o buffer usando funções integradas e descartamos todos os pacotes não ICMP e não Echo. Retornar NF_ACCEPT significa aceitação do pacote, mas você também pode devolver os pacotes devolvendo NF_DROP.
      iph = ip_hdr(skb);
      icmph = icmp_hdr(skb);
    
      if (iph->protocol != IPPROTO_ICMP) {
        return NF_ACCEPT;
      }
      if (icmph->type != ICMP_ECHO) {
        return NF_ACCEPT;
      }

    Não testei o que acontecerá sem verificar os cabeçalhos IP. Meu conhecimento mínimo de C me diz que, sem verificações adicionais, algo terrível está prestes a acontecer. Ficarei feliz se você me dissuadir disso!

  7. Agora que o pacote é do tipo exato que você precisa, você pode extrair os dados. Sem uma função integrada, primeiro você precisa obter um ponteiro para o início da carga útil. Isso é feito em um só lugar, você precisa apontar o ponteiro para o início do cabeçalho ICMP e movê-lo para o tamanho deste cabeçalho. Tudo usa estrutura icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    O final do cabeçalho deve corresponder ao final da carga útil em skb, portanto, obtemos usando meios nucleares da estrutura correspondente: tail = skb_tail_pointer(skb);.

    Concha nuclear sobre ICMP

    A foto foi roubada por isso, você pode ler mais sobre o buffer de soquete.

  8. Depois de ter ponteiros para o início e o fim, você pode copiar os dados em uma string cmd_string, verifique a presença de um prefixo run: e descarte o pacote se estiver faltando ou reescreva a linha novamente, removendo este prefixo.
  9. É isso, agora você pode chamar outro manipulador: schedule_work(&my_work);. Como não será possível passar um parâmetro para tal chamada, a linha com o comando deverá ser global. schedule_work() colocará a função associada à estrutura passada na fila geral do agendador de tarefas e será concluída, permitindo que você não espere a conclusão do comando. Isto é necessário porque o gancho deve ser muito rápido. Caso contrário, sua escolha é que nada será iniciado ou você terá um kernel panic. Atraso é como a morte!
  10. É isso, você pode aceitar o pacote com a devolução correspondente.

Chamando um programa no espaço do usuário

Esta função é a mais compreensível. Seu nome foi dado em DECLARE_WORK(), o tipo e os argumentos aceitos não são interessantes. Pegamos a linha com o comando e a passamos inteiramente para o shell. Deixe-o lidar com a análise, a busca por binários e tudo mais.

static void work_handler(struct work_struct * work)
{
  static char *argv[] = {"/bin/sh", "-c", cmd_string, NULL};
  static char *envp[] = {"PATH=/bin:/sbin", NULL};

  call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
}

  1. Defina os argumentos para uma matriz de strings argv[]. Presumo que todos saibam que os programas são executados dessa maneira, e não como uma linha contínua com espaços.
  2. Defina variáveis ​​de ambiente. Inseri apenas PATH com um conjunto mínimo de caminhos, esperando que todos já estivessem combinados /bin с /usr/bin и /sbin с /usr/sbin. Outros caminhos raramente importam na prática.
  3. Pronto, vamos lá! Função do kernel call_usermodehelper() aceita entrada. caminho para o binário, matriz de argumentos, matriz de variáveis ​​de ambiente. Aqui também presumo que todos entendam o significado de passar o caminho para o arquivo executável como um argumento separado, mas você pode perguntar. O último argumento especifica se deve-se esperar a conclusão do processo (UMH_WAIT_PROC), início do processo (UMH_WAIT_EXEC) ou não esperar (UMH_NO_WAIT). Existe mais algum UMH_KILLABLE, eu não olhei para isso.

montagem

A montagem dos módulos do kernel é realizada através do make-framework do kernel. Chamado make dentro de um diretório especial vinculado à versão do kernel (definido aqui: KERNELDIR:=/lib/modules/$(shell uname -r)/build), e a localização do módulo é passada para a variável M nos argumentos. Os alvos icmpshell.ko e clean usam essa estrutura inteiramente. EM obj-m indica o arquivo objeto que será convertido em módulo. Sintaxe que refaz main.o в icmpshell.o (icmpshell-objs = main.o) não me parece muito lógico, mas que assim seja.

KERNELDIR:=/lib/modules/$(shell uname -r)/build

obj-m = icmpshell.o
icmpshell-objs = main.o

all: icmpshell.ko

icmpshell.ko: main.c
make -C $(KERNELDIR) M=$(PWD) modules

clean:
make -C $(KERNELDIR) M=$(PWD) clean

Nós coletamos: make. Carregando: insmod icmpshell.ko. Pronto, você pode verificar: sudo ./send.py 45.11.26.232 "date > /tmp/test". Se você tiver um arquivo em sua máquina /tmp/test e contém a data em que a solicitação foi enviada, o que significa que você fez tudo certo e eu fiz tudo certo.

Conclusão

Minha primeira experiência com desenvolvimento nuclear foi muito mais fácil do que eu esperava. Mesmo sem experiência em desenvolvimento em C, com foco nas dicas do compilador e nos resultados do Google, consegui escrever um módulo funcional e me sentir como um hacker de kernel e, ao mesmo tempo, um script kiddie. Além disso, fui ao canal Kernel Newbies, onde me disseram para usar schedule_work() em vez de ligar call_usermodehelper() dentro do próprio anzol e o envergonhou, suspeitando com razão de uma fraude. Cem linhas de código me custaram cerca de uma semana de desenvolvimento no meu tempo livre. Uma experiência bem-sucedida que destruiu meu mito pessoal sobre a enorme complexidade do desenvolvimento de sistemas.

Se alguém concordar em fazer uma revisão de código no Github, ficarei grato. Tenho certeza de que cometi muitos erros estúpidos, especialmente ao trabalhar com cordas.

Concha nuclear sobre ICMP

Fonte: habr.com

Adicionar um comentário