├── LICENSE ├── README.md ├── boko.png └── boko.py /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Jesse Nebling 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # boko Application Hijack Scanner for macOS ![This is boko](https://github.com/bashexplode/boko/blob/master/boko.png) 2 | boko.py is an application scanner for macOS that searches for and identifies potential dylib hijacking and 3 | weak dylib vulnerabilities for application executables, as well as scripts an application may use that 4 | have the potential to be backdoored. The tool also calls out interesting files and lists them instead of manually 5 | browsing the file system for analysis. With the active discovery function, there's no more guess work if an executable is vulnerable to dylib hijacking! 6 | 7 | The reason behind creating this tool was because I wanted more control over the data Dylib Hijack Scanner discovered. Most publicly available scanners stop once they discover the first case of a vulnerable dylib without expanding the rest of the rpaths. Since sometimes the first result is expanded in a non-existent file within a SIP-protected area, I wanted to get the rest of those expanded paths. Because of this, there are false positives, so the tool assigns a certainty field for each item. 8 | | **Certainty** | **Description** | 9 | |--------------------:|:-----------------------------------| 10 | | Definite | The vulnerability is 100% exploitable | 11 | | High | If the vulnerability is related to a main executable and rpath is 2nd in the load order, there is a good chance the vulnerability is exploitable | 12 | | Potential | This is assigned to dylibs and backdoorable scripts, worth looking into but may not be exploitable | 13 | | Low | Low chance this is exploitable because of late load order, but knowledge is power | 14 | 15 | The backbone of this tool is based off of scan.py from [DylibHijack](https://github.com/synack/DylibHijack) by Patrick Wardle (@synack). 16 | 17 | [Check out the Wiki for more information on how to use boko and how to use it as a class for your own purposes.](https://github.com/bashexplode/boko/wiki) 18 | 19 | #### Usage: 20 | ```Python 21 | boko.py [-h] (-r | -i | -p /path/to/app) (-A | -P | -b) [-oS outputfile | -oC outputfile | -oA outputfile] [-s] [-v] 22 | ``` 23 | 24 | #### Parameters: 25 | | **Argument** | **Description** | 26 | |--------------------:|:-----------------------------------| 27 | | -h, --help | Show this help message and exit | 28 | | -r, --running | Check currently running processes | 29 | | -i, --installed | Check all installed applications | 30 | | -p /file.app | Check a specific application i.e. /Application/Safari.app | 31 | | -A, --active | Executes executable binaries discovered to actively identify hijackable dylibs | 32 | | -P, --passive | Performs checks only by viewing file headers (Default) | 33 | | -b, --both | Performs both methods of vulnerability testing | 34 | | -oS outputfile | Outputs standard output to a .log file | 35 | | -oC outputfile | Outputs results to a .csv file | 36 | | -oA outputfile | Outputs results to a .csv file and standard log | 37 | | -s, --sipdisabled | Use if SIP is disabled on the system to search typically read-only paths| 38 | | -v, --verbose | Output all results in verbose mode while script runs, without this only Definite certainty vulnerabilities are displayed to the console | 39 | 40 | It is recommended **only** to use active mode (`-A`) with the `-p` flag and selecting a specific program. Also, it's a good idea to use `-v` with `-oS` or `-oA`, unless you are only looking for definite certainty vulnerabilities. 41 | 42 | **Warning Note:** 43 | It is highly discouraged to run this tool with the `-i` and (`-A` or -`b`) flags together. This combination will open every executable on your system for 3 seconds at a time. I do not take any responsibility for your system crashing or slowing down because you ran that. Additionally, if you have dormant malware on your system, this will execute it. I also recommend not scanning the whole `/Applications` directory if you have Xcode installed because it takes a very long time. 44 | 45 | #### Requirements: 46 | 47 | * Python 3 48 | * `python -m pip install psutil` 49 | 50 | #### Process Flow: 51 | 52 | ##### Passive mode: 53 | 54 | ###### Running: 55 | * Identify all running processes on system 56 | * Obtain full path of running executable 57 | * Read executables and identify macho headers 58 | * Identify dylib relative paths that are loaded and check if files exist in that location 59 | * Output hijackable dylibs and weak dylibs for running applications 60 | 61 | ###### Installed/Application: 62 | * Scan full directory of application for all files 63 | * Identify executable files, scripts, and other interesting files in application directory 64 | * Read executables and identify macho headers or if the file is a script 65 | * Identify dylib relative paths that are loaded and check if files exist in that location 66 | * Output hijackable dylibs, weak dylibs, backdoorable scripts, and interesting files (verbose only) 67 | 68 | ##### Active mode: 69 | 70 | ###### Running: 71 | * Identify all running processes on system 72 | * Obtain full path of running executable 73 | * Read executables and identify macho headers 74 | * Execute the executable binaries for 3 seconds and analyze rpaths that are attempted to load 75 | * Output hijackable dylibs and weak dylibs for running applications 76 | 77 | ###### Application: 78 | * Scan full directory of application for all files 79 | * Identify executable files, scripts, and other interesting files in application directory 80 | * Read executables and identify macho headers or if the file is a script 81 | * Execute the executable binaries for 3 seconds and analyze rpaths that are attempted to load 82 | * Output hijackable dylibs, weak dylibs, backdoorable scripts, and interesting files (verbose only) 83 | 84 | 85 | #### Suggested Improvements: 86 | 87 | * Multi-threading for quicker full system scan 88 | 89 | ## Coded by Jesse Nebling (@bashexplode) 90 | -------------------------------------------------------------------------------- /boko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashexplode/boko/41e99893e1cef612d35d8b40ed1b105f765bd12e/boko.png -------------------------------------------------------------------------------- /boko.py: -------------------------------------------------------------------------------- 1 | # Author: Jesse Nebling (@bashexplode) 2 | # The dylib scanning functions are based off of Patrick Wardle's tool Dylib Hijack Scanner 3 | # 4 | # boko.py is a static application scanner for macOS that searches for and identifies 5 | # potential dylib hijacking and weak dylib vulnerabilities for application executables, as well as 6 | # identifies scripts an application may use that have the potential to be backdoored. It also calls out interesting 7 | # files and lists them instead of manually browsing the file system for analysis. 8 | # 9 | # Dictionary format: 10 | # Filepath: { 11 | # 'writeable': Bool, # indicates whether or not the current user that ran the tool has write permissions on the file 12 | # 'execution': { # dictionary for execution output, only applicable if the --active or --both flag is used 13 | # 'standardoutput': [], 14 | # 'erroroutput' [] 15 | # }, 16 | # 'load': { 17 | # 'LC_RPATHs': [], 18 | # 'LC_LOAD_WEAK_DYLIBs': [], 19 | # 'LC_LOAD_DYLIBs': [] 20 | # }, 21 | # 'filetypeh': 'String', # indicates the file type as a string : Executable, Dylib, Bundle, KextBundle, Script, Misc (based on file extension) 22 | # 'filetype': mach_header filetype, 23 | # 'parse': 'String', # indicates if the file was an executable and was parsed for weaknesses 24 | # 'filename': 'String', # File name without full path 25 | # 'vulnerable': { 26 | # 'WeakDylib': [ # list of weak dylibs, each weak dylib has its own dictionary 27 | # { 28 | # 'Certainty': 'String', # indicates how certain the vulnerability exists based on load path ordering and file type : Definite, High, Potential, Low 29 | # 'hijackPath': 'String', # full path a malicious dylib can be placed to hijack the load order of the base file 30 | # 'WriteAccess': Bool, # indicates whether or not the current user that ran the tool can write to the hijackPath 31 | # 'LoadOrder': int, # indicates the order in which the main binary Filename loads the dylib relative path, starts at 0 32 | # 'ReadOnlyPartition': False # indicates whether or not the directory is in a SIP-protected partition 33 | # } 34 | # ], 35 | # 'DylibHijack': [ # list of hijackable dylibs, each dylib has its own dictionary 36 | # { 37 | # 'Certainty': 'String', # indicates how certain the vulnerability exists based on load path ordering and file type : Definite, High, Potential, Low 38 | # 'hijackPath': 'String', # full path a malicious dylib can be placed to hijack the load order of the base file 39 | # 'WriteAccess': Bool, # indicates whether or not the current user that ran the tool can write to the hijackPath 40 | # 'LoadOrder': int, # indicates the order in which the main binary Filename loads the dylib relative path, starts at 0 41 | # 'ReadOnlyPartition': False # indicates whether or not the directory is in a SIP-protected partition 42 | # } 43 | # ], 44 | # 'BackdoorableScript': [ # list of potentially backdoorable scripts, each script has its own dictionary 45 | # { 46 | # 'Certainty': 'String', # indicates how certain the vulnerability exists based on load path ordering and file type : Definite, High, Potential, Low 47 | # 'hijackPath': 'String', # full path a malicious dylib can be placed to hijack the load order of the base file 48 | # 'WriteAccess': Bool, # indicates whether or not the current user that ran the tool can write to the hijackPath 49 | # 'LoadOrder': int, # indicates the order in which the main binary Filename loads the dylib relative path, starts at 0 50 | # 'ReadOnlyPartition': False # indicates whether or not the directory is in a SIP-protected partition 51 | # } 52 | # ] 53 | # } 54 | # } 55 | 56 | from __future__ import print_function 57 | import ctypes 58 | import argparse 59 | import os 60 | import sys 61 | import io 62 | import struct 63 | import psutil 64 | import subprocess 65 | from datetime import datetime 66 | from multiprocessing.dummy import Pool as ThreadPool 67 | import threading 68 | 69 | screenlock = threading.Semaphore(value=1) 70 | 71 | 72 | class mach_header(ctypes.Structure): 73 | _fields_ = [ 74 | ("magic", ctypes.c_uint), 75 | ("cputype", ctypes.c_uint), 76 | ("cpusubtype", ctypes.c_uint), 77 | ("filetype", ctypes.c_uint), 78 | ("ncmds", ctypes.c_uint), 79 | ("sizeofcmds", ctypes.c_uint), 80 | ("flags", ctypes.c_uint) 81 | ] 82 | 83 | 84 | class mach_header_64(ctypes.Structure): 85 | _fields_ = mach_header._fields_ + [('reserved', ctypes.c_uint)] 86 | 87 | 88 | class load_command(ctypes.Structure): 89 | _fields_ = [ 90 | ("cmd", ctypes.c_uint), 91 | ("cmdsize", ctypes.c_uint) 92 | ] 93 | 94 | 95 | class ExecutableScanner: 96 | def __init__(self, verbose, sipdisabled): 97 | self.verbose = verbose 98 | self.results = {} 99 | self.sipreadonlydefaults = ['//', 100 | '/.fseventsd', 101 | '/.vol', 102 | '/System', 103 | '/System/Applications', 104 | '/System/DriverKit', 105 | '/System/Library', 106 | '/System/Volumes', 107 | '/System/iOSSupport', 108 | '/bin', 109 | '/private/var', 110 | '/sbin', 111 | '/usr', 112 | '/usr/bin', 113 | '/usr/lib', 114 | '/usr/libexec', 115 | '/usr/sbin', 116 | '/usr/share', 117 | '/usr/standalone', 118 | '/Library/Developer/CommandLineTools/SDKs/'] 119 | self.uninterestingexts = ['.3pm', '.3', '.3tcl', '.3x', '.3g', '.aiff', '.dll', '.doc', '.dub', '.dylib', '.filters', '.frag', '.gif', '.html', 120 | '.icns', '.idm', '.iqy', '.lab', '.lex', '.manifest', '.meta', '.metainfo', 121 | '.mp4', '.nls', '.node', '.nrr', '.odf', '.olb', '.plist', '.png', '.ppd', '.ppt', '.pst', '.qml', 122 | '.qmltypes', '.qrc', '.svg', '.tib', '.tiff', '.tlb', '.transition', '.ttc', '.ttf', '.typ', 123 | '.vert', '.wmf', '.xlam', '.xll', '.xls', '.jpg', '.jpeg', '.bmp', '.css', '.tif', '.nib', '.strings', '.tcl', '.wav', '.pcm', '.mp3'] 124 | self.knownpopscriptexts = ['.applescript', '.scpt', '.command', '.sh', '.py', '.rb', '.pl', '.lua', '.jsp', 125 | '.jxa', '.php'] 126 | 127 | if sipdisabled: 128 | self.readonly = None 129 | self.sipdisabled = True 130 | else: 131 | self.readonly = self.sipreadonlydefaults 132 | self.sipdisabled = False 133 | 134 | # supported archs 135 | self.SUPPORTED_ARCHITECTURES = ['i386', 'x86_64'] 136 | 137 | self.LC_REQ_DYLD = 0x80000000 138 | self.LC_LOAD_WEAK_DYLIB = self.LC_REQ_DYLD | 0x18 139 | self.LC_RPATH = (0x1c | self.LC_REQ_DYLD) 140 | self.LC_REEXPORT_DYLIB = 0x1f | self.LC_REQ_DYLD 141 | 142 | ( 143 | self.LC_SEGMENT, self.LC_SYMTAB, self.LC_SYMSEG, self.LC_THREAD, self.LC_UNIXTHREAD, self.LC_LOADFVMLIB, 144 | self.LC_IDFVMLIB, self.LC_IDENT, self.LC_FVMFILE, self.LC_PREPAGE, self.LC_DYSYMTAB, self.LC_LOAD_DYLIB, 145 | self.LC_ID_DYLIB, self.LC_LOAD_DYLINKER, self.LC_ID_DYLINKER, self.LC_PREBOUND_DYLIB, 146 | self.LC_ROUTINES, self.LC_SUB_FRAMEWORK, self.LC_SUB_UMBRELLA, self.LC_SUB_CLIENT, 147 | self.LC_SUB_LIBRARY, self.LC_TWOLEVEL_HINTS, self.LC_PREBIND_CKSUM 148 | ) = range(0x1, 0x18) 149 | 150 | self.MH_MAGIC = 0xfeedface 151 | self.MH_CIGAM = 0xcefaedfe 152 | self.MH_MAGIC_64 = 0xfeedfacf 153 | self.MH_CIGAM_64 = 0xcffaedfe 154 | 155 | self._CPU_ARCH_ABI64 = 0x01000000 156 | self.CPU_TYPE_NAMES = { 157 | -1: 'ANY', 158 | 1: 'VAX', 159 | 6: 'MC680x0', 160 | 7: 'i386', 161 | self._CPU_ARCH_ABI64 | 7: 'x86_64', 162 | 8: 'MIPS', 163 | 10: 'MC98000', 164 | 11: 'HPPA', 165 | 12: 'ARM', 166 | 13: 'MC88000', 167 | 14: 'SPARC', 168 | 15: 'i860', 169 | 16: 'Alpha', 170 | 18: 'PowerPC', 171 | self._CPU_ARCH_ABI64 | 18: 'PowerPC64', 172 | } 173 | 174 | # executable binary 175 | self.MH_EXECUTE = 2 176 | 177 | # dylib 178 | self.MH_DYLIB = 6 179 | 180 | # bundles 181 | self.MH_BUNDLE = 8 182 | 183 | # kext bundle 184 | self.MH_KEXT_BUNDLE = 0xb 185 | self.LC_Header_Sz = 0x8 186 | 187 | def isSupportedArchitecture(self, machoHandle): 188 | machoHandle.seek(0) 189 | headersz = 28 190 | header64sz = 32 191 | supported = False 192 | header = "" 193 | try: 194 | magic = struct.unpack(' 0: 300 | appname = '.'.join(rootDirectory.split('/')[indices[-1]].split('.')[:-1]) 301 | if appname not in applications: 302 | applications.append(appname) 303 | 304 | for filecheck in self.results.keys(): 305 | try: 306 | if fullName in self.results[filecheck]["fullpath"]: 307 | continue 308 | except KeyError: 309 | pass 310 | 311 | # Check if file is executable and not an uninteresting file 312 | if (os.access(fullName, os.X_OK) and os.path.splitext(fullName)[ 313 | -1].lower() not in self.uninterestingexts) or (os.path.splitext(fullName)[-1].lower() not in self.uninterestingexts and os.path.splitext(fullName)[-1].lower() in self.knownpopscriptexts): 314 | # If the files an normally named executable or script add to parsing list and check if current 315 | # user context can write to the file 316 | 317 | if os.path.splitext(fullName)[-1] == '.dyblib' or os.path.splitext(fullName)[-1] == '' or \ 318 | os.path.splitext(fullName)[-1] in self.knownpopscriptexts or os.path.split(fullName)[-1].startswith(tuple(applications)): 319 | self.initializeDictionaryItem(filename, fullName, True) 320 | if self.verbose: 321 | print("[+] Found executable file: %s" % fullName) 322 | # If the file doesn't meet the above criteria save as a potential interesting file to dict. 323 | else: 324 | self.initializeDictionaryItem(filename, fullName, False) 325 | if '.' in filename: 326 | self.results[fullName]['filetypeh'] = filename.split('.')[-1] 327 | else: 328 | self.results[fullName]['filetypeh'] = "Misc" 329 | if self.verbose: 330 | print("[+] Found interesting file: %s" % fullName) 331 | # Save the file even if it's not executable to the interesting files list 332 | elif os.path.splitext(fullName)[-1].lower() not in self.uninterestingexts: 333 | self.initializeDictionaryItem(filename, fullName, False) 334 | if '.' in filename: 335 | self.results[fullName]['filetypeh'] = filename.split('.')[-1] 336 | else: 337 | self.results[fullName]['filetypeh'] = "Misc" 338 | if self.verbose: 339 | print("[+] Found interesting file: %s" % fullName) 340 | 341 | if self.verbose: 342 | print("[+] Finished gathering executable files in %s" % rootDirectory) 343 | 344 | def resolvePath(self, binaryPath, unresolvedPath): 345 | resolvedPath = unresolvedPath 346 | unresolvedPath = str(unresolvedPath) 347 | 348 | if self.results[binaryPath]['filetype'] == self.MH_EXECUTE: 349 | # resolve '@loader_path' 350 | if unresolvedPath.startswith('@loader_path'): 351 | resolvedPath = os.path.abspath( 352 | os.path.split(binaryPath)[0] + unresolvedPath.replace('@loader_path', '')) 353 | # resolve '@executable_path' 354 | elif unresolvedPath.startswith('@executable_path'): 355 | resolvedPath = os.path.abspath( 356 | os.path.split(binaryPath)[0] + unresolvedPath.replace('@executable_path', '')) 357 | else: 358 | matchindices = [i for i, x in enumerate(binaryPath.split('/')) if x == unresolvedPath.split('/')[-1]] 359 | unmatchindicies = [i for i, x in enumerate(binaryPath.split('/')) if x == 'Contents'] 360 | if len(matchindices) > 0: 361 | if unresolvedPath.startswith('@loader_path'): 362 | resolvedPath = os.path.abspath( 363 | '/'.join(binaryPath.split('/')[0:matchindices[-1]]) + "/MacOS" + unresolvedPath.replace( 364 | '@loader_path', '')) 365 | elif unresolvedPath.startswith('@executable_path'): 366 | resolvedPath = os.path.abspath( 367 | '/'.join(binaryPath.split('/')[0:matchindices[-1]]) + "/MacOS" + unresolvedPath.replace( 368 | '@executable_path', '')) 369 | elif len(unmatchindicies) > 0: 370 | if unresolvedPath.startswith('@loader_path'): 371 | resolvedPath = os.path.abspath( 372 | '/'.join(binaryPath.split('/')[0:unmatchindicies[-1]]) + "/Contents/MacOS" + unresolvedPath.replace( 373 | '@loader_path', '')) 374 | elif unresolvedPath.startswith('@executable_path'): 375 | resolvedPath = os.path.abspath( 376 | '/'.join(binaryPath.split('/')[0:unmatchindicies[-1]]) + "/Contents/MacOS" + unresolvedPath.replace( 377 | '@executable_path', '')) 378 | 379 | return resolvedPath.rstrip(b'\x00') 380 | 381 | def parseExecutables(self): 382 | if self.verbose: 383 | print("[*] Parsing executable files for validity as an executable, script, dylib or bundle") 384 | for binarypath in self.results.keys(): 385 | binary = self.results[binarypath]["filename"] 386 | try: 387 | f = open(binarypath, 'rb') 388 | if not f: 389 | if self.verbose: 390 | print("[-] Could not open: %s" % binary) 391 | continue 392 | except: 393 | if self.verbose: 394 | print("[-] Could not open: %s" % binary) 395 | continue 396 | 397 | # Check if it is an interesting file and not an executable 398 | if self.results[binarypath]["parse"] is False: 399 | continue 400 | 401 | # passed checks as an executable create dictionary placeholder for vulnerabilities 402 | self.results[binarypath]["vulnerable"] = {'DylibHijack': [], 'WeakDylib': [], "BackdoorableScript": []} 403 | 404 | isScript = self.scriptCheck(f, binary, binarypath) 405 | if isScript: 406 | if self.verbose: 407 | print("[+] Potential backdoor-able script: %s" % binarypath) 408 | continue 409 | 410 | 411 | 412 | # check if it's a supported (intel) architecture 413 | # ->also returns the supported mach-O header 414 | (isSupported, machoHeader) = self.isSupportedArchitecture(f) 415 | if not isSupported: 416 | if self.verbose: 417 | print("[-] Either not supported architecture or not a binary: %s" % binary) 418 | self.results[binarypath]["parse"] = False 419 | continue 420 | 421 | # skip binaries that aren't main executables, dylibs or bundles 422 | if machoHeader.filetype not in [self.MH_EXECUTE, self.MH_DYLIB, self.MH_BUNDLE, self.MH_KEXT_BUNDLE]: 423 | if self.verbose: 424 | print("[-] Not an executable, dylib, bundle, or kext bundle: %s" % binary) 425 | self.results[binarypath]["parse"] = False 426 | continue 427 | 428 | self.results[binarypath]["parse"] = "Started Parse" 429 | if self.verbose: 430 | print("[*] Parsing: %s" % binary) 431 | print("\tFull binary path: %s" % binarypath) 432 | 433 | # init dictionary for process 434 | self.results[binarypath]["load"] = {'LC_RPATHs': [], 'LC_LOAD_DYLIBs': [], 'LC_LOAD_WEAK_DYLIBs': []} 435 | 436 | # save filetype 437 | self.results[binarypath]['filetype'] = machoHeader.filetype 438 | 439 | if self.results[binarypath]['filetype'] == self.MH_EXECUTE: 440 | self.results[binarypath]['filetypeh'] = "Executable" 441 | elif self.results[binarypath]['filetype'] == self.MH_DYLIB: 442 | self.results[binarypath]['filetypeh'] = "Dylib" 443 | elif self.results[binarypath]['filetype'] == self.MH_BUNDLE: 444 | self.results[binarypath]['filetypeh'] = "Bundle" 445 | elif self.results[binarypath]['filetype'] == self.MH_KEXT_BUNDLE: 446 | self.results[binarypath]['filetypeh'] = "KextBundle" 447 | else: 448 | self.results[binarypath]['filetypeh'] = "Misc" 449 | 450 | # iterate over all load 451 | # ->save LC_RPATHs, LC_LOAD_DYLIBs, and LC_LOAD_WEAK_DYLIBs 452 | if self.CPU_TYPE_NAMES.get(machoHeader.cputype) == 'x86_64': 453 | f.seek(32, io.SEEK_SET) 454 | else: 455 | f.seek(28, io.SEEK_SET) 456 | 457 | for cmd in range(machoHeader.ncmds): 458 | # handle LC_RPATH's 459 | # ->resolve and save 460 | # save offset to load commands 461 | try: 462 | lc = load_command.from_buffer_copy(f.read(self.LC_Header_Sz)) 463 | except Exception as e: 464 | break # break out of the nested loop and continue with the parent loop 465 | size = lc.cmdsize 466 | if lc.cmd == self.LC_RPATH: 467 | # grab rpath 468 | pathoffset = struct.unpack('save (as is) 483 | elif lc.cmd == self.LC_LOAD_DYLIB: 484 | # tuple, last member is path to import 485 | pathoffset = struct.unpack('resolve (except for '@rpaths') and save 497 | elif lc.cmd == self.LC_LOAD_WEAK_DYLIB: 498 | # tuple, last member is path to import 499 | pathoffset = struct.unpack('is the rpath'd import there!? 550 | for loadorder, rpath in enumerate(self.results[binarypath]['load']['LC_RPATHs']): 551 | hijackpath = rpath + dylib 552 | 553 | # if not found means this binary is potentailly vulnerable! 554 | if not os.path.exists(hijackpath): 555 | # Check if current user context has write permissions to the last existing path and if rpath is in the read only partition 556 | readonlypartition, contextwriteperm = self.readWriteCheck(hijackpath) 557 | 558 | # Set logic statements for ease of reading 559 | notreadonly = (not readonlypartition) 560 | executablefirstloaded = (self.results[binarypath]['filetype'] == self.MH_EXECUTE and loadorder == 0) 561 | executablenextloaded = (self.results[binarypath]['filetype'] == self.MH_EXECUTE and loadorder < 2) 562 | allfiletypesfirstloaded = (loadorder == 0) 563 | allfiletypesnextloaded = (loadorder < 2) 564 | 565 | if (executablefirstloaded and notreadonly) or (executablefirstloaded and self.sipdisabled): 566 | certainty = 'Definite' 567 | indicator = '!' 568 | self.vulnerabilityDictInput(vulntype, binarypath, hijackpath, loadorder, certainty, 569 | contextwriteperm, readonlypartition, indicator, mode) 570 | elif (executablenextloaded and notreadonly) or (executablenextloaded and self.sipdisabled): 571 | certainty = 'High' 572 | indicator = '+' 573 | self.vulnerabilityDictInput(vulntype, binarypath, hijackpath, loadorder, certainty, 574 | contextwriteperm, readonlypartition, indicator, mode) 575 | elif (allfiletypesnextloaded and notreadonly) or (allfiletypesnextloaded and self.sipdisabled): 576 | certainty = 'Potential' 577 | indicator = '+' 578 | self.vulnerabilityDictInput(vulntype, binarypath, hijackpath, loadorder, certainty, 579 | contextwriteperm, readonlypartition, indicator, mode) 580 | elif executablefirstloaded and readonlypartition and not self.sipdisabled: 581 | certainty = 'Definite' 582 | indicator = '-' 583 | self.vulnerabilityDictInput(vulntype, binarypath, hijackpath, loadorder, certainty, 584 | contextwriteperm, readonlypartition, indicator, mode) 585 | elif allfiletypesfirstloaded and readonlypartition and not self.sipdisabled: 586 | certainty = 'High' 587 | indicator = '-' 588 | self.vulnerabilityDictInput(vulntype, binarypath, hijackpath, loadorder, certainty, 589 | contextwriteperm, readonlypartition, indicator, mode) 590 | elif allfiletypesnextloaded and readonlypartition and not self.sipdisabled: 591 | certainty = 'Potential' 592 | indicator = '-' 593 | self.vulnerabilityDictInput(vulntype, binarypath, hijackpath, loadorder, certainty, 594 | contextwriteperm, readonlypartition, indicator, mode) 595 | else: 596 | certainty = 'Low' 597 | indicator = '-' 598 | self.vulnerabilityDictInput(vulntype, binarypath, hijackpath, loadorder, certainty, 599 | contextwriteperm, readonlypartition, indicator, mode) 600 | continue 601 | 602 | 603 | def processBinariesPassive(self): 604 | mode = "Passive" 605 | if self.verbose: 606 | print("[*] Processing results to identify weaknesses") 607 | print("[ID] [FileType] [FileName] [Vulnerability] [Certainty] [Permission] Directory") 608 | # scan all parsed binaries 609 | for binarypath in self.results.keys(): 610 | # grab binary entry 611 | binary = self.results[binarypath]["filename"] 612 | 613 | # Check if the file was actually parsed 614 | if self.results[binarypath]["parse"] == "Complete": 615 | # STEP 1: check for vulnerable @rpath'd imports 616 | # Note: changed the check for potentials cuz knowledge is power 617 | # check for primary @rpath'd import that doesn't exist 618 | if len(self.results[binarypath]['load']['LC_RPATHs']): 619 | vulntype = "DylibHijack" 620 | # check all @rpath'd imports for the executable 621 | # ->if there is one that isn't found in a primary LC_RPATH, the executable is vulnerable :) 622 | for importedDylib in self.results[binarypath]['load']['LC_LOAD_DYLIBs']: 623 | importedDylib = str(importedDylib) 624 | # skip non-@rpath'd imports 625 | if not importedDylib.startswith('@rpath'): 626 | continue 627 | 628 | # chop off '@rpath' 629 | importedDylib = importedDylib.replace('@rpath', '') 630 | 631 | # send binary path, dylib, and vulnerablity type and process findings/output 632 | self.passiveDylibVulnProcessor(binarypath, importedDylib, vulntype) 633 | 634 | # STEP 2: check for vulnerable weak imports 635 | # can check all binary types... 636 | # check binary 637 | for weakDylib in self.results[binarypath]['load']['LC_LOAD_WEAK_DYLIBs']: 638 | weakDylib = str(weakDylib) 639 | vulntype = "WeakDylib" 640 | # got to resolve weak @rpath'd imports before checking if they exist 641 | if weakDylib.startswith('@rpath'): 642 | # skip @rpath imports if binary doesn't have any LC_RPATHS 643 | if not len(self.results[binarypath]['load']['LC_RPATHs']): 644 | continue 645 | 646 | # chop off '@rpath' 647 | weakDylib = weakDylib.replace('@rpath', '') 648 | 649 | # send binary path, dylib, and vulnerablity type and process findings/output 650 | self.passiveDylibVulnProcessor(binarypath, weakDylib, vulntype) 651 | 652 | # path doesn't need to be resolved 653 | # ->check/save those that don't exist 654 | elif not os.path.exists(weakDylib): 655 | readonlypartition, contextwriteperm = self.readWriteCheck(weakDylib) 656 | if self.results[binarypath]['filetype'] == self.MH_EXECUTE: 657 | certainty = 'High' 658 | indicator = '+' 659 | loadorder = 'unknown' 660 | self.vulnerabilityDictInput(vulntype, binarypath, weakDylib, loadorder, certainty, 661 | contextwriteperm, readonlypartition, indicator, mode) 662 | else: 663 | certainty = 'Potential' 664 | indicator = '+' 665 | loadorder = 'unknown' 666 | self.vulnerabilityDictInput(vulntype, binarypath, weakDylib, loadorder, certainty, 667 | contextwriteperm, readonlypartition, indicator, mode) 668 | continue 669 | if self.verbose: 670 | print("[+] Completed weakness identification") 671 | 672 | def processBinariesActive(self): 673 | def on_timeout(proc, status_dict): 674 | # Kill process on timeout and note as status_dict['timeout']=True 675 | status_dict['timeout'] = True 676 | print("[*] Forced time out") 677 | proc.kill() 678 | 679 | vulntype = "DylibHijack" 680 | mode = "Active" 681 | 682 | if self.verbose: 683 | print("[*] Actively identifying weaknesses") 684 | print("[ID] [FileType] [FileName] [Vulnerability] [Certainty] [Permission] Directory") 685 | for binarypath in self.results.keys(): 686 | if self.results[binarypath]["parse"] == "Complete": 687 | binary = self.results[binarypath]["filename"] 688 | if self.results[binarypath]['filetype'] == self.MH_EXECUTE: 689 | # Set up execution key 690 | self.results[binarypath]["execution"] = {'standardoutput': [], 'erroroutput': []} 691 | 692 | # Since this is active output to console instead of asking for verbose 693 | print("[*] Executing %s for 3 seconds" % binary) 694 | 695 | # Copy standard shell environment 696 | hijackenv = os.environ.copy() 697 | 698 | # Add DYLD_PRINT_RPATHS debugging mode to current Python shell environment 699 | hijackenv["DYLD_PRINT_RPATHS"] = "1" 700 | 701 | # Set timeout dictionary to false before execution 702 | status_dict = {'timeout': False} 703 | 704 | # Open executable 705 | proc = subprocess.Popen(binarypath, shell=False, env=hijackenv, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 706 | 707 | # Set thread timer to 3 seconds 708 | timer = threading.Timer(3, on_timeout, [proc, status_dict]) 709 | 710 | # Start thread timer to timeout after 3 seconds 711 | timer.start() 712 | 713 | # Set process to wait until it is forced to timeout 714 | proc.wait() 715 | 716 | # in case we didn't hit timeout 717 | timer.cancel() 718 | 719 | print("[*] Killed %s process" % binary) 720 | 721 | # Pull stdout and stderr from killed process 722 | result = proc.communicate() 723 | 724 | if result[0] != '': 725 | # Add standard out to binaries dictionary 726 | self.results[binarypath]["execution"]['standardoutput'].append(result[0]) 727 | if self.verbose: 728 | print("[*] %s had standard output when executed:" % binary) 729 | if len(result[0].split('\n')) > 1: 730 | standardoutput = result[0].split('\n') 731 | for line in standardoutput: 732 | print("\t%s" % line) 733 | else: 734 | print("\t" + result[0]) 735 | 736 | if result[1] != '': 737 | # Add standard error/debugging messages to binaries dictionary 738 | self.results[binarypath]["execution"]['erroroutput'].append(result[0]) 739 | 740 | # Split standard error output and look for RPATH failed expanding, then add to vulnerabilities dict 741 | for line in result[1].split('\n'): 742 | if 'RPATH failed expanding' in line: 743 | # Properly split failed load path and turn into an absolute path for reporting 744 | relativepath = line.split('to: ')[-1] 745 | hijackpath = os.path.abspath(relativepath) 746 | 747 | if self.verbose: 748 | print("\tOriginal failed rpath: %s" % relativepath) 749 | #print("\tAbsolute path: %s" % hijackpath) 750 | 751 | # Check if current user context has write permissions to the last existing path 752 | readonlypartition, contextwriteperm = self.readWriteCheck(hijackpath) 753 | 754 | if not readonlypartition or self.sipdisabled: 755 | certainty = 'Definite' 756 | indicator = '+' 757 | loadorder = 0 758 | self.vulnerabilityDictInput(vulntype, binarypath, hijackpath, loadorder, certainty, 759 | contextwriteperm, readonlypartition, indicator, mode) 760 | else: 761 | certainty = 'Definite' 762 | indicator = '-' 763 | loadorder = 0 764 | self.vulnerabilityDictInput(vulntype, binarypath, hijackpath, loadorder, certainty, 765 | contextwriteperm, readonlypartition, indicator, mode) 766 | if self.verbose: 767 | print("[*] Completed active weakness identification") 768 | 769 | def ProcessScriptBackdoors(self): 770 | mode = "Passive" 771 | if self.verbose: 772 | print("[*] Parsing backdoorable script list") 773 | vulntype = "BackdoorableScript" 774 | for filepath in self.results.keys(): 775 | # Check if current user context has write permissions 776 | readonlypartition, contextwriteperm = self.readWriteCheck(filepath) 777 | contextwriteperm = self.results[filepath]["writeable"] 778 | 779 | if self.results[filepath]["parse"] == "Script" and contextwriteperm: 780 | certainty = 'Potential' 781 | indicator = '+' 782 | loadorder = 0 783 | self.vulnerabilityDictInput(vulntype, filepath, filepath, loadorder, certainty, 784 | contextwriteperm, readonlypartition, indicator, mode) 785 | elif self.results[filepath]["parse"] == "Script" and self.verbose: 786 | certainty = 'Potential' 787 | indicator = '-' 788 | loadorder = 0 789 | self.vulnerabilityDictInput(vulntype, filepath, filepath, loadorder, certainty, 790 | contextwriteperm, readonlypartition, indicator, mode) 791 | if self.verbose: 792 | print("[*] Finished listing scripts") 793 | 794 | def ProcessInterestingFiles(self): 795 | if self.verbose: 796 | print("[*] Parsing interesting files list") 797 | for fullpath in self.results.keys(): 798 | file = self.results[fullpath]["filename"] 799 | if self.results[fullpath]["parse"] is False and self.verbose: 800 | if '.' in file: 801 | filetype = file.split('.')[-1] 802 | else: 803 | filetype = "Misc" 804 | print("[*] [%s] [%s] [InterestingFile] %s" % (filetype, file, fullpath)) 805 | if self.verbose: 806 | print("[*] Finished listing files") 807 | 808 | def GetResults(self): 809 | return self.results 810 | 811 | 812 | class CSVout: 813 | def __init__(self, filename, results, sipdisabled): 814 | if filename.lower().endswith('.csv'): 815 | self.filename = '.'.join(filename.split('.')[:-1]) 816 | else: 817 | self.filename = filename 818 | 819 | self.vulnfilename = self.filename + '-vulnerabilities.csv' 820 | self.interestingfilename = self.filename + '-interesting-files.csv' 821 | self.results = results 822 | self.sipdisabled = sipdisabled 823 | 824 | self.QUOTE = '"' 825 | self.sep = ',' 826 | 827 | def csvlinewrite(self, row): 828 | self.f.write(self.joinline(row) + '\n') 829 | 830 | def closecsv(self): 831 | self.f.close() 832 | self.f = None 833 | 834 | def quote(self, value): 835 | if not isinstance(value, str): 836 | value = str(value) 837 | return self.QUOTE + value + self.QUOTE 838 | 839 | def joinline(self, row): 840 | return self.sep.join([self.quote(value) for value in row]) 841 | 842 | def writevulns(self): 843 | self.f = open(self.vulnfilename, 'w') 844 | 845 | # Needed to sort vulnerabilities by Certainty and if they occur in a read-only partition; also prioritize scripts for Potential certainty 846 | sortbycertainty = ['Definite', 'High', 'Potential', 'Low'] 847 | if self.sipdisabled: 848 | sortbysip = [False] 849 | else: 850 | sortbysip = [False, True] 851 | scriptsort = ["Script", "Complete"] 852 | 853 | self.csvlinewrite(['Filename', 'Full path', 'File type', 'Discovery Mode', 'Vulnerability', 'Certainty', 'Read Only Partition (SIP)','Write permission', 'Hijack This Path', 'Dylib Load Order']) 854 | for sipcheck in sortbysip: 855 | for certain in sortbycertainty: 856 | for scriptcheck in scriptsort: 857 | for fullpath in self.results.keys(): 858 | file = self.results[fullpath]['filename'] 859 | if self.results[fullpath]['parse'] == 'Complete' or self.results[fullpath]['parse'] == 'Script': 860 | filetype = self.results[fullpath]['filetypeh'] 861 | if self.results[fullpath]['parse'] == 'Script': 862 | if self.results[fullpath]['parse'] != scriptcheck: 863 | continue 864 | scriptinfo = self.results[fullpath]['vulnerable']['BackdoorableScript'][0] 865 | vulnerability = "Backdoorable Script" 866 | certainty = scriptinfo["Certainty"] 867 | if certain != certainty: 868 | continue 869 | writeperms = self.results[fullpath]['writeable'] 870 | sip = scriptinfo["ReadOnlyPartition"] 871 | if not self.sipdisabled and sip != sipcheck: 872 | continue 873 | hijackpath = "See Full path" 874 | loadorder = "Unknown" 875 | mode = scriptinfo["Mode"] 876 | row = [file, fullpath, filetype, mode, vulnerability, certainty, sip, writeperms, hijackpath, loadorder] 877 | self.csvlinewrite(row) 878 | elif self.results[fullpath]['parse'] == 'Complete': 879 | if self.results[fullpath]['parse'] != scriptcheck: 880 | continue 881 | if len(self.results[fullpath]['vulnerable']['DylibHijack']) > 0: 882 | vulnerability = "Dylib Hijack" 883 | for hijackabledylib in self.results[fullpath]['vulnerable']['DylibHijack']: 884 | certainty = hijackabledylib["Certainty"] 885 | if certain != certainty: 886 | continue 887 | writeperms = hijackabledylib["WriteAccess"] 888 | sip = hijackabledylib["ReadOnlyPartition"] 889 | if not self.sipdisabled and sip != sipcheck: 890 | continue 891 | hijackpath = hijackabledylib["hijackPath"] 892 | loadorder = hijackabledylib["LoadOrder"] 893 | mode = hijackabledylib["Mode"] 894 | row = [file, fullpath, filetype, mode, vulnerability, certainty, sip, writeperms, hijackpath, loadorder] 895 | self.csvlinewrite(row) 896 | if len(self.results[fullpath]['vulnerable']['WeakDylib']) > 0: 897 | vulnerability = "Weak Dylib" 898 | for hijackabledylib in self.results[fullpath]['vulnerable']['WeakDylib']: 899 | certainty = hijackabledylib["Certainty"] 900 | if certain != certainty: 901 | continue 902 | writeperms = hijackabledylib["WriteAccess"] 903 | sip = hijackabledylib["ReadOnlyPartition"] 904 | if not self.sipdisabled and sip != sipcheck: 905 | continue 906 | hijackpath = hijackabledylib["hijackPath"] 907 | loadorder = hijackabledylib["LoadOrder"] 908 | mode = hijackabledylib["Mode"] 909 | row = [file, fullpath, filetype, mode, vulnerability, certainty, sip, writeperms, hijackpath, loadorder] 910 | self.csvlinewrite(row) 911 | 912 | self.closecsv() 913 | print('[%s] Created %s' % ('*', self.vulnfilename)) 914 | 915 | def writeinterestingfiles(self): 916 | # Example entry: 917 | # Filename: { 918 | # 'writeable': Bool, # indicates whether or not the current user that ran the tool has write permissions on the file 919 | # 'filetypeh': 'String', # indicates the file type as a string : Executable, Dylib, Bundle, KextBundle, Script, Misc (based on file extension) 920 | # 'parse': 'String', # indicates if the file was an executable and was parsed for weaknesses 921 | # 'fullpath': 'String', # full file path to file 922 | # } 923 | self.f = open(self.interestingfilename, 'w') 924 | self.csvlinewrite(['Filename', 'Full path', 'File type', 'Write permission']) 925 | for fullpath in self.results.keys(): 926 | if self.results[fullpath]['parse'] is False: 927 | file = self.results[fullpath]['filename'] 928 | if 'filetypeh' in self.results[fullpath].keys(): 929 | filetype = self.results[fullpath]['filetypeh'] 930 | else: 931 | filetype = "" 932 | writeperms = self.results[fullpath]['writeable'] 933 | row = [file, fullpath, filetype, writeperms] 934 | self.csvlinewrite(row) 935 | 936 | self.closecsv() 937 | print('[%s] Created %s' % ('*', self.interestingfilename)) 938 | 939 | # Class that utilizes system standard output and writes to a file. 940 | # ----------------------------------------------- 941 | class Logger(object): 942 | def __init__(self, filename="Default.log"): 943 | self.terminal = sys.stdout 944 | self.log = open(filename, "a") 945 | 946 | def write(self, message): 947 | self.terminal.write(message) 948 | self.log.write(message) 949 | 950 | 951 | # For standalone use of dyib-hijacker 952 | class Main(): 953 | def __init__(self): 954 | parser = argparse.ArgumentParser(description='Application Hijack Scanner for macOS') 955 | typeofsearch = parser.add_mutually_exclusive_group(required=True) 956 | typeofsearch.add_argument('-r', '--running', action='store_true', help='Check currently running processes') 957 | typeofsearch.add_argument('-i', '--installed', action='store_true', default=False, 958 | help='Check all installed applications') 959 | typeofsearch.add_argument('-p', '--application', default=False, 960 | help='Check a specific application i.e. /Application/Safari.app') 961 | 962 | output = parser.add_mutually_exclusive_group(required=False) 963 | output.add_argument('-oS', '--outputstandard', default=False, help='Outputs standard output to a .log file') 964 | output.add_argument('-oC', '--outputcsv', default=False, help='Outputs results to a .csv file') 965 | output.add_argument('-oA', '--outputall', default=False, help='Outputs results to a .csv file and standard log') 966 | 967 | parser.add_argument('-s', '--sipdisabled', default=False, action='store_true', 968 | help='Use if SIP is disabled on the system to search typically read-only paths') 969 | aggression = parser.add_mutually_exclusive_group(required=True) 970 | aggression.add_argument('-A', '--active', default=False, action='store_true', 971 | help='Executes main executable binaries with env var export DYLD_PRINT_RPATHS="1"') 972 | aggression.add_argument('-P', '--passive', default=False, action='store_true', 973 | help='Performs classic Dylib Hijack Scan techniques') 974 | aggression.add_argument('-b', '--bothchecks', default=False, action='store_true', 975 | help='Performs both active and passive checks') 976 | parser.add_argument('-v', '--verbose', default=False, action='store_true', 977 | help='Output in verbose mode while script runs') 978 | parser.add_argument('-d', '--debug', default=False, action='store_true', help=argparse.SUPPRESS) 979 | 980 | args = parser.parse_args() 981 | self.verbosity = args.verbose 982 | self.sipdisabled = args.sipdisabled 983 | 984 | self.running = args.running 985 | self.installed = args.installed 986 | self.application = args.application 987 | 988 | self.active = args.active 989 | self.passive = args.passive 990 | if args.bothchecks: 991 | self.active = True 992 | self.passive = True 993 | 994 | self.debug = args.debug 995 | 996 | self.outputcsv = args.outputcsv 997 | self.outputstandard = args.outputstandard 998 | 999 | if args.outputall: 1000 | self.outputcsv = args.outputall 1001 | self.outputstandard = args.outputall 1002 | 1003 | if self.outputstandard: 1004 | self.outputfile = ['log', self.outputstandard] 1005 | sys.stdout = Logger(self.outputfile[1] + '.log') 1006 | 1007 | 1008 | 1009 | def execute(self): 1010 | scanner = ExecutableScanner(self.verbosity, self.sipdisabled) 1011 | startTime = datetime.now() 1012 | 1013 | if self.running is True: 1014 | # get list of loaded binaries 1015 | scanner.loadedBinaries() 1016 | elif self.application: 1017 | # get list of executable files 1018 | scanner.installedBinaries(self.application) 1019 | elif self.installed: 1020 | # get list of executable files on the file-system 1021 | scanner.installedBinaries() 1022 | 1023 | scanner.parseExecutables() 1024 | 1025 | if self.passive: 1026 | scanner.processBinariesPassive() 1027 | if self.active: 1028 | scanner.processBinariesActive() 1029 | scanner.ProcessScriptBackdoors() 1030 | scanner.ProcessInterestingFiles() 1031 | 1032 | print("Scan completed in " + str(datetime.now() - startTime)) 1033 | 1034 | self.results = scanner.GetResults() 1035 | if self.outputcsv: 1036 | csvwrite = CSVout(self.outputcsv, self.results, self.sipdisabled) 1037 | csvwrite.writevulns() 1038 | csvwrite.writeinterestingfiles() 1039 | if self.outputstandard: 1040 | print('[%s] Created %s.log' % ('*', self.outputstandard)) 1041 | 1042 | def banner(self): 1043 | solid_pixel = unichr(0x2588) * 2 1044 | light_shade_pixel = unichr(0x2591) * 2 1045 | med_shade_pixel = unichr(0x2592) * 2 1046 | dark_shade_pixel = unichr(0x2593) * 2 1047 | blank_pixel = unichr(0x00A0) * 2 1048 | 1049 | sp = solid_pixel 1050 | bp = blank_pixel 1051 | lsp = light_shade_pixel 1052 | msp = med_shade_pixel 1053 | dsp = dark_shade_pixel 1054 | 1055 | canvas_dimensions = [19, 19] 1056 | 1057 | #build blank canvas 1058 | canvas = [[bp] * canvas_dimensions[0] for i in range(canvas_dimensions[1])] 1059 | 1060 | fill = [[1, range(4, 14)], 1061 | [2, [4] + range(14, 16)], 1062 | [3, range(5, 8) + [12, 13, 14, 16]], 1063 | [4, [6, 8, 12, 17]], 1064 | [5, [7, 12, 14, 17]], 1065 | [6, [2, 7, 17]], 1066 | [7, [1, 3, 8, 17] + range(12, 16)], 1067 | [8, [2, 4, 5, 8, 12] + range(15, 17)], 1068 | [9, [1, 3, 6, 7, 13]], 1069 | [10, [2, 14]], 1070 | [11, [3, 4, 14]], 1071 | [12, [5, 14]], 1072 | [13, [6, 7, 13]], 1073 | [14, [8, 9, 11, 12]], 1074 | [15, [8, 10, 11]], 1075 | [16, [7, 12]], 1076 | [17, range(7, 13)] 1077 | ] 1078 | dark = [[2, [5, 9, 10, 11]], 1079 | [6, [14, 15]], 1080 | [7, [9, 10]], 1081 | [9, [2]], 1082 | [10, [3]], 1083 | [11, [9, 10]], 1084 | [12, [8, 11]], 1085 | [13, [8, 12]], 1086 | [15, [9]], 1087 | [16, [8, 9, 10]] 1088 | ] 1089 | medium = [[2, [6, 7, 8, 12, 13]], 1090 | [3, [8, 9, 10, 11, 15]], 1091 | [4, [7, 9, 10, 11, 15]], 1092 | [5, [8, 9, 10, 11, 15]], 1093 | [6, [8, 9, 10, 11, 12, 13]], 1094 | [7, [2, 11]], 1095 | [8, [3, 9, 10, 11]], 1096 | [9, [4, 5, 8, 9, 10, 11, 12]], 1097 | [10, range(4, 14)], 1098 | [11, [5, 6, 7, 8, 11, 12, 13]], 1099 | [12, [6, 7, 9, 10, 12, 13]], 1100 | [13, [9, 10, 11]], 1101 | [14, [10]] 1102 | ] 1103 | light = [[4, [16]], 1104 | [5, [16]], 1105 | [6, [16]], 1106 | [7, [16]], 1107 | [16, [11]] 1108 | ] 1109 | coloring = [fill, dark, medium, light] 1110 | 1111 | # build pixel art 1112 | for indx, type in enumerate(coloring): 1113 | color = bp 1114 | if indx == 0: 1115 | color = sp 1116 | elif indx == 1: 1117 | color = dsp 1118 | elif indx == 2: 1119 | color = msp 1120 | elif indx == 3: 1121 | color = lsp 1122 | for coords in type: 1123 | y = coords[0] 1124 | for x in coords[1]: 1125 | canvas[x][y] = color 1126 | 1127 | 1128 | # add signature and tool name 1129 | center = len(canvas) / 2 1130 | toolname = u"boko.py" 1131 | tooldescription = u"Application Hijack Scanner for macOS" 1132 | signature = u"Jesse Nebling (@bashexplode)" 1133 | canvas[canvas_dimensions[0] - 1][center] += toolname 1134 | canvas[canvas_dimensions[0] - 1][center + 1] += tooldescription 1135 | canvas[canvas_dimensions[0] - 1][center + 3] += signature 1136 | 1137 | # print canvas 1138 | for y in range(len(canvas)): 1139 | for x in range(len(canvas[y])): 1140 | print(canvas[x][y].encode('utf-8'), end='') 1141 | print() 1142 | 1143 | 1144 | if __name__ == "__main__": 1145 | try: 1146 | standalone = True 1147 | scan = Main() 1148 | scan.banner() 1149 | scan.execute() 1150 | except KeyboardInterrupt: 1151 | print("You killed it.") 1152 | sys.exit() 1153 | --------------------------------------------------------------------------------