Skip to content

Instantly share code, notes, and snippets.

@richiercyrus
Created February 12, 2019 14:17
Show Gist options
  • Save richiercyrus/720e083e9b86c9bafd7c4dc9c40c2890 to your computer and use it in GitHub Desktop.
Save richiercyrus/720e083e9b86c9bafd7c4dc9c40c2890 to your computer and use it in GitHub Desktop.
Python code for checking whether there are any processes running on a macOS system that are missing the LC_CODE_SIGNATURE command. This may be indicative of a LC_LOAD_DYLIB addition attack: https://attack.mitre.org/techniques/T1161/
import os
import sys
import shlex
import argparse
import subprocess
import macholib
import json
import hashlib
#This script is designed to detect the following MITRE ATT&CK Technique:
#https://attack.mitre.org/techniques/T1161/
#If an attacker adds an dynamic library to be loaded duirng execution time, they need to remove the LC_CODE_SIGNATURE command
#from loading in order for the attack to work.
#The output of this script presents binaries which are currently running and have the LC_CODE_SIGNATURE command missing
#supported archs - changed to only support x86_64
SUPPORTED_ARCHITECTURES = ['x86_64']
#these are the filetype fields for mach-o binaries
#executable binary
MH_EXECUTE = 2
#dylib
MH_DYLIB = 6
#bundles
MH_BUNDLE = 8
LC_REQ_DYLD = 0x80000000
(
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)
LC_LOAD_WEAK_DYLIB = LC_REQ_DYLD | 0x18
LC_SEGMENT_64 = 0x19
LC_ROUTINES_64 = 0x1a
LC_UUID = 0x1b
LC_RPATH = (0x1c | LC_REQ_DYLD)
LC_CODE_SIGNATURE = 0x1d
LC_CODE_SEGMENT_SPLIT_INFO = 0x1e
LC_REEXPORT_DYLIB = 0x1f | LC_REQ_DYLD
LC_LAZY_LOAD_DYLIB = 0x20
LC_ENCRYPTION_INFO = 0x21
LC_DYLD_INFO = 0x22
LC_DYLD_INFO_ONLY = 0x22 | LC_REQ_DYLD
LC_LOAD_UPWARD_DYLIB = 0x23 | LC_REQ_DYLD
LC_VERSION_MIN_MACOSX = 0x24
LC_VERSION_MIN_IPHONEOS = 0x25
LC_FUNCTION_STARTS = 0x26
LC_DYLD_ENVIRONMENT = 0x27
LC_MAIN = 0x28 | LC_REQ_DYLD
LC_DATA_IN_CODE = 0x29
LC_SOURCE_VERSION = 0x2a
LC_DYLIB_CODE_SIGN_DRS = 0x2b
LC_ENCRYPTION_INFO_64 = 0x2c
LC_LINKER_OPTION = 0x2d
LC_LINKER_OPTIMIZATION_HINT = 0x2e
LC_VERSION_MIN_TVOS = 0x2f
LC_VERSION_MIN_WATCHOS = 0x30
LC_NOTE = 0x31
LC_BUILD_VERSION = 0x32
LC_NAMES = {
LC_SEGMENT:'LC_SEGMENT',
LC_IDFVMLIB: 'LC_IDFVMLIB',
LC_LOADFVMLIB: 'LC_LOADFVMLIB',
LC_ID_DYLIB: 'LC_ID_DYLIB',
LC_LOAD_DYLIB: 'LC_LOAD_DYLIB',
LC_LOAD_WEAK_DYLIB: 'LC_LOAD_WEAK_DYLIB',
LC_SUB_FRAMEWORK: 'LC_SUB_FRAMEWORK',
LC_SUB_CLIENT: 'LC_SUB_CLIENT',
LC_SUB_UMBRELLA: 'LC_SUB_UMBRELLA',
LC_SUB_LIBRARY: 'LC_SUB_LIBRARY',
LC_PREBOUND_DYLIB: 'LC_PREBOUND_DYLIB',
LC_ID_DYLINKER: 'LC_ID_DYLINKER',
LC_LOAD_DYLINKER: 'LC_LOAD_DYLINKER',
LC_THREAD: 'LC_THREAD',
LC_UNIXTHREAD: 'LC_UNIXTHREAD',
LC_ROUTINES: 'LC_ROUTINES',
LC_SYMTAB: 'LC_SYMTAB',
LC_DYSYMTAB: 'LC_DYSYMTAB',
LC_TWOLEVEL_HINTS: 'LC_TWOLEVEL_HINTS',
LC_PREBIND_CKSUM: 'LC_PREBIND_CKSUM',
LC_SYMSEG: 'LC_SYMSEG',
LC_IDENT: 'LC_IDENT',
LC_FVMFILE: 'LC_FVMFILE',
LC_SEGMENT_64: 'LC_SEGMENT_64',
LC_ROUTINES_64: 'LC_ROUTINES_64',
LC_UUID: 'LC_UUID',
LC_RPATH: 'LC_RPATH',
LC_CODE_SIGNATURE: 'LC_CODE_SIGNATURE',
LC_CODE_SEGMENT_SPLIT_INFO: 'LC_CODE_SEGMENT_SPLIT_INFO',
LC_REEXPORT_DYLIB: 'LC_REEXPORT_DYLIB',
LC_LAZY_LOAD_DYLIB: 'LC_LAZY_LOAD_DYLIB',
LC_ENCRYPTION_INFO: 'LC_ENCRYPTION_INFO',
LC_DYLD_INFO: 'LC_DYLD_INFO',
LC_DYLD_INFO_ONLY: 'LC_DYLD_INFO_ONLY',
LC_LOAD_UPWARD_DYLIB: 'LC_LOAD_UPWARD_DYLIB',
LC_VERSION_MIN_MACOSX: 'LC_VERSION_MIN_MACOSX',
LC_VERSION_MIN_IPHONEOS: 'LC_VERSION_MIN_IPHONEOS',
LC_FUNCTION_STARTS: 'LC_FUNCTION_STARTS',
LC_DYLD_ENVIRONMENT: 'LC_DYLD_ENVIRONMENT',
LC_MAIN: 'LC_MAIN',
LC_DATA_IN_CODE: 'LC_DATA_IN_CODE',
LC_SOURCE_VERSION: 'LC_SOURCE_VERSION',
LC_DYLIB_CODE_SIGN_DRS: 'LC_DYLIB_CODE_SIGN_DRS',
LC_LINKER_OPTIMIZATION_HINT: 'LC_LINKER_OPTIMIZATION_HINT',
LC_VERSION_MIN_TVOS: 'LC_VERSION_MIN_TVOS',
LC_VERSION_MIN_WATCHOS: 'LC_VERSION_MIN_WATCHOS',
LC_NOTE: 'LC_NOTE',
LC_BUILD_VERSION: 'LC_BUILD_VERSION',
}
#check to see if the right python verision is installed before running.
def checkEnv():
#global import
global macholib
#get python version
pythonVersion = sys.version_info
#check that python is at least 2.7
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
#err msg
print('ERROR: requires python 2.7+ (found: %s)' % (pythonVersion))
#bail
return False
#try import macholib
try:
#import
import macholib.MachO
#handle exception
# ->bail w/ error msg
except ImportError:
#err msg
print('ERROR: could not load required module (macholib)')
#bail
return False
#got to here
# ->env looks ok!
return True
def isSupportedArchitecture(macho):
#flag
supported = False
#check macho headers for supported arch
for machoHeader in macho.headers:
#check
if macholib.MachO.CPU_TYPE_NAMES.get(machoHeader.header.cputype, machoHeader.header.cputype) in SUPPORTED_ARCHITECTURES:
#ok!
supported = True
#bail
break
#return True/False and the machoHeader for the macho passed to the function
return (supported, machoHeader)
def loadedBinaries():
#list of loaded bins
binaries = []
#exec lsof
lsof = subprocess.Popen(["lsof", "/"], stdout=subprocess.PIPE)
#get output
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 to array "binaries"
binaries.append(binary)
except:
#ignore
pass
#filter out dup's through the use of the set command
binaries = list(set(binaries))
# return the array of binaies from lsof
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
#array of suspicious binaries
suspicious_binaries = []
rPaths = []
#scan all binaries
for binary in binaries:
parsedBinaries = {}
binary_loadcmds = []
dylibs = []
#containsSigLoad = False
try:
#try load it (as mach-o)
macho = macholib.MachO.MachO(binary)
if not macho:
#skip
continue
except:
#skip
continue
#check if it's a supported (intel) architecture & also returns the supported mach-O header
(isSupported, machoHeader) = isSupportedArchitecture(macho)
if not isSupported:
#skip
continue
#skip binaries that aren't main executables, dylibs or bundles
#if machoHeader.header.filetype not in [MH_EXECUTE, MH_DYLIB, MH_BUNDLE]:
if machoHeader.header.filetype not in [MH_EXECUTE, MH_BUNDLE]:
#skip
continue
for loadCommand in machoHeader.commands:
if macholib.MachO.LC_LOAD_DYLIB == loadCommand[0].cmd:
dylibs.append(loadCommand[-1].rstrip('\0'))
binary_loadcmds.append(LC_NAMES.get(loadCommand[0].cmd))
print binary
print binary_loadcmds
print dylibs
print "*****************"
return suspicious_binaries
def getSignatureInfo(binary):
signature = []
sig = ''
codesign_info = subprocess.Popen('codesign -d -vvvv '+ "'" + binary + "'", shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output = codesign_info.stderr.read()
codesign_info.stderr.close()
codesign_info.wait()
codesigned = False
sig = output
return sig
def getHashInfo(binary):
hasher = hashlib.sha256()
with open(binary, 'rb') as hashed_binary:
buf = hashed_binary.read()
hasher.update(buf)
return hasher.hexdigest()
#hash256 = ''
#hash_info = subprocess.Popen('shasum -a 256 '+ "'" + binary + "'", shell=False, stdout=subprocess.PIPE)
#output = hash_info.stdout.read()
#hash_info.stdout.close()
#hash_info.wait()
#hash256 = output.split(" ")[0]
#return hash256
if __name__ == '__main__':
if not checkEnv():
sys.exit(-1)
binaries = loadedBinaries()
#suspicious_binaries = parseBinaries(binaries)
parseBinaries(binaries)
'''for binary in suspicious_binaries:
#binary.update({'signature': getSignatureInfo(binary.get("binary",""))})
binary.update({'hash': getHashInfo(binary.get("binary",""))})
#print binary
#print "*******"
final_output = (json.dumps(suspicious_binaries, indent=4))'''
#print final_output
#Make each binary output its own dictionary to be printed to a file
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment