#! /bin/usr/python # # GREATERSURGEON # this script decrypts and/or decompresses the log files that are generated by GREATERDOCTOR # modified from the GreaterSurgeon parser # # usage: # greatersurgeon.py [-i ] [-o ] [-d] [-x] [-e ] [-s] [-p ] # # options: # -i # path to the input file to decrypt/decompress # -o # path to the output file # -d # decompress the file (for the json output file only) # -x # decrypt the file # -e # extract any embedded binaries to the specified directory (for the json output file only) # -s # write a summary of the JSON output file # -p # password to decrypt the file # -n # used for output of NTFS module # -u # used when decrypted log file to process the unicode # # example: # # As an example of how this tool is used, assume that GREATERDOCTOR was executed # with the following command line arguments: # # vtuner.exe -v -system -l log.enc -o json.gz.enc -x -w mypassword # # In this case, GREATERDOCTOR will perform a system scan with verbose output that # is logged to a file called "log.enc" in the current working directory. The "log.enc" # file will be encrypted. GREATERDOCTOR will also create a JSON output file called # "json.gz.enc" in the current working directory. The JSON output is first compressed # using GZIP compression and then encrypted. Once these files have been exfiltrated to # a trusted host, the Python script "parse.py" will be used to process these log files. # Each file must the operated on individually. Here is an example of how this is done: # # python.exe greatersurgeon.py -i log.enc -o log.txt -x -p mypassword # python.exe greatersurgeon.py -i json.gz.enc -o json.txt -d -x -e C:\BINARIES # # After executing the tool as described above, the log file will be decrypted and # saved as "log.txt" in the current working directory. The original encrypted file # remains unchanged. The JSON output file is first decrypted and then decompressed with # the results being saved as "json.txt" in the current working directory. In addition, any # binaries contained within the JSON output file are extracted and saved within the # "C:\BINARIES" directory. # # expected output: # # In the example below this script is being used to decrypt and then decompress a JSON # output file that was generated with GREATERDOCTOR. For more information regarding the # "trying again" message below reference the comment block right before the decompression # code block below. # # C:\TEST>python.exe greatersurgeon.py -i json.gz.enc -o json.txt -x -d # # GREATERSURGEON [ output parser ] # # +decrypting 'json.gz.enc' [ this may take some time... ] # decryption complete # # +decompressing 'json.gz.enc' # trying again with null byte appended to file due to malformed gzip # decompression complete # # +saving raw json to json.txt # # +parsing complete # # import json, zlib, gzip, sys, binascii, base64, os, struct, hashlib from optparse import OptionParser class ProcessGreaterSurgeonJson: def __init__(self,data,dirname=None): try: self.dirname = dirname self.j = json.loads(data) self.system = self.j['system'] self.processes = self.j['system']['processes'] self.modules = self.j['system']['modules'] self.drivers = self.j['system']['drivers'] self.keyloggerHooks = [] self.keyloggerModules = [] if self.system.has_key('keylogger_hooks'): self.keyloggerHooks = self.system['keylogger_hooks'] if self.system.has_key('keylogger_candidate_modules'): self.keyloggerModules = self.system['keylogger_candidate_modules'] except: print "Unexpected error:", sys.exc_info()[0] sys.exit(0) def dump(self): print self def write_file(self,name,data): #create dir if needed if(not os.path.isdir(self.dirname)): os.makedirs(self.dirname) fileName = self.dirname+'\\'+os.path.basename(name)+'.bin' f = open(fileName,'wb') f.write(data) f.close() print ' extracted file to %s' % (fileName,) def dump_module(self,m,name,indent=0): ret_str = [] ret_str.append(' '*indent+'Module Score:%d\n' % m['score']) ret_str.append(' '*indent+'MD5:%s\n' % binascii.hexlify(self.decrypt_blob(m['md5']))) ret_str.append(' '*indent+'SHA1:%s\n' % binascii.hexlify(self.decrypt_blob(m['sha1']))) ret_str.append(' '*indent+'Entropy:%d\n' % m['entropy']) ret_str.append(' '*indent+'Service:%s\n' % m['service']) ret_str.append(' '*indent+'Hijacked Service:%s\n' % m['hijacked_service']) ret_str.append(' '*indent+'Signed:%s\n' % m['signed']) ret_str.append(' '*indent+'Microsoft:%s\n' % m['ms']) ret_str.append(' '*indent+'Packed:%s\n' % m['packed']) ret_str.append(' '*indent+'PE Checksum:%s\n' % m['pe_checksum']) ret_str.append(' '*indent+'PE Header Size:%s\n' % m['header_size']) ret_str.append(' '*indent+'PE Section Ordering:%s\n' % m['section_ordering']) ret_str.append(' '*indent+'PE Size Of Code:%s\n' % m['size_of_code']) ret_str.append(' '*indent+'Cache Match:%s\n' % m['cache_match']) ret_str.append(' '*indent+'Linker:%s\n' % m['linker']) ret_str.append(' '*indent+'Registry Persistence:%s\n' % m['reg_persist']) ret_str.append(' '*indent+'Protected:%s\n' % m['protected']) ret_str.append(' '*indent+'Keylogger:%s\n' % m['keylogger']) if(m.has_key('file') and self.dirname != None): self.write_file(name, self.decrypt_blob(m['file'])) return ''.join(ret_str) def decrypt_blob(self,blob): return zlib.decompress(base64.b64decode(blob)) def dump_injected_thread(self,t,indent=0): MEM_TYPE = { 'MEM_IMAGE': 0x1000000, 'MEM_PRIVATE':0x20000} MEM_PROTECT = {'PAGE_NOACCESS':0x01, 'PAGE_READONLY':0x02, 'PAGE_READWRITE':0x04 , 'PAGE_WRITECOPY':0x08 , 'PAGE_EXECUTE':0x10 , 'PAGE_EXECUTE_READ':0x20 , 'PAGE_EXECUTE_READWRITE':0x40 } ret_str = [] ret_str.append(' '*indent+'Start Address:%08X\n' % t[0]) ret_str.append(' '*indent+'State:%08X\n' % t[1]) ret_str.append(' '*indent+'Type:%08X ( ' % t[2]) for k,v in MEM_TYPE.iteritems(): if((t[2] & v) == v): ret_str.append('%s ' % k) ret_str.append(')\n') ret_str.append(' '*indent+'Protect:%08X ( ' % t[3]) for k,v in MEM_PROTECT.iteritems(): if((t[3] & v) == v): ret_str.append('%s ' % k) ret_str.append(')\n') #ret_str.append('Start Address:%08X' % t[4]) #Not sure how to dump disasm return ''.join(ret_str) def dump_process(self,p,indent=0): ret_str = [] ret_str.append(' '*indent+'Process Score:%d\n' % p['score']) ret_str.append(' '*indent+'Executable:%s\n' % p['exe']) ret_str.append(' '*indent+'Command Line:%s\n' % p['cmdline']) ret_str.append(' '*indent+'Hidden:%s\n' % p['hidden']) ret_str.append(' '*indent+'Suspended:%s\n' % p['suspended']) ret_str.append(' '*indent+'GUI:%s\n' % p['gui']) ret_str.append(' '*indent+'Reg Persistence:%s\n' % p['run']) ret_str.append(' '*indent+'Service:%s\n' % p['service']) ret_str.append(' '*indent+'Entry Point mismatch:%s\n' % p['entrypoint']) name = p['module'].keys()[0] ret_str.append(' '*indent+'Module:\n') ret_str.append(self.dump_module(p['module'][name],name,indent=indent+1)) if(len(p['injected_threads']) > 0): for t in p['injected_threads']: ret_str.append(' '*indent+'Injected Thread:\n') ret_str.append(self.dump_injected_thread(t,indent=indent+1)) return ''.join(ret_str) def dump_driver(self,p,indent=0): ret_str = [] ret_str.append(' '*indent+'Driver Score: %d\n' % p['score']) ret_str.append(' '*indent+'Path: %s\n' % p['module'].keys()[0]) name = p['module'].keys()[0] ret_str.append(' '*indent+'Module:\n') ret_str.append(self.dump_module(p['module'][name],name,indent=indent+1)) return ''.join(ret_str) def print_summary(self,indent=0): ret_str = [] if 0 != len(self.keyloggerHooks): ret_str.append(' '*indent+'Keylogger Hooks:\n') for h in self.keyloggerHooks: ret_str.append(' '*indent+' handle: 0x%x / type 0x%x / offset 0x%x\n' % (h[0], h[1], h[2],)) if 0 != len(self.keyloggerModules): ret_str.append(' '*indent+'Keylogger Candidate Modules:\n') for k in self.keyloggerModules: ret_str.append(' '*indent+'%s\n' % k) for p in self.processes: name = p['module'].keys()[0] if((p['score']+p['module'][name]['score'] ) >= 50): ret_str.append(' '*indent+'Process:\n') ret_str.append(self.dump_process(p,indent=2)) ret_str.append('\n\n') for name,m in self.modules.iteritems(): if(m['score'] >= 50): ret_str.append(' '*indent+'Module:\n') ret_str.append(' '*1+'%s \n' % name) ret_str.append(self.dump_module(m,name,indent=2)) ret_str.append('\n\n') for d in self.drivers: name = d['module'].keys()[0] if((d['score']+d['module'][name]['score'] ) >= 50): ret_str.append(' '*indent+'Driver:\n') ret_str.append(self.dump_driver(d,indent=2)) ret_str.append('\n\n') return ''.join(ret_str) def dump_summary(self): t = self.print_summary() if len(t) == 0: #nothing malicious found t = "Scan did not flag anything malicious" return t def __str__(self): return json.dumps(self.j,indent=4,ensure_ascii=False) ############## # DECRYPTION # ############## def decrypt(data,decryptkey): def MX(): return ((z>>5^y<<2) + (y>>3^z<<4)) ^((sum^y) + (decryptkey[(p&3)^e]^z)) decrypteddata = "" while(len(data)%512<>0): data+='\x00' sum = 0 delta=0x9e3779b9 blocksize = 512 for t in range(0, len(data), blocksize): v = list(struct.unpack("%dI" % (blocksize / 4),data[t:t+blocksize])) mask = 0xffffffff n = len(v) q = 6 + 52/n sum = (q * delta) & mask y = v[0] while sum != 0: e = ((sum >>2)&mask) &3 p = n - 1 while p > 0: z = v[p-1] y = v[p] = (v[p] - MX()) & mask p -= 1 z = v[n-1] y = v[0] = (v[0] - MX()) & mask sum = (sum - delta) & mask decrypteddata += struct.pack("%dI" % (blocksize / 4), *v) return decrypteddata def main(): global key plainText = [] outputFile = "" # define the command line arguments for this script parser = OptionParser() parser.add_option("-i", "--inputFile", action="store", type="string", dest="inputFile",help="input file to parse (decrypt, decompress, etc...)") parser.add_option("-o", "--outputFile", action="store", type="string",dest="outputFile",help="output file") parser.add_option("-d", "--decompress", action="store_true", dest="decompress",help="decompress the file (json only)") parser.add_option("-x", "--decrypt", action="store_true", dest="decrypt",help="decrypt the file") parser.add_option("-e", "--extract",action="store", type="string", dest="extractDir",help="extract any embedded binaries to the specified directory (json only)") parser.add_option("-s", "--summary",action="store_false",dest="summary",help="summarize the data (json only)") parser.add_option("-p", "--password",action="store", type="string", dest="password", help="Password for decrypting the GreaterDoctor files.") parser.add_option("-z", "--zlibdecompress",action="store_true", dest="zlibdecompress", help="Decompress using zlib, used from NTFSMFT data.") parser.add_option("-u", "--unicode",action="store_true", dest="unicode", help="Process using unicode, used for encrypted Log Data.") parser.add_option("-n", "--ntfsmft",action="store_true", dest="ntfsmft", help="Process ntfsmft encryptionblocks.") # parse the options given on the command line (options, args) = parser.parse_args() # make sure input/output files were provided if ( (options.inputFile == None) or (options.outputFile == None) ): print '\nError: input/output file required\n' parser.print_help() sys.exit(0) # do sanity checks of the command line arguments # 'extract' / 'summary' should only be used with 'decompress' (json) if ( (None != options.extractDir) or (None != options.summary) ): if (None == options.decompress): print '\nError: compressed json file required for this option\n' parser.print_help() sys.exit(0) if( (options.extractDir == None) and (options.summary == None) and (options.decrypt == None) and (options.decompress == None) and (options.zlibdecompress == None) and (options.ntfsmft == None) ): print '\nError: at least one option [-d / -e / -s / -x / -z / -n] is required\n' parser.print_help() sys.exit(0) if(options.decrypt == True and options.password == None): print '\nError: specified decryption but no password (-p) given.\n' parser.print_help() sys.exit(0) # print a quick heading to the screen print '\nGREATERSURGEON [ output parser ]\n' if(options.ntfsmft != None): if(options.password): hash = hashlib.md5() hash.update(options.password) decryptkey = struct.unpack("4I",hash.digest()) fi = open(options.inputFile, "rb") fo = open(options.outputFile, "wb") encryptionblock = fi.read(16) dataprocessed=16 blockcount = 0 while encryptionblock != "": uncompressedSize = struct.unpack("0): if not(options.decrypt): print "Error - file data is encrypted but decrypt option was not selected" sys.exit(0) datablock = decrypt(datablock,decryptkey) datablock = datablock[:compressedSize] #do decryption if(compressedFlag>0): datablock = zlib.decompress(datablock) fo.write(datablock) #sys.exit(0) encryptionblock = fi.read(16) dataprocessed+=16 blockcount+=1 fi.close() fo.close() sys.exit(0) if(options.decrypt): def MX(): return ((z>>5^y<<2) + (y>>3^z<<4)) ^((sum^y) + (k[(p&3)^e]^z)) fi = open(options.inputFile, "rb") fo = open(options.outputFile, "wb") #hash the password to make the key hash = hashlib.md5() hash.update(options.password) k = struct.unpack("4I",hash.digest()) try: data = fi.read() fi.close() while(len(data)%512<>0): data+='\x00' sum = 0 delta=0x9e3779b9 blocksize = 512 for t in range(0, len(data), blocksize): v = list(struct.unpack("%dI" % (blocksize / 4),data[t:t+blocksize])) mask = 0xffffffff n = len(v) q = 6 + 52/n sum = (q * delta) & mask y = v[0] while sum != 0: e = ((sum >>2)&mask) &3 p = n - 1 while p > 0: z = v[p-1] y = v[p] = (v[p] - MX()) & mask p -= 1 z = v[n-1] y = v[0] = (v[0] - MX()) & mask sum = (sum - delta) & mask fo.write(struct.pack("%dI" % (blocksize / 4), *v)) fo.close() except: print "ERROR DECRYPTING: \n%s\n%s\nsum: %d" % (sys.exc_info()[1], sys.exc_info()[2], sum) exit() print "Finished decrypting." ################# # DECOMPRESSION # ################# # # OK, so this is a bit of a hack... Notice that when we decrypt the file (code block # directly above this) trailing zeros are chopped off (they were required as padding for # our encryption algorithm. The problem is that this truncation sometimes destroys the # GZIP file format. According to RFC-1952 "GZIP File Format Specification Version 4.3" # the last four bytes of the file contain the size of the original (uncompressed) input # data modulo 2 ^ 32. If too many zeros are chopped off this exception will be thrown: # # struct.error: unpack requires a string argument of length 4 # # So either way your script is going to BSOD if the GZIP file is mangled during the null byte # truncation festival in the previous code block. How do we fix this? There may be a better # method to do this but I much prefer the "git 'r done" solution. The solution presented here # is a lot like walking through a mine field. We start off by attempting to read the GZIP file. # If an exception wasn't thrown then we profit. If an exception was thrown then we append a null # byte to the end of the file and try to read the GZIP file again. This is only attempted four # times before the script reports and error and exits. # if (options.decompress != None): # determine the path to the file that contains the GZIP data gzipPath = '' if (options.decrypt == True): gzipPath = options.outputFile else: gzipPath = options.inputFile # echo that we are starting decompression print ' +decompressing \'%s\'' % (gzipPath) # initialize this variable uncompressedData = '' # loop that does the decompressing using the gzip module for i in range(4): tmpFileObj = '' try: # try to read the gzip file tmpFileObj = gzip.open(gzipPath, 'rb') uncompressedData = tmpFileObj.read() tmpFileObj.close() # no exception was thrown so we bail from this for loop break except struct.error: # print a warning print(' trying again with null byte appended to file due to malformed gzip') # close the file if it is still open if (tmpFileObj): tmpFileObj.close() # add a null byte to the end of the file tmpFileObj = open(gzipPath, 'ab') tmpFileObj.write('\x00') tmpFileObj.close() except: # if we are here then we recieved an unexpected exception so we bail print('\n FATAL: unexpected exception: %s\n%s' % (sys.exc_info()[1], sys.exc_info()[2])) sys.exit(1) # at this point make sure the read was successful if (uncompressedData == ''): print('\n FATAL: could not uncompress GZIP file') sys.exit(1) # we were able to successfully read the GZIP file print(' decompression complete') # summarize output - will also extract files if options.extractDir has been set if(options.summary != None): print '\n +saving summary of scanner results to \'%s\'' % (options.outputFile) p = ProcessGreaterSurgeonJson(uncompressedData,dirname=options.extractDir) output = p.dump_summary() #save summary f = open(options.outputFile, 'wb') f.write(output.encode('utf8')) f.close() # dump full json - will also extract files if options.extractDir has been set else: print '\n +saving raw json to %s' % (options.outputFile) p = ProcessGreaterSurgeonJson(uncompressedData,dirname=options.extractDir) #invoke to extract files if needed if(options.extractDir != None): p.dump_summary() #write out raw json f = open(options.outputFile,'wb') t = p.__str__() f.write(t.encode('utf8')) f.close() if(options.zlibdecompress != None): print "Doing zlib decompression\n" if(options.decrypt): file = open(options.outputFile,'rb') else: file = open(options.inputFile,'rb') data = file.read() file.close() file = open(options.outputFile,'wb') file.write(zlib.decompress(data)) file.close() if(options.unicode != None): print "Doing unicode modification\n" file = open(options.outputFile,'rb') data = file.read() file.close() file = open(options.outputFile,'wb') file.write(data.rstrip('\x00')) file.close() # echo that we are done print '\n +parsing complete' if __name__ == "__main__": main()