Jordan & Tunisia National CTF 2018 - Stupid Rand

The following was provided by the challenge creators:

  • A rev100b binary which contained the encryption module of the ransomware
  • A flag.txt file which contained the encrypted flag
  • The first few characters of the flag plaintext: flag{

Analysis

I started my analysis by determining the binary type, and executing it:

1
2
3
4
$ file rev100b
rev100b: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=b47b99169b807180fe02d0dac1a820acc3429931, not stripped
$ chmod +x rev100b && ./rev100b
Usage: ./rev100b <path_to_file> <path_to_key>

Following the expected program args, I created some fake input to try and understand how the binary worked before looking under the hood.
I noticed that running the binary twice with the same input resulted in a decrypted test.txt:

1
2
3
4
5
6
7
8
9
$ echo "this is a test" > test.txt && echo "" > key.txt
$ ./rev100b test.txt key.txt
$ xxd test.txt && xxd key.txt
00000000: ef49 aa97 9c5a b5a5 8a80 68c5 abb2 34 .I...Z....h...4
00000000: b27c 0000 .|..
$ ./rev100b test.txt key.txt
$ xxd test.txt && xxd key.txt
00000000: 7468 6973 2069 7320 6120 7465 7374 0a this is a test.
00000000: b27c 0000 .|..

Disassembly

Binary Ninja showed a call to an encrypt_file function from within the main function:

Right before that call, some calculations were performed using the current system time and some weird constant 0x48d159e26af37c05, then the result were passed to the encrypt_file function.
The following pseudo-code gives a rough idea of what was going on:

1
2
3
4
cur_time = time(0)
bignum = 0x48d159e26af37c05
calc = *some weird bit shifting math with bignum and cur_time that I ignored *
encrypt_file(calc, filename)

Reversing the encrypt_file function:

  1. Open the file passed to it with fopen
  2. Use the calc value passed to it, and feed it to the srand function. Also, calculate the length of the file using fseek and ftell
  3. Read each character/byte with fread
  4. Generate a random number using rand and xor it with the character read
  5. Write the result of the xor back to the file, replacing the original character. (not shown above)

The rest of the binary had a function which simply saved the created key to the file specified as a command line argument.

Decryption

The binary used xor for encryption, and we are given part of the plaintext: flag{ and the ciphertext in flag.txt
Since its an xor operation, the following holds:

1
key XOR plaintext = ciphertext

After running some tests, I noticed the key always resulted in a 2 byte value, making brute force an option.
I wrote a small C program that did the following:

  1. Generate all possible keys (0x0..0xFFFF)
  2. xor each key with the first 5 bytes of the ciphertext in flag.txt
  3. If the result of the xor = flag{, then use that key to decrypt the flag.txt file
decrypt.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
FILE *flag;
flag = fopen("/home/me/Desktop/flag.txt", "rb");
if(!flag)
{
printf("couldn't open flag.txt!");
return -1;
}

char ciphertext[6] = "";
char eplaintext[] = "flag{"; // expected plaintext
char aplaintext[6] = ""; // actual plaintext

// read first 5 chars in flag.txt (ciphertext)
fread(&ciphertext, 1, 5, flag);

// brute force the key
for (int i = 0; i <= 0xFFFF; i++)
{
// generate first 5 chars with current key
srand(i);
for (int c = 0; c < 5; c++)
aplaintext[c] = ciphertext[c] ^ rand();

// check if key is valid
if (strncmp(aplaintext, eplaintext, strlen(eplaintext)) == 0)
{
// we've got a valid key! print flag!
char f;
printf("flag{");
while (fread(&f, 1, 1, flag))
printf("%c", f^rand());
printf("\n");
break;
}
}

fclose(flag);
}

Running the program gave us the flag:

1
2
3
$ gcc -g decrypt.c -o decrypt
$ ./decrypt
flag{rand_is_not_enough_for_encryption}

FLAG: flag{rand_is_not_enough_for_encryption}