from builtins import object


class Module(object):

    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': 'Dylib Hijack Vulnerability Scanner',

            # list of one or more authors for the module
            'Author': ['@patrickwardle','@xorrior'],

            # more verbose multi-line description of the module
            'Description': ('This module can be used to identify applications vulnerable to dylib hijacking on a target system. This has been modified from the original to remove the dependancy for the macholib library.'),

            'Software': '',

            'Techniques': ['T1157'],

            # True if the module needs to run in the background
            'Background' : False,

            # File extension to save the file as
            'OutputExtension' : None,

            'NeedsAdmin' : False,

            # True if the method doesn't touch disk/is reasonably opsec safe
            'OpsecSafe' : True,

            # the module language
            'Language' : 'python',

            # the minimum language version needed
            'MinLanguageVersion' : '2.6',

            # list of any references/other comments
            'Comments': [
                'Heavily adapted from @patrickwardle\'s script: https://github.com/synack/DylibHijack/blob/master/scan.py'
            ]
        }

        # 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 run the module on.',
                'Required'      :   True,
                'Value'         :   ''
            },
            'Path' : {
                'Description'   :   'Scan all binaries recursively, in a specific path.',
                'Required'      :   False,
                'Value'         :   ''
            },
            'LoadedProcesses' : {
                'Description'   :   'Scan only loaded process executables',
                'Required'      :   True,
                'Value'         :   'False'
            }
        }

        # 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.

        scanPath = self.options['Path']['Value']
        LoadedProcesses = self.options['LoadedProcesses']['Value']

        script = """
from ctypes import *
def run():

    import ctypes
    import os
    import sys
    import shlex
    import subprocess
    import io
    import struct
    from datetime import datetime

    #supported archs
    SUPPORTED_ARCHITECTURES = ['i386', 'x86_64']

    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)
        ]

    #executable binary
    MH_EXECUTE = 2

    #dylib
    MH_DYLIB = 6

    #bundles
    MH_BUNDLE = 8


    LC_Header_Sz = 0x8

    def isSupportedArchitecture(machoHandle):

        #flag
        headersz = 28
        header64sz = 32
        supported = False
        header = ""
        
        #header = mach_header.from_buffer_copy(machoHandle.read())
        try:
            magic = struct.unpack('<L',machoHandle.read(4))[0]
            cputype = struct.unpack('<L',machoHandle.read(4))[0]
            machoHandle.seek(0, io.SEEK_SET)
        
        
            if CPU_TYPE_NAMES.get(cputype) == 'i386':

                header = mach_header.from_buffer_copy(machoHandle.read(headersz))
                supported = True

            elif CPU_TYPE_NAMES.get(cputype) == 'x86_64':

                header = mach_header_64.from_buffer_copy(machoHandle.read(header64sz)) 
                supported = True

            else:
                header = None 
        except:
            pass
            

        return (supported, header)

    def loadedBinaries():

        #list of loaded bins
        binaries = []

        #exec lsof
        lsof = subprocess.Popen('lsof /', shell=True, stdout=subprocess.PIPE)

        #get outpu
        output = lsof.stdout.read()

        #close
        lsof.stdout.close()

        #wait
        lsof.wait()

        #parse/split output
        # ->grab file name and check if its executable
        for line in output.split('\\n'):

            try:

                #split on spaces up to 8th element
                # ->this is then the file name (which can have spaces so grab rest/join)
                binary = ' '.join(shlex.split(line)[8:])

                #skip non-files (fifos etc....) or non executable files
                if not os.path.isfile(binary) or not os.access(binary, os.X_OK):

                    #skip
                    continue

                #save binary
                binaries.append(binary)

            except:

                #ignore
                pass

        #filter out dup's
        binaries = list(set(binaries))
        
        return binaries

    def installedBinaries(rootDirectory = None):

        #all executable binaries
        binaries = []
        
        #init
        if not rootDirectory:

            rootDirectory = '/'

        #recursively walk (starting at r00t)
        for root, dirnames, filenames in os.walk(rootDirectory):

            #check all files
            for filename in filenames:

                #make full
                # ->use realpath to resolve symlinks
                fullName = os.path.realpath(os.path.join(root, filename))

                #skip non-files (fifos etc....)
                if not os.path.isfile(fullName):

                    #skip
                    continue

                #only check executable files
                if os.access(fullName, os.X_OK) and (os.path.splitext(fullName)[-1] == '.dyblib' or os.path.splitext(fullName)[-1] == ''):

                    #save
                    
                    binaries.append(fullName)
        print("Finished with installed binaries\\n")
        return binaries

    def resolvePath(binaryPath, unresolvedPath):

        #return var
        # ->init to what was passed in, since might not be able to resolve
        resolvedPath = unresolvedPath

        #resolve '@loader_path'
        if unresolvedPath.startswith('@loader_path'):

            #resolve
            resolvedPath = os.path.abspath(os.path.split(binaryPath)[0] + unresolvedPath.replace('@loader_path', ''))

        #resolve '@executable_path'
        elif unresolvedPath.startswith('@executable_path'):

            #resolve
            resolvedPath = os.path.abspath(os.path.split(binaryPath)[0] + unresolvedPath.replace('@executable_path', ''))

        return resolvedPath

    def parseBinaries(binaries):

        #dictionary of parsed binaries
        
        parsedBinaries = {}
        #scan all binaries
        for binary in binaries:

            #wrap
            try:

                #try load it (as mach-o)
                f = open(binary, 'rb')
                if not f:

                    #skip
                    continue

            except:

                #skip
                continue

            #check if it's a supported (intel) architecture
            # ->also returns the supported mach-O header
            (isSupported, machoHeader) = isSupportedArchitecture(f)
            if not isSupported:

                #skip
                continue

            #skip binaries that aren't main executables, dylibs or bundles
            if machoHeader.filetype not in [MH_EXECUTE, MH_DYLIB, MH_BUNDLE]:

                #skip
                continue

            #dbg msg
            

            #init dictionary for process
            parsedBinaries[binary] = {'LC_RPATHs': [], 'LC_LOAD_DYLIBs' : [], 'LC_LOAD_WEAK_DYLIBs': [] }

            #save type
            parsedBinaries[binary]['type'] = machoHeader.filetype

            #iterate over all load
            # ->save LC_RPATHs, LC_LOAD_DYLIBs, and LC_LOAD_WEAK_DYLIBs
            
            if CPU_TYPE_NAMES.get(machoHeader.cputype) == 'x86_64':
                f.seek(32, io.SEEK_SET)
            else:
                f.seek(28, io.SEEK_SET) 
            
            
            for cmd in range(machoHeader.ncmds):

                #handle LC_RPATH's
                # ->resolve and save
                
                 #save offset to load commands
                try:
                    lc = load_command.from_buffer_copy(f.read(LC_Header_Sz))
                except Exception as e:
                    break #break out of the nested loop and continue with the parent loop
                size = lc.cmdsize
                if lc.cmd == LC_RPATH:

                    #grab rpath
                    
                    pathoffset = struct.unpack('<L',f.read(4))[0]
                    f.seek(pathoffset - (LC_Header_Sz + 4), io.SEEK_CUR)
                    path = f.read(lc.cmdsize - pathoffset)
                    rPathDirectory = path.rstrip('\\0')

                    #always attempt to resolve '@executable_path' and '@loader_path'
                    rPathDirectory = resolvePath(binary, rPathDirectory)

                    #save
                    parsedBinaries[binary]['LC_RPATHs'].append(rPathDirectory)

                    

                #handle LC_LOAD_DYLIB
                # ->save (as is)
                elif lc.cmd == LC_LOAD_DYLIB:

                    #tuple, last member is path to import
                    
                    pathoffset = struct.unpack('<L',f.read(4))[0]
                    #skip over version and compatibility
                    f.seek(pathoffset - (LC_Header_Sz + 4), io.SEEK_CUR)
                    path = f.read(size - pathoffset)
                    importedDylib = path.rstrip('\\0')

                    #save
                    parsedBinaries[binary]['LC_LOAD_DYLIBs'].append(importedDylib)

                    

                #handle for LC_LOAD_WEAK_DYLIB
                # ->resolve (except for '@rpaths') and save
                elif lc.cmd == LC_LOAD_WEAK_DYLIB:

                    #tuple, last member is path to import
                    
                    pathoffset = struct.unpack('<L',f.read(4))[0]
                    #skip over version and compatibility
                    f.seek(pathoffset - (LC_Header_Sz + 4), io.SEEK_CUR)
                    path = f.read(size - pathoffset)
                    weakDylib = path.rstrip('\\0')

                    #always attempt to resolve '@executable_path' and '@loader_path'
                    weakDylib = resolvePath(binary, weakDylib)

                    #save
                    parsedBinaries[binary]['LC_LOAD_WEAK_DYLIBs'].append(weakDylib)

                    
                else:
                    f.seek(size - LC_Header_Sz, io.SEEK_CUR)
                    
        print("finished parsing load commands")
        return parsedBinaries

    def processBinaries(parsedBinaries):

        #results
        # ->list of dictionaries

        vulnerableBinaries = {'rpathExes': [], 'weakBins': []}

        #scan all parsed binaries
        for key in parsedBinaries:

            #grab binary entry
            binary = parsedBinaries[key]

            #STEP 1: check for vulnerable @rpath'd imports
            # note: only do this for main executables, since dylibs/bundles can share @rpath search dirs w/ main app, etc
            #       which we can't reliably resolve (i.e. this depends on the runtime/loadtime env)

            #check for primary @rpath'd import that doesn't exist
            if binary['type']== MH_EXECUTE and len(binary['LC_RPATHs']):

                #check all @rpath'd imports for the executable
                # ->if there is one that isn't found in a primary LC_RPATH, the executable is vulnerable :)
                for importedDylib in binary['LC_LOAD_DYLIBs']:

                    #skip non-@rpath'd imports
                    if not importedDylib.startswith('@rpath'):

                        #skip
                        continue

                    

                    #chop off '@rpath'
                    importedDylib = importedDylib.replace('@rpath', '')

                    #check the first rpath directory (from LC_RPATHs)
                    # ->is the rpath'd import there!?
                    if not os.path.exists(binary['LC_RPATHs'][0] + importedDylib):

                        #not found
                        # ->means this binary is vulnerable!
                        vulnerableBinaries['rpathExes'].append({'binary': key, 'importedDylib': importedDylib, 'LC_RPATH': binary['LC_RPATHs'][0]})

                        #bail
                        break

            #STEP 2: check for vulnerable weak imports
            # can check all binary types...

            #check binary
            for weakDylib in binary['LC_LOAD_WEAK_DYLIBs']:

                #got to resolve weak @rpath'd imports before checking if they exist
                if weakDylib.startswith('@rpath'):

                    #skip @rpath imports in dylibs and bundles, since they can share @rpath search dirs w/ main app, etc
                    # which we can't reliably resolve (i.e. this depends on the runtime/loadtime env.)
                    if binary['type'] != MH_EXECUTE:

                        #skip
                        continue

                    #skip @rpath imports if binary doesn't have any LC_RPATHS
                    if not len(binary['LC_RPATHs']):

                        #skip
                        continue

                    #chop off '@rpath'
                    weakDylib = weakDylib.replace('@rpath', '')

                    #just need to check first LC_RPATH directory
                    if not os.path.exists(binary['LC_RPATHs'][0] + weakDylib):

                        #not found
                        # ->means this binary is vulnerable!
                        vulnerableBinaries['weakBins'].append({'binary': key, 'weakDylib': weakDylib, 'LC_RPATH': binary['LC_RPATHs'][0]})

                        #bail
                        break

                #path doesn't need to be resolved
                # ->check/save those that don't exist
                elif not os.path.exists(weakDylib):

                    #not found
                    # ->means this binary is vulnerable!
                    vulnerableBinaries['weakBins'].append({'binary': key, 'weakBin': weakDylib})

                    #bail
                    break

        return vulnerableBinaries

    path = "%s"

    ProcBinaries = "%s"
    startTime = datetime.now()
    if ProcBinaries.lower() == "true":
    

        #get list of loaded binaries
        binaries = loadedBinaries()
    elif path :
        #dbg msg
        

        #get list of executable files
        binaries = installedBinaries(path)
    else:
        

        #get list of executable files on the file-system
        binaries = installedBinaries()

    parsedBinaries = parseBinaries(binaries)

    #process/scan em
    vulnerableBinaries = processBinaries(parsedBinaries)

    #display binaries that are vulnerable to rpath hijack
    if len(vulnerableBinaries['rpathExes']):

        #dbg msg
        print('\\nfound %%d binaries vulnerable to multiple rpaths:' %% len(vulnerableBinaries['rpathExes']))

        #iterate over all and print
        for binary in vulnerableBinaries['rpathExes']:

            #dbg msg
            print('%%s has an rpath vulnerability: (%%s%%s)\\n' %% (binary['binary'], binary['LC_RPATH'],binary['importedDylib']))

    #binary didn't have any
    else:

        #dbg msg
        print('\\ndid not find any vulnerable to multiple rpaths')

    #display binaries that are vulnerable to weak import hijack
    if len(vulnerableBinaries['weakBins']):

            #dbg msg
            print('\\nfound %%d binaries vulnerable to weak dylibs:' %% len(vulnerableBinaries['weakBins']))

            #iterate over all and print
            for binary in vulnerableBinaries['weakBins']:

                #dbg msg
                print('%%s has weak import (%%s)\\n' %% (binary['binary'], binary))

    #binary didn't have any
    else:

        #dbg msg
        print('\\ndid not find any missing LC_LOAD_WEAK_DYLIBs')


    #dbg msg
    
    print("Scan completed in " + str(datetime.now() - startTime) + "\\n")

    print("[+] To abuse an rpath vulnerability...\\n")
    print("[+] Find the legitimate dylib: find / -name <dylibname>, and note the path\\n")
    print("[+] Run the CreateHijacker module in /persistence/osx/. Set the DylibPath to the path of the legitimate dylib.\\n")

run()
""" % (scanPath, LoadedProcesses)

        return script
