CryptOfTheUndead
3 minutos de lectura
Se nos proporciona un binario llamado crypt:
$ file crypt
crypt: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=5ac213d86cdb95af5f911c357cdc45c66b6bffc1, for GNU/Linux 4.4.0, not stripped
Si ejecutamos el binario, nos permite cifrar un archivo:
$ ./crypt
Usage: ./crypt file_to_encrypt
También se nos proporciona un archivo llamado flag.txt.undead, que probablemente está cifrado con esta herramienta. Por lo tanto, necesitaremos descifrarlo.
Descompilación
Si abrimos el binario en IDA, veremos la siguiente función main:
int __fastcall main(int argc, const char** argv, const char** envp) {
const char* v3; // rbp
size_t v4; // r12
char* v5; // rbx
size_t v6; // r13
void* v7; // r12
int v8; // ebp
int v9; // eax
int v10; // ebx
const char* v12; // rax
void* buf; // [rsp+8h] [rbp-40h] BYREF
size_t n[7]; // [rsp+10h] [rbp-38h] BYREF
n[1] = __readfsqword(0x28u);
if (argc <= 1) {
v12 = "crypt";
if (argc == 1) {
v12 = *argv;
}
v8 = 1;
printf("Usage: %s file_to_encrypt\n", v12);
return v8;
}
v3 = argv[1];
if ((unsigned int) ends_with(v3, ".undead", envp)) {
v8 = 2;
puts("error: that which is undead may not be encrypted");
return v8;
}
v4 = strlen(v3) + 9;
v5 = (char*) malloc(v4);
strncpy(v5, v3, v4);
*(_QWORD*) &v5[strlen(v5)] = 0x646165646E752ELL;
if (!(unsigned int) read_file(v3, n, &buf)) {
v6 = n[0];
v7 = buf;
encrypt_buf(buf, n[0], "BRAAAAAAAAAAAAAAAAAAAAAAAAAINS!!");
v8 = rename(v3, v5);
if (v8) {
v8 = 4;
perror("error renaming file");
} else {
v9 = open(v5, 513);
v10 = v9;
if (v9 < 0) {
v8 = 5;
perror("error opening new file");
} else {
write(v9, v7, v6);
close(v10);
puts("successfully zombified your file!");
}
}
return v8;
}
return main_cold();
}
En resumen, verifica si hay un nombre de archivo en el primer argumento de línea de comandos, lee su contenido, lo cifra y lo guarda con el mismo nombre de archivo más la extensión .undead.
Esta es la función de cifrado:
unsigned __int64 __fastcall encrypt_buf(__int64 a1, __int64 a2, __int64 a3) {
_BYTE v4[204]; // [rsp+0h] [rbp-F8h] BYREF
__int64 v5; // [rsp+CCh] [rbp-2Ch] BYREF
int v6; // [rsp+D4h] [rbp-24h]
unsigned __int64 v7; // [rsp+D8h] [rbp-20h]
v7 = __readfsqword(0x28u);
v5 = 0;
v6 = 0;
chacha20_init_context(v4, a3, &v5, 0);
chacha20_xor(v4, a1, a2);
return v7 - __readfsqword(0x28u);
}
Está usando ChaCha20 como algoritmo de cifrado, que es un cifrado en flujo.
Solución
Al observar nuevamente la función main, podemos ver la clave y el nonce usados para construir el cifrador:
encrypt_buf(buf, n[0], "BRAAAAAAAAAAAAAAAAAAAAAAAAAINS!!");
Entonces, sabemos que la clave es "BRAAAAAAAAAAAAAAAAAAAAAAAAAINS!!", y el nonce es simplemente cero ya que n[0] probablemente contiene un entero de 64 bits con valor 0.
Flag
Ahora que sabemos el algoritmo de cifrado y la clave y el nonce usados, podemos ir a CyberChef o usar Python para descifrar el archivo flag.txt.undead:
$ python3 -q
>>> from Crypto.Cipher import ChaCha20
>>>
>>> with open('flag.txt.undead', 'rb') as f:
... ChaCha20.new(key=b'BRAAAAAAAAAAAAAAAAAAAAAAAAAINS!!', nonce=b'\0' * 8).decrypt(f.read())
...
b'HTB{und01ng_th3_curs3_0f_und34th}\n'
Sin embargo, como ChaCha20 es un cifrado en flujo, el cifrado en sí es simplemente un cifrado XOR entre el texto claro y un key stream generado por ChaCha20. Como resultado, cifrado y descifrado son la misma operación. Por lo tanto, si “ciframos” el archivo cifrado flag.txt.undead, obtendremos su descifrado. Solo necesitamos renombrar el archivo para que no termine en .undead y usar el mismo binario para “cifrarlo”:
$ mv flag.txt.undead flag.txt
$ ./crypt flag.txt
successfully zombified your file!
$ cat flag.txt.undead
HTB{und01ng_th3_curs3_0f_und34th}