githubEditar

HackTheBox - Simple Encryptor

CHALLENGE DESCRIPTION

On our regular checkups of our secret flag storage server we found out that we were hit by ransomware! The original flag data is nowhere to be found, but luckily we not only have the encrypted file but also the encryption program itself.

Archivos iniciales:

  • encrypt: ELF 64-bit. El programa de cifrado

  • flag.enc: El Flag cifrado

Análisis inicial

Usando el program strings podemos ver si hay datos detectados como strings dentro de cualquiera de los dos archivos:

$ strings encrypt
/lib64/ld-linux-x86-64.so.2
libc.so.6
srand #Generación del seed de nums aleatorios
fopen #Abrir archivo en memoria
ftell #Ver dirección del puntero del archivo
time #Ver tiempo del sistema
__stack_chk_fail #Por el nombre, se intuye que comprueba la integridad del stack
fseek #Mover puntero del archivo a un punto específico
fclose #Cerrar archivo
malloc #Asignar memoria
fwrite #Escribir en archivo
fread #Leer archivo
__cxa_finalize
[...]
$

Del archivo encrypt podemos ver varias funciones que pueden dar una primera imagen del funcionamiento del binario. Por otro lado, para flag.enc:


Al ejecutar el programa con strace (syscall trace) podemos ver las syscalls que realiza el programa en runtime:

Aquí podemos ver que, al ejecutar el programa, este busca un archivo flag en el mismo directorio, y al no encontrarlo, genera un segfault. De esto podemos imaginar que el funcionamiento del programa es tomar un archivo flag, cifrarlo, y generar un archivo flag.enc, como el que tenemos.

Decompilando el binario

Abrimos el binario con ghidra y lo decompilamos, obteniendo el siguiente código:

Los nombres de funciones y variables se pierden al compilar el binario, así que habrá que ir viendo qué función cumple cada variable y asignarle un nombre identificativo.

Stack Canary

En primer lugar, vemos que a la variable local_10 se le asigna el valor que haya en la dirección en memoria in_FS_OFFSET + 0x28:

Y luego, al final del código, se comprueba si su valor sigue igual:

Si por un stack overflow, el valor de local_10 se ha sobreescrito, este ya no coincidirá con el valor almacenado en (in_FS_OFFSET + 0x28), la comprobación dará true y se llamará a __stack_chk_fail().

Con esto, podremos cambiarle el nombre a local_10 a algo como stackcanary.

Primeros archivos abiertos y asignación de memoria.

Tras asignar el valor al canary, se abre un archivo flag y se guarda su puntero en local_30, a la que llamaremos flagSOURCE. Después, se mueve el puntero al final del archivo en memoria flag y se comprueba la ubicación del puntero. Esto es una técnica usada para conocer el tamaño del archivo, cuyo valor luego se guarda en local_28, al que llamaremos tamañoFLAG.

Después vemos que se asigna una cantidad en memoria equivalente al tamaño del flag, se copia todo el archivo flag a memoria y se cierra el file descriptor de flagSOURCE.

Podemos intuir que la copia de flagSOURCE a local_20 se ha hecho con la idea de cifrar este archivo en memoria y luego guardarlo en el ya conocido flag.enc.

Dado que el programa va a trabajar con el archivo en local_20, podemos llamar a esta variable, p.ej, Workspace.

Aleatorización

Tras copiar el flag original, vemos que se realiza lo siguiente: Se guarda en tVar2 el tiempo del sistema en el momento de ejecución, como entero con signo (int), el formato default que devuelve la función time(). Luego, se cambia a entero sin signo (uint) y se guarda en local_40.

Después se inicializa una semilla basada en tiempoDelSistemaUINT para la generación de números aleatorios:

Modificación del archivo

Aquí empieza un bucle, con la variable a iterar siendo local_38:

Para más claridad, le cambiamos el nombre a uno común, i:

Y aquí podemos entender que lo que va a hacerse es iterar sobre cada byte individual del archivo (recordar que tamañoFLAG es el tamaño del archivo en bytes.) y modificarlo.

El bucle, simplificado para más claridad (quitando castings como (long)):

Aquí vemos que:

  1. iVar1 es un número aleatorio dado por rand(), por lo que le llamaremos random

  2. local_3c es un número aleatorio, del que luego se hace Bitwise AND con 7 (binario 111), por lo que su valor estará entre 0 y 7, así que le llamaremos entre0y7. Así queda que:

*(byte *) significa que se está trabajando con bytes individuales, sin importar el tipo de cada dato individual dentro del propio archivo (char, int, uint, etc.), todo se trata como bytes sin más. Tras dejar esto claro y para centrarnos solo en la estructura, omitimos los *(byte *):

Por cada byte vemos que:

  1. Se hace un Bitwise XOR (^) del byte específico y random.

  2. El byte se convierte en el resultado de una operación OR entre:

    • El byte desplazado a la izquierda entre0y7 bits (los bits que salen fuera del byte se pierden)

    • El byte desplazado a la derecha (8-entre0y7) bits, es decir, el byte que contiene a los bits antes perdidos al desplazar a la izquierda entre0y7 bits

Escritura en archivo cifrado

Tras iterar sobre todo el archivo, se abre local_18, al que llamaremos flagENCRYPTED, que corresponde al archivo flag.enc en modo write, y se escribe sobre él:

  • Los primeros 4 bytes de tiempoDelSistemaUINT, que, al tener UINT 4 bytes, es toda la variable.

  • Todo el Workspace, es decir, el archivo cifrado Y se cierra flagENCRYPTED

Y así, finalmente, se tiene el archivo cifrado.

Ahora, conociendo el algoritmo de cifrado, hay que hacer un programa encargado de descifrarlo.

Descifrado

El objetivo de nuestro programa de descifrado es tomar los primeros 4 bytes del archivo cifrado, que corresponden a tiempoDelSistemaUINT, e inicializar el seed con esa variable. Luego, por cada byte de flag cifrado (ignorando los 4 primeros del seed):

  • Llamar a rand() y guardar su resultado en un byte para el XOR (Pues el valor de random para el XOR es resultado de la primera llamada a rand() al cifrar, y si los cambiamos de orden se descifrará mal.)

  • Llamar a rand() para sacar entre0y7, con la segunda llamada y el Bitwise AND 7. Después, tendremos que modificar el byte cifrado siguiendo el camino inverso:

  • Rotar a la derecha entre0y7 bytes, sin perderlos (como al cifrar).

  • Hacer el XOR con random Y finalmente copiar ese byte específico al byte del resultado final.

En C, el código sería algo así (convendría hacer comprobaciones tras abrir archivos con fopen):

Y al ejecutarlo en el mismo directorio que flag.enc:

Última actualización