Shell nuclear sobre ICMP





TL; DR : Estou escrevendo um módulo do kernel que lerá comandos do payload ICMP e os executará no servidor mesmo se o SSH travar. Para os mais impacientes, todo o código está no github .



Cuidado! Programadores C experientes correm o risco de explodir em lágrimas de sangue! Posso estar errado até na terminologia, mas qualquer crítica é bem-vinda. O post é direcionado para aqueles que têm uma ideia geral da programação C e querem dar uma olhada nos detalhes internos do Linux.



Nos comentários do meu primeiro artigomencionou SoftEther VPN, que pode imitar alguns protocolos "normais", em particular, HTTPS, ICMP e até DNS. Só posso imaginar o trabalho do primeiro deles, pois estou muito familiarizado com HTTP (S), e tive que aprender tunelamento sobre ICMP e DNS.



imagem



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



Pegamos um editor de texto, habilidades básicas de programação em Python e C, google e uma máquina virtual que você não se importaria de colocar na faca se tudo quebrar (opcional - VirtualBox / KVM / etc local) e vamos lá!



Parte do cliente



Pareceu-me que do lado do cliente eu teria que escrever um roteiro de 80 linhas, mas houve pessoas gentis que fizeram todo o trabalho para mim . O código acabou sendo surpreendentemente simples, ele se encaixa 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 de enviar, a carga útil é precedida por uma chave run:, precisaremos dela para excluir pacotes com uma carga aleatória.



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



Executar e emitir um 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!




É assim que fica 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 útil no pacote de resposta não muda.



Módulo Kernel



Para construir uma máquina virtual com o Debian, você precisará de pelo menos makee linux-headers-amd64, o resto será restringido como dependências. Não vou dar todo o código no artigo, você pode cloná-lo no github.



Configuração de gancho



Primeiro, precisamos de duas funções para carregar o módulo e para descarregá-lo. A função de descarregamento não é obrigatória, mas então rmmodnão funcionará, o módulo será descarregado somente quando estiver 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 puxados para manipular o próprio módulo e o netfilter.
  2. , . , . — , : nfho.hook = icmp_cmd_executor; .

    : NF_INET_PRE_ROUTING , . NF_INET_POST_ROUTING .

    IPv4: nfho.pf = PF_INET;.

    : nfho.priority = NF_IP_PRI_FIRST;

    : nf_register_net_hook(&init_net, &nfho);
  3. .
  4. , .
  5. module_init() module_exit() .




Agora precisamos extrair a carga útil, o que acabou sendo a tarefa mais difícil. O kernel não tem funções integradas para trabalhar com carga útil, você só pode analisar os 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 == '\0')
      break;

    if (j == MAX_CMD_LEN) {
      cmd_string[j] = '\0';
      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] == '\0')
	break;
    }
  }

  schedule_work(&my_work);

  return NF_ACCEPT;
}


O que está acontecendo:



  1. Tive que incluir arquivos de cabeçalho adicionais, desta vez para manipular os cabeçalhos IP e ICMP.
  2. Especifica o comprimento máximo de uma string: #define MAX_CMD_LEN 1976. Por que exatamente isso? Porque o compilador jura pelo grande! Eles já me disseram que preciso lidar com a pilha e o heap, algum dia com certeza farei isso e talvez até corrija o código. Imediatamente define uma string em que a equipe será baseado: char cmd_string[MAX_CMD_LEN];. Deve ser visível em todas as funções, falarei sobre isso com mais detalhes no parágrafo 9.
  3. (struct work_struct my_work;) (DECLARE_WORK(my_work, work_handler);). , , .
  4. , . , skb. , , .
  5. , , .



      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. . ICMP Echo, ICMP- Echo-. NF_ACCEPT , , 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;
      }




    , IP. C : - . , !
  7. , , . . , ICMP . icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));

    skb, : tail = skb_tail_pointer(skb);.



    imagem



    , .
  8. , cmd_string, run: , , , .
  9. , : schedule_work(&my_work);. , . schedule_work() , . . , , kernel panic. !
  10. , .




Esta função é a mais direta. Seu nome foi especificado em DECLARE_WORK(), o tipo e os argumentos aceitos não são interessantes. Pegamos a linha de comando e a passamos inteiramente para o shell. Deixe que ele lide com a análise, procurando por binários e tudo mais sozinho.



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. Definimos os argumentos para um array de strings argv[]. Vou assumir que todos sabem que os programas são realmente executados dessa maneira, e não uma linha contínua com espaços.
  2. Configurando variáveis ​​de ambiente. Inseri apenas PATH com um conjunto mínimo de caminhos, esperando que todos já tenham sido combinados /bincom /usr/bine /sbincom /usr/sbin. Outros caminhos raramente importam na prática.
  3. , ! call_usermodehelper() . , , . , , . , (UMH_WAIT_PROC), (UMH_WAIT_EXEC) (UMH_NO_WAIT). UMH_KILLABLE, .




A construção de módulos do kernel é feita através de um framework make do kernel. É chamado makedentro 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 Mnos argumentos. Os destinos icmpshell.ko e clean usam essa estrutura inteiramente. Em obj-mespecifica o arquivo-objeto que será convertido em um módulo. A sintaxe que ele remove main.oem icmpshell.o( icmpshell-objs = main.o) não parece muito lógica para mim, mas deixe assim. Colocando: . Carga: . Feito, você pode verificar: . Se aparecer um arquivo em sua máquina com a data de envio da solicitação, você fez tudo certo e eu fiz tudo certo.



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




makeinsmod icmpshell.kosudo ./send.py 45.11.26.232 "date > /tmp/test"/tmp/test



Conclusão



Minha primeira experiência com engenharia nuclear foi muito mais simples do que eu esperava. Mesmo sem experiência em desenvolvimento C, com foco em dicas do compilador e saída do Google, fui capaz de 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 em schedule_work()vez de chamar call_usermodehelper()dentro do próprio gancho e me envergonharam, com razão, suspeitando de um golpe. Cem linhas de código me custaram cerca de uma semana de desenvolvimento em meu tempo livre. Uma experiência de sucesso 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, eu ficaria grato. Tenho certeza de que cometi muitos erros estúpidos, especialmente ao lidar com cordas.






All Articles