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}