[Flare-On 2021 write-ups] 01 - credchecker y 02 - known

Empezamos una serie de posts con write-ups para el 8º desafío de Flare-On que tuvo lugar del 10 de septiembre de 2021 al 22 de octubre de 2021. 

Flare-On es una CTF enfocada a la ingeniería inversa y análisis de malware que organiza anualmente la empresa FireEye, podéis leer más info en el blog de FireEye

Una nota importante antes de empezar, no soy un experto en absoluto y estoy bastante seguro de que resolví muchos de los desafíos de la forma más tonta posible. Después de leer algunos de los write-ups que ha sacado la gente me siento como un neandertal de la ingeniería inversa, así que espero que después de leer esto, todo el mundo salga con la sensación de que puede terminar Flare-On el próximo año. Me gustaría animaros también a intentar los retos por vuestra cuenta mientras vais siguiendo los write-ups, los podréis encontrar una vez los publiquen en la web de Flare-On en la sección PastResults.

01 - credchecker

Este es un reto realmente fácil, lo que se llamaría un “Sanity Check” en una CTF. Al descargar el reto obtenemos un archivo HTML, al abrir este archivo en el navegador se muestra una sencilla página de inicio de sesión que pide un nombre de usuario y una contraseña.

Si ahora abrimos el archivo HTML con un editor de texto podemos ver que contiene código Javascript que comprueba el texto introducido en los campos con la siguiente función.

function checkCreds() {
	if (username.value == "Admin" && atob(password.value) == "goldenticket") 
	{
		var key = atob(encoded_key);
		var flag = "";
		for (let i = 0; i < key.length; i++)
		{
			flag += String.fromCharCode(key.charCodeAt(i) ^ password.value.charCodeAt(i % password.value.length))
		}
		document.getElementById("banner").style.display = "none";
		document.getElementById("formdiv").style.display = "none";
		document.getElementById("message").style.display = "none";
		document.getElementById("final_flag").innerText = flag;
		document.getElementById("winner").style.display = "block";
	}
	else
	{
		document.getElementById("message").style.display = "block";
	}
}

Mirando este código, podemos ver que el nombre de usuario debe ser “Admin” y la contraseña debe ser “goldenticket” después de ser decodificada en base64 por la función atob(). Podemos utilizar una herramienta como CyberChef para codificar el texto “goldenticket” como base64, lo que nos dará la contraseña “Z29sZGVudGlja2V0”. Tras introducir el nombre de usuario y la contraseña previamente calculados en la web, obtenemos la primera flag.

02 - known

En este reto obtenemos un ejecutable llamado UnlockYourFiles.exe y algunos archivos cifrados que tienen la extensión .encrypted. Si ejecutamos el exe, nos sale lo que parece una nota de ransomware pidiendo una clave para descifrar los archivos. Podemos introducir una clave aleatoria como por ejemplo “test” y vemos que intenta descifrar los archivos con la clave erronea generando basura.

Algo importante que debemos pararnos a pensar es que conocemos algunos de los contenidos originales de los archivos encriptados, como las cabeceras PNG, JPEG y GIF, y podemos suponer que el contenido de latin_alphabet.txt es probablemente "ABCD… " o "abcd… ". Es hora de empezar a hacer reversing del ejecutable y ver si podemos deducir la clave a partir de su algoritmo de descifrado. En mi caso, uso IDA y su descompilador, pero Ghidra también es una buena opción.

Después de seguir el flujo de ejecución encontramos la función responsable del descifrado y cómo se está utilizando. A continuación podeis encontrar una versión simplificada y limpiada del pseudocódigo de descifrado.

while (NumberOfBytesRead)
{
  // Read encrypted file 8 bytes at a time
  ReadFile(hEncryptedFile, file_data, 8u, &NumberOfBytesRead, 0);
  // Decrypt using inputed key
  decrypt(file_data, key);
  // Write decrypted 8 bytes to new file
  WriteFile(hFile, file_data, NumberOfBytesRead, &NumberOfBytesWritten, 0);
}

void decrypt(char *file_data, char *key)
{
  for ( int i = 0; i < 8; i++ )
  {
    // Xor key with file data, rotate left i times and substract i
    file_data[i] = __ROL1__(key[i] ^ file_data[i], i) - i;
  }
}

Como podemos ver en el bucle for de decrypt(), la clave sólo tiene 8 bytes de longitud. Podemos utilizar la inversa del algoritmo utilizado para descifrar y los bytes originales conocidos de cualquiera de los archivos para calcular la clave de 8 bytes. En mi caso, he utilizado la cabecera del archivo PNG, ya que los primeros 8 bytes son siempre “\x89PNG\r\n\x1a\n”. Con ese conocimiento, podemos crear un simple script en python para calcular la clave original como el que vemos a continuación.

# Implementación de Rotate Right copiada de stackoverflow
ror = lambda val, r_bits: \
    ((val & (2**8-1)) >> r_bits%8) | \
    (val << (8-(r_bits%8)) & (2**8-1))

# Bytes conocidos de la cabecera PNG
original = b"\x89PNG\r\n\x1a\n"
# Leemos los 8 primeros bytes del archivo cifrado
with open("Files/capa.png.encrypted", "rb") as f:
    encrypted = f.read(8)
# Algoritmo de descifrado inverso usando los bytes originales conocidos
for i in range(8):
    b = (original[i] + i) & 0xff
    k = ror(b, i)
    print(chr(k ^ encrypted[i]), end="")
print()
# Key: No1Trust

Si ejecutamos el script obtenemos la clave “No1Trust”. Ahora podemos utilizar esa clave en UnlockYourFiles.exe y descifrar todos los archivos. Si abrimos el archivo descifrado critical_data.txt, obtenemos la flag.

(>0_0)> You_Have_Awakened_Me_Too_Soon_EXE@flare-on.com <(0_0<)

Contribución gracias a @0xGsch

Comentarios