Opening up this Windows executable quickly reveals that we’re working with some shellcode. If this wasn’t apparent, the clue provided gave some hints as to what you’d be dealing with.

Discover the key in the sh>E11C0DE to rescue the Princess! @jgrunzweig

To make things easier for challengers, I went ahead and compiled the shellcode into a working executable, versus simply giving you the raw shellcode bytes. Once opened we see that there are no imports and only seven functions.

Figure 1 Functions in shellcode and import table

Without even debugging this shellcode we can quickly scan the seven functions provided to see if anything jumps out. Sure enough, we quickly identify a function that is almost certainly RC4 at 0x40106C. The two loops iterating 256 times gives us a big hint. Working through this function, we can confirm it is in fact RC4.

Figure 2 RC4 function

We also identify a very small function which starts by loading fs:0x30, which should get a reverser’s attention fairly quickly. For those unaware, fs:0x30 points to the Process Environment Block (PEB), which holds a wealth of information. This function in question is specifically looking at the PEB’s LoaderData offset, which holds information about the loaded modules in the process. We then get the third loaded module, which is kernel32.dll, and grab this DLL’s base address (offset 0x10). This function is essentially grabbing the base address of kernel32.dll, which is most likely going to be used to load further functions.

Figure 3 Function getting kernel32 base address

We continue to identify yet another function that appears to be hashing data, as evident by the ROR13 call.

Figure 4 Possibly hashing function

At this point, let’s start stepping through our shellcode in a debugger. We quickly see multiple calls to our function that got kernel32’s base address, followed by another function that takes this base address and a DWORD as arguments. Looking through this function we see it walking through all of kernel32’s exported functions, hashing the name, and comparing it against the provided DWORD. This is a simple shellcode trick that will allow attackers to obfuscate what functions are being loaded by the malware when viewed statically. There are a few ways we can approach this. We can debug the code and rename as we encounter them. Alternatively, we can simply search for the hashes on Google. Since the ROR13 technique is so common, there are many places online that have documented these hashes, like this one.

After getting over this minor hurdle we can start to see what the code is doing to understand what it’s looking for. Looking at the code in detail, we can see that it’s building a buffer of 54 bytes and attempting to decode it against a key that is generated using RC4. In the event the key starts with ‘PAN{‘, it will display it in a messagebox dialog window.

The key is generated using a number of variables that are pulled from the machine it is running on. The first four bytes of the key are a static value of ‘b00!’. Following this, the code looks for the following data:

  • Current month plus 0x2D

  • Current day plus 0x5E

  • Current hour plus 0x42

  • The operating system major version plus 0x3C

  • The operating system minor version plus 0x3F

  • The isDebugged flag, which is pulled from the PEB, plus 0x69

  • The language version plus 0x5E

These values together give us a key that is eleven bytes long. With only that information, it would be very difficult to brute force. However, since we know how each byte in the key is generated, we can limit our key space for the brute force and hopefully determine what the malware is looking for.

Knowing that there are only 12 months in a year, we can assume the first generated byte is in the range between 1 and 12. Similarly, there are a maximum of 31 days in a month, giving our second byte a range of 1 to

  1. We continue this pattern on the rest of the bytes in the RC4 key. Most people looked to have the most trouble limiting the key space on the operating system versions, and the language version. Fortunately, there are very few legimate operating system (OS) versions overall. The major OS version will have a value of either 5, 6, or 10. The minor OS version will have a value of 0, 1, 2, or 3.

For the language version, there is a check early on in the execution flow where the result of GetUserDefaultUILanguage has its primary language identifier verified to be 0x0, or LANG_NEUTRAL. Knowing this, we can limit the possibilities of to values of 0x0, 0x04, 0x08, 0x0c, 0x10, or 0x14.

Using all of this information, we can generate a brute-forcing script such as the following.

import sys
from datetime import datetime
startTime = datetime.now()

def rc4_crypt( data , key ):
  S = range(256)
  j = 0
  out = []
  for i in range(256):
    j = (j + S[i] + ord( key[i % len(key)] )) % 256
    S[i] , S[j] = S[j] , S[i]
  i = j = 0
  for char in data:
    i = ( i + 1 ) % 256
    j = ( j + S[i] ) % 256
    S[i] , S[j] = S[j] , S[i]
    out.append(chr(ord(char) ^ S[(S[i] + S[j]) % 256]))
  return ''.join(out)


c = 0
for current_month in range(1, 13):
  for current_day in range(1, 32):
    for current_hour in range(1, 25):
      for os_major_version in [5, 6, 10]:
        for os_minor_version in [0, 1, 2, 3]:
          for peb_isdebugged_flag in [0, 1]:
            for language_version in [0x0, 0x04, 0x08, 0x0c, 
0x10, 0x14]:
              key = 'b00!' + chr(current_month+0x2d) + 
chr(current_day+0x5e) + chr(current_hour+0x42) + 
chr(os_major_version+0x3c) + chr(os_minor_version+0x3f) + 
chr(peb_isdebugged_flag+0x69) + chr(language_version+0x5e)
              x = 
rc4_crypt('\xba\xafMU<\xe3\x03"\xb0\xdf\xf3\xd3W\xd0\xe1@\xf9\x1
3\x1f\xba\x8d\x12\xf1\xffH\xc2\x8e\x00\xfdT\x97\x9duq0\x8fC(\xfe
i6G\x8f\xa2\xefIt|\xe1LoO\xd4\x82', key)
              if x[0:4] == 'PAN{':
                print "Winner winner, chicken dinner:", x
                print "Month: {}".format(current_month)
                print "Day: {}".format(current_day)
                print "Hour: {}".format(current_hour)
                print "OS Major Version: 
{}".format(os_major_version)
                print "OS Minor Version: 
{}".format(os_minor_version)
                print "Debugged: {}".format(peb_isdebugged_flag)
                print "Language Version: 
{}".format(language_version)
                print "Time taken:", datetime.now() - startTime
                sys.exit(1)
              c+=1
              if c % 100000 == 0: print '%d attempts made.' % c

print "Time taken:", datetime.now() - startTime

After running the script for roughly 3 minutes, we are presented with the following:

100000 attempts made.
200000 attempts made.
300000 attempts made.
400000 attempts made.
500000 attempts made.
600000 attempts made.
700000 attempts made.
800000 attempts made.
900000 attempts made.
1000000 attempts made.
1100000 attempts made.
1200000 attempts made.
Winner winner, chicken dinner: PAN{th0se_puPP3ts_creeped_m3_out_and_I_h4d_NIGHTMARES}
Month: 12
Day: 13
Hour: 10
OS Major Version: 5
OS Minor Version: 1
Debugged: 1
Language Version: 8
Time taken: 0:03:04.102476