Flare-On 6 CTF WriteUp (Part 10)

This is the tenth part of the Flare-On 6 CTF WriteUp Series.

10 - Mugatu

The challenge reads

Hello,
I’m working an incident response case for Derek Zoolander. He clicked a link and was infected with MugatuWare! As a result, his new headshot compilation GIF was encrypted. To secure an upcoming runway show, Derek needs this GIF decrypted; however, he refuses to pay the ransom. We received an additional encrypted GIF from an anonymous informant. The informant told us the GIF should help in our decryption efforts, but we were unable to figure it out. We’re reaching out to you, our best malware analyst, in hopes that you can reverse engineer this malware and decrypt Derek’s GIF.

I've included a directory full of files containing:
MugatuWare malware
Ransom note (GIFtToDerek.txt)
Encrypted headshot GIF (best.gif.Mugatu)
Encrypted informant GIF (the_key_to_success_0000.gif.Mugatu)

Thanks, Roy

The challenge is a reference to the movie Zoolander 2. The main objective is to decrypt a GIF file which has been encrypted by the ransomware. In addition to these two files we have an additional encrypted GIF which has been provided for our help. Since we are dealing with a ransomware it's best to use a Virtual Machine. Doing so will also enable us to run the sample and observe its behavior.

Dynamic Analysis

For dynamic analysis I'll be using API Monitor. This free tool allows us to trace all the API calls made by an program while its running. Using the tool we notice the following:

Figure 1: Twitrss.me
  • A POST request to mugatu.flare-on.com. Interestingly, querying with nslookup from the terminal returns NXDOMAIN which implies it's a non existent domain.
Figure 2: POST request to mugatu.flare-on.com
Figure 3: NXDOMAIN

Let's add the following entry to our host file (C:\Windows\System32\drivers\etc\host) for redirecting traffic to mugatu.flare-on.com to localhost.

127.0.0.1 mugatu.flare-on.com

We need to set up a HTTP server for receiving the POST request. We can code one in Python (like below) or use a tool like Packet Sender. Once the server is setup let's run the binary.

from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler

class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        content_length = int(self.headers['Content-Length'])
        body = self.rfile.read(content_length)
        print body
        self.send_response(200)
        self.end_headers()

httpd = HTTPServer(('localhost', 80), SimpleHTTPRequestHandler)
httpd.serve_forever()

The malware sends out POST requests which looks like Base64 encoded data.

λ python post_server.py
7u6sH0UAAAAIQk4RXEsKXQ5UQVxLHX8dVUIfDX4WV14IYQVNGRwGAAATFRFPBhQmGi8+EwtEABFTHRBRQF1VVlZVRhcDHllUVhRbEVg=
127.0.0.1 - - [30/Sep/2019 13:54:11] "POST / HTTP/1.1" 200 -
7u6sH0UAAABBZG1pbnwxMC4wLjIuMTV8Ni0xLTc2MDF8QWRtaW5pc3RyYXRvcnxDOlxXaW5kb3dzfDA5LzMwLzIwMTktMDg6MjQ6MTA=
127.0.0.1 - - [30/Sep/2019 13:54:17] "POST / HTTP/1.1" 200 -
7u6sH0UAAAAIQk4RXEsKXQ5UQVxLHX8dVUIfDX4WV14IYQVNGRwGAAATFRFPBhQmGi8+EwtEABFTHRBRQF1VVlZVRhcDHllUVhRbEVg=
127.0.0.1 - - [30/Sep/2019 13:54:22] "POST / HTTP/1.1" 200 -

If we look at the POST request in a debugger we can find expects a response to the POST request which apparently should also be Base64 encoded as shown in Figure 4.

Figure 4: The response should be base64 encoded

After Base64 decoding the response, it's xored with 0x4D and it must match the string orange mocha frappuccino\0. Appended with this string we need to send another 4 byte value which will later be used as a key to encrypt files as we will soon see. In the code below, we are sending the key \x01\x02\x03\x04.

from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
import base64

class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        content_length = int(self.headers['Content-Length'])
        body = self.rfile.read(content_length)
        print body
        self.send_response(200)
        self.end_headers()
        start = str(bytearray(map(lambda x: x^0x4d, bytearray('orange mocha frappuccino\0'))))
        key = '\x01\x02\x03\x04'
        to_send = start + key + '\xff'*34
        self.wfile.write(base64.b64encode(to_send))
        
httpd = HTTPServer(('localhost', 80), SimpleHTTPRequestHandler)
httpd.serve_forever()

Let's go back to API Monitor. We spot a series of the following function calls

  • GetLogicalStringsA
  • GetDriveTypeA
  • FindFirstFileA

Typically, in a ransomware these functions are generally used to enumerate all the files in a directory.

Figure 5: Call to FindFirstFileA

Let's set a breakpoint on the functions and try to know what it's really doing. To thwart analysis, all the API calls have been obfuscated. Shown in Figure 6 are the stubs which ultimately lead to the real API.

Figure 6: Obfuscated API calls stub

For example in Figure 7, it is actually a call to FindFirstFileA. Instead of a direct call, it calls the corresponding stub which ultimately lands on the real function.

Figure 7: Obfuscated call to FindFirstFileA

Deobfuscating API calls

Figure 8: Addresses of the stubs

The addresses of the stubs are stored in a pointer table as shown in Figure 8. To deobfuscate, we can simply write the address of the final function in the pointer table bypassing the stub. The following x64dbg script does that

addr = dump.sel()
i = 0

loop_1:
mov api_jump, [addr]
api = dis.imm(api_jump)
not api
mov [addr], api
add addr, 4

inc i
cmp i, 0x50
jl loop_1

After running the script, the pointer table look like Figure 9.

Figure 9: Updated pointer table

Correspondingly, the disassembly is now readable as we have removed all the indirect calls.

Figure 10: Call indirection removed

The malware recursively iterates over all directories starting from C:\ drive. As we can see in Figure 11, it compares the directory name with the string "really, really, really, ridiculously good looking gifs".

Figure 11: The malware it searching for a special directory

Which means its searching for a directory with that specific name. Lets create one such directory like C:\$\really, really, really, ridiculously good looking gifs. We have created the directory under a directory named $ so that it will be found first. Within the directory we keep a GIF file since we know it encrypts them. Instead of a real GIF we can use a dummy file filled with the a bunch of A's such that it will be easier to spot in the debugger If at this point we let the malware run freely, we'll see that it goes on to encrypt the dummy GIF with .Mugatu extension appended.

Finding the encryption algorithm

We need to locate the encryption code by which it encrypts the files. A quick way is to set breakpoint on File Handling APIs like CreateFileA, ReadFile, WriteFile etc. Using this approach we can quickly zero in on the relevant code as shown in Figure 12.

Figure 12: Code related to encryption

The malware loads the file in memory using CreatFileA, CreateFileMappingA and MapViewOfFile. A few lines below we notice an indirect call to a function which does the encryption.

Figure 13: Call to encrypt function

This function takes in three parameters:

  • A pointer to the buffer containing the file contents to encrypt
  • Length of the above buffer
  • Pointer to the 4 byte key (which in our case was \x01\x02\x03\x04)
push ebp
mov ebp,esp
push ebx
push esi
mov esi,dword ptr ss:[ebp+C]
push edi
--------------snip--------------

add edx,esi
xor ecx,eax
sub esi,61C88647
add ecx,ebx

--------------snip--------------
pop esi
pop ebx
pop ebp
ret

Inspecting the code we notice the constant 61C88647. Searching for this value on Google points us to Tiny Encryption Algorithm (TEA). Note that the actual constant used in TEA source code is 9E3779B9 (which is the same as -61C88647when treated as an unsigned 32-bit integer). Apart from TEA, the XTEA cipher also uses the same constant and have a similar structure.

Let's try to decompile the encryption code. We can do this by copying the assembly to a new file and assemble it using fasm.

; enc-algo.asm

format PE

encrypt:
    push ebp
    mov ebp, esp
    push ebx
    push esi
    mov esi, dword [ebp+0xC]
    push edi
    mov edi, dword [esi]
    mov ebx, dword [esi+0x4]
    xor esi, esi

  here:
    mov ecx, dword [ebp+0x10]
    mov eax, esi
    and eax, 0x3
    movzx edx, byte [eax+ecx*1]
    mov ecx, ebx
    shl ecx, 0x4
    mov eax, ebx
    shr eax, 0x5
    add edx, esi
    xor ecx, eax
    sub esi, 0x61C88647
    add ecx, ebx
    xor ecx, edx
    mov edx, dword [ebp+0x10]
    add edi, ecx
    mov ecx, edi
    mov eax, edi
    shr eax, 0x5
    shl ecx, 0x4
    xor ecx, eax
    mov eax, esi
    shr eax, 0xB
    add ecx, edi
    and eax, 0x3
    movzx eax, byte [eax+edx]
    add eax, esi
    xor ecx, eax
    add ebx, ecx
    dec dword [ebp+0x8]
    jne short here
    mov esi, dword [ebp+0xC]
    mov dword [esi], edi
    pop edi
    mov dword [esi+0x4], ebx
    pop esi
    pop ebx
    pop ebp
    ret

After renaming the variables appropriately, our decompiled code looks like Figure 14.

Figure 14: Decompiled encryption function

The TEA cipher uses a fixed number (32) of rounds which is not the case with here. This encryption algorithm here is actually a modified version of the XTEA cipher using a 32 bit key instead of the standard 64 bit.

Decrypting the informant GIF

The informant GIF has the filename the_key_to_success_0000.gif.Mugatu. The name hints that the key used to encrypt this file is \x00\x00\x00\x00. Using the following code we can decrypt the informant GIF.

#include <stdio.h>

typedef unsigned int uint32_t;
typedef unsigned char uint8_t;

void xtea_decipher(unsigned int num_rounds, uint32_t v[2], uint8_t const key[4]) {
    unsigned int i;
    uint32_t v0=v[0], v1=v[1], delta=0x9E3779B9, sum=delta*num_rounds;
    for (i=0; i < num_rounds; i++) {
        v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
        sum -= delta;
        v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
    }
    v[0]=v0; v[1]=v1;
}

void main()
{
  FILE *inf = fopen("the_key_to_success_0000.gif.Mugatu", "rb");
  FILE *out = fopen("the_key_to_success_0000.gif", "wb");
  uint8_t key[] = {0, 0, 0, 0};
  
  fseek(inf, 0, SEEK_END);
  int file_size = ftell(inf);
  rewind(inf);
  int remaining = file_size;
  uint32_t ct[2];  

  while (remaining > 8)
  {
    fread(ct, sizeof(uint32_t), 2, inf);
    xtea_decipher(32, ct, key);
    fwrite(ct, sizeof(uint32_t), 2, out);
    remaining -= 8;
  }

  uint8_t buffer[8];

  if (remaining > 0)
  {
    fread(buffer, remaining, 1, inf);
    fwrite(buffer, remaining, 1, out);
  }
  fclose(inf);
  fclose(out);
}
Figure 15: the_key_to_success_0000.gif

The GIF hints that the first key byte used for encrypting best.gif is 0x31. Now all we need is to bruteforce the other 3 bytes of the key. We know that a GIF file starts with the bytes "GIF89". If we use the correct key the decrypted buffer must start with those bytes. Further, to speed up bruteforce we can try to decrypt just the first 8 bytes instead of the entire file.

int main()
{
  for (uint8_t k1 = 0; k1 < 0xff; k1++)
  {
    for (uint8_t k2 = 0; k2 < 0xff; k2++)
    {
      for (uint8_t k3 = 0; k3 < 0xff; k3++)
      {
        // First 8 bytes of best.gif.Mugatu
        uint32_t ct[] = {0x50B08E24, 0x6F68B2E8};

        uint8_t key[] = {0x31, k1, k2, k3};

        xtea_decipher(32, ct, key);
        if (ct[0] == 0x38464947) //GIF
        {
          printf("Key bytes 31 %x %x %x\n", k1, k2, k3);
          return 0;
        }
      }
    }
  }
  return -1;
}

Running our bruteforcer we get the full key in seconds as shown in Figure 16.

FIgure 16: Bruteforcing the key

Decrypting best.gif

Using the key we can XTEA decrypt best.gif.Mugatu to obtain the flag.

Figure 17: best.gif

Flag: FL4rE-oN_5o_Ho7_R1gHt_NoW@flare-on.com