For this challenge, we’re given a Windows PE “drone.exe” and when we run it we’re greeted with a Borg Cube and some text about an apparent encryption.

Based on the text, it appears the URL (https://www.youtube.com/watch?v=AyenRCJ_4Ww - the Borg montra!) and key “borgdata” are encrypted to form the hash “374316062B033D0A3E6A746B46560377367A3328393720611641435A400C0C0B7E6E696E392C2C394E5B5B1717061B0A”. Then an error occurs and another hash is displayed and the program exits.

Looking at the strings for the program, a number of them immediately stand out and, after a quick trip to Google, imply that this PE was built with PyInstaller.

__main__
__file__
%s returned %d
pyi-windows-manifest-filename
Cannot allocate memory for ARCHIVE_STATUS
_MEIPASS2
Cannot open self %s or archive %s
PATH
Failed to get executable path.
GetModuleFileNameW: %s
Failed to convert executable path to UTF-8.
Py_DontWriteBytecodeFlag
Cannot GetProcAddress for Py_DontWriteBytecodeFlag
Py_FileSystemDefaultEncoding
Cannot GetProcAddress for Py_FileSystemDefaultEncoding
Py_FrozenFlag
Cannot GetProcAddress for Py_FrozenFlag
Py_IgnoreEnvironmentFlag

PyInstaller is “a program that packages Python programs into stand-alone executables.”Now we know what we’re dealing with. I decided that the quickest way to tackle this challenge would be to extract the Python script instead of trying to reverse-engineer a 9MB PE that wraps a Python script.

After some more Google-Fu, we find PyInstaller Extractor on Sourceforge and try to run it against our binary but immediately receive a traceback.

Traceback (most recent call last):

File “pyinstxtractor11.py”, line 115, in

fd=open(name,’wb’)

IOError: [Errno 2] No such file or directory: ‘’

Analyzing the traceback and the section of code where it happened, it looks like it had issues opening a file.

    #Remove trailing null bytes from name
    name=name.rstrip('\00')

    bpath=os.path.dirname(name)
    if bpath!='':
        #Check if path exists, create if not
        if os.path.exists(bpath)==False:
            os.makedirs(bpath)
    fd=open(name,'wb')
    fd.write(buf)
    fd.close()

There were also some interesting files dropped before the program had the issue, which may be useful later.

Testing our assumption, we modify the script to print the ‘name’ field and can validate it’s printing out the file names we saw written to disk.

Given this, we simply wrap that action in a try/except where we specify the filename as “broke” if it is empty.

    try:
        fd=open(name,'wb')
    except:
        print "broke file"
        fd=open("broke”,'wb')

Running it again, we see a slew of files now get written to disk.

Looking at our “broke” file, we see it’s actually the script for the program!

Looking at the script shows some level of obfuscation.

#!/usr/bin/env python
import requests, time, sys, json, ast, logging, base64
logging.getLogger("scapy.runtime").setLevel(logging.CRITICAL)
from scapy.all import *

def AABBBC(AABBCF, AABBCE, AABBBF):
    AABBCC = len(AABBCF)/float(len(AABBCE))
    if str(AABBCC).split(".")[1] == "0":
        AABBCD = int(str((AABBCC)).split(".")[0]) * 8
    else:
        while str(AABBCC).split(".")[1] != "0":
            AABBCF += "@"
            AABBCC = len(AABBCF)/float(len(AABBCE))
        AABBCD = int(str((AABBCC)).split(".")[0]) * 8
    AABBB0 = []
    AABBCF = list(AABBCF)
    AABBCE = list(AABBCE)
    while AABBCF != []:
        p_AABBCF = AABBCF[0:8]
        p_AABBCE = AABBCE[0:8]
        AABBB1 = []
        for i in xrange(0,8):
            if type(p_AABBCE[i]) == int: # [+] *** ALERT ALERT *** [+]
                AABBB2 = (ord(chr(p_AABBCE[i])) ^ ord(p_AABBCF[0])) # [+] HUMANS HAVE BROKEN THROUGH [+]
            else: # [+] MODULATE SHIELDS [+]
                AABBB2 = (ord(p_AABBCE[i]) ^ ord(p_AABBCF[0])) # [+] *** ALERT ALERT *** [+]
            AABBB0.append(AABBB2)
            AABBB1.append(AABBB2)
            AABBCF.pop(0)
            p_AABBCF.pop(0)
            AABBCE = AABBB1
        AABBCE.reverse()
    AABBB0.reverse()
    AABBB4 = []
    for i in AABBB0:
        AABBB3 = hex(i)
        if len(AABBB3) != 4:
            AABBB4.append("0" + hex(i)[2:])
        else:
            AABBB4.append(hex(i)[2:])
    AABBB4 = "".join(AABBB4).upper()
    return AABBB4

def AABBB7(AABBBE):
    return AABBBE[::-1]

def AABBCB(AABBBE):
    print "\t[-] *** ERROR CONNECTING ***"
    print "\n[+] SHUTTING DOWN DRONE [+]\n"
    time.sleep(2)
    sys.exit()

def main():
    AABBBD = """
            ___________
           /-/_"/-/_/-/|
          /"-/-_"/-_//||
         /__________/|/|
         |"|_'='-]:+|/||
         |-+-|.|_'-"||//
         |[".[:!+-'=|//
         |='!+|-:]|-|/
          ----------
"""
    print AABBBD
    print "[+] BORG DRONE BOOTUP STARTING [+]"
    time.sleep(2)
    try:
        AABBBA = json.load(open("borgstruct.cfg"))
        print "\t[-] CONFIGURATION", str(AABBBA['key'][1]) + ".0 LOADED"
    except:
        AABBBA = {"warp": ["d0rw$54p", "lss", "p//:ptth", "nimda//:ptf"],
                  "coil": ["r/moc.nibets", "exe.1\:c", "tropmmoc"],
                  "dilithium": ["praw", "-redrocirt", "FfPE6AFw/w"],
                  "scalar": [874, 34, 666],
                  "array": [69, 80, 443, 25, 22, 2600, 666, 8443, 27500],
                  "LoadLibraryA": ["IsDebuggerPresent", "IsDebuggerDetected", "NtQueryInformationProcess", "GetTickCount"],
                  "LoadLibraryB": ["CheckRemoteDebuggerPresent", "UnhandledExceptionFilter", "CloseHandle", "QueryPerformanceCounter"],
                  "LoadLibraryC": ["NtGetContextThread", "NtSetContextThread", "NtClose"],
                  "adb": ["0xCD", "0x03"],
                  "targets": ["squirtle", "humans", "ferengi", "rick astley"],
                  "key": ["borgdata", 1, 2, 3, 4, 5, 6, 7, 8, "startrek", "cloaking"],
                  "commands": ["ping", "shutdown", "nslookup"],
                  "lore": ["grab", "the", "flag"]}
        json.dump(AABBBA, open("borgstruct.cfg", "w"))
        print "\t[-] CONFIGURATION VERSION", str(AABBBA['key'][1]) + ".0 WRITTEN"
    if AABBBA['key'][1] == 1:
        print "\n[+] FETCHING STARTUP VALUE FROM MATRIX"
        time.sleep(2)
        try:
            AABBB5 = AABBB7((AABBBA['dilithium'][2]) + "a" + (AABBBA['coil'][0]) + "a" + (AABBBA['warp'][2]))
            AABBB6 = requests.get(AABBB5, verify=False)
        except:
            AABBCB(AABBBA['LoadLibraryA'][0])
        AABBCE = AABBB6.content.split("\n")[1]
        print "\t[-] DATA =", AABBCE
    if AABBBA['key'][1] == 2: # [+] *** ALERT ALERT *** [+]
        print "\n[+] SEND FLAG REQUEST WITH ENCRYPTED DATA AND CODE [+]" # [+] HUMANS HAVE BROKE NEXT DEFENSE [+]
        AABBCE = AABBB7((AABBBA['dilithium'][2]) + "o" + (AABBBA['coil'][0]) + "o" + (AABBBA['warp'][2])) # [+] INITIATE LOW ORBIT ION CANNON [+]
        print "\t[-] DATA =", AABBCE # [+] *** ALERT ALERT *** [+]
    AABBCF = AABBBA['key'][0]
    print "\t[-] INITIALIZATION KEY =", AABBCF
    if len(AABBCF) != 8:
        sys.exit()
    print "\n[+] STARTING BORG ENCRYPTION ROUTINE [+]"
    time.sleep(2)
    AABBBB = AABBBC(AABBCE, AABBCF, AABBBA['array'][3])
    print "\t[-] RESULT = " + AABBBB
    print "\n[+] STARTING DRONE COMMUNICATION PROTOCOLS [+]"
    time.sleep(2)
    scalar_array = AABBBA['scalar'][0]
    FEEDDEAD = base64.b64decode('cGFuYm9yZ2Ryb25lLmNvbQ==')
    try:
        AABBB8 = IP(dst=FEEDDEAD)/TCP(dport=AABBBA['array'][5],window=scalar_array,flags="S")/AABBBB
        AABBB9 = sr1(AABBB8, verbose=False)
    except:
        AABBCB(AABBBA['LoadLibraryB'][1])
    if AABBB9[TCP].window == 666:
        print "\t*** ERROR RECEIVED ***"
        print "\t[-] RETURNED = ", AABBB9[Raw]
        print "\n[+] SHUTTING DOWN DRONE [+]\n"
        time.sleep(2)
        sys.exit()
    elif AABBB9[TCP].window == 34:
        print "\t[-] UPDATE SUCCESSFUL"
        AABBBA = ast.literal_eval(str(AABBB9[Raw]).strip("\n"))
        print "\t[-] CONFIGURATION VERSION", str(AABBBA['key'][1]) + ".0 WRITTEN"
        json.dump(AABBBA, open("borgstruct.cfg", "w"))
        print "\t[-] PROCESSING COMMANDS"
        print "\t[-] EXECUTING COMMAND =", AABBBA['commands'][3], AABBBA['adb'][2], "/"
        time.sleep(10)
        print "\t*** ERROR WITH COMMAND ***"
        print "\t[-] NEW SERVER FUNCTION ADDED - FLAG REQUEST"
        print "\t[-] FLAG REQUEST REQUIRED FOR CURRENT ENCRYPTED DATA"
        print "\n[+] SHUTTING DOWN DRONE [+]\n"
        time.sleep(2)
        sys.exit()
    else:
        print "\t[-] RETURNED = ", AABBB9[Raw], "\n"

if __name__ == "__main__":
    main()

Lots of things going on in the script, the main ones of interest are some of the ones that immediately jump out for investigation.

# Called before encryption message prints
AABBBB = AABBBC(AABBCE, AABBCF, AABBBA['array'][3])
# Scapy sending data to a server (part of the “COMMUNICATION PROTOCOL”?)
AABBB8 = IP(dst=FEEDDEAD)/TCP(dport=AABBBA['array'][5],window=scalar_array,flags="S")/AABBBB
#  Branch leads to messages about the flag
    elif AABBB9[TCP].window == 34:
        print "\t[-] NEW SERVER FUNCTION ADDED - FLAG REQUEST"
        print "\t[-] FLAG REQUEST REQUIRED FOR CURRENT ENCRYPTED DATA"

First things first, we’ll take a look at the encryption function and decipher that. We set a breakpoint on the main() function and begin to step through the code to understand what it’s doing.

1) Tries to load the file “borgstruct.cfg” and if that fails, it writes a dictionary to disk as that file.

2) Check if dictionary ‘key’[1] is equal to 1 and if so, puts together a string from various locations within the dictionary. This URL contains the Youtube video mentioned above.

3) Sets another variable to “borgdata”.

4) Calls the encryption routine AABBBC(URL,”borgdata”,25).\ \ height=”1.0968175853018374in”}

5) Checks if URL is divisible by 8 and, if not, pads it with “@”.

6) Splits each variable into a list and begins the XoR the URL by key “borgdata”.\ \ height=”2.1694444444444443in”}

7) Once it has the first set of 8 ordinals it reverses them and uses this set as the next XoR key, which continues on for the full length of the URL.\ \ height=”1.25in”}\ This is classic cipher block chaining where each encrypted ciphertext is used as the encryption key for the next block.

8) If we let the process continue until the end, it reverses the order of the final ordinal list and then converts it to hex.

Since we know the ciphertext and now we know how the key is derived, we have enough pieces of the puzzle to build a decryptor. By copying the code from the script and de-obfuscating it, we can build our reverse decryption function.

#!/usr/bin/env python

def decrypt(hash):
    final_key = []
    key = []
    count = 0
    while count != len(hash):
        key.append(hash[count:count+2])
        count += 2
    key = key[::-1]
    temp = []
    for value in key:
        ord_value = ord(value.decode("hex"))
        temp.append(ord_value)
    count = 0
    block_count = len(temp)
    while block_count != 0:
        cipher = []
        block = temp[block_count - 8:block_count]
        if block_count != 0:
            for value in block:
                cipher.append(value)
        else:
            for value in block:
                cipher.append(value)
        xor_key = (temp[block_count - 16:block_count - 8])[::-1]

        string_key = []
        count = 0
        if block_count - 8  != 0:
            while count != 8:
                string_key.append(chr(cipher[count] ^ xor_key[count]))
                count +=1
        else:
            string_key = ["????????"]
        final_key.append("".join(string_key))

        block_count -= 8
        xor_key = []
    final_key = "".join(final_key[::-1])
    return "".join(final_key)

def encrypt(pt, key, add_value):
    pt_size = len(pt)/float(len(key)) # Grab Key Length
    if str(pt_size).split(".")[1] == "0": # Check if divisible by 8
        multiply_size = int(str((pt_size)).split(".")[0]) * 8
    else:
        while str(pt_size).split(".")[1] != "0": # Pad pt to be divisible by 8
            pt += "@"
            pt_size = len(pt)/float(len(key))
        multiply_size = int(str((pt_size)).split(".")[0]) * 8
    cipher = []
    pt = list(pt) # Put plaintext and key into their respective list for processing in 8 byte chunks
    key = list(key)
    while pt != []: # Stop when all plaintext processed
        p_pt = pt[0:8]
        p_key = key[0:8]
        temp_list = []
        for i in xrange(0,8): # Process 8 bytes at a time
            if type(p_key[i]) == int: # Second 8 bytes and on will always be integers
                new_ct = (ord(chr(p_key[i])) ^ ord(p_pt[0])) # XOR each PT byte with key byte
            else: # First run of XOR, assuming ASCII key
                new_ct = (ord(p_key[i]) ^ ord(p_pt[0]))
            cipher.append(new_ct) # Add each byte to CT list
            temp_list.append(new_ct)
            pt.pop(0)
            p_pt.pop(0)
            key = temp_list
        key.reverse() # Reverse Key list each run so now Z->A on second run+ (integers)
    cipher.reverse() # Reverse entire CT
    cipher_text = []
    for i in cipher: # Convert each integer to hex equivalent
        hex_value = hex(i)
        if len(hex_value) != 4: # Pad to get consistent output with leading 0's
            cipher_text.append("0" + hex(i)[2:])
        else:
            cipher_text.append(hex(i)[2:])
    cipher_text = "".join(cipher_text).upper() # Join it into one string
    return cipher_text

def main():
    pt = "https://www.youtube.com/watch?v=AyenRCJ_4Ww"
    key = "borgdata"
    numb = 25
    enc_hash = encrypt(pt, key, numb)
    dec_hash = decrypt(enc_hash)
    print "Encrypted Hash: %s\nDecrypted Hash: %s" % (enc_hash, dec_hash)

if __name__ == "__main__":
    main()

Running the code with the known plain text and initial key shows we get the same ciphertext shown in the initial run of the drone.exe executable, along with our known YouTube URL.

Now we can take the returned value and put it through our decryptor to see what we get.

The hash “405E520E4A0E6F3401584E0A4E121E00322C24793B7E6C3304594B0E41131B032C6867207A3E2A2B484553174C0E064724696363753C372F5B40550117061B0A” becomes “????????twitter.com/borgcommlink/status/755587712267104257@@@@@@”.

Browsing to that Twitter address, we’re greeted with yet another hash.

This hash decrypts to another excellent Star Trek Youtube video.

Looking back at our script, we can tell that the script sends a hash to panborgdrone.com on port TCP/2600 and based on the result of what the server sends back, either shuts down or updates its configuration with a new “function” called “FLAG REQUEST”. Sounds promising.

Let’s try editing the script and sending the hash from Borg Head.

We get a slightly different error message this time and no hash like we did originally. Instead of “HIVE|MIND|HASH” it’s “HIVE|ERROR|DATA|874”. Looking at the Scapy command again, the TCP Window Size is set by variable “scalar_array” which pulls from the configuration dictionary.

Setting the Window Size to 34 nets us a change in response from the server.

Looking at the 2.0 configuration, this key and values stand out immediately.

   "datashat" : [
      "submit",
      "the",
      "flag",
      "manually"
   ],

Running the bot again with the new configuration, it loads a new URL (with quite possibly the best Star Trek video ever made) and XoR key of “borgcube” to generate a new hash. Placing the new hash in our send command, we get the following error “HIVE|ERROR|CMD|2E”, which is different than the previous “ERROR|DATA” message we received.

Changing the Window Size back to 874 didn’t result in any change of the message; however, while looking through the rest of Borg Head’s Tweets, we find this little gem among the memes and Borgs talking to each other.

In the background of this message is a spreadsheet with a table showing various commands (CMD) and their respective Window Size. We can see that value 874 corresponds to “DRONE CHECKIN”, value 34 is “DRONE UPDATE”, and value 824 is “FLAG REQUEST”!

Updating our script one last time with our new Window Size we are rewarded with our key.

A Star Wars troll for all the Star Trek fans…

PAN{m4yTh3f0rc3beWIThyOu}