Pwning windows console application – Bypass ASLR. Leaks and more ROP! 0x3

Muy buenas a todos, seguimos haciendo retos de Ricardo Narvaja de exploiting cuyo objetivo sigue siendo el mismo, obtener ejecución de código como por ejemplo la calculadora de Windows. Este reto pueden encontrarlo con el nombre de Desafío3 en el grupo de Telegram CLSExploits. En el anterior reto vimos cosas tan interesantes como leak de la memoria, primitiva de escritura y hijack de una llamada a una función que nos interesaba para poder renovar la primitiva de escritura. Por último vimos como poder encadenar un ropchain y aprovechar un return-to-text y ejecutar la función system con su correspondiente argumento. Este reto es muy parecido salvo que cambian algunas cosas:

  • El reto tiene DEP y ASLR, mientras que el anterior no tenía DEP y nos permitía la opción de ejecutar una shellcode sin la necesidad de montar un ropchain llamando a la api VirtualProtect y cambiar los permisos. Aunque como todos bien sabéis si habéis leído el anterior tutorial, teniendo la función system y pudiendo montar una estrategia que permita su ejecución no es necesario una shellcode.
  • Necesitamos más leaks además del stack. Leak de ConsoleApplication y KernelBase.dll. (Bypass ASLR)
  • Explotación de primitiva de escritura de las funciones strcpy y memcpy.
  • Ropchain distinta a la anterior con doble Stack-pivot.
  • No stdout para leaks, excepto leak del stack aprovechando return-to-text y call printf.

Leaks KernelBase.dll, ConsoleApplication y Stack.

Cuando ejecutamos la aplicación de consola nos llama la atención que se queda en pausa para que presionemos Enter y así continuar. Analizamos las primeras instrucciones de la función main y sacamos las siguientes conclusiones:

  • Con la instrucción lea se mueve el puntero de la string «pause». Nos damos cuenta que esa string esta en .rdata y no en .data, lo cual tenemos un problema ya que es read-only y no podremos sobreescribir con la string calc.
  • Se mueve un size de 0x3c al stack en [rbp+10h+var_2C]. Sabemos que es el size del strcpy por instrucciones más adelante.
  • En [rsp+110h+var_98] se mueve el puntero correspondiente a la primitiva de escritura de la función memcpy.

Posteriormente abrirá el fichero file.txt en modo lectura donde se leerá con fgets nuestro contenido.

Si analizamos la memoria podemos apreciar que se identifica nuestro puntero correspondiente al destination del memcpy y que de algún modo deberemos de aprovechar para tener primitiva de escritura. Si se dan cuenta también esta el contenido del fichero file.txt en la memoria copiada con fgets y lo más resañable sin duda es que hemos sobreescrito el size que usa la función strcpy. Boom! Primer fallo o vulnerabilidad. Si vieron con anterioridad el size de la función strcpy se setea al principio de la función main con 0x3c y ahora esta sobreescrito con 0x24. Y ahora os preguntaréis, ¿por qué has sobreescrito con ese size y no lo has dejado como estaba por defecto? Bueno creo todos sabemos que para poder crear una estrategia de explotación el paradigma cambia dependiendo del escenario y las oportunidades que nos brinda la lógica de la aplicación al mismo modo que sus posibles vulnerabilidades. El poder controlar con escritura el size de la función strcpy nos va a permitir escribir en el destination de dicha función justo el size que nosotros queramos.

Tenemos que observar por lo tanto desde el destination del strcpy hasta el size máximo a copiar que ventajas podemos sacar al respecto. La ventaja es más que obvia ya que como veremos a continuación nos va a permitir lo siguiente:

  • Leak de direcciones de ConsoleApplication y de KernelBase.dll.
  • Control del puntero destination de la función memcpy obteniendo full-write primitive.

Es evidente que la vulnerabilidad es crítica. Apreciamos en la imagen de a continuación que el argumento destination que se le pasa a la función strcpy esta en el stack en la posición [rsp+110h+var_DC] y nuevamente aclaro como se le pasa al registro r8 el size modificado en este caso es 0x24.

Ahora que tenemos controlado la posición en el stack del destination siendo en la dirección de memoria 0x28FE04, observamos que tenemos en el stack que nos resulte provechoso para poder realizar leak.

Si se fijan justo con un size de 0x24 desde donde empieza a copiar hasta el final de dicho size podemos solapar con una dirección correspondiente a la librería KernelBase.dll y así lekearla. Y os preguntaréis, ¿cómo vas a lekearlo? Bueno en el propio programa existe al final del bucle de la función main un fwrite y un fclose, además se vuelve a abrir el fichero file.txt en modo escritura y lectura, por lo tanto podríamos volver a escribir con lo contenido en memoria en el fichero file.txt y así lekear dicha dirección del KernelBase.dll y así bypassear ASLR. Pero claro aquí tenemos otra vulnerabilidad, y es que se usa la misma variable correspondiente al destination de la función strcpy como destination del fwrite al fichero, por lo tanto, todo lo que nosotros copiemos con strcpy se añadirá con fwrite a fichero file.txt desde la misma posición. Como podéis observar se usa la misma variable [rsp+110h+var_DC] para el fwrite. Otro fallo o vulnerabilidad!!. Esto nos brinda la oportunidad de poder realizar dos leaks de manera consecutiva.

Además si se fijan en la memoria un poco más adelante con un size de 0x3e tenemos una dirección de una sección del propio binario ConsoleApplication por lo tanto podremos lekear y del mismo modo bypassear ASLR. También vemos que seguidamente después del strcpy tenemos otro input (Width, Heigth) siendo el control de copia del memcpy. En este punto para lekear lo dicho anteriormente no nos interesa realizar nada con el memcpy por lo tanto nuestro input por consola será simplemente 0 y 0 respectivamente. Ejecutamos con 0 y 0 y vemos el resultado de la primera iteracción en el bucle.

Boom! Allí tenemos nuestro primer leak de la librería KernelBase.dll con dirección 0x07fefda3408a. Seguramente también os preguntaréis, ¿para que quieres la dirección de esta librería? Bueno es sencillo, cuando estuve haciendo el ropchain tuve bastantes problemas usando directamente gadgets del propio binario ConsoleApplication y teniendo la dirección en memoria un puntero de la librería KernelBase.dll para poder lekear, opte por usar gadgets de esta ya que con ella si que pude montar el ropchain aunque también con algún que otro problema. Bien seguidamente de nuevo andamos en el inicio del bucle exactamente cuando abre el fichero file.txt en modo lectura, por lo tanto antes de que llame a la función modificamos nuestro payload del fichero quitando lo que se añadió y modificando el size de 0x24 a 0x3e para así lekear código del propio binario.

Como pueden observar ya tenemos nuestro leak con 0x013f8b. Nótese que el byte 0x8b es la mitad de toda una dirección y es donde esta implementado el ASLR, por lo tanto, ya lo tenemos bypasseado. Ahora que tenemos los dos leaks vemos en la memoria como hemos pisado ya esa zona por lo tanto ya no hay manera de poder volver a lekear las direcciones, y menos aún cuando hagamos el leak del stack jeje.

Para poder lekear una dirección del stack usaremos la misma técnica que hicimos con el anterior reto. La técnica básicamente consta de lo siguiente:

  • Modificar de nuevo el size del strcpy a 0x45 para así escribir el último byte del puntero del stack destination del memcpy.
  • Bruteforce low-byte de la primitiva de escritura del destination de la función memcpy.
  • Primitiva de escritura en el return address de la función memcpy escribiendo la parte baja (últimos dos bytes) y return-to-text en 0x13F8B1259 lea rcx, aAreaD ; "Area: %d \n". De este modo sabiendo que en el registro rdx queda el puntero del stack con retornar en el seteo del registro rcx ya llamará luego a printf y nos devolverá en decimal la dirección del stack.

El paradigma de explotación aplicando un pequeño bruteforce es asumible ya que solo es un byte y siendo el último byte solo son 16 combinaciones en las que debemos acertar y por lo tanto sale muy rápido si se hiciese en script o de forma manual en la explotación, siendo aproximadamente attacheando con un debugger de 15 minutos. También debo resaltar que no es como el anterior reto, debemos primero lekear por el orden antes descrito primero la librería KernelBase.dll y una dirección de una sección del binario ConsoleApplication. Debe ser así ya que vamos pisando de menos a más la memoria modificando el size del destination de la función strcpy. Sabiendo que la distancia desde la primitiva de escritura del memcpy a la dirección de retorno de dicha función es bastante pequeña, podemos siempre usar el bruteforce low-byte. Exactamente vemos la distancia existente desde la primitiva a la dirección de retorno en la siguiente imagen:

En nuestro payload asumiendo el bruteforce, debemos añadir el byte random siempre terminando en 8 y esperar a acertar. En nuestro caso es 0xc8. Si se fijan en la imagen siguiente vemos nuestro payload ya escrito para así lekear la dirección del stack.

Como son dos bytes los que tenemos que copiar con la primitiva de escritura del memcpy ingresaremos por teclado respectivamente 2 y 1. Si se fijan una vez retornamos de la función memcpy con el return-to-text estamos justo donde queríamos y si analizamos los registros debemos retornar justo en la instrucción que setea el registro rcx ya que si no lo hacemos cuando entre a la función printf romperá generando una excepción.

Y vemos como hemos lekeado la dirección del stack justo en 0x28fe04.

Boom! Listo el leak. Ahora debemos saber que con esta dirección debemos aplicar los distintos cálculos para la explotación manual del binario.

Write string calc in .data

Ahora seguidamente para poder escribir en .data debemos modificar el size del strcpy de nuevo de 0x45 a 0x49 debido a que ahora no vamos a pisar el último byte de la primitiva de escritura del memcpy en el stack, sino directamente queremos que la primitiva de escritura apunte a una dirección de .data. Sabiendo que de las direcciones de .text a la que yo quiero en .data contando con el ASLR, sería un +2. Es decir, si la dirección del prólogo de la función main es 0x013F8B1130 la dirección de .data será 0x013F8D7348. Y esto siempre es así, siempre será en este caso ASLR +2 en el byte del medio que tuvimos que hacer leak. Sin el leak, no podríamos escribir en .data con ASLR activado.

El payload en nuestro fichero file.txt quedaría de la siguiente manera:

En nuestro input deberemos poner el size del memcpy, en este caso 4 y 1 respectivamente. Ejecutamos y ya tendríamos escrita la string en .data.

Write ptr calc in .data

Posteriormente debemos realizar la misma operación pero ahora escribiendo el puntero donde está la string calc, es decir, 0x013F8D7348 a la dirección 0x013F8D73A8. Y de nuevo como no puede ser de otra manera os preguntaréis, ¿y por qué realizas esta operación de escritura? Bueno esto es debido a los problemas que tuve para montar el ropchain que veréis a continuación. Tuve que hacer esto para poder mover con un gadget directamente el puntero y no contenido de un puntero ya que no había ningún gadget que me cumpliera el paradigma de explotación según mi estrategia y sabiendo que tenemos full-write primitive con strcpy y memcpy, podemos escribir las veces que queramos. Nuestro payload quedaría de la siguiente manera:

En nuestro input deberemos poner el size del memcpy, en este caso 5 y 1 respectivamente. Ejecutamos y ya tendríamos escrito el puntero en esa dirección en .data.

Restablecer primitiva de escritura con dirección del stack en return address

Ahora es cuando necesitamos nuestro leak del stack ya que debemos escribir la dirección del stack donde queremos tener primitiva de escritura con la función memcpy. Sabiendo que siempre está en la misma posición la dirección que apunta a la escritura con memcpy, simplemente debemos realizar unos cálculos con nuestro leak a dónde está la escritura del return address. En este caso también no debemos modificar el size del strcpy ya que sabemos que las direcciones del stack son menores que las de .data del binario pero debemos rellenar con ceros para que así apunte bien. El payload quedaría de la siguiente manera en file.txt.

Nótese que ahora veis otra dirección distinta del stack. Eso es que tuve que quitarlo y volver a continuar de nuevo con otro proceso. Vosotros os pasará lo mismo cuando estéis explotando y cerréis el proceso, el ASLR os dará direcciones distintas variando el byte en la posición más intermedia al igual que las direcciones del stack, pero en definitiva el paradigma de explotación es el mismo contando con los leaks y teniendo calculado las distancias. Os recomiendo que hagáis esto y así tenéis controlado siempre la aleatoriedad y así siempre será más fácil saber que leaks se necesita.

Vemos en la memoria como tenemos ya reset la primitiva de escritura justo apuntando en el return address de la función memcpy.

Return-Oriented Programming

Antes de montar el ropchain debemos tener en cuenta lo siguiente:

  • La distancia del registro rsp a donde comience el ropchain.
  • Sabemos que strcpy copia hasta que se encuentra un NULL byte.
  • El ropchain debe estar contenido cuando se copia directamente del fichero file.txt a memoria.
  • Tenemos que conseguir setear en el registro rcx el puntero de .data con la string calc y así ejecutar system.

Con estas premisas empezamos analizando la memoria. Debemos pivotar usando dos gadget sumando a rsp la distancia necesaria hasta llegar a la parte del stack donde se copia desde file.txt a memoria. Los gadget usados serán los siguientes:

  • 0x00007FEFDA35D12 add rsp, 58h # retn KernelBase.dll.
  • 0x00007FEFDA617BC pop rax # retn KernelBase.dll.
  • 0x00007FEFDA3CAA9 mov rcx, [rax+60h] # movzx eax, byte ptr [rcx+2] # retn KernelBase.dll.

Además de estos gadget necesitamos espacio en memoria para escribir el puntero en .data de la string calc y finalmente la dirección de memoria en la función main del call system. Nuestro payload quedaría de la siguiente manera:

Ejecutamos con nuestro payload y de input respectivamente 6 y 1. Antes de la llamada a memcpy vemos en memoria como están nuestro gadget colocados a falta de la escritura de memcpy del return address.

Se ve claramente el doble pivot hasta finalmente al gadget pop rax pudiendo cuadrar la estrategia de explotación según los recursos disponibles. No lo comente anteriormente destacando que en el payload pusimos de size 0x44 del strcpy, y es debido para no pisar el último byte del puntero de primitiva de escritura del memcpy. Antes de ejecutar el memcpy me gustaría pararme a explicar porque elegí el gadget pop rax y no pop rcx. Básicamente es debido a que no existía ese gadget y tuve que hacer previamente la escritura de la string en .data de calc y luego la escritura del puntero de la string en una dirección de .data con un desplazamiento de 0x60, debido al tercer gadget ya que mueve el contenido de rax+60h al registro rcx, y si bien saben el contenido debería de ser un puntero y no la string. Por lo tanto, tuve que hacer dos escrituras en .data en vez de una.

Ejecutamos hasta el retorno de la función memcpy y vemos que estamos en el primer gadget. Si pueden apreciar en la imagen la distancia existente desde el puntero rsp al siguiente gadget a ejecutar siendo básicamente cuando se le sume a rsp 0x58 caerá en el mismo gadget jeje. De ahí que sea doble Stack-Pivot.

Ahora caemos en el segundo gadget y vemos la misma distancia entorno a 0x58 hasta el siguiente gadget correspondiendo a pop rax.

Seguidamente vemos como se popea al registro rax el puntero de la string calc.

En el siguiente gadget vimos lo que comente anteriormente se le suma a rax 0x60 apuntando al puntero en .data donde escribimos el puntero de .data que contiene la string.

Una vez ejecutado esas dos instrucciones vemos que nuestro objetivo está cumplido teniendo en el registro rcx el puntero de la string calc.

Volvemos a ejecutar y retornamos en el call system y nuestra preciada calculadora.

Espero que os haya servido el tutorial y como siempre encantado de poder redactar este tipo de writeup. Un saludo @naivenom.