lunes, 11 de junio de 2007

Buffer Overflow: hackeando la pila (II)

EliuX [aorozco@infomed.sld.cu]

El primer ejemplo::

Luego de la introducción previa a los asuntos del Stack, veamos ahora nuestro primer ejemplo en C, porque como dice el dicho: “Vista hace fe”.

Nota: El compilador de C que utilizo es el C++ Builder 6.

Example1.c

void function (int a, int b, int c)
{
  char buffer1[5];
  char buffer2[10];
}

void main()
{
  function(1,2,3);
}


Ahora deberemos coger un desensamblador y tratar de estudiar el código. Es posible que no elijan todos un mismo desensamblador, por lo que decidí explicar los pedazos más importantes en código usando la opción del Builder para ver la CPU, en tiempo de ejecución: View » Debug Windows » CPU.

;las siguientes líneas son fragmentos del código ensamblador del compilado anteriormente en C
;éste se describirá según el orden de ejecución
;cuando se llama a function (1,2,3)
push 0x03 ;meto los parámetros en orden inverso
push 0x02
push 0x01
call function(int, int, int)

;Llamamos al procedimiento function y pushea a la pila el ret
add esp,0x0c ;corro la pila 12 Bytes(3 words) para reservar espacio para los 3 parámetros
;el código en asm de void function (int a, int b, int c) sería
push ebp ;guardo BP (más conocido como el FP guardado)
mv ebp,esp ;muevo el Stack Pointer(SP) hacia el BP, para hacerlo el nuevo FP
;en la siguiente instrucción disminuimos sp en 20, para que la pila crezca (12 decimal = 0x14 hexadecimal) ;así se reservará espacio para las cadenas char buffer1 y char buffer2.
add esp,-0x14

Debemos recordar que la memoria en los procesadores Intel sólo puede ser direccionada en múltiplos de WORD (4 BYTE), por lo que nuestro buffer de 5 (char buffer1[5];) en verdad ocupará 8 BYTE y el de 10 (char buffer2[10];) 12 BYTE. Este es el motivo por el que a SP se le resta 20 BYTE (8+12) cuando se pasan los parámetros a la pila. Veamos cómo quedaría la pila cuando se llama a function:

Un Buffer Overflow es el resultado de poner más datos dentro del buffer de los que se pueden manejar. Como estos bugs de la programación los podemos usar para crear un efecto colateral (BOF) beneficioso a nuestra causa, veamos otro ejemplo:

Example2.c

void function (char *c)
{
  char buffer1[16];
// exactamente 4 WORD, porque cada char es un BYTE
  strcpy(buffer1,c); // copio el contenido del parámetro en el buffer
}
void main()
{
  char large_str[256];
// una línea de 256 caracteres
  int i;
  for( i=0; i< 255; i++)
  large_str[i] = ‘A’;
// la empiezo a llenar
  function(large_str); // cuando llamo a la función, introduzco las 256 “A” en la cadena de 16
}

Para que observe bien lo que pasa, póngale un breakpoint a la línea que dice large_str[i] = 'A'. Luego abra la ventana de CPU (Ctrl + Alt + C) y veamos qué es lo que pasa: Vaya presionando la tecla F8 (Ejecución paso a paso) y observe el cuadro inferior derecho y vea cómo la memoria se va llenando de la letra “A”. Luego quite este breakpoint y cree uno en la declaración de la función void function (char *c) y presione F9 para ir al inicio de la función. Presione F8 una vez para ir a la instrucción strcpy(buffer1,c);. Entonces en la ventana de CPU busquemos en el cuadro inferior izquierdo el buffer large_str que de seguro aparecerá como una columna llena de “A”. Si ejecutamos la siguiente instrucción con un F8, el procesador escribirá 256 elementos (char) a partir de la dirección en memoria de la variable c mediante la instrucción strcpy, la variable c almacenará un carácter “A” según su capacidad y los otros 255 se colocarán a continuación sobrescribiendo registros cómo el FP guardado, los valores de los parámetros (1,2,3) y el RET que almacenaba el valor de retorno de la función. En este momento la memoria quedaría así:

Si presionamos F8 para ver qué se ejecutaría cuando se sale de la función, veremos que el CPU nos lleva a una zona de memoria que no pertenece a nuestro programa (puede estar vacía o no), por lo que el procesador se da cuenta que se está efectuando una violación de segmentación y lanza el mensaje de error siguiente:

Si lo anterior se ha realizado sin problemas, entonces ¡felicidades! Ya has creado tu primer Buffer Overflow.

¿Entonces quiere decir que puedo cambiar el flujo de ejecución del programa? Sí, eso y mucho más ¿No se te ocurre nada que se pudiera hacer con solamente sobrecargar la pila? Pues a mí, infinidades de cosas, partiendo de que se puede machacar un código que se vaya a ejecutar y colocar el nuestro. Por eso vamos a tornarnos más agresivos y modificar nuestro primer código para que sobrescriba la dirección de retorno, y demuestre cómo podemos hacerlo ejecutar un código arbitrario ¡Vamos a ver cómo se crea la magia!

Cojamos como punto de referencia el primer ejemplo. Antes de buffer1 [] está el SP guardado y antes de él la dirección de retorno (RET). Eso es 1 WORD (4 BYTE) pasando la dirección de buffer1 [] que ocupa 2 WORD (8 BYTE) por lo que son 12 BYTE de offset (desplazamiento).

Nuestra misión será modificar el RET para saltarnos el segmento de código x=1;. Para hacer esto agregaremos 8 BYTE a la dirección de retorno. El código a ejecutar es el siguiente:

Ejemplo3.c

void function(int a, int b, int c)
{
  char buffer1[5];
  char buffer2[10];
  int *ret;
// variable apuntadora
  ret= buffer1 + 12;// RET ahora apunta al RET en la pila la dirección de buffer1 offset 12
  (*ret)+=8; // en la aritmética de apuntadores los # que se suman son BYTES, por lo
} // que se le aumenta la dirección a la que apunta RET en 8 BYTE
void main (){
  int x;
  x=’A’;
  function (1,2,3);
  x=’B’;
  printf(“%c\n”,x);
}

Veámoslo en la ventana del CPU ahora, por orden de ejecución, y las partes que se agregaron diferentes al ejemplo 1:

;cuando se ejecuta el código void main{
;main es la función que da inicio a nuestro programa y, como toda función, se realizan
;los mismos pasos iniciales
push ebp ;guardo el fp anterior (recuerda que se guarda en BP)
mov ebp,esp ;corro el bp hacia adelante donde esta el SP
push ecx ;guardo cx el registro contador (irrelevante)
;la instrucción x=0 se codifica
move byte ptr [ebp-0x01],0x00
;fíjense cómo usa el ebp como puntero pase y luego accede a la variable x mediante el offset 0x01
;el signo negativo significa que la variable se encuentra después de ebp, o sea, una una zona de memoria inferior (recuerda que la pila crece hacia zonas inferiores de memoria)
;luego llama a function(1,2,3) y se ejecuta
push ebp ;lo mismo que en todas las funciones
mov ebp,esp ;creo un nuevo FP para apuntar a los variables
add esp,-0x18 ;1word del puntero RET, 2 word de buffer1 y 3 word de buffer2
;la instrucción ret= buffer1 + 12 se codifica
lea eax,[ebp+0x04]
; pongo buffer1 +12 en ax= +0x04 de offset del BP (apunta al RET de function); se hace así porque el CPU interpreta buffer1 como ebp+-0x08 y si le sumamos 12 sería ebp+0x04
mov [ebp-0x18],eax ;muevo el contenido la dirección de buffer1+12 a *ret (el apuntador)
mov edx,[ebp-0x18] ;copio lo que está en *ret a dx (registro usado para direccionar como BP)
add dword ptr [edx],0x08 ;le sumo 8 al contenido que está en la dirección que apunta *ret en dx
mov esp,ebp ;corrijo el SP
pop ebp ;rescato el ebp anterior (FP guardado)
ret ;salgo de la función

;cuando regresa de la función se salta la siguiente instrucción
add esp,0x0c
;para corregir el offset de SP con respecto a los parámetros de la función, pero como no se ejecuta, abra ;después un error de segmentación de pila.
;La instrucción x+=1 se codifica
inc dword ptr [ebp-0x04];el offset de x es el primero (-0x04 de esta función)
;pero como las dos anteriores se saltan se ejecuta la siguiente
printf(“%c\n”,x);
El resultado que se obtendría al final del programa sería la salida en pantalla del 0, puesto que no se ejecutó la línea x=1 y se mantuvo su último valor asignado, que fue el impreso.
¿Ahora sí se les ocurren más cosas para hacer, no? Pues esto no es todo... ¡todavía hay más!

Para saber más...



Artículos relacionados


No hay comentarios: