CryptOfTheUndead
3 minutes to read
We are given a binary called 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
If we run the binary, it allows us to encrypt a file:
$ ./crypt
Usage: ./crypt file_to_encrypt
We are also given a file called flag.txt.undead
, which is likely to be encrypted with this tool. Therefore, we will need to decrypt it.
Decompilation
If we open the binary in IDA, we will see the following main
function:
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();
}
In brief, it checks if there is a filename in the first command-line argument, reads its contents, encrypts it and saves it with the same filename plus .undead
as an extension.
This is the encryption function:
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);
}
It is using ChaCha20 as the encryption algorithm, which is a stream cipher.
Solution
Taking a look again at main
, we can see the key and nonce used to build the cipher:
encrypt_buf(buf, n[0], "BRAAAAAAAAAAAAAAAAAAAAAAAAAINS!!");
So, we know that the key is "BRAAAAAAAAAAAAAAAAAAAAAAAAAINS!!"
, and the nonce is just zero since n[0]
is likely to hold a 64-bit 0
integer.
Flag
Now that we know the encryption algorithm and the key and nonce used for that, we can go to CyberChef or use Python to decrypt the flag.txt.undead
file:
$ 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'
However, since ChaCha20 is a stream cipher, the encryption itself is just a XOR cipher between the plaintext and a key stream generated by ChaCha20. As a result, encryption and decryption is the same operation. Therefore, if we “encrypt” the encrypted file flag.txt.undead
we will get its decryption. We only need to rename the file so that it doesn’t end in .undead
, and use the same binary to “encrypt” it:
$ mv flag.txt.undead flag.txt
$ ./crypt flag.txt
successfully zombified your file!
$ cat flag.txt.undead
HTB{und01ng_th3_curs3_0f_und34th}