Módulo del núcleo para Solaris orientado a la seguridad

En el anterior ártículo hicimos una aproximación al mundo de los Loadable Kernel Modules (LKM) en Solaris. En esta ocasión desarrollaremos un módulo del núcleo orientado a mejorar la seguridad del sistema por parte del administrador. Es un módulo muy sencillo, pero sirve para conocer cómo programar módulos más complejos.

 

El LKM quark

Funcionalidades

El módulo de kernel “quark” está orientado a la protección de nuestros sistemas Solaris frente a ataques que utilicen otros LKMs, además de proporcionar otras funcionalidades como proteger el demonio de log del sistema, ocultar archivos, directorios y procesos, etc.

Para utilizar “quark” en entornos de pruebas, se ha utilizado una opción de compilación (-DDEBUG) mediante la cual el funcionamiento del módulo perderá transparencia con objeto de informar de las modificaciones al sistema que se van realizando en ejecución. Al incluirse esta opción como optativa, podemos usar “quark” tanto para pruebas como en un entorno de producción, sin tener que hacer modificaciones en el código.

El objetivo principal de “quark” es proteger el sistema, por lo que necesita a su vez protegerse a sí mismo para evitar que sus esfuerzos en mantener la integridad del sistema sean deshabilitados. Por ello, “quark” no muestra información acerca de sí mismo a modinfo. Esto evitará que atacantes poco exhaustivos caigan en la cuenta de su presencia (un intruso con paciencia y conocimientos podría delatar su presencia investigando la tabla de símbolos del kernel).

Otra funcionalidad reseñable es la posibilidad de configurar una cadena de caracteres concreta, y todo fichero que la contenga se ocultará a todos los usuarios, siendo invisible incluso para root. Esto puede servirnos para ocultar ficheros o directorios confidenciales, puntos débiles del sistema, etc.

A pesar de ocultarse, alguien podría intuir su presencia y desinstalarlo o instalar un nuevo LKM que invalide la acción de nuestro módulo. Para protegernos de esto, “quark” no permite que se descarguen o se carguen más módulos del sistema. Así, si nos aseguramos de que nuestro módulo se ha cargado antes que un posible módulo atacante, la tabla de peticiones al sistema estará protegida y el mecanismo de carga y descarga de módulos quedará bloqueado.

Todas las funcionalidades comentadas están regidas por el valor de una variable, secureflag, mediante la que definimos si los criterios de seguridad deben aplicarse o no. El valor de esta variable se puede modificar una vez “quark” esté cargado, así que no es necesaria la recarga o compilación del módulo. Por lo tanto, podemos mantener al sistema protegido y cuando sea necesario cargar un nuevo módulo, por ejemplo, deshabilitamos momentáneamente la seguridad, cargamos el módulo y la volvemos a habilitar. Esta forma de funcionar permite aunar seguridad y flexibilidad.

Una de las cosas más típicas que se producen al sufrir un ataque es la pérdida o manipulación de los logs del sistema. Como ya hemos comentado, “quark” puede ser utilizado para proteger determinados ficheros, pero, además, actúa con un cuidado especial a la hora de asegurar el demonio encargado de los logs del sistema (syslogd). Su manera de funcionar es la siguiente: por defecto, el demonio de logs del sistema se ejecutará normalmente y está visible dentro de la lista de procesos. Cuando un atacante trata de detener su ejecución, “quark” simula que esa detención ha tenido éxito y oculta el proceso del demonio. El sistema seguirá generando logs, pero de manera invisible para los usuarios. El intruso en cuestión creerá estar a salvo y obrará de manera menos cautelosa, sin saber que sus pasos están siendo vigilados.

Para que todo esto sea posible, ha sido preciso parchear varias llamadas al sistema, como veremos en la siguiente sección.

Programación

La estructura básica del módulo es muy simple:

  • Las funciones y estructuras de datos obligatorias para todo módulo de kernel (_init(), _info(), _fini()).
  • Funciones destinadas a suplantar las peticiones al sistema originales.
  • Funciones adicionales.

Lo principal dentro de las funciones de carga y descarga del módulo es ser cuidadoso para poder dejar todo como estaba anteriormente a su carga. Por ello, a pesar de que durante la carga del módulo se suplantan varias llamadas al sistema, las funciones originales se guardan en punteros a funciones específicos, para poder restaurarlas durante la descarga del módulo.

Comentemos ahora cada una de las llamadas al sistemas que ha sido necesario suplantar para conseguir las funcionalidades que ofrece “quark”:

  • Ocultación de ficheros

En los sistemas UNIX estándar los ficheros y directorios se organizan en función de unas estructuras denominadas dirent. Cuando queremos mostrar el contenido de un directorio, o hacer una búsqueda en el disco duro se hace una petición al sistema (getdents()), por lo tanto, parcheando esa syscall podremos decidir qué se mostrará. En Solaris esto es exactamente igual, con la salvedad de que la syscall utilizada es la versión de 64 bits (getdents64()). La lista de ficheros que devuelve esta petición al sistema es filtrada antes de devolverse al área de usuario, eliminando los ficheros que hayamos creído conveniente.

En “quark” definimos una cadena de caracteres (#define HIDEFILENAME «hide.me») y todos los ficheros o directorios que contengan esa cadena serán eliminados de la salida que se proporcionará a la función que solicitó la información desde el área de usuario.

Para proteger los ficheros y directorios no basta con ocultarlos, en “quark” hemos parcheado también open(), open64() y chdir(). La causa de tener que modificar dos veces open reside en la arquitectura híbrida de Solaris: mientras que muchos programas utilizan ya la versión de open de 64 bits, existen comandos que todavía siguen empleando la versión de 32 bits. Si solamente modificáramos una de las dos, los ficheros podrían ser accedidos por los comandos que utilizasen la otra petición al sistema. Parcheando chdir() evitamos que se accedan a los directorios protegidos. Todas estas modificaciones en la tabla de peticiones al sistema pueden habilitarse y deshabilitarse, así que no es necesario desinstalar el módulo para crear o editar ficheros y directorios que contengan la cadena de caracteres que hay que ocultar. Cuando alguien intente acceder a ellos el sistema devolverá un error, similar a los que devuelve cuando los ficheros no existen, para evitar sospechas (“No such file or directory”).

  • Ocultación de procesos

Existen varios métodos para ocultar procesos. Muchos rootkits instalan versiones troyanizadas de los comandos que muestran los procesos del sistema (ps, pgrep, ptree…). Desde un enfoque de módulo de kernel, esto no es necesario, sino que es mucho más efectivo modificar las syscalls y que cualquier comando que las use, proporcione información modificada convenientemente por nosotros.

Plasmoid utilizó en sus primeras aproximaciones a un método de ocultación eficiente una modificación doble: dado que el pseudo sistema de ficheros /proc contiene un fichero por cada proceso con la información del fichero que contiene el ejecutable que desencadenó la creación del proceso y demás información (/proc/<pid>/psinfo), modificando open() y read() para evitar mostrar información acerca de los procesos que queremos ocultar. Esto realmente funciona correctamente, pero supone una gran sobrecarga global para el sistema, ya que open() y read() se utilizan para muchas otras cosas y la comprobación se hace cada vez que se utilizan.

En un análisis más elaborado, el mismo autor diseñó otro enfoque para ocultar procesos: como /proc es un directorio a todos los efectos, comprobamos en getdents() si el fichero que hay que mostrar es un número y, de ser así, si corresponde al descriptor de proceso que queremos ocultar. Cuando esto se produzca, ocultamos el fichero que describe el proceso de la misma manera que ocultábamos los ficheros en la sección anterior.

Nosotros utilizamos este segundo enfoque para ocultar procesos cuando es necesario: al principio ningún proceso permanece oculto, pero cuando es preciso ocultar syslogd, nos aseguramos de que el fichero corresponde al descriptor del proceso de syslogd, y lo ocultamos como si se tratara de un fichero normal.

  • Creación de un switch

Todas las funcionalidades de seguridad que aporta “quark” pueden habilitarse y deshabilitarse a voluntad. Utilizando la creación de un fichero con un nombre extraño (#define TOGGLE «quark_LKM») podremos modificar el valor de la variable secureflag durante la ejecución del módulo. Para ello es preciso parchear la petición al sistema encargada de crear ficheros (creat64()). En ella comprobamos si el fichero que nos encargan crear contiene la cadena definida como biestable (TOGGLE) y si es así, variamos el valor de secureflag y generamos artificialmente un mensaje de error.

  • Protección de syslogd

Nuestro módulo trata de proteger el demonio del sistema encargado de recoger los logs, impidiendo que un posible atacante detenga su ejecución, y ocultando su presencia cuando alguien lo intente. Necesitamos programar entonces dos mecanismos: uno para detectar un intento de “matar” el proceso relacionado con syslogd, y otro para ocultarlo después de ese intento. Es decir, es preciso parchear la syscall kill(), impidiendo su uso con syslogd y modificando la variable hidesyslogd (encargada de definir si debemos ocultar el proceso o no), y utilizar el método anteriormente explicado para ocultar procesos.

  • Protección del sistema contra la carga de módulos

La protección que “quark” proporciona podría verse desbaratada si permitimos la carga de nuevos módulos que sobrescriban los parches que nuestro módulo efectúa en la tabla de peticiones al sistema. Es por esto que “quark”, por defecto, deshabilita la carga de nuevos módulos en cuanto es instalado, modificando la syscall modctl() que es la encargada de gestionar qué módulos de kernel se cargan.

Este hecho podría provocar que una vez instalado, nuestro módulo no pudiera desinstalarse, pero esta funcionalidad también está regida por el “switch” o biestable comentado anteriormente. Cuando queramos agregar un nuevo módulo, modificamos el valor de secureflag, lo cargamos y volvemos a habilitar la protección.

Código fuente

Este código está protegido bajo la Licencia Pública GNU (GPL), es decir, es software libre. Para obtener una copia actualizada de esta licencia, visite http://www.fsf.org.

#include <sys/systm.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/dirent.h>
#include <sys/proc.h>
#include <sys/procfs.h>
#include <sys/kmem.h>
#include <sys/errno.h>
#include <sys/modctl.h>
#include <fcntl.h>
#include <unistd.h>

#define	TRUE		1
#define FALSE		0
#define MAXSYSCALLS	256
#define TOGGLE		"quark_LKM"
#define SYSLOGDNAME	"syslogd"
#define HIDEFILENAME	"hide.me"

extern struct mod_ops mod_miscops;

static struct modlmisc modlmisc =
{
    &mod_miscops,
#ifdef DEBUG
    "Quark LKM",
#else
    ""
#endif
};

static struct modlinkage modlinkage =
{
    MODREV_1,
    (void *) &modlmisc,
    NULL
};

extern struct sysent sysent[];
static struct sysent sysent_backup[MAXSYSCALLS];

int (*oldcreat64) (const char *path, mode_t mode);
int (*oldkill) (int, int);
int (*oldgetdents64) (int, struct dirent64 *, size_t);
int (*oldopen64) (const char *path, int oflag, mode_t mode);
int (*oldopen) (const char *path, int oflag, mode_t mode);
int (*oldchdir) (const char *path);

char toggle[] = TOGGLE;
char syslogdname[] = SYSLOGDNAME;
char hidefilename[] = HIDEFILENAME;
int secureflag = TRUE;
int hidesyslogd = FALSE;

/*
 * Functions to know if a process is a syslogger, in order to
 * hide it. Code ripped from Plasmoid's sift.c.
 */
int issyslogd(pid_t pid)
{
    proc_t *proc;
    char *psargs;
    int ret;

    proc = (proc_t *) prfind(pid);
    psargs = (char *) kmem_alloc(PSARGSZ, KM_SLEEP);
    if (proc != NULL)
	memcpy(psargs, PTOU(proc)->u_psargs, PSARGSZ);
    else
	return FALSE;

    if (strstr(psargs, (char *) &syslogdname) != NULL)
	ret = TRUE;
    else
	ret = FALSE;

    kmem_free(psargs, PSARGSZ);
    return ret;
}

int checkbyname(char *name)
{
    if (isdigit(name) && issyslogd(myatoi(name)))
	return TRUE;
    else
	return FALSE;
}

int isdigit(char *str)
{
    int i, ret;

    ret = TRUE;
    for (i = 0; ret && i < strlen(str); i++)
	if (str[i] < '0' || str[i] > '9')
	    ret = FALSE;

    return ret;
}

int myatoi(char *str)
{
    int res = 0;
    int mul = 1;
    char *ptr;
    for (ptr = str + strlen(str) - 1; ptr >= str; ptr--) {
	if (*ptr < '0' || *ptr > '9')
	    return (-1);
	res += (*ptr - '0') * mul;
	mul *= 10;
    }
    return res;
}

int savesysent()
{
    int i;

    for(i = 0; i < MAXSYSCALLS; i++) {
        memcpy(&sysent_backup[i], &sysent[i], sizeof(struct sysent));
    }
}

int checksysent()
{
    int i, ret;

    ret = FALSE;

    for(i = 0; i < MAXSYSCALLS; i++) {
        if(sysent[i].sy_call != sysent_backup[i].sy_call) {
#ifdef DEBUG
	    cmn_err(CE_NOTE, "system call %d has been modified (old: %p new: %p)\n",
                    i, sysent_backup[i].sy_call, sysent[i].sy_call);
#endif
            ret = TRUE;
            sysent[i].sy_call = sysent_backup[i].sy_call;
        }
    }

    return ret;
}

/* NEW FUNCTIONS... */

/*
 * New syscall creat64 in order to set a security toggle
 */
int newcreat64(const char *path, mode_t mode)
{
    char namebuf[1028];
    int len;

    copyinstr(path, namebuf, 1028, (size_t *) & len);

    if (strstr(namebuf, (char *) &toggle) != NULL) {
	if (secureflag) {
#ifdef DEBUG
	    cmn_err(CE_NOTE, "quark: exiting from secure mode");
#endif
	    secureflag = FALSE;
	} else {
#ifdef DEBUG
	    cmn_err(CE_NOTE, "quark: entering into secure mode");
#endif
	    secureflag = TRUE;
	}
        if (checksysent()) {
	    set_errno(ESRCH);
	    return -1;
        }
	set_errno(ENFILE);
	return -1;
    } else
	return oldcreat64(path, mode);
}

/*
 * New syscall kill
 */
int newkill(int pid, int signal)
{
    if (secureflag && issyslogd(pid)) {
#ifdef DEBUG
        cmn_err(CE_NOTE, "quark: not allowing to kill process (%i)", pid);
        cmn_err(CE_NOTE, "quark: process %i hidden", pid);
#endif
        hidesyslogd = TRUE;
	set_errno(ESRCH);
        return -1;
    } else
        return oldkill(pid, signal);
}

/*
 * If the secureflag feature is enabled (see above), this syscall will avoid
 * entering directories that contain the syslogdname word in their name.
 * Security switch: see newcreat64() function.
 */
int newchdir(const char *path)
{
    char namebuf[1028];
    int len;

    copyinstr(path, namebuf, 1028, (size_t *) & len);

    if (secureflag && strstr(namebuf, (char *) &hidefilename) != NULL) {
#ifdef DEBUG
	cmn_err(CE_NOTE, "quark: hiding directory (%s)", namebuf);
#endif
	set_errno(ENOENT);
	return -1;
    } else
	return oldchdir(path);
}

/*
 * If the secureflag feature is enabled (see above), this syscall will avoid
 * reading the content of files containing the syslogdname word in their name.
 * Security switch: see newcreat64() function.
 */
int newopen64(const char *path, int oflag, mode_t mode)
{
    int ret;
    int len;
    char namebuf[1028];

    ret = oldopen64(path, oflag, mode);

    if (ret >= 0) {
	copyinstr(path, namebuf, 1028, (size_t *) & len);

	if (secureflag && strstr(namebuf, (char *) &hidefilename) != NULL) {
#ifdef DEBUG
	    cmn_err(CE_NOTE, "quark: hiding content of file (%s)", namebuf);
#endif
	    set_errno(ENOENT);
	    return -1;
	}
	return ret;
    }
}

int newopen(const char *path, int oflag, mode_t mode)
{
    int ret;
    int len;
    char namebuf[1028];

    ret = oldopen(path, oflag, mode);

    if (ret >= 0) {
	copyinstr(path, namebuf, 1028, (size_t *) & len);

	if (secureflag && strstr(namebuf, (char *) &hidefilename) != NULL) {
#ifdef DEBUG
	    cmn_err(CE_NOTE, "quark: hiding content of file (%s)", namebuf);
#endif
	    set_errno(ENOENT);
	    return -1;
	}
	return ret;
    }
}

/*
 * This function has been recoded from the original source of itf.c
 * by plaguez. It does not work properly with files containing the
 * syslogdname string more than once. Don`t play with it, files containing
 * more than one syslogdname string definitely cause crashes, this bug is
 * even present in the original code.
 * This might sound like a bug, but I don't care, I never came to
 * the situation renaming a file containing the syslogdname word more
 * than once.
 */
int newgetdents64(int fildes, struct dirent64 *buf, size_t nbyte)
{
    int ret, oldret, i, reclen;
    struct dirent64 *buf2, *buf3;

    oldret = (*oldgetdents64) (fildes, buf, nbyte);
    ret = oldret;

    if (ret > 0) {
	buf2 = (struct dirent64 *) kmem_alloc(ret, KM_SLEEP);
	copyin((char *) buf, (char *) buf2, ret);
	buf3 = buf2;

	i = ret;
	while (i > 0) {
	    reclen = buf3->d_reclen;
	    i -= reclen;

	    if ((strstr((char *) &(buf3->d_name), (char *) &syslogdname) != NULL)
                || checkbyname((char *) &(buf3->d_name))
               ) {
#ifdef DEBUG
		cmn_err(CE_NOTE, "quark: hiding file/process (%s)", buf3->d_name);
#endif
		if (i != 0)
/*
		    memmove(buf3, (char *) buf3 + buf3->d_reclen, i);
*/
		    buf3->d_off = 1024;
		else
		    buf3->d_off = 1024;
		ret -= reclen;

	    }
	    if (buf3->d_reclen < 1) {
		ret -= i;
		i = 0;
	    }
	    if (i != 0)
		buf3 = (struct dirent64 *) ((char *) buf3 + buf3->d_reclen);
	}
	copyout((char *) buf2, (char *) buf, ret);
	kmem_free(buf2, oldret);
    }
    return ret;
}

/* STANDARD LKM FUNCTIONS */

int _init(void)
{
    int i;

    if ((i = mod_install(&modlinkage)) != 0)
	cmn_err(CE_NOTE, "Could not install module\n");
#ifdef DEBUG
    else
	cmn_err(CE_NOTE, "quark: successfully installed");
#endif

    oldcreat64 = (void *) sysent[SYS_creat64].sy_callc;
    oldkill = (void *) sysent[SYS_kill].sy_callc;
    oldgetdents64 = (void *) sysent[SYS_getdents64].sy_callc;
    oldopen64 = (void *) sysent[SYS_open64].sy_callc;
    oldopen = (void *) sysent[SYS_open].sy_callc;
    oldchdir = (void *) sysent[SYS_chdir].sy_callc;

    sysent[SYS_creat64].sy_callc = (void *) newcreat64;
    sysent[SYS_kill].sy_callc = (void *) newkill;
    sysent[SYS_getdents64].sy_callc = (void *) newgetdents64;
    sysent[SYS_open64].sy_callc = (void *) newopen64;
    sysent[SYS_open].sy_callc = (void *) newopen;
    sysent[SYS_chdir].sy_callc = (void *) newchdir;

    savesysent();

    return i;
}

int _info(struct modinfo *modinfop)
{
    return (mod_info(&modlinkage, modinfop));
}

int _fini(void)
{
    int i;

    if ((i = mod_remove(&modlinkage)) != 0)
	cmn_err(CE_NOTE, "Could not remove module\n");
#ifdef DEBUG
    else
	cmn_err(CE_NOTE, "quark: successfully removed");
#endif

    sysent[SYS_creat64].sy_callc = (void *) oldcreat64;
    sysent[SYS_kill].sy_callc = (void *) oldkill;
    sysent[SYS_getdents64].sy_callc = (void *) oldgetdents64;
    sysent[SYS_open64].sy_callc = (void *) oldopen64;
    sysent[SYS_open].sy_callc = (void *) oldopen;
    sysent[SYS_chdir].sy_callc = (void *) oldchdir;

    return i;
}

Conclusiones

A lo largo de estos dos artículos hemos podido comprobar como Solaris tiene una arquitectura modular, en la que los módulos de kernel son parte fundamental de la potencia y flexibilidad del sistema. Tantas son las posibilidades que este mecanismo proporciona que se ha convertido en los últimos tiempos en un nuevo campo de batalla en cuanto a la seguridad e integridad en sistemas.

Son muchos los esfuerzos tanto por parte de programadores de xploits, rootkits, y LKMs troyanizados, como por la de los coordinadores de proyectos de seguridad como StMichael, Papillon, etc. de explorar este nuevo espacio de acción. Atrás quedaron los días en los que conseguir una escalada de privilegios hasta ser root consistían los objetivos de los primeros, y guardar por que eso no se produzca el de los segundos. Nuestro módulo pretende ser una nueva aportación a este grupo de herramientas de protección.

Este texto puede entenderse como un acercamiento al kernel de Solaris, así como un manual de programación de nuevos LKMs que modifiquen el comportamiento del sistema en beneficio del administrador del mismo. Hemos pretendido ser claros y concisos, sin entrar en detalles que puedan confundir al lector y sin quedarnos en lo meramente superficial.

Por último nos gustaría agradecer la colaboración involuntaria de todos aquellos creadores de LKMs que han hecho esfuerzos por explicar sus progresos, facilitar documentación detallada y redactar artículos explicativos. Esperamos que este texto contribuya a esa lista de documentos que ayuden a otros a crear sus propios módulos.

Referencias

Cuando programé el LKM para Solaris me fijé en todo lo que había desarrollado sobre el tema hasta el momento, muchas de las referencias tienen varios años ya, seguro que cada uno de los autores ha ido renovando sus proyectos y documentaciones, pero aquí están las fuentes:

Un pensamiento en “Módulo del núcleo para Solaris orientado a la seguridad

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *