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.
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 menos
echo i > /proc/sysrq-trigger
para 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
make
y el linux-headers-amd64
resto 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
rmmod
no 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:
- Se extraen dos archivos de encabezado para manipular el módulo en sí y el filtro de red.
- , . , . — , :
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);
- .
- , .
-
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:
- Tuve que incluir archivos de encabezado adicionales, esta vez para manipular encabezados IP e ICMP.
- 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. - (
struct work_struct my_work;
) (DECLARE_WORK(my_work, work_handler);
). , , . - , . ,
skb
. , , . - , , .
struct iphdr *iph; struct icmphdr *icmph; unsigned char *user_data; unsigned char *tail; unsigned char *i; int j = 0;
- . 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 : - . , ! - , , . . , ICMP .
icmph
:user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
skb
, :tail = skb_tail_pointer(skb);
.
, . - ,
cmd_string
,run:
, , , . - , :
schedule_work(&my_work);
. , .schedule_work()
, . . , , kernel panic. ! - , .
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);
}
- 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. - Configuración de variables de entorno. Inserté solo PATH con un conjunto mínimo de rutas, esperando que todas ya se hayan combinado
/bin
con/usr/bin
y/sbin
con/usr/sbin
. Otros caminos rara vez importan en la práctica. - , !
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
make
dentro 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 M
en los argumentos. Icmpshell.ko y los objetivos limpios utilizan este marco por completo. En obj-m
especifica el archivo de objeto que se convertirá en un módulo. La sintaxis que elimina main.o
en 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
make
insmod icmpshell.ko
sudo ./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.