xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



Exploit Development

Fundamentals


Antes de pasar a explotar, es necesario aprender algunos conceptos básicos, las explotaciones se realizarán en arquitectura x86 ya que conociendo como se realiza en ella se puede adaptar muy facilmente a x64 con un par de cambios minimos


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