Exemplo n.º 1
0
 def setUp(self):
     if MAS_APP:
         self.mas_app = Bundle.make(MAS_APP)
     else:
         self.mas_app = None
     # The Calculator.app app has been part of macOS for as long as I can think.
     # There is no risk this app is going anywhere.
     self.system_app = Bundle.make("/Applications/Calculator.app")
Exemplo n.º 2
0
def folder_for_app(results_dir : str, app : Bundle) -> str:
    """Return the output directory for a certain application
    inside the user-specified results directory.

    Note that this function _does not_ touch the filesystem.
    Therefore, no directories are created as part of this functionality"""
    app_bundle_id = app.bundle_identifier()
    if app_bundle_id is '' or app_bundle_id is None:
        app_bundle_id = 'b.UNKNOWN'

    app_version = app.version()
    if app_version is '' or app_version is None:
        app_version = 'v.UNKNOWN'

    return os.path.join(results_dir, app_bundle_id, app_version)
Exemplo n.º 3
0
def all_apps(at: str = "/Applications",
             mas_only: bool = False,
             sandboxed_only: bool = False):
    """
    Returns all apps from a target folder

    :param at: The base folder where to search for applications
    :param mas_only: Whether to only consider applications from the Mac App Store
    :param sandboxed_only: Whether to only return sandboxed applications
    :return: Filepaths to applications fulfilling the criteria specified
    """
    all_entries = [
        os.path.join(at, x) for x in os.listdir(at) if x.endswith(".app")
    ]

    for entry in all_entries:
        try:
            app_bundle = Bundle.make(entry)
            if mas_only and not app_bundle.is_mas_app():
                continue
            if sandboxed_only and not app_bundle.is_sandboxed():
                continue
            yield entry
        except InvalidBundle:
            continue
Exemplo n.º 4
0
def container_for_app(app):
    """
    Returns the container directory used by the application or None if the container does not exist.

    :param app: The app for which to find the container directory. Note that valid arguments are both
                a filepath to the application and a bundle for that application
    :return: Filepath to the container or None, if the lookup failed.
    """
    # Handle code that already has a bundle for an app
    if isinstance(app, Bundle):
        app_bundle = app
    elif isinstance(app, str):
        try:
            app_bundle = Bundle.make(app)
        except InvalidBundle:
            return None

    bid = app_bundle.bundle_identifier(normalized=True)

    # Verify the container exists.
    container_path = os.path.join(os.path.expanduser("~/Library/Containers/"),
                                  bid)
    if not os.path.exists(container_path):
        return None

    # Also verify that the metadata file is present, else the container is invalid and of
    # no use to other code
    container_metadata = os.path.join(container_path, "Container.plist")
    if not os.path.exists(container_metadata):
        return None

    return container_path
Exemplo n.º 5
0
    def run(
        self,
        apps_dir: str,
        out_dir: str,
        select: Selection = Selection.ALL,
    ) -> None:
        if not os.path.exists(apps_dir):
            print(f"Directory does not exist: {apps_dir}", file=sys.stderr)
            exit(1)

        exit_watcher = SignalIntelligence()

        self.logger.info(f"{self.name} starting")

        for app_dir in Driver.iterate_applications(apps_dir):
            if exit_watcher.should_exit:
                break

            app = Bundle.make(app_dir)

            if select == Selection.MAS and not app.is_mas_app():
                continue

            app_out_dir = folder_for_app(out_dir, app)

            os.makedirs(app_out_dir, exist_ok=True)

            print(f"[    ] Analysing {app.filepath}")
            reset_cursor = "\r\033[1A["
            result = self.analyse(app, app_out_dir)
            print(reset_cursor + result.colored)

        self.logger.info(f"{self.name} stopping")
Exemplo n.º 6
0
    def test_sub_frameworks(self):
        app = Bundle.make("/Applications/iTunes.app")
        sub_frameworks = app.sub_frameworks()

        self.assertEqual(1, len(sub_frameworks))
        self.assertEqual("/Applications/iTunes.app/Contents/Frameworks/iPodUpdater.framework",
                         sub_frameworks[0].filepath)
Exemplo n.º 7
0
    def extract_data(self, app: Bundle, result_path: str) -> bool:
        # Metadata is stored in a dictionary, which is later serialised to the disk.
        dependencies_metadata = dict()

        if not isinstance(app, Application):
            self.log_error("Supplied app {} is not an application".format(
                app.filepath))
            return False

        app_base = os.path.realpath(app.filepath) + "/"

        executable = app.executable()

        app_dependencies = executable.application_libraries(
        ) if executable else []
        for dependency in app_dependencies:
            dependency_bundle = Bundle.from_binary(dependency)
            # Relative path component from the underlying app to the dependency.
            dependency_rel = fs.path_remove_prefix(dependency, app_base)

            dependency_infos = None

            if dependency_bundle:
                # Use Info.plist from bundle
                dependency_infos = dependency_bundle.info_dictionary()
            else:
                # Try to instead use embedded information
                if lief.is_macho(dependency):
                    dependency_infos = common.extract_embedded_info(
                        lief_extensions.macho_parse_quick(dependency))

            if dependency_infos:
                # Record entry along with further information
                dependencies_metadata[
                    dependency_rel] = DependenciesExtractor._extract_dependency_infos(
                        dependency_infos)
            else:
                # Just record the path to the dependency
                dependencies_metadata[dependency_rel] = {}

        # Store the information to the filesystem
        with open(os.path.join(result_path, "dependencies.json"),
                  "w") as outfile:
            json.dump(dependencies_metadata, outfile, indent=4)

        return True
Exemplo n.º 8
0
    def extract_data(self, app: Bundle, result_path: str) -> bool:
        executable_path = app.executable_path()
        if not os.path.exists(executable_path):
            self.log_error("Executable for {} {} could not be found.".format(
                app.bundle_type, app.filepath))
            return False

        fs.copy(executable_path, os.path.join(result_path, "executable.bin"))
        return True
Exemplo n.º 9
0
    def extract_data(self, app: Bundle, result_path: str) -> bool:
        info_path = app.info_dictionary_path()
        if not os.path.exists(info_path):
            self.log_error("Info.plist for {} {} could not be found.".format(
                app.bundle_type, app.filepath))
            return False

        fs.copy(info_path, os.path.join(result_path, "Info.plist"))
        return True
Exemplo n.º 10
0
    def _check_capabilities_internal_static(cls, app_bundle : Bundle,
                                            entitlement_keys : List[str]):
        """Checks an applications capabilities solely statically"""
        app_entitlements = app_bundle.entitlements()
        # Check whether there is a entitlement allowing the capability
        for key in entitlement_keys:
            if app_entitlements.get(key, False):
                return True

        return False
Exemplo n.º 11
0
    def extract_data(self, app: Bundle, result_path: str) -> bool:
        app_bundle_id = app.bundle_identifier()
        if not app.is_mas_app():
            self.log_info(
                'Application {} is not from the Mac App Store. Will not query iTunes for metadata.'
                .format(app_bundle_id))
            return True

        metadata = itunes_api.lookup_metadata(bundleId=app_bundle_id)
        if not metadata:
            self.log_info('Metadata could not be extracted for {}'.format(
                app.filepath))
            return True

        outpath = os.path.join(result_path, 'itunes_metadata.json')
        with open(outpath, "w") as outfile:
            json.dump(metadata, outfile, indent=4)

        return True
Exemplo n.º 12
0
    def test_sub_bundles(self):
        app = Bundle.make("/Applications/iTunes.app")
        sub_bundle_paths = [x.filepath for x in app.sub_bundles()]

        required_paths = [
            "/Applications/iTunes.app/Contents/XPCServices/VisualizerService.xpc",
            "/Applications/iTunes.app/Contents/PlugIns/iTunesStorageExtension.appex",
            "/Applications/iTunes.app/Contents/MacOS/iTunesHelper.app"
        ]

        for path in required_paths:
            self.assertIn(path, sub_bundle_paths)
Exemplo n.º 13
0
def _entitlements_can_be_parsed(app_bundle: Bundle) -> bool:
    """
    Check whether an application's entitlements can be parsed by libsecinit.
    We only check part of the process, namely the parsing of entitlements via xpc_create_from_plist.

    :param app_bundle: Bundle for which to check whether the entitlements can be parsed
    :type app_bundle: Bundle

    :return: True, iff the entitlements of the main executable can be parsed, else false.
    """
    # No entitlements, no problem
    # If the app contains no entitlements, entitlement validation cannot fail.
    if not app_bundle.has_entitlements():
        return True

    exe_path = app_bundle.executable_path()
    raw_entitlements = Binary.get_entitlements(exe_path, raw=True)

    # Call the local xpc_vuln_checker program that does the actual checking.
    exit_code, _ = tool_named("xpc_vuln_checker")(input=raw_entitlements)

    return exit_code != 1
Exemplo n.º 14
0
 def extract_data(self, app: Bundle, result_path: str) -> bool:
     iap_path = app.path_for_resource(
         "Contents/Resources/InternetAccessPolicy.plist")
     if not iap_path:
         self.log_info("IAP for application {} does not exist.".format(
             app.filepath))
         # This method _always_ returns True, because IAPs are optional and almost always
         # absent from applications
         return True
     else:
         self.log_info("IAP for application {} does exist.".format(
             app.filepath))
         fs.copy(iap_path, os.path.join(result_path, "iap.plist"))
         return True
Exemplo n.º 15
0
def sandbox_status(app_bundle: Bundle,
                   logger: logging.Logger) -> Optional[int]:
    process = subprocess.Popen([app_bundle.executable_path()],
                               stdout=subprocess.DEVNULL,
                               stderr=subprocess.DEVNULL)

    # Sandbox initialisation should be almost instant. If the application is still
    # running after a couple of seconds, the sandbox failed to initialise or is
    # not enabled at all.
    # We use 10 seconds as an arbitrary cutoff time.

    time.sleep(10)

    pid = str(process.pid)

    if process.poll() is not None:
        logger.error("Process terminated early: {}".format(
            app_bundle.executable_path()))
        return None

    sb_status = subprocess.run([tool_named("sandbox_status"), pid],
                               stdout=subprocess.PIPE,
                               stderr=subprocess.DEVNULL)

    process.kill()

    rx = re.compile(r'^Sandbox status for PID {} is (\d+)$'.format(pid))
    m = rx.match(sb_status.stdout.decode().strip())
    if m:
        return int(m.group(1))

    logger.error(
        "`sandbox_status` did not return a status for executable at {}. Skipping."
        .format(app_bundle.executable_path()))

    return None
Exemplo n.º 16
0
    def check_app(cls, app_bundle : Bundle):
        try:
            static_sandbox = app_bundle.is_sandboxed()
            dynamic_sandbox = app_utils.init_sandbox(app_bundle, logger)
            sandbox_status = app_utils.sandbox_status(app_bundle, logger)
            if not static_sandbox and (dynamic_sandbox or sandbox_status == 1):
                logger.info("App Sandbox active though not enabled for app {}".format(app_bundle.filepath))

            return (True, {
                'static': static_sandbox,
                'dynamic': dynamic_sandbox,
                'status': sandbox_status,
            })
        except:
            return False, dict()
Exemplo n.º 17
0
def install_app(app_path: str, logger, output: str):
    """Install an app from the `app_path` into the `output` directory."""

    app = Bundle.make(app_path)
    output_folder = os.path.join(folder_for_app(output, app), os.path.basename(app_path))

    try:
        shutil.copytree(app_path, output_folder, symlinks=True)
    except FileExistsError:
        logger.error("Application already exists: {}. Skipping.".format(output_folder))
        print('\r[' + colored('skip', 'yellow'))
        return
    except e:
        logger.error("Could not install application: {}".format(output_folder))
        print('\r[' + colored('err ', 'red'))
        return

    logger.info("Installed application: {}".format(output_folder))
    print('\r[' + colored(' ok ', 'green'))
Exemplo n.º 18
0
    def test_bundle_from_binary(self):
        # For single-file utility, this function should return None, because the file is not
        # part of any bundle
        self.assertIsNone(Bundle.from_binary("/bin/ls"))
        self.assertIsNone(Bundle.from_binary("/bin/ln"))

        # For binaries that are part of a bundle but not the main executable in such a bundle,
        # the function is also supposed to return None

        # Note: iTunes is used here, because it comes pre-installed on macOS
        # and it has the most complicated dependencies of any installed app, thus
        # it can be used for many different purposes
        self.assertIsNone(Bundle.from_binary("/Applications/iTunes.app/Contents/MacOS/iTunesASUHelper"))

        # For non-existent binaries, the function should throw a ValueError
        with self.assertRaises(ValueError):
            # This binary surely does not exist
            Bundle.from_binary("/bin/useful_test03837246")

        # If the main executable is supplied, the corresponding bundle should be found
        self.assertEqual(
            Bundle.from_binary(
                "/Applications/iTunes.app/Contents/MacOS/iTunes"
            ).filepath,
            "/Applications/iTunes.app")

        # This should also work for other types of bundles
        self.assertEqual(
            Bundle.from_binary(
                "/Applications/iTunes.app/Contents/Frameworks/iPodUpdater.framework/Versions/A/iPodUpdater"
            ).filepath,
            "/Applications/iTunes.app/Contents/Frameworks/iPodUpdater.framework"
        )
        self.assertEqual(
            Bundle.from_binary(
                "/Applications/iTunes.app/Contents/PlugIns/iTunesStorageExtension.appex/Contents/MacOS/iTunesStorageExtension"
            ).filepath,
            "/Applications/iTunes.app/Contents/PlugIns/iTunesStorageExtension.appex"
        )
Exemplo n.º 19
0
    def extract_data(self, app: Bundle, result_path: str) -> bool:
        from extractors.executable import ExecutableExtractor
        from extractors.info import InfoExtractor

        # Find all XPC bundles that are inside the overall application bundle
        for sub_bundle in app.sub_bundles():
            if not sub_bundle.bundle_type == BundleType.XPC_EXTENSION:
                continue

            # Create a new folder for the XPC bundle found previously.
            if not sub_bundle.has_bundle_identifier():
                self.log_error(
                    "{} found in {} @ {} does not have a bundle id.".format(
                        sub_bundle.bundle_type, app.bundle_type, app.filepath))
                # Abort on error
                return False

            # Create sub-directory for XPC bundles found.
            bundle_id = sub_bundle.bundle_identifier()
            xpc_result_path = os.path.join(result_path, bundle_id)
            os.mkdir(xpc_result_path)

            # Use ExecutableExtractor and InfoExtractor on the XPC bundle
            success = ExecutableExtractor().extract_data(
                sub_bundle, xpc_result_path)
            success &= InfoExtractor().extract_data(sub_bundle,
                                                    xpc_result_path)

            # Abort immediately on failure. Overall extraction only works if every plugins could be extracted
            if not success:
                self.log_error(
                    "Data extraction failed for xpc plugin extraction from {} @ {}. Aborting."
                    .format(app.bundle_type, app.filepath))

                # Cleanup: Delete directory
                shutil.rmtree(xpc_result_path)

                # Propagate errors
                return False

        return True
Exemplo n.º 20
0
def process_app(app_path, info_extractors, logger, output, source_hint: str=None):
    """Process an app using the supplied `info_extractors`

    Log potentially relevant information to `logger` and return results
    at `output`. If supplied, the `source_hint` will be written to
    `output/source`
    """

    app = Bundle.make(app_path)

    output_folder = folder_for_app(output, app)
    if os.path.exists(output_folder):
        logger.info(
            "Skipping processing of {} @ {}, because the app has already been processed.".format(app, app.filepath)
        )
        return

    # Make basefolder
    os.makedirs(output_folder)

    try:
        # The source_hint can for example be used to store an application's identifier for
        # the third-party platform where the app was downloaded from (i.e macupdate)
        if source_hint is not None:
            with open(os.path.join(output_folder, 'source'), 'w') as source_file:
                source_file.write(source_hint)

        extraction_status = functools.reduce(
            lambda status, extractor: status & run_extractor(extractor, app, output_folder),
            info_extractors,
            True
        )
        if not extraction_status:
            logger.info("Processing failed for {} @ {}".format(app, app.filepath))
    except:
        logger.error(
            "Exception occurred during processing of {} @ {}".format(app, app.filepath)
        )
Exemplo n.º 21
0
def analyse_app(app_path, produce_text = False):
    """
    Analyses an app to provide answers to the questions
    posed in the comment at the beginning of this file.
    Returns a dictionary containing the responses.
    """
    result = dict()

    try:
        app_bundle = Bundle.make(app_path)
    except:
        print(termcolor.colored("App processing failed. Make sure the supplied application is valid.", COLOR_NEGATIVE))
        return

    # Run all analysers
    analysers = AbstractAppChecker.__subclasses__()

    # Check if sandbox enabled
    success, key, local_result = run_analyser(AppSandboxStatusChecker, app_bundle, produce_text)
    if not success:
        return result

    result[key] = local_result
    # If the sandbox is not enabled, all other checks are mood.
    if not local_result['dynamic']:
        return result

    for analyser in analysers:
        if analyser == AppSandboxStatusChecker:
            continue

        success, key, local_result = run_analyser(analyser, app_bundle, produce_text)
        if not success:
            return result

        result[key] = local_result

    return result
Exemplo n.º 22
0
    def test_make_bundle(self):
        # Check common applications
        self.assertEqual(Bundle.make("/Applications/Calculator.app").bundle_type,
                         BundleType.APPLICATION)
        self.assertEqual(Bundle.make("/Applications/Safari.app").bundle_type,
                         BundleType.APPLICATION)

        # Check framework
        self.assertEqual(Bundle.make("/System/Library/Frameworks/Accelerate.framework").bundle_type,
                         BundleType.FRAMEWORK)

        # KEXTs, even though actual proper support is not implemented yet.
        self.assertEqual(Bundle.make("/System/Library/Extensions/AppleHWSensor.kext").bundle_type,
                         BundleType.KEXT)

        # Check failure cases
        with self.assertRaises(InvalidBundle):
            Bundle.make("/System/Library/Frameworks/Kernel.framework")
Exemplo n.º 23
0
 def setUp(self):
     self.framework = Bundle.make(
         "/System/Library/Frameworks/DVDPlayback.framework")
Exemplo n.º 24
0
 def test_normalize_path(self):
     self.assertEqual(Bundle.normalize_path("/Applications/Xcode.app/"), "/Applications/Xcode.app")
     self.assertEqual(Bundle.normalize_path("/Applications/Xcode.app"), "/Applications/Xcode.app")
     self.assertEqual(Bundle.normalize_path("/Applications/Xcode.app/Test"), "/Applications/Xcode.app")
Exemplo n.º 25
0
 def test_get_sandbox_rules(self):
     bundle = Bundle.make("/Applications/Calculator.app")
     self.assertIsNotNone(app_utils.get_sandbox_rules(bundle))
Exemplo n.º 26
0
 def setUp(self):
     # The Calculator.app app has been part of macOS for as long as I can think.
     # There is no risk this app is going anywhere.
     self.app = Bundle.make("/Applications/Calculator.app")
Exemplo n.º 27
0
def main():
    logger.info("appxtractor starting")

    parser = argparse.ArgumentParser(description='Extract information from Mac Apps.')
    parser.add_argument('-i', '--input', required=True,
                        help='The directory that contains the applications to analyse.')
    parser.add_argument('-t', '--type', 
                        default='app_folder', const='app_folder', 
                        nargs='?', choices=['app_folder', 'archive_folder'],
                        help='''Process input folder as folder containing .app bundles
                                or as folder full of archives containing .app bundles.
                                Supported archives formats are zip, tar, gz and dmg. 
                                Default type: app_folder''')
    parser.add_argument('-o', '--output', required=True,
                        help='Output directory: This directory shall also be passed to this program to update an existing output folder.')
    parser.add_argument('--all-apps', dest='all_apps', default=False, action='store_true',
                        help='Analyse all apps. By default, only Mac AppStore apps are analysed.')
    parser.add_argument('--install-only', default=False, action='store_true',
                        help='''Install archived applications into the output directory.
                                This option only works with archive folders.''')

    args = parser.parse_args()

    if args.type != 'archive_folder' and args.install_only:
        print("Option '--install-only' is only supported for archive folders.", file=sys.stderr)
        exit(1)

    exit_watcher = SignalIntelligence()

    if args.install_only:
        print("[+] Installing apps from \"{}\" to \"{}\"".format(args.input, args.output))
        print("[+] Press Ctrl+C to cancel installation\n")
    else:
        print("[+] Analysing apps at \"{}\"".format(args.input))
        print("[+] Press Ctrl+C to cancel analysis (can later be resumed)\n")

    if args.type == 'app_folder':
        app_candidates = iterate_apps_folder(args.input)
    elif args.type == 'archive_folder':
        app_candidates = iterate_archived_apps_folder(args.input)
    else:
        assert False and 'Iteration type not supported.'

    for path, hint in app_candidates:
        if exit_watcher.should_exit:
            break

        if not Bundle.is_bundle(path):
            continue

        bundle = Bundle.make(path)
        if not bundle.is_mas_app() and not args.all_apps and not args.install_only:
            continue

        if args.install_only:
            print('[    ] Installing {}'.format(path), end='')
            install_app(app_path=path,
                        logger=logger,
                        output=args.output)
        else:
            print('[+] Processing {}'.format(path))
            process_app(app_path=path,
                        info_extractors=info_extractors,
                        logger=logger,
                        output=args.output,
                        source_hint=hint)

    logger.info("appxtractor stopping")
Exemplo n.º 28
0
def init_sandbox(app_bundle: Bundle,
                 logger: logging.Logger,
                 force_initialisation: bool = False) -> bool:
    """
    Initialises the sandbox for a particular app bundle.

    :param app_bundle: The App for which to initialise the App Sandbox
    :param logger: Logger object used to record failure cases
    :param force_initialisation: Whether to overwrite / start initialisation even if metadata
           exists that indicates the sandbox has already been initialised
    :return: Boolean value indicating whether the sandbox was successfully initialised
             (or was already initialised)
    """
    # Guarding against a few applications that ship with entitlements libsecinit cannot parse.
    if not _entitlements_can_be_parsed(app_bundle):
        return False

    # Super useful environment variable used by libsecinit. If this variable is set, the application
    # is terminated after its sandbox is initialised.
    init_sandbox_environ = {
        **os.environ, 'APP_SANDBOX_EXIT_AFTER_INIT': str(1)
    }

    app_container = container_for_app(app_bundle)
    if app_container is not None and not force_initialisation:
        if logger:
            logger.info(
                "Container directory already existed. Skipping sandbox initialisation."
            )
        return True

    if logger:
        logger.info("Starting process {} to initialize sandbox.".format(
            app_bundle.executable_path()))
    process = subprocess.Popen([app_bundle.executable_path()],
                               stdout=subprocess.DEVNULL,
                               stderr=subprocess.DEVNULL,
                               env=init_sandbox_environ)

    # Sandbox initialisation should be almost instant. If the application is still
    # running after a couple of seconds, the sandbox failed to initialise.
    # We use 10 seconds as an arbitrary cutoff time.
    try:
        process.wait(10)
    except subprocess.TimeoutExpired:
        process.kill()
        if logger:
            logger.error(
                "Sandbox was not initialised successfully for executable at {}. Skipping."
                .format(app_bundle.executable_path()))
        return False

    # Check that there now is an appropriate container
    if container_for_app(app_bundle) is None:
        if logger:
            logger.info("Sandbox initialisation for executable {} succeeded \
                but no appropriate container metadata was created.".format(
                app_bundle.executable_path()))
        return False

    return True
Exemplo n.º 29
0
 def test_is_bundle(self):
     self.assertTrue(Bundle.is_bundle("/Applications/Calculator.app"))
     self.assertTrue(Bundle.is_bundle("/System/Library/Frameworks/WebKit.framework"))