Exploitation: Integer Overflow

Los desbordamientos de entero (integer overflow) son una de las vulnerabilidades más difíciles de detectar en un programa. En primer lugar, porque la forma de pensar para dar con ellos puede parecer obtusa al principio. En segundo lugar, porque un desbordamiento de entero no puede detectarse una vez ha sucedido, así que no hay forma de que una aplicación sepa si el resultado que acaba de calcular es correcto o no. Además, este tipo de vulnerabilidad no es explotable en la mayor parte de los casos, ya que la memoria no se sobreescribe, por lo que normalmente llevan a comportamientos impredecibles. Aún así, en las ocasiones en que el entero que se desborda tiene que ver con el cálculo del temaño de un buffer o con cuántas posiciones se deben llenar de un array es posible lograr un desbordamiento de buffer (stack overflow I y II). Vamos con un poco de teoría básica.

Un entero no deja de ser una representación en memoria de un valor, por lo que, aunque estemos acostumbrados a representarlos en formato decimal, en nuestro pc se almacenarán en binario. Un entero ocupa en memoria una longitud que (normalmente) es igual al tamaño de los punteros en esa arquitectura. Así, para x86 el tamaño de entero es de 32 bits, mientras que en x86_64 es de 64 bits. Concretamente en 32 bits tenemos principalmente int (32 bits, igual que long) y short (16 bits). Es importante resaltar, que dado que existe la necesidad de almacenar valores enteros negativos, hay un mecanismo para identificar estos en binario. El asunto es sencillo, si el primer bit es un 1, el número es negativo, si no, es positivo. Esto quiere decir que a la hora de definir variables de tipo entero tendremos enteros con signo (signed) y enteros sin signo (unsigned). Ambos tipos ocupan el mismo espacio en memoria, por lo que si tenemos los mismos bits para representar números positivos en un caso, y positivos y negativos en otro, es evidente que los rangos de representación variarán.

TIPO VALOR MIN VALOR MAX
Int -2147483648 2147483647
Unsigned int 0 4294967296
Short -32768 32767
Unsigned short 0 65536

Por otro lado, cuando se realiza un cálculo en el que los operandos involucrados son de distinto tamaño, el más pequeño se crece al tamaño del mayor para la operación (extensión de signo). Se realizará la operación con estos tamaños y, si el resultado debe almacenarse en la variable de menor tamaño, se truncará para que quepa en ella. Veamos un ejemplo:


#include <stdio.h>

int main(int argc, char* argv[]){
 int i;
 short s;

 i = 0xdefabada;
 s = i + 1;

 printf("[i] = 0x%x\n",i);
 printf("[i+1] = 0x%x\n",i+1);
 printf("[s] = 0x%x\n",s);

 return 0;
}

Tenemos un int (32 bits) y un short (16). En la suma intervienen operandos de distintos tamaños, y se almacena el resultado en s, de tamaño short, por lo que si el resultado es mayor que el valor que puede almacenar un short, los 16 bits más significativos se echarán a perder.


adrian@Andromeda-virt:~/exploiting$ ./a.out
[i] = 0xdefabada
[i+1] = 0xdefabadb
[s] = 0xffffbadb

Como printf al imprimir hexadecimal (%x) por defecto toma 32bits (una palabra) de tamaño, ha extendido el bit de signo de s, pero se ve cómo el resultado real ha quedado truncado. Con estas ideas captadas, podemos meternos en el tema.

El impaciente lector estará pensando “Todo esto está muy bien, ¿pero cual es el problema?”. Pues bien, el problema viene cuando debido a un error de programación, somos capaces de desbordar un entero. “¿Y qué pasa cuando lo desbordas?” Cuando se desborda un entero, se realiza la operación conocida como módulo, es decir, que VALOR_MAX + 1 = 0 en el caso de enteros sin signo, y que VALOR_MAX_POS + 1 = VALOR_MAX_NEG en enteros con signo. ¿Que no sabes lo que es el módulo? Bueno, rápidamente, el módulo consiste en realizar la división entera de dos números y quedarse el resto. O visto de otra forma, cuando te pasas por arriba, sigues por abajo y viceversa. Si no está muy claro, echa un ojo aquí.

Vamos a ver un ejemplo de lo que podría pasar. Pensad en un programa tipo dd, que copia de un origen a un destino bloques de tamaño dado. Este programa recibe del usuario el origen de la copia, el destino, y el tamaño. Nuestro siguiente código no recibe el destino (por simplicidad) y simplemente escribe el contenido en un array que luego podría copiarse a un destino hipotético o ser procesado o lo que fuera.


#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[]){
 int len;
 char buf[256];

 if (argc < 3){
 printf("Uso %s <longitud> <contenido>\n", argv[0]);
 exit(0);
 }

 len = atoi(argv[1]);
 if (len > 256){
 printf("Aquí viene un listillo\n");
 exit(-1);
 }
 printf("[int]  len=%d\n",len);
 printf("[uint] len=%u\n",len);

 memcpy(buf, argv[2], len);

 return 0;
}

El usuario que a primera vista no ha detectado el problema, tratará de comprobar el funcionamiento del programa con algunos ejemplos, intentando colocar en buf más de 256 caracteres y así sobreescribir nuestra tan valorada dirección de retorno.


adrian@Andromeda-virt:~/exploiting$ ./int
Uso ./int <longitud> <contenido>
adrian@Andromeda-virt:~/exploiting$ ./int 5 AAA
 [int]  len=5
 [uint] len=5
adrian@Andromeda-virt:~/exploiting$ ./int 5 AAAAA
[int]  len=5
[uint] len=5
adrian@Andromeda-virt:~/exploiting$ ./int 5 AAAAAAAAAA
[int]  len=5
[uint] len=5
adrian@Andromeda-virt:~/exploiting$ ./int 5 $(perl -e 'print "A"x400')
[int]  len=5
[uint] len=5
adrian@Andromeda-virt:~/exploiting$ ./int 400 $(perl -e 'print "A"x400')
Aquí viene un listillo

Parece que la comprobación sobre la longitud que hacemos es suficientemente robusta, ya que si el tamaño que pedimos es mayor que el buffer el programa nos insulta y finaliza. Y si el tamaño es menor, pero la cadena que le pasamos es mayor, el programa copia sólo hasta el valor de la longitud, por lo que tampoco desborda el buffer. ¿Dónde está entonces el truco? La magia en este ejemplo está en que se trata la variable len de dos maneras diferentes. En primer lugar, se comprueba len < 256, tratando el tipo de len como valor entero (int) con signo. En segundo lugar, se utiliza len como tercer argumento de memcpy, que lo que espera es un entero sin signo (unsigned int). Sabiendo esto, podemos aprovecharnos de los conocimientos que tenemos acerca de los enteros en esta arquitectura y tratar de saltarnos la restricción. Lo que necesitamos es que en el momento de comprobar la longitud (len < 256, tipo int) el valor sea menor que 256, y que a la hora de realizar la copia de memoria (memcpy(buf, argv[2], len), el valor sea mayor para provocar un buffer overflow.


adrian@Andromeda-virt:~/exploiting$ ./int -1 $(perl -e 'print "A"x400')
[int]  len=-1
[uint] len=4294967295
Fallo de segmentación

¡Bingo! Como se muestra en la salida de arriba, el valor -1 que le hemos introducido, al convertirse a unsigned int es 4294967295, por lo que pasa el primer control de longitud y a la hora del memcpy desborda el buffer. Sin embargo, en este caso la vulnerabilidad no es explotable, o al menos no lo es de manera sencilla, ya que la cantidad de espacio que estamos ocupando en la pila es 4294967295 bytes, que si no me equivoco al dividir son unos 4GB de memoria, por lo que habríamos destruido mucho más que la pila de nuestra función o programa y el fallo de segmentación llega antes de que se produzca el return de main. Aunque podamos buscar un número negativo que al pasar a unsigned int sea menor, el valor más bajo que vamos a poder conseguir es 0x80000000.


adrian@Andromeda-virt:~/exploiting$ gdb -q
(gdb) p 0x80000000
$1 = 2147483648
(gdb) quit
adrian@Andromeda-virt:~/exploiting$ ./int -2147483648 $(perl -e 'print "A"x400')
[int]  len=-2147483648
[uint] len=2147483648
Fallo de segmentación

Aún así 2097152 bytes siguen siendo 2GB de memoria. Mala suerte esta vez. Sin embargo, existen situaciones en las que sí resulta explotable un integer overflow. Observad suspicazmente el siguiente código:


#include <stdlib.h>
#include <stdio.h>

int main(int argc, char* argv[]){
 int i,len;
 char *buf,*ptr;

 if (argc < 3){
 printf("Uso: %s <longitud> <mensaje>\n", argv[0]);
 exit(0);
 }

 len = atoi(argv[1]);
 ptr = argv[2];
 printf("[len]   (signed)   = %d\n", len);
 printf("[len*4] (unsigned) = %u\n",(len*4));

 buf = malloc(len*sizeof(int));

 for (i=0;i<len;i++){
 buf[i] = *(ptr+i);
 }

 printf("[DEBUG] Buf %s\n",buf);
 free(buf);
 return 0;
}

¿No? ¿Nada? En principio parece que reserva espacio para len enteros, y que rellena el buffer hasta len, con lo que no deberíamos poder engañarlo. Vamos a ejecutarlo en un par de casos a ver como se comporta.

adrian@Andromeda-virt:~/exploiting$ ./iovfw 5 12345
[len]   (signed)   = 5
[len*4] (unsigned) = 20
[DEBUG] Buf 12345
adrian@Andromeda-virt:~/exploiting$ ./iovfw 5 1234567890
[len]   (signed)   = 5
[len*4] (unsigned) = 20
[DEBUG] Buf 12345

Volvemos a estar en el caso (quizá algo menos visible) de que necesitamos que len valga distinto a la hora de reservar memoria, y a la hora de rellenar el buffer, de tal forma que se rellenen más bytes de los reservados. La forma de lograr esto es aprovechando la multiplicación que se realiza dentro del malloc. Sabemos que sizeof(int) devolverá 4, por lo que necesitamos que len*4 sea un número menor que len. Eso está hecho, ¿no?


adrian@Andromeda-virt:~/exploiting$ gdb -q
(gdb) p 0xffffffff
$1 = 4294967295
(gdb) p 4294967295/4
$2 = 1073741823
(gdb) p (unsigned int)1073741823
$3 = 1073741823
(gdb) p 1073741824*4
$4 = 0
(gdb) p 1073741825*4
$5 = 4
(gdb) p 1073741826*4
$6 = 8
(gdb) p 1073741827*4
$7 = 12
(gdb) quit
adrian@Andromeda-virt:~/exploiting$ ./iovfw2 1073741827 AAAAAAAAAA
[len]   (signed)   = 1073741827
[len*4] (unsigned) = 12
Fallo de segmentación

adrian@Andromeda-virt:~/exploiting$ gdb -q iovfw2
 Leyendo símbolos desde /home/adrian/exploiting/iovfw2...hecho.
 (gdb) run 1073741827 AAAAAAAAAA
 Starting program: /home/adrian/exploiting/iovfw2 1073741827 AAAAAAAAAA
 [len]   (signed)   = 1073741827
 [len*4] (unsigned) = 12

 Program received signal SIGSEGV, Segmentation fault.
 0x0804857f in main (argc=3, argv=0xbffff524) at int_ovflow2.c:22
 22            buf[i] = *(ptr+i);
 (gdb) p i
 $1 = 2382

Con un poco de ayuda de gdb podemos encontrar el valor que desborde un entero sin signo al multiplicarse por cuatro. En primer lugar obtenemos el mayor entero representable y lo dividimos por cuatro. A ese valor le sumamos uno, y al multiplicarlo por cuatro observamos que da cero; lo hemos desbordado. En esta situación, malloc reservará 12 bytes, pero el bucle tratará de escribir 1073741827. En este último fragmento vemos cómo el bucle ha escrito 2382 posiciones de memoria antes de fallar estrepitosamente. Una situación de este tipo en las condiciones adecuadas es explotable mediante una técnica denominada heap overflow, que aún no hemos tratado en este blog, pero que lo haremos más adelante.

Esta entrada ha quedado un tanto larga y hay muchas cosas que asimilar, así que si tenéis alguna aportación, aclaración o duda, estaré encantado de leerla en los comentarios 😉

[+] Más ejemplos y referencias: http://www.phrack.org/issues.html?issue=60&id=10#article

Anuncios
Tagged with: , , ,
Publicado en exploiting, hacking
2 comments on “Exploitation: Integer Overflow
  1. Manuel Abeledo dice:

    Es difícil encontrar información en español sobre estos temas, tan importantes no sólo para auditores de seguridad sino para desarrolladores que quieren mantener un cierto nivel de calidad en su código.

    ¡Felicidades por el blog!

  2. Adrián dice:

    Me alegro de que te guste Manuel 🙂 La idea es poner a disposición de la gente lo que uno sabe o va aprendiendo (o parte, jeje), siempre tratando de aportar la experiencia personal en el tema concreto o la forma que tengo de entenderlo y explicarlo.

    En cuanto a los programadores, desgraciadamente la mayoría no tienen demasiado interés por la seguridad, y las empresas tampoco hacen un esfuerzo por concienciar a su plantilla en el tema, por no hablar de que en muchas universidades de informática los ingenieros no han visto nada acerca de seguridad en todo el tiempo que han estado allí.

    Esperemos que las cosas vayan cambiando con el tiempo y el granito de arena de cada uno.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Archive
A %d blogueros les gusta esto: