Este es el primer artículo de una serie en la que iremos viendo algunas técnicas que se emplean para llevar a cabo la explotación de vulnerabilidades en aplicaciones. Está escrito a modo de tutorial y en él expondré un pequeño ejemplo de cómo sería una vulnerabilidad producida por el desbordamiento de una variable. A este tipo de vulnerabilidades, por darse fundamentalmente con el tipo de datos entero, se le conoce como Integer Overflow.
Este tipo de vulnerabilidades podría darse ya no sólo por la posibilidad de introducir al programa un valor que exceda la capacidad del tipo de dato, lo cual es muy fácil de controlar, sino que también sería posible por operaciones aritméticas cuyo resultado fuese un desbordamiento, y esto ya es más difícil de controlar.
No voy a explicar mucho más sobre esto porque en la red ya existe abundante material explicativo. También existen magníficos ejemplos como éste, éste o este más parecido. Sin embargo, a diferencia de estos otros artículos, voy a ir un poco más al detalle, analizando a nivel ensamblador y movimiento de registros cómo se produce la condición de desbordamiento.
1. Vayamos al ejemplo
El ejemplo se ha realizado sobre un Ubuntu 12.04LTS de 32 bits. En estos
primeros artículos de la serie trabajaremos con arquitectura de 32 bits por ser
más sencilla. Compilaremos con gcc y debuggearemos con gdb .
|
Para hacerlo más didáctico, voy a utilizar el desbordamiento en un byte
correspondiente a un tipo de dato char
. Como sabemos, 1 byte son 8 bits, lo
que permite en binario el rango de 0
a 255
y en complemento a dos de -128
a 127
.
Supongamos que un programador novato ha aprendido que el tipo char
ocupa menos
espacio en memoria que un int
y que también puede usarse para almacenar
números. Por ello decide "optimizar" su código utilizando un dato cuyo tipo es
char
para almacenar la longitud de una cadena y así hacer que su programa
utilice menos memoria. El programador obviamente no ha leído todavía a Donald
Knutt y su famosa frase: "La optimización temprana es la raíz de todo mal" y
acaba escribiendo el siguiente código:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_ARG 30
int main(int argc, char *argv[]){
char argumento[MAX_ARG];
char tam = 0;
if(argc != 2) {
printf("Uso: %s argumento\n", argv[0]);
return 2;
}
tam = strlen(argv[1]);
printf("El tam del argumento es: %d\n", tam);
if(tam > MAX_ARG-1) {
printf("El argumento excede el max\n");
return 2;
}
strcpy(argumento, argv[1]);
printf("El argumento: %s\n", argumento);
return 0;
}
Como vemos en el programa, utiliza un tipo char
para almacenar el tamaño de la
cadena pasada como argumento y, siguiendo buenas prácticas de programación,
posteriormente comprueba que el tamaño no sea mayor que la memoria definida
para su almacenamiento.
2. Desbordando el valor de la variable
Un atacante rápidamente comprueba que es posible saltarse la comprobación del
tamaño de la cadena simplemente desbordando el tamaño que el tipo de dato char
es capaz de almacenar. Veamos las ejecuciones del programa.

Nótese que estamos usando el intérprete de perl en línea para generar una cadena de entrada con la letra A repetida el número de veces pasado. |
Como vemos en la figura, en la primera ejecución se introduce una cadena cuyo
tamaño es inferior al buffer reservado para su almacenamiento y se ejecuta
correctamente. En la segunda ejecución, se introduce una cadena de tamaño 30
,
siendo superior al permitido (ya que se incluye el byte nulo de cierre de
cadena, lo que hace 31
). En la tercera ejecución se ejecuta una cadena de
tamaño 127
, nuevamente el programa funciona correctamente indicando que el
argumento excede el máximo. En la cuarta, se introduce 128
, pero en esta
ocasión vemos que nos dice que el tamaño es de… ¡¡-128
!! y muestra un
error de violación de segmento. Si en nuestro programa hubiésemos también
chequeado que tam sea mayor que cero, habríamos evitado el error. Pero en la
quinta ejecución introducimos de 257
, mostrándonos un tamaño de 1
y
obteniendo nuevamente un error de violación de segmento.
3. Explicación del desbordamiento
Veamos qué ha ocurrido, haciendo uso de la herramienta gdb
.
(gdb) disass main
Dump of assembler code for function main:
0x08048424 <+0>: push %ebp
0x08048425 <+1>: mov %esp,%ebp
0x08048427 <+3>: push %edi
0x08048428 <+4>: and $0xfffffff0,%esp
0x0804842b <+7>: sub $0x40,%esp
0x0804842e <+10>: movb $0x0,0x3f(%esp)
0x08048433 <+15>: cmpl $0x2,0x8(%ebp)
0x08048437 <+19>: je 0x8048459 <main+53>
0x08048439 <+21>: mov 0xc(%ebp),%eax
0x0804843c <+24>: mov (%eax),%edx
0x0804843e <+26>: mov $0x80485c0,%eax
0x08048443 <+31>: mov %edx,0x4(%esp)
0x08048447 <+35>: mov %eax,(%esp)
0x0804844a <+38>: call 0x8048320 <printf@plt>
0x0804844f <+43>: mov $0x2,%eax
0x08048454 <+48>: jmp 0x80484e5 <main+193>
0x08048459 <+53>: mov 0xc(%ebp),%eax
0x0804845c <+56>: add $0x4,%eax
0x0804845f <+59>: mov (%eax),%eax
0x08048461 <+61>: movl $0xffffffff,0x1c(%esp)
0x08048469 <+69>: mov %eax,%edx
0x0804846b <+71>: mov $0x0,%eax
0x08048470 <+76>: mov 0x1c(%esp),%ecx
0x08048474 <+80>: mov %edx,%edi
0x08048476 <+82>: repnz scas %es:(%edi),%al
0x08048478 <+84>: mov %ecx,%eax
0x0804847a <+86>: not %eax
0x0804847c <+88>: sub $0x1,%eax
0x0804847f <+91>: mov %al,0x3f(%esp)
0x08048483 <+95>: movsbl 0x3f(%esp),%edx
0x08048488 <+100>: mov $0x80485d3,%eax
0x0804848d <+105>: mov %edx,0x4(%esp)
0x08048491 <+109>: mov %eax,(%esp)
0x08048494 <+112>: call 0x8048320 <printf@plt>
0x08048499 <+117>: cmpb $0x1d,0x3f(%esp)
0x0804849e <+122>: jle 0x80484b3 <main+143>
0x080484a0 <+124>: movl $0x80485f0,(%esp)
0x080484a7 <+131>: call 0x8048340 <puts@plt>
0x080484ac <+136>: mov $0x2,%eax
0x080484b1 <+141>: jmp 0x80484e5 <main+193>
0x080484b3 <+143>: mov 0xc(%ebp),%eax
0x080484b6 <+146>: add $0x4,%eax
0x080484b9 <+149>: mov (%eax),%eax
0x080484bb <+151>: mov %eax,0x4(%esp)
0x080484bf <+155>: lea 0x21(%esp),%eax
0x080484c3 <+159>: mov %eax,(%esp)
0x080484c6 <+162>: call 0x8048330 <strcpy@plt>
0x080484cb <+167>: mov $0x804860b,%eax
0x080484d0 <+172>: lea 0x21(%esp),%edx
0x080484d4 <+176>: mov %edx,0x4(%esp)
0x080484d8 <+180>: mov %eax,(%esp)
0x080484db <+183>: call 0x8048320 <printf@plt>
0x080484e0 <+188>: mov $0x0,%eax
0x080484e5 <+193>: mov -0x4(%ebp),%edi
0x080484e8 <+196>: leave
0x080484e9 <+197>: ret
End of assembler dump.
Las cuatro primeras líneas son el preámbulo de la función, en ellas se
inicializan correctamente los registros de gestión de la pila y se reservan
0x40
bytes. A continuación vemos que realiza la inicialización a 0
de la
variable tam
con la instrucción:
movb $0x0,0x3f(%esp)
Como se ve, esta variable reside en la pila y está usando el movimiento a byte
al offset 0x3f
de la cima de la pila.
Las variables locales definidas en una función se implementan en la pila, para ello el compilador genera en el llamado preámbulo de la función el código que reservará e inicializará si es preciso la memoria. |
En la siguiente parte, se realiza el cálculo del tamaño de cadena (como vemos
la función strlen
se implementa inline, ya que no hay llamada).
0x08048476 <+82>: repnz scas %es:(%edi),%al
0x08048478 <+84>: mov %ecx,%eax
0x0804847a <+86>: not %eax
0x0804847c <+88>: sub $0x1,%eax
0x0804847f <+91>: mov %al,0x3f(%esp)
0x08048483 <+95>: movsbl 0x3f(%esp),%edx
Como vemos, se realiza la asignación de los últimos 8 bits del registro EAX
usando AL
sobre el valor de la variable tam
en la pila.
Las funciones inline son funciones cuyo código es directamente incluido
por el compilador en cada llamada a la función, evitando así el desperdicio de
recursos en tiempo de ejecución que una llamada a una función convencional
implica. La parte negativa es que su uso generará más código de programa al
tener que incluirse una y otra vez cada vez que esta función sea llamada,
generando un programa más grande. Por ello sólo se hace con funciones que
tengan muy poco código o que sean llamadas desde muy pocos lugares.
|
Veamos el desbordamiento en tiempo de ejecución. Para ello pondré puntos de interrupción y ejecutaré con desbordamiento, de la siguiente forma:
(gdb) break *main+15
Punto de interrupcion 1 at 0x8048433
(gdb) break *main+95
Punto de interrupcion 2 at 0x8048483
(gdb) run `perl -e 'print "A"x257'`
Starting program: /home/luis/pec/intoverflow `perl -e 'print "A"x257'`
Breakpoint 1, 0x08048433 in main ()
Veíamos que el offset en la pila de la variable era 0x3f
. Además de ser
direccionamiento a byte, dicho offset no está alineado. Por ello Comprobaré el
valor de su palabra y la adyacente Además comprobaré el valor con formato de
byte de la siguiente forma:
(gdb) x/2xw $esp+0x38
0xbffff608: 0x080484f9 0x00fd1ff4
(gdb) x/xb $esp+0x3f
0xbffff60f: 0x00
Así vemos el estado de la memoria tras la inicialización a cero inicial. Pasemos a ver qué pasa en el segundo punto de interrupción.
(gdb) c
Continuando.
Breakpoint 2, 0x08048483 in main ()
(gdb) x/2xw $esp+0x38
0xbffff608: 0x080484f9 0x01fd1ff4
(gdb) x/xb $esp+0x3f
0xbffff60f: 0x01
Como vemos, el valor del byte es 0x01
. Pero cuál es el valor del registro EAX
en el que se había calculado la longitud de la cadena y el valor del registro
AL
(el byte más bajo).
(gdb) info registers eax
eax 0x101 257
(gdb) info registers al
al 0x1 1
Como vemos, el cálculo se hizo correctamente, el registro EAX
contenía el valor
correcto, sin embargo cuando se mueve únicamente la información del byte menos
significativo usando AL
(truncamos la información), el valor trasladado a la
memoria es el 0x01
.
En la captura de pantalla siguiente vemos qué ocurre cuando se
introduce una cadena de 128
. En este caso, vemos que el valor es exactamente el
mismo (0x80
) y no se pierde información pero al tratarse la representación en
complemento a dos, al estar el bit más significativo a valor alto indica que se
trata de un número negativo.

4. Más allá del desbordamiento
Hemos visto cómo hemos sido capaces de cambiar el flujo de un programa utilizando
un desbordamiento de enteros (aunque en este caso se ha empleado el tipo
char
). La explotación de este fallo nos ha conducido a que la
condición sea evaluada como falsa, el programa continúe y se produzca una
violación de segmento. Esto nos va a permitir posteriormente la explotación de
una vulnerabilidad de tipo buffer overflow en la pila… Pero eso ya es otra
historia.