.. | ||
release | ||
src | ||
README.md |
CryptOfTheUndead
9th 10 24 / Document No. D24.102.170
Prepared By: clubby789
Challenge Author: clubby789
Difficulty: Very Easy
Classification: Official
Synopsis
CryptOfTheUndead is a Very Easy reversing challenge. Players will reverse engineer a ransomware binary to uncover a static key, then decrypt a file to gain the flag.
Skills Required
- Decompiler usage
Skills Learned
- Recognizing cryptographic primitives
- Bypassing program checks
Solution
We're provided two files; crypt
and flag.txt.undead
. Running crypt
gives us an error:
Usage: ./crypt file_to_encrypt
so we can surmise that this is possibly some kind of ransomware, used to encrypt files. flag.txt.undead
seems to contain random bytes, so it is likely encrypted.
Analysis
Opening it in a decompiler, we can see the program is not stripped.
int32_t main(int32_t argc, char** argv, char** envp)
int32_t return
if (argc s<= 1) {
char const* const program_name = "crypt"
if (argc == 1)
program_name = *argv
return = 1
printf(format: "Usage: %s file_to_encrypt\n", program_name)
We begin by checking the args, and printing a help string if used incorrectly.
We then check the file argument passed, and ensure it doesn't end with .undead
:
char* old_filename = argv[1]
if (ends_with(old_filename, ".undead") != 0) {
return = 2
puts(str: "error: that which is undead may not be encrypted")
We'll then allocate space for our new filename, likely used for the post-encryption fike, and append .undead
to the old filename.
uint64_t new_filename_len = strlen(old_filename) + 9
char* new_filename_buf = malloc(bytes: new_filename_len)
strncpy(new_filename_buf, old_filename, new_filename_len)
*(new_filename_buf + strlen(new_filename_buf)) = '.undead'
We then perform the encryption.
if (read_file(old_filename, &nbytes_1, &buf_1) != 0)
return main.cold() __tailcall
uint64_t nbytes = nbytes_1
void* buf = buf_1
encrypt_buf(buf, nbytes, "BRAAAAAAAAAAAAAAAAAAAAAAAAAINS!!")
int32_t return_1 = rename(old: old_filename, new: new_filename_buf)
return = return_1
main.cold
is a short function that prints an error message then exits. If the read is successful, we call encrypt_buf
on it. We then use rename
to rename the file to the new name.
Looking into encrypt_buf
:
int64_t encrypt_buf(char* buf, uint64_t buflen, uint8_t* key)
uint8_t nonce[0xc]
nonce[0].q = 0
nonce[8].d = 0
struct chacha_ctx ctx
chacha20_init_context(&ctx, key, &nonce, 0)
chacha20_xor(&ctx, buf, buflen)
We can see it uses chacha20
to encrypt a buffer with a given key and a nonce of 0. Even if this file was stripped, we could recognize this as chacha20
by the cryptographic constant used in chacha20_init_context
:
__builtin_strncpy(dest: &arg1[0x10], src: "expand 32-byte k", n: 0x10)
Decryption
Knowing the algorithm (chacha20) and the key (BRAAAAAAAAAAAAAAAAAAAAAAAAAINS!!
) is enough to encrypt this file. However, we can use a useful property of chacha20 instead. It is a reversible stream cipher, meaning that the 'encrypt' operation can be re-run on ciphertext with the same key to produce the plaintext.
The binary prevents this by checking for the .undead
suffix. However, we can either patch that check out, or simply rename flag.txt.undead
to flag.txt
. We then run the binary on flag.txt
, and read flag.txt.undead
to reveal the flag.