-
-
Save nanzhipro/7c672f61c5b548abdca5edc2c610470e 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/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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