Concha nuclear sobre ICMP

Concha nuclear sobre ICMP

TL, RD: Estou escribindo un módulo do núcleo que lerá os comandos da carga útil ICMP e executalos no servidor aínda que o seu SSH falle. Para os máis impacientes, todo o código é GitHub.

Atención! Os programadores C experimentados corren o risco de estourar en bágoas de sangue. Incluso quizais me equivoque na terminoloxía, pero calquera crítica é benvida. A publicación está destinada a aqueles que teñen unha idea moi aproximada da programación en C e queren explorar o interior de Linux.

Nos comentarios ao meu primeiro Artigo mencionou SoftEther VPN, que pode imitar algúns protocolos "regulares", en particular HTTPS, ICMP e incluso DNS. Podo imaxinar que só o primeiro deles funciona, xa que estou moi familiarizado co HTTP(S) e tiven que aprender a facer túneles a través de ICMP e DNS.

Concha nuclear sobre ICMP

Si, en 2020 aprendín que pode inserir unha carga útil arbitraria nos paquetes ICMP. Pero mellor tarde que nunca! E como se pode facer algo respecto diso, hai que facelo. Dado que na miña vida diaria uso máis a miúdo a liña de comandos, incluso a través de SSH, a idea dun shell ICMP veume primeiro á cabeza. E para montar un bingo completo, decidín escribilo como un módulo Linux nunha linguaxe da que só teño unha idea aproximada. Tal shell non estará visible na lista de procesos, pode cargalo no núcleo e non estará no sistema de ficheiros, non verá nada sospeitoso na lista de portos de escoita. En canto ás súas capacidades, este é un rootkit completo, pero espero melloralo e usalo como shell de último recurso cando a media de carga sexa demasiado alta para iniciar sesión a través de SSH e executalo polo menos. echo i > /proc/sysrq-triggerpara restaurar o acceso sen reiniciar.

Levamos un editor de texto, habilidades básicas de programación en Python e C, Google e virtuais que non che importa poñer debaixo do coitelo se todo se rompe (opcional - VirtualBox/KVM/etc local) e imos!

Lado do cliente

Pareceume que pola parte do cliente tería que escribir un guión cunhas 80 liñas, pero houbo xente amable que o fixo por min. todo o traballo. O código resultou ser inesperadamente sinxelo, encaixándose en 10 liñas 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 toma dous argumentos, un enderezo e unha carga útil. Antes de enviar, a carga útil vai precedida dunha clave run:, necesitarémolo para excluír paquetes con cargas útiles aleatorias.

O núcleo require privilexios para crear paquetes, polo que o script terá que ser executado como superusuario. Non esquezas dar permisos de execución e instalar o propio scapy. Debian ten un paquete chamado python3-scapy. Agora podes comprobar como funciona todo.

Execución e saída do 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!

Así se ve no sniffer
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 paquete de resposta non cambia.

Módulo do núcleo

Para construír unha máquina virtual Debian necesitará polo menos make и linux-headers-amd64, o resto virá en forma de dependencias. Non proporcionarei o código completo no artigo; podes clonalo en Github.

Configuración do gancho

Para comezar, necesitamos dúas funcións para cargar o módulo e para descargalo. A función de descarga non é necesaria, pero despois rmmod non funcionará; o módulo só se descargará cando estea desactivado.

#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);

Que está pasando aquí:

  1. Incorpóranse dous ficheiros de cabeceira para manipular o propio módulo e o netfilter.
  2. Todas as operacións pasan por un netfilter, podes establecer ganchos nel. Para iso, cómpre declarar a estrutura na que se configurará o gancho. O máis importante é especificar a función que se executará como gancho: nfho.hook = icmp_cmd_executor; Pasarei á función en si máis tarde.
    Despois configurei o tempo de procesamento do paquete: NF_INET_PRE_ROUTING especifica procesar o paquete cando aparece por primeira vez no núcleo. Pódese usar NF_INET_POST_ROUTING para procesar o paquete cando sae do núcleo.
    Definei o filtro en IPv4: nfho.pf = PF_INET;.
    Doulle ao meu gancho a máxima prioridade: nfho.priority = NF_IP_PRI_FIRST;
    E rexistro a estrutura de datos como o gancho real: nf_register_net_hook(&init_net, &nfho);
  3. A función final elimina o gancho.
  4. A licenza está claramente indicada para que o compilador non se queixa.
  5. Funcións module_init() и module_exit() establecer outras funcións para inicializar e finalizar o módulo.

Recuperando a carga útil

Agora necesitamos extraer a carga útil, esta resultou ser a tarefa máis difícil. O núcleo non ten funcións incorporadas para traballar con cargas útiles; só pode analizar as cabeceiras de protocolos de nivel 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;
}

Que pasa:

  1. Tiven que incluír ficheiros de cabeceira adicionais, esta vez para manipular as cabeceiras IP e ICMP.
  2. Define a lonxitude máxima da liña: #define MAX_CMD_LEN 1976. Por que exactamente isto? Porque o compilador quéixase diso! Xa me suxeriron que teño que entender a pila e o montón, algún día definitivamente farei isto e quizais ata corrixir o código. Inmediatamente configurei a liña que conterá o comando: char cmd_string[MAX_CMD_LEN];. Debería ser visible en todas as funcións; falarei sobre isto con máis detalle no parágrafo 9.
  3. Agora necesitamos inicializar (struct work_struct my_work;) estrutura e conéctao con outra función (DECLARE_WORK(my_work, work_handler);). Tamén falarei sobre por que isto é necesario no noveno parágrafo.
  4. Agora declaro unha función, que será un gancho. O tipo e os argumentos aceptados son ditados polo netfilter, só nos interesa skb. Este é un búfer de socket, unha estrutura de datos fundamental que contén toda a información dispoñible sobre un paquete.
  5. Para que a función funcione, necesitará dúas estruturas e varias variables, incluíndo dous iteradores.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Podemos comezar pola lóxica. Para que o módulo funcione, non se necesitan paquetes que non sexan ICMP Echo, polo que analizamos o búfer usando funcións integradas e eliminamos todos os paquetes que non sexan ICMP e non Echo. Volver NF_ACCEPT significa a aceptación do paquete, pero tamén pode soltar os paquetes 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;
      }

    Non probei o que pasará sen comprobar as cabeceiras IP. O meu coñecemento mínimo de C di que sen comprobacións adicionais, algo terrible está obrigado a ocorrer. Estarei feliz se me disuades disto!

  7. Agora que o paquete é do tipo exacto que necesitas, podes extraer os datos. Sen unha función integrada, primeiro ten que obter un punteiro para o inicio da carga útil. Isto faise nun só lugar, cómpre levar o punteiro ao inicio da cabeceira ICMP e movelo ao tamaño desta cabeceira. Todo usa estrutura icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    O final da cabeceira debe coincidir co final da carga útil skb, polo tanto obtemos mediante medios nucleares da estrutura correspondente: tail = skb_tail_pointer(skb);.

    Concha nuclear sobre ICMP

    A imaxe foi roubada por iso, podes ler máis sobre o socket buffer.

  8. Unha vez que teñas punteiros ao principio e ao final, podes copiar os datos nunha cadea cmd_string, comprobe a presenza dun prefixo run: e, descarta o paquete se falta, ou reescribe a liña de novo, eliminando este prefixo.
  9. Isto é todo, agora podes chamar a outro controlador: schedule_work(&my_work);. Dado que non será posible pasar un parámetro a tal chamada, a liña co comando debe ser global. schedule_work() colocará a función asociada á estrutura pasada na cola xeral do planificador de tarefas e completarase, o que lle permitirá non esperar a que se complete o comando. Isto é necesario porque o gancho debe ser moi rápido. En caso contrario, a túa elección é que non comezará nada ou terás pánico no núcleo. A demora é coma a morte!
  10. Xa está, podes aceptar o paquete cunha devolución correspondente.

Chamar un programa no espazo de usuario

Esta función é a máis comprensible. O seu nome foi dado en DECLARE_WORK(), o tipo e os argumentos aceptados non son interesantes. Collemos a liña co comando e pasámola totalmente ao shell. Que se ocupe de analizar, buscar binarios e todo o demais.

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. Establece os argumentos nunha matriz de cadeas argv[]. Supoño que todo o mundo sabe que os programas realmente se executan deste xeito, e non como unha liña continua con espazos.
  2. Establecer variables de ambiente. Inserei só PATH cun conxunto mínimo de camiños, esperando que xa estivesen todos combinados /bin с /usr/bin и /sbin с /usr/sbin. Outros camiños raramente importan na práctica.
  3. Feito, imos facelo! Función do núcleo call_usermodehelper() acepta entrada. camiño ao binario, matriz de argumentos, matriz de variables de ambiente. Aquí tamén supoño que todos entenden o significado de pasar o camiño ao ficheiro executable como un argumento separado, pero pode preguntar. O último argumento especifica se hai que esperar a que se complete o proceso (UMH_WAIT_PROC), inicio do proceso (UMH_WAIT_EXEC) ou non esperar nada (UMH_NO_WAIT). Hai algún máis UMH_KILLABLE, non o mirei.

Asemblea

A montaxe dos módulos do núcleo realízase a través do marco de creación do núcleo. Chamado make dentro dun directorio especial vinculado á versión do núcleo (definido aquí: KERNELDIR:=/lib/modules/$(shell uname -r)/build), e a localización do módulo pásase á variable M nos argumentos. O icmpshell.ko e os obxectivos limpos usan este cadro por completo. EN obj-m indica o ficheiro obxecto que se converterá nun módulo. Sintaxe que rehace main.o в icmpshell.o (icmpshell-objs = main.o) non me parece moi lóxico, pero así sexa.

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

Recollemos: make. Cargando: insmod icmpshell.ko. Feito, podes comprobar: sudo ./send.py 45.11.26.232 "date > /tmp/test". Se tes un ficheiro na túa máquina /tmp/test e contén a data na que se enviou a solicitude, o que significa que fixeches todo ben e eu fixen todo ben.

Conclusión

A miña primeira experiencia co desenvolvemento nuclear foi moito máis fácil do que esperaba. Aínda sen experiencia no desenvolvemento en C, centrándose nas suxestións do compilador e nos resultados de Google, puiden escribir un módulo de traballo e sentirme como un hacker do núcleo e, ao mesmo tempo, un neno de guións. Ademais, fun á canle Kernel Newbies, onde me dixeron que usase schedule_work() en vez de chamar call_usermodehelper() dentro do propio gancho e avergoñábao, sospeitando con razón dunha estafa. Un cento de liñas de código custoume preto dunha semana de desenvolvemento no meu tempo libre. Unha experiencia exitosa que destruíu o meu mito persoal sobre a abafadora complexidade do desenvolvemento do sistema.

Se alguén acepta facer unha revisión do código en Github, estarei agradecido. Estou bastante seguro de que cometín moitos erros estúpidos, especialmente cando traballaba con cordas.

Concha nuclear sobre ICMP

Fonte: www.habr.com

Engadir un comentario