Sunday 20 May 2018

Solving MalwareBytes Crackme 2

This is a web version of the writeup I submitted for the second Malwarebytes crackme. It requires a few techniques including python decompilation, x86 reverse engineering and some minor cryptanalysis - which was pretty cool as you could solve it in a few different ways.

Stage 1

I usually start my analysis by looking at the strings in a binary as it gives a good 'feel' for the nature of the target, as well as hinting to any open source code that the author used which can make reversing easier. Running it through FLOSS (FireEye's version of Strings-on-steroids) gives us lots of helpful looking Python-related error messages such as "Error detected starting Python VM” - suggesting that at this stage we are dealing with a python program packed as windows executable.

These are usually easy to unpack if we can find out which packer was used -the top Google results confirm that the error message above is from 'PyInstaller'. It's documentation describes PyInstaller binaries as a bootloader with an archive appended to the end (start bytes PYZ), which you can unpack with the PyInstaller archive viewer. I found a tool called 'pyinstxtractor' which did the work for me, and it produced the following output:



The 'another' file immediately jumps out at the most promising location for the target code, with its odd name and exe manifest and sure enough a string search of it shows all the strings about secrets and flags that we see when the program runs. But what to do with it? I assumed it was python bytecode but running the 'uncompyle6' decompiler on it just gave me an error about it not being a .py or .pyc file.

Lets look at the magic bytes at the start of the file (in Binary Ninja):




63 00 00 00? What? I couldn't find it on any list of file signatures. Looking at more search results for “Decompiling Pyinstaller Binaries” and what do we see 5 results down? Hasherezades blog... solving a variation of this exact problem. I'm not sure this way of solving it is really in the spirit of the challenge, but that's what knowledge sharing is for.

Anyway, now I know that the file is just a .pyc file with the signature irritatingly stripped off – it's time to fix that (typing “03 f3 0d 0a 00 00 00 00” in Binary Ninjas hex mode) and start decompiling.

With the python code in our hands we can now tackle the credential check.

Check_login() just checks that we use the name 'hackerman' and check_password() succeeds if the md5 hash of our password is '42f749ade7f9e195bf475f37a44cafcb'. According to the md5 lookup page at https://hashkiller.co.uk – that password is 'Password123'. Hmm.

The final gate at this stage is a PIN check. The PIN is used as a seed for the random number generator, then create_url_key() appends 32 “random” decimal digits into a key. This PIN-derived key is compared in a similar way to the password, with its md5 hash having to match 'fb4b322c518e9f6a52af906e32aee955'.

The problem with PIN's (or PIN numbers if you like redundant acronyms) is that they only work if you can't brute force them – but since we have the hash and the hash-checking algorithm we can do:

    for i in range(10000):
        key = get_url_key(int(i))
        if check_key(key):
            print "Correct key is: %d"%i
            break

And instantly get a result of 9667. Note to self: Do PIN searches in reverse.

Stage 2

Using the credentials above will drop us down to decode_and_fetch_url(), which uses the key to decrypt an embedded string to: “https://i.imgur.com/dTHXed7.png“.

I'm always slightly worried about what I'm going to see when opening images embedded in suspicious binaries but this turned out to be a corrupt-looking png. 

I assumed it was going to be a steganography challenge, but looking further at the Python code we see it being fed to prepare_stage(), where the pixel values are converted to a PE file by get_encoded_data(), moved into executable memory with the help of VirtualAlloc and executed from offset +2.

I had some trouble running it from a decompiled python script as it kept crashing but stepping through it with a debugger seemed to fix it, oddly. I also dumped the payload to a file to run it without having to wait for an imgur download each time.

We can debug this payload by waiting until the “Are you ready?” dialog pops up and setting a breakpoint at VirtualAlloc() to find the buffer it gets placed in, but lets use static analysis as far as possible.
Off to our disassembler – since this isn't executed as by the normal PE loader mechanism we have to switch BinaryNinja to 'Raw View' so it doesn't try to impose PE section mappings on our disassembly.


(virtual_buf + 2) :



Above: The first two instructions use the call-pop technique to determine the memory location of the code, then adds 0x6d9 to it (destination +0x6e0) and then “returns” there.

The next section checks for valid PE/MZ signature bytes and retrieves the process environment block. It grabs the InMemoryOrderModuleList from the PEB and iterates through the module names with a simple hashing/checksum algorithm until it finds the entry for kernel32.dll.



It then uses the same technique to iterate through the symbols to find LoadLibraryA, GetProcAddress and VirtualAlloc (if you Google the constants you will find the loader was written by a Petya fan). The payload now has what it needs to load its plaintext list of imports at 0x302da.

Loading done, it creates a thread which calls EnumWindows every second. I'll switch back into PE mode and examine the callback.


After sending a WM_GETTEXT command to read the title text of each window, it searches for 'Notepad' and 'secret_console'. If a window with a suitable title exists it will enter a 'waiting for the command' loop which uses EnumChildWindows to look for text in its widgets in the same way – this time for 'dump_the_key'.

Once a window satisfying all these conditions has been found, it decrypts the blob of data at payload offset 0x30A00 . I'm not sure if the algorithm is a well known one but it's quite simple – though understanding it isn't required to get the flag I'll cover how it works:

It uses the RC4 key expansion algorithm to turn “dump_the_key” into an S-Box, however using the RC4 keystream generator only recovers the first byte.

The following python will decrypt the embedded blob:


#standard RC4 key expansion - https://gist.github.com/cdleary/188393
def expandkey(key):

    keybytes = [ord(x) for x in key]
    k = [x for x in range(256)]
    j = 0
    for i in range(256):
        j = (j +k[i]+keybytes[i % len(keybytes)]) % 256
        k[i], k[j] = k[j], k[i]
    return k

def decrypt(ciphertext, key):
    
    plainText = ""
    keyIndex = previous_keyIndex = tempVar = 0
    SBOX = expandkey(key)
    
    for ctIdx in range(len(ciphertext)):
        
        keyByte1 = SBOX[(keyIndex+1) & 0xff]
        keyIndex = (keyByte1 + tempVar) & 0xff
        keyByte2 = SBOX[keyIndex]

        keyStreamKeyIndex = (keyByte2+keyByte1)&0xff
        
        plainText += chr(SBOX[keyStreamKeyIndex] ^ ciphertext[ctIdx])
        
        tempVar = previous_keyIndex + 1
        previous_keyIndex = keyIndex
        
    return plainText

cipherTextBytesList = [0x4f, 0x08, …, 0x75, 0x98]
key = “dump_the_key”
plaintext = decrypt(cipherTextBytesList, key)



After decryption, Actxprxy.dll is loaded. This seemed odd at first glance - an Internet explorer activeX controls library? (So much badness) - but the first page is actually marked READ_WRITE and used as a covert channel to pass our 'plaintext' back to the python script, where it is Base64Decoded and decompressed. The python script can be modified to print it out for us to play with if dumping and decrypting it is too much work (and it is).

Stage 3

Now the script tells us that we are awesome - but not awesome enough to get the flag. We need to guess a colour first. The three RGB colour bytes are the key - each key byte taking turns being XOR'ed with a ciphertext byte.

Some observations about the ciphertext
  • Exec() is called on it, so it should be valid python
  • It might have “flag{“ in it
  • It looks quite interesting!


At the top we see what looks like a comma separated Python tuple and at the bottom we can make out a garbled but suspiciously flag-like “æláç{“ --------- “}” structure. In fact the printable ASCII bytes are separated in intervals of three, suggesting that one of the three key bytes is zero. As it has 'l' and '{' in the right place, i'm certain that 'æláç{' is 'flag{'.

This string is at offset 1065 in the ciphertext so the correct key offset must be (1065 % 3) -> 0. We can use a known-plaintext attack – xoring the ciphertext with the plaintext in a python console to recover the key:
>>> [ord(“æláç{“[i]) ^ ord(“flag{“[i]) for i in range(3)]

[128, 0, 128]

Using this key to decipher the rest of the blob gives us some python code:

def print_flag():

flag_hex = ( 0x73, 0x75, 0x72, 0x64, 0x65, 0x61, 0x68, 0x50, 0x20, 0x2D, 0x20, 0x22,0x2E, 0x6E, 
0x65, 0x64, 0x64, 0x69, 0x68, 0x20, 0x79, 0x6C, 0x6C, 0x75, 0x66, 0x65, 0x72, 0x61, 0x63, 0x20, 
0x6E, 0x65, 0x65, 0x62, 0x20, 0x73, 0x61, 0x68, 0x20, 0x74, 0x61, 0x68, 0x77, 0x20, 0x73, 0x65, 
0x76, 0x69, 0x65, 0x63, 0x72, 0x65, 0x70, 0x20, 0x77, 0x65, 0x66, 0x20, 0x61, 0x20, 0x66, 0x6F, 
0x20, 0x65, 0x63, 0x6E, 0x65, 0x67, 0x69, 0x6C, 0x6C, 0x65, 0x74, 0x6E, 0x69, 0x20, 0x65, 0x68, 
0x74, 0x20, 0x3B, 0x79, 0x6E, 0x61, 0x6D, 0x20, 0x73, 0x65, 0x76, 0x69, 0x65, 0x63, 0x65, 0x64, 
0x20, 0x65, 0x63, 0x6E, 0x61, 0x72, 0x61, 0x65, 0x70, 0x70, 0x61, 0x20, 0x74, 0x73, 0x72, 0x69, 
0x66, 0x20, 0x65, 0x68, 0x74, 0x20, 0x3B, 0x6D, 0x65, 0x65, 0x73, 0x20, 0x79, 0x65, 0x68, 0x74, 
0x20, 0x74, 0x61, 0x68, 0x77, 0x20, 0x73, 0x79, 0x61, 0x77, 0x6C, 0x61, 0x20, 0x74, 0x6F, 0x6E,
 0x20,0x65, 0x72, 0x61, 0x20, 0x73, 0x67, 0x6E, 0x69, 0x68, 0x54, 0x22 )

flag_str = ""

for i in flag_hex:

flag_str = chr(i) + flag_str

init()

print(Style.BRIGHT + Back.MAGENTA) + "flag{" + flag_str + "}" + (Style.RESET_ALL)

print_flag()

Running it yields our flag -

flag{"Things are not always what they seem; the first appearance deceives many; the intelligence of a few perceives what has been carefully hidden." - Phaedrus}






No comments:

Post a Comment