Memory
Cuando se ejecuta un programa en Windows a este se le asigna memoria, desde la dirección mas baja 0x00000000
hasta la mas alta 0x7fffffff
entra dentro del rango de "user-mode" y de la dirección 0x80000000
hasta 0xffffffff
en "kernel-mode".
Al crearse un proceso se crean con el las estructuras PEB y TEB:
• PEB: Contiene los parametros de "windows-user" en el proceso actual, como lo son la dirección al ejecutable o el puntero a el loader asi como información sobre el heap.
• TEB: Contiene información sobre el hilo, como lo puede ser la dirección de la estructura PEB, ubicación a la pila del hilo actual o el puntero hacia la estructura SEH.
Stack
Cuando se crea un hilo, este ejecuta codigo desde el programa o librerías, este hilo requiere un área de acceso rápido para funciones, variables e información del programa, esto se conoce como pila, cada hilo crea su propio stack o pila.
El stack trabaja bajo una estructura LIFO
(Last-In-First-Out), esto significa que los últimos datos que han sido empujados usando la instrucción push
, serán también los primeros en eliminarse cuando se ejecute una instrucción pop
para eliminar datos.
Cuando se crea una pila el puntero a ella apunta a la parte superior, esto significa que al introducir información en la pila este puntero disminuye, lo que quiere decir que la pila crece al revés, desde la dirección mas alta hasta la dirección mas baja.
Calling Conventions
Las convenciones de llamada se refieren a la forma en que las funciones reciben los parámetros y el valor de la dirección de retorno en cada arquitectura, en x86
los argumentos se empujan a la pila junto con la dirección de retorno y esta se limpia después de hacer la llamada a la función para poder utilizarse después.
Cuando se llama a una función esta necesita saber a donde apuntar cuando finaliza su ejecución asi que antes de realizar la llamada se guarda la dirección de la siguiente instrucción en el stack y cuando esta llegue al final y ejecute la instrucción ret
tomará la dirección guardada en el stack y volverá ahí para ejecutar lo que esta en ella.
Registers
Para ser eficientes al ejecutar un codigo la CPU
utiliza 9 registros de 32
bits, los registros son pequeñas ubicaciones donde los datos se pueden leer y/o modificar eficientemente, algo a tener en cuenta es que cada registro se puede dividir en subregistros de 16
y/o 8
bits como se muestra en la siguiente tabla.
En el caso del registro de 32 bits (EAX
) se divide en un subregistro de 16 bits (AX
) que a su vez se puede dividir en 2 subregistros de 8 bits (AH
) y (AL
) respectivamente
Varios de los registros se usan como registros de propósito general para almacenar datos temporales, algunos de los propositos de cada uno son:
• EAX (Accumulator): Instrucciones aritméticas y lógicas.
• EBX (Base): Puntero base para direcciones de memoria.
• ECX (Counter): Puntero usado como contador.
• EDX (Data): Direccionamiento, multiplicación y división.
• ESI (Source Index): Puntero a la fuente en operaciones de cadenas.
• EDI (Destination Index): Puntero al destino en operaciones de cadenas.
Otros registros bastante importantes que almacenan punteros son:
• ESP (Stack Pointer): Almacena el puntero a la parte superior del stack.
• EBP (Base Pointer): Puntero a la parte superior de la pila cuando se llama a la función.
• EIP (Instruction Pointer): Apunta a la dirección de la siguiente instrucción a ejecutar.
Endianness
Hay diferentes formas de representar los valores en memoria, las mas comunes son:
• Big Endian: Adoptado por Motorola y otros, consiste en representar los bytes en el orden natural, por lo que el valor hexadecimal 0x01020304
se guardaría en memoria con los bytes ordenados como 01 02 03 04
por lo que no sufriria cambios.
• Little Endian: Adoptado por Intel, el mismo valor 0x01020304
se guardaría en orden inverso con los bytes 04 03 02 01
de manera que se hace más intuitivo el acceso a datos, porque se efectúa fácilmente de forma incremental de menos a más relevante.
Example
Un ejemplo de codigo vulnerable seria el siguiente, este llama a la función vuln()
pasándole la string del primer argumento, esta función copia la string hacia la variable dest
con solo un buffer de 64 bytes, asi que... ¿que pasa si se envian más bytes?
#include <string.h>
#include <stdio.h>
void vuln(char *src) {
char dest[64];
strcpy(dest, src);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <string>\n", argv[0]);
return 1;
}
vuln(argv[1]);
return 0;
}
❯ i686-w64-mingw32-gcc code.c -no-pie -o file.exe
Luego de compilarlo podemos pasarlo a un desensamblador como IDA
donde vemos que el puntero donde guardará los datos copiados apunta a ebp - 72
Usaremos como argumento una cadena de 72 A's
que se escribiran antes de ebp, 4 B's
que sobrescribiran ebp, 4 C's
que se escribirán el stack como las 20 D's
❯ python3 -q
>>> ("A" * 72) + ("B" * 4) + ("C" * 4) + ("D" * 20)
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCDDDDDDDDDDDDDDDDDDDD'
>>>
Para analizar el programa necesitamos un debugger, en este caso usaremos WinDbg
Si nos detenemos con un breakpoint en el ret
podemos ver que escribimos A's
hasta antes de sobrescribir ebp que ahora vale 0x42424242
(valor de BBBB
en hexadecimal) y las C's
y D's
se guardan justo después de esto en el stack
Al ejecutar el ret
intentara apuntar a la dirección de memoria guardada en la parte superior del stack pero como esa dirección la hemos sobrescrito con CCCC
intentara apuntar a 0x43434343
y al no encontrar esa dirección el programa corrompe
Resumiendo, el Buffer Overflow mas conocido generalmente ocurre cuando no se controla la cantidad de datos que se escriben y al sobrescribir la dirección de retorno esta apuntará a un valor que el atacante introduce por lo que puede tomar control