Cáscara nuclear sobre ICMP





TL; DR : Estoy escribiendo un módulo de kernel que leerá los comandos de la carga útil ICMP y los ejecutará en el servidor incluso si su SSH falla. Para los más impacientes, todo el código en github .



¡Precaución! ¡Los programadores C experimentados corren el riesgo de estallar en lágrimas de sangre! Puedo equivocarme incluso en terminología, pero cualquier crítica es bienvenida. La publicación está destinada a aquellos que tienen la idea más aproximada de la programación en C y desean conocer los aspectos internos de Linux.



En los comentarios a mi primer artículo.mencionó SoftEther VPN, que puede imitar algunos protocolos "normales", en particular, HTTPS, ICMP e incluso DNS. Puedo imaginar el trabajo de solo el primero de ellos, ya que estoy familiarizado con HTTP (S), y tuve que aprender a hacer túneles sobre ICMP y DNS.



imagen



Sí, aprendí en 2020 que puede insertar una carga útil arbitraria en paquetes ICMP. ¡Pero mejor tarde que nunca! Y dado que puede hacer algo al respecto, entonces debe hacerlo. Dado que en mi vida cotidiana utilizo con mayor frecuencia la línea de comandos, incluso a través de SSH, primero se me ocurrió la idea de un shell ICMP. Y para armar un bingo de mierda completo, decidí escribir como un módulo de Linux en un lenguaje del que solo tengo una idea aproximada. Dicho shell no estará visible en la lista de procesos, puede cargarlo en el kernel y no estará en el sistema de archivos, no verá nada sospechoso en la lista de puertos de escucha. En términos de sus capacidades, este es un rootkit completo, pero espero modificarlo y usarlo como shell de último recurso, cuando el promedio de carga es demasiado alto para iniciar sesión a través de SSH y ejecutar al menosecho i > /proc/sysrq-triggerpara restaurar el acceso sin reiniciar.



Tomamos un editor de texto, habilidades básicas de programación en Python y C, google y una máquina virtual que no te importa poner bajo el cuchillo si todo se rompe (opcional - VirtualBox local / KVM / etc) y ¡vamos!



Parte del cliente



Me pareció que para el lado del cliente tendría que escribir un guión de 80 líneas, pero había gente amable que hizo todo el trabajo por mí . El código resultó ser sorprendentemente simple, encaja en 10 líneas 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()


El script tiene dos argumentos, una dirección y una carga útil. Antes de enviar, la carga útil está precedida por una clave run:, la necesitaremos para excluir paquetes con una carga útil aleatoria.



El kernel requiere privilegios para crear paquetes, por lo que el script deberá ejecutarse con derechos de superusuario. No olvide dar permiso de ejecución e instalar scapy. Debian tiene un paquete llamado python3-scapy. Ahora puedes comprobar cómo funciona todo.



Ejecutando y dando salida a un 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í es como se ve en el 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





La carga útil en el paquete de respuesta no cambia.



Módulo de kernel



Para construir una máquina virtual con Debian, necesitará al menos makey el linux-headers-amd64resto se reforzará en forma de dependencias. No daré el código completo en el artículo, puedes clonarlo en github.



Configuración del gancho



Primero, necesitamos dos funciones para cargar el módulo y descargarlo. La función de descarga no es necesaria, pero luego rmmodno funcionará, el módulo se descargará solo cuando esté apagado.



#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 aqui:



  1. Se extraen dos archivos de encabezado para manipular el módulo en sí y el filtro de red.
  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() .




Ahora tenemos que extraer la carga útil, que resultó ser la tarea más difícil. El kernel no tiene funciones integradas para trabajar con carga útil, solo puede analizar los encabezados 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 == '\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;
}


Qué esta pasando:



  1. Tuve que incluir archivos de encabezado adicionales, esta vez para manipular encabezados IP e ICMP.
  2. Especifica la longitud máxima de una cadena: #define MAX_CMD_LEN 1976. ¿Por qué exactamente esto? ¡Porque el compilador jura por el grande! Ya me dijeron que necesito lidiar con la pila y el montón, algún día definitivamente haré esto y tal vez incluso corregiré el código. Inmediatamente establece una cadena en la que se basará el equipo: char cmd_string[MAX_CMD_LEN];. Debería estar visible en todas las funciones, hablaré de esto con más detalle en el párrafo 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);.



    imagen



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




Esta función es la más sencilla. Su nombre se especificó en DECLARE_WORK(), el tipo y los argumentos aceptados no son interesantes. Tomamos la línea de comando y la pasamos por completo al shell. Deje que se ocupe del análisis, la búsqueda de binarios y todo lo demás él mismo.



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. Establecemos los argumentos en una matriz de cadenas argv[]. Asumiré que todos saben que los programas realmente se ejecutan de esta manera, y no como una línea sólida con espacios.
  2. Configuración de variables de entorno. Inserté solo PATH con un conjunto mínimo de rutas, esperando que todas ya se hayan combinado /bincon /usr/biny /sbincon /usr/sbin. Otros caminos rara vez importan en la práctica.
  3. , ! call_usermodehelper() . , , . , , . , (UMH_WAIT_PROC), (UMH_WAIT_EXEC) (UMH_NO_WAIT). UMH_KILLABLE, .




La construcción de módulos del núcleo se realiza a través de un marco de creación del núcleo. Se llama makedentro de un directorio especial vinculado a la versión del kernel (definida aquí :) KERNELDIR:=/lib/modules/$(shell uname -r)/build, y la ubicación del módulo se pasa a la variable Men los argumentos. Icmpshell.ko y los objetivos limpios utilizan este marco por completo. En obj-mespecifica el archivo de objeto que se convertirá en un módulo. La sintaxis que elimina main.oen icmpshell.o( icmpshell-objs = main.o) no me parece muy lógica, pero que así sea. Poner: . Cargar: . Hecho esto, puede comprobar: . Si aparece un archivo en su máquina y contiene la fecha en que se envió la solicitud, entonces hizo todo bien y yo hice todo bien.



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



Conclusión



Mi primera experiencia con la ingeniería nuclear fue mucho más simple de lo que esperaba. Incluso sin experiencia en el desarrollo de C, centrándome en las sugerencias del compilador y la salida de Google, pude escribir un módulo funcional y sentirme como un hacker de kernel y, al mismo tiempo, un script kiddie. Además, fui al canal Kernel Newbies, donde me dijeron que usara en schedule_work()lugar de llamar call_usermodehelper()dentro del gancho y me avergonzaron, con razón, sospechando una estafa. Cien líneas de código me costaron aproximadamente una semana de desarrollo en mi tiempo libre. Una experiencia exitosa que destruyó mi mito personal sobre la abrumadora complejidad del desarrollo de sistemas.



Si alguien acepta hacer una revisión de código en github, estaría agradecido. Estoy bastante seguro de que he cometido muchos errores estúpidos, especialmente cuando se trata de cuerdas.






All Articles