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 llamadas definen lo que se necesita para llamar a una función:
• Como se pasan los argumentos a la función
• Como se prepara la pila antes de la llamada
• Como se restaura la pila después de la llamada
Por tanto es importante que se utilice la convención de la llamada correcta para la función, las funciones de la API
de Win32 utilizan la convencion de llamadas __stdcall
mientras que las funciones en tiempo de ejecución C
utilizan la convención __cdecl
, en los 2 casos los parametros se empujan a la pila en orden inverso, la diferencia es que cuanso se utiliza __stdcall
la pila es limpiada por el callee mientras que cuando se utiliza __cdecl
es limpilada por el caller
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
: Guarda el valor de retorno de la función.
• EBX
: Puntero base a la sección data.
• ECX
: Contador en operaciones de bucles.
• EDX
: Puntero de entrada/salida.
• ESI
: Puntero a la fuente en operaciones de cadenas.
• EDI
: Puntero al destino en operaciones de cadenas.
Otros registros bastante importantes que almacenan punteros son:
• ESP
: Almacena el puntero a la parte superior del stack.
• EBP
: Apunta a la parte superior de la pila cuando se llama a la función.
• EIP
: 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 desde el byte mas significativo o MSB (Most Significant Byte), 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 ya que inicia desde el byte menos significativo LSB (Low Significant Byte) 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
, la función vuln
inicia guardando el estado de los registros esp
y ebp
al entrar a la función con push ebp; mov ebp, esp;
, luego llama a strcpy
usando como src
el argumento y estableciendo el destino en ebp - 72
, luego de la llamada restaura los valores con leave
que equivale a mov esp, ebp; pop ebp;
y ejecuta el ret
para salir de vuln
Con ello podemos crear un pequeño payload, llenamos con 72 A's♠
los bytes de buffer antes de ebp, luego 4 B's
que sobrescribirán ebp
y 4 C's
que sobresscirirán el return address además de algunas D's
adicionales que se guardarán en el stack
❯ python3 -q
>>> ("A" * 72) + ("B" * 4) + ("C" * 4) + ("D" * 20)
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCDDDDDDDDDDDDDDDDDDDD'
>>>
Para analizar el programa necesitamos un debugger, en este caso usaremos WinDbg
Llegamos a la llamada a strcpy
, el primer argumento en esp
es el destino donde copiarán los datos y el segundo en esp + 4
es la fuente de donde los copiará
Una vez llamamos a strcpy
la dirección de destino tiene los mismos datos que la fuente y el registro eax
contiene como valor de retorno el puntero del destino
Cuando llegamos al leave
en ebp
tenemos los 2 dwords de B's
Y C's
, cuando se ejecuta mov esp, ebp
ahora el stack apunta a ellos y cuando ejecuta el pop ebp
toma el dword de las B's
y lo guarda en ebp
, entonces cuando llega el ret
en el stack se encuentra el dword de las C's
y cuando lo ejecuta intenta retornar a 0x43434343
, el resto de los datos los vemos representados en el stack que son las 20 D's
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