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:
- URL
http://twitrss.me/twitter_user_to_rss/?user=ACenterForAnts
. This was called throughInternetOpenURLA
.
- A POST request to
mugatu.flare-on.com
. Interestingly, querying withnslookup
from the terminal returnsNXDOMAIN
which implies it's a non existent domain.
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.
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.
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.
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.
Deobfuscating API calls
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.
Correspondingly, the disassembly is now readable as we have removed all the indirect calls.
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".
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.
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.
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 -61C88647
when 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.
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);
}
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.
Decrypting best.gif
Using the key we can XTEA decrypt best.gif.Mugatu to obtain the flag.