Ejemplo n.º 1
0
    def parse(self, filepath: str, extended_processing: bool = True):
        """
        :param filepath: path of the APK file
        :param extended_processing: (optional) whether should parse all information or only a summary. True by default.
        :return: the parsed APK file
        :raise: ApkParsingError if cannot parse the file as an APK
        """
        self.logger.debug("Parsing APK file: filepath=\"%s\"", filepath)
        if not self.looks_like_apk(filepath):
            raise ApkParsingError

        file = self.file_parser.parse(filepath)
        cert = None
        manifest = None
        dex_files = []
        other_files = []

        with ZipFile(filepath) as apk:
            tmpdir = self.__create_temporary_directory(ApkParser.__TEMPORARY_DIR)
            for filename in apk.namelist():
                entry_filepath = apk.extract(filename, tmpdir)
                self.logger.debug("Extracting APK resource %s to %s", filename, entry_filepath)
                try:
                    if AndroidManifestParser.looks_like_manifest(filename):
                        self.logger.debug("%s looks like an AndroidManifest.xml file", filename)
                        manifest = self.manifest_parser.parse(entry_filepath, True, filepath, True)
                    elif CertParser.looks_like_cert(filename):
                        self.logger.debug("%s looks like a CERT file", filename)
                        cert = self.__parse_cert(entry_filepath, filename, extended_processing)
                    elif DexParser.looks_like_dex(filename):
                        self.logger.debug("%s looks like a dex file", filename)
                        dex = self.__parse_dex(entry_filepath, filename, extended_processing)
                        dex_files.append(dex)
                    else:
                        self.logger.debug("%s looks like a generic file", filename)
                        entry = self.__parse_file(entry_filepath, filename, extended_processing)
                        if entry is not None:
                            other_files.append(entry)
                except (AndroidManifestParsingError, CertParsingError, FileParsingError) as error:
                    self.__remove_directory(tmpdir)
                    raise ApkParsingError from error
            self.__remove_directory(tmpdir)

        if manifest is None or cert is None or not dex_files:
            raise ApkParsingError

        return APK(
            filename=file.get_file_name(),
            size=file.get_size(),
            md5hash=file.get_md5(),
            sha1hash=file.get_sha1(),
            sha256hash=file.get_sha256(),
            sha512hash=file.get_sha512(),
            app_name=Aapt.get_app_name(filepath),
            cert=cert,
            manifest=manifest,
            dex_files=dex_files,
            other_files=other_files
        )
Ejemplo n.º 2
0
 def __init__(self, logger: Logger = default_logger):
     self.logger = logger
     self.file_parser = FileParser(logger)
     self.manifest_parser = AndroidManifestParser(logger)
     self.cert_parser = CertParser(logger)
     self.dex_parser = DexParser(logger)
    def test_looks_like_manifest(self, filename, expected):
        result = AndroidManifestParser.looks_like_manifest(filename)

        self.assertEqual(expected, result)
Ejemplo n.º 4
0
class ApkParser:
    """
    Parser implementation for Android APK packages.
    """

    __TEMPORARY_DIR = ".ninjadroid"

    def __init__(self, logger: Logger = default_logger):
        self.logger = logger
        self.file_parser = FileParser(logger)
        self.manifest_parser = AndroidManifestParser(logger)
        self.cert_parser = CertParser(logger)
        self.dex_parser = DexParser(logger)

    def parse(self, filepath: str, extended_processing: bool = True):
        """
        :param filepath: path of the APK file
        :param extended_processing: (optional) whether should parse all information or only a summary. True by default.
        :return: the parsed APK file
        :raise: ApkParsingError if cannot parse the file as an APK
        """
        self.logger.debug("Parsing APK file: filepath=\"%s\"", filepath)
        if not self.looks_like_apk(filepath):
            raise ApkParsingError

        file = self.file_parser.parse(filepath)
        cert = None
        manifest = None
        dex_files = []
        other_files = []

        with ZipFile(filepath) as apk:
            tmpdir = self.__create_temporary_directory(ApkParser.__TEMPORARY_DIR)
            for filename in apk.namelist():
                entry_filepath = apk.extract(filename, tmpdir)
                self.logger.debug("Extracting APK resource %s to %s", filename, entry_filepath)
                try:
                    if AndroidManifestParser.looks_like_manifest(filename):
                        self.logger.debug("%s looks like an AndroidManifest.xml file", filename)
                        manifest = self.manifest_parser.parse(entry_filepath, True, filepath, True)
                    elif CertParser.looks_like_cert(filename):
                        self.logger.debug("%s looks like a CERT file", filename)
                        cert = self.__parse_cert(entry_filepath, filename, extended_processing)
                    elif DexParser.looks_like_dex(filename):
                        self.logger.debug("%s looks like a dex file", filename)
                        dex = self.__parse_dex(entry_filepath, filename, extended_processing)
                        dex_files.append(dex)
                    else:
                        self.logger.debug("%s looks like a generic file", filename)
                        entry = self.__parse_file(entry_filepath, filename, extended_processing)
                        if entry is not None:
                            other_files.append(entry)
                except (AndroidManifestParsingError, CertParsingError, FileParsingError) as error:
                    self.__remove_directory(tmpdir)
                    raise ApkParsingError from error
            self.__remove_directory(tmpdir)

        if manifest is None or cert is None or not dex_files:
            raise ApkParsingError

        return APK(
            filename=file.get_file_name(),
            size=file.get_size(),
            md5hash=file.get_md5(),
            sha1hash=file.get_sha1(),
            sha256hash=file.get_sha256(),
            sha512hash=file.get_sha512(),
            app_name=Aapt.get_app_name(filepath),
            cert=cert,
            manifest=manifest,
            dex_files=dex_files,
            other_files=other_files
        )

    def __parse_cert(self, filepath: str, filename: str, extended_processing: bool) -> Union[Cert,File]:
        if extended_processing:
            return self.cert_parser.parse(filepath, filename)
        return self.file_parser.parse(filepath, filename)

    def __parse_dex(self, filepath: str, filename: str, extended_processing: bool) -> Union[Dex,File]:
        if extended_processing:
            return self.dex_parser.parse(filepath, filename)
        return self.file_parser.parse(filepath, filename)

    def __parse_file(self, filepath: str, filename: str, extended_processing: bool) -> Optional[File]:
        if extended_processing and not FileParser.is_directory(filepath):
            try:
                return self.file_parser.parse(filepath, filename)
            except FileParsingError:
                self.logger.error("Could not parse file '%s'!", filename)
        return None

    @staticmethod
    def __create_temporary_directory(path: str) -> str:
        return mkdtemp(path)

    @staticmethod
    def __remove_directory(path: str):
        try:
            rmtree(path)
        except OSError:
            pass

    @staticmethod
    def looks_like_apk(filename: str) -> bool:
        return FileParser.is_zip_file(filename)
class TestAndroidManifestParser(unittest.TestCase):
    """
    Test AndroidManifest parser.
    """

    sut = AndroidManifestParser()

    @staticmethod
    def any_axmlprinter() -> Mock:
        axmlprinter = Mock()
        axmlprinter.get_buff.return_value = "any-axml-raw-value"
        return axmlprinter

    @staticmethod
    def any_aapt_apk_info(package_name: str, version_code: int,
                          version_name: str, sdk_min: str, sdk_target: str,
                          sdk_max: str) -> Dict:
        return {
            "package_name": package_name,
            "version": {
                "code": version_code,
                "name": version_name
            },
            "sdk": {
                "min": sdk_min,
                "target": sdk_target,
                "max": sdk_max
            }
        }

    @staticmethod
    def any_aapt_manifest_info(activities: List[str], services: List[str],
                               receivers: str) -> Dict:
        return {
            "activities": activities,
            "services": services,
            "receivers": receivers
        }

    @staticmethod
    def any_axmlprinter_xml(package_name: str, version_code: str,
                            version_name: str, sdk_min: str, sdk_target: str,
                            sdk_max: str, permissions: List[str]) -> Mock:
        xml = Mock()
        xml.documentElement.getAttribute.side_effect = [
            package_name, version_code, version_name
        ]
        sdk_xml = Mock()
        sdk_xml.hasAttribute.return_value = True
        sdk_xml.getAttribute.side_effect = [sdk_min, sdk_target, sdk_max]
        permissions_xml = Mock()
        permissions_xml.getAttribute.side_effect = permissions
        xml.documentElement.getElementsByTagName.side_effect = [[
            sdk_xml
        ], [permissions_xml, permissions_xml, permissions_xml]]
        return xml

    @staticmethod
    def any_axmlprinter_xml_with_extended_processing(
            package_name: str, version_code: str, version_name: str,
            sdk_min: str, sdk_target: str, sdk_max: str,
            permissions: List[str], activities: List[str], services: List[str],
            receivers: List[str]) -> Mock:
        xml = Mock()
        xml.documentElement.getAttribute.side_effect = [
            package_name, version_code, version_name
        ]
        sdk_xml = Mock()
        sdk_xml.hasAttribute.return_value = True
        sdk_xml.getAttribute.side_effect = [sdk_min, sdk_target, sdk_max]
        permissions_xml = Mock()
        permissions_xml.getAttribute.side_effect = permissions
        application_xml = Mock()
        activity_xml = Mock()
        activity_xml.getAttribute.side_effect = activities
        activity_xml.hasAttribute.return_value = False
        activity_xml.getElementsByTagName.return_value = []
        service_xml = Mock()
        service_xml.getAttribute.side_effect = services
        service_xml.hasAttribute.return_value = False
        service_xml.getElementsByTagName.return_value = []
        receiver_xml = Mock()
        receiver_xml.getAttribute.side_effect = receivers
        receiver_xml.hasAttribute.return_value = False
        receiver_xml.getElementsByTagName.return_value = []
        application_xml.getElementsByTagName.side_effect = [[activity_xml],
                                                            [service_xml],
                                                            [receiver_xml]]
        xml.documentElement.getElementsByTagName.side_effect = [[
            application_xml
        ], [sdk_xml], [permissions_xml, permissions_xml, permissions_xml]]
        return xml

    def assert_manifest_equal(self, manifest: AndroidManifest,
                              package_name: str, version: AppVersion,
                              sdk: AppSdk, permissions: List[str],
                              activities: List[AppActivity],
                              services: List[AppService],
                              receivers: List[AppBroadcastReceiver]):
        self.assertEqual(package_name, manifest.get_package_name())
        self.assertEqual(version, manifest.get_version())
        self.assertEqual(sdk, manifest.get_sdk())
        self.assertEqual(permissions, manifest.get_permissions())
        self.assertEqual(activities, manifest.get_activities())
        self.assertEqual(services, manifest.get_services())
        self.assertEqual(receivers, manifest.get_broadcast_receivers())

    @patch('ninjadroid.parsers.manifest.minidom')
    @patch('ninjadroid.parsers.manifest.FileParser')
    @patch("builtins.open", new_callable=mock_open)
    def test_parse(self, mock_file, mock_file_parser, mock_minidom):
        file = any_file(filename="AndroidManifest.xml")
        mock_parser_instance = any_file_parser(file=file)
        mock_file_parser.return_value = mock_parser_instance
        mock_minidom.parse.return_value = self.any_axmlprinter_xml(
            package_name="any-package-name",
            version_code="1",
            version_name="any-version-name",
            sdk_max="20",
            sdk_min="10",
            sdk_target="15",
            permissions=[
                "any-permission-1", "any-permission-2", "any-permission-0"
            ])

        manifest = self.sut.parse(filepath="any-file-path",
                                  binary=False,
                                  apk_path="any_apk_path",
                                  extended_processing=False)

        assert_file_parser_called_once_with(mock_parser_instance,
                                            filepath="any-file-path",
                                            filename="AndroidManifest.xml")
        mock_file.assert_called_with("any-file-path", "rb")
        mock_minidom.parse.assert_called_with("any-file-path")
        assert_file_equal(self, expected=file, actual=manifest)
        self.assert_manifest_equal(manifest=manifest,
                                   package_name="any-package-name",
                                   version=AppVersion(code=1,
                                                      name="any-version-name"),
                                   sdk=AppSdk(min_version="10",
                                              target_version="15",
                                              max_version="20"),
                                   permissions=[
                                       "any-permission-0", "any-permission-1",
                                       "any-permission-2"
                                   ],
                                   activities=[],
                                   services=[],
                                   receivers=[])

    @patch('ninjadroid.parsers.manifest.minidom')
    @patch('ninjadroid.parsers.manifest.FileParser')
    @patch("builtins.open", new_callable=mock_open)
    def test_parse_with_extended_processing(self, mock_file, mock_file_parser,
                                            mock_minidom):
        file = any_file(filename="AndroidManifest.xml")
        mock_parser_instance = any_file_parser(file=file)
        mock_file_parser.return_value = mock_parser_instance
        mock_minidom.parse.return_value = self.any_axmlprinter_xml_with_extended_processing(
            package_name="any-package-name",
            version_code="1",
            version_name="any-version-name",
            sdk_max="20",
            sdk_min="10",
            sdk_target="15",
            permissions=[
                "any-permission-1", "any-permission-2", "any-permission-0"
            ],
            activities=["any-activity-name"],
            services=["any-service-name"],
            receivers=["any-broadcast-receiver-name"])

        manifest = self.sut.parse(filepath="any-file-path",
                                  binary=False,
                                  apk_path="any_apk_path",
                                  extended_processing=True)

        assert_file_parser_called_once_with(mock_parser_instance,
                                            filepath="any-file-path",
                                            filename="AndroidManifest.xml")
        mock_file.assert_called_with("any-file-path", "rb")
        mock_minidom.parse.assert_called_with("any-file-path")
        assert_file_equal(self, expected=file, actual=manifest)
        self.assert_manifest_equal(
            manifest=manifest,
            package_name="any-package-name",
            version=AppVersion(code=1, name="any-version-name"),
            sdk=AppSdk(min_version="10", target_version="15",
                       max_version="20"),
            permissions=[
                "any-permission-0", "any-permission-1", "any-permission-2"
            ],
            activities=[AppActivity(name="any-activity-name")],
            services=[AppService(name="any-service-name")],
            receivers=[
                AppBroadcastReceiver(name="any-broadcast-receiver-name")
            ])

    @patch('ninjadroid.parsers.manifest.minidom')
    @patch('ninjadroid.parsers.manifest.FileParser')
    @patch("builtins.open", new_callable=mock_open)
    def test_parse_with_invalid_version_code(self, mock_file, mock_file_parser,
                                             mock_minidom):
        file = any_file(filename="AndroidManifest.xml")
        mock_parser_instance = any_file_parser(file=file)
        mock_file_parser.return_value = mock_parser_instance
        mock_minidom.parse.return_value = self.any_axmlprinter_xml(
            package_name="any-package-name",
            version_code="A",
            version_name="any-version-name",
            sdk_max="20",
            sdk_min="10",
            sdk_target="15",
            permissions=[
                "any-permission-1", "any-permission-2", "any-permission-0"
            ])

        manifest = self.sut.parse(filepath="any-file-path",
                                  binary=False,
                                  apk_path="any_apk_path",
                                  extended_processing=False)

        assert_file_parser_called_once_with(mock_parser_instance,
                                            filepath="any-file-path",
                                            filename="AndroidManifest.xml")
        mock_file.assert_called_with("any-file-path", "rb")
        mock_minidom.parse.assert_called_with("any-file-path")
        self.assert_manifest_equal(
            manifest=manifest,
            package_name="any-package-name",
            # NOTE: no version code is returned
            version=AppVersion(code=None, name="any-version-name"),
            sdk=AppSdk(min_version="10", target_version="15",
                       max_version="20"),
            permissions=[
                "any-permission-0", "any-permission-1", "any-permission-2"
            ],
            activities=[],
            services=[],
            receivers=[])

    @patch('ninjadroid.parsers.manifest.minidom')
    @patch('ninjadroid.parsers.manifest.AXMLPrinter')
    @patch('ninjadroid.parsers.manifest.FileParser')
    @patch("builtins.open", new_callable=mock_open)
    def test_parse_binary(self, mock_file, mock_file_parser, mock_axmlprinter,
                          mock_minidom):
        file = any_file(filename="AndroidManifest.xml")
        mock_parser_instance = any_file_parser(file=file)
        mock_file_parser.return_value = mock_parser_instance
        mock_axmlprinter.return_value = self.any_axmlprinter()
        mock_minidom.parseString.return_value = self.any_axmlprinter_xml(
            package_name="any-package-name",
            version_code="1",
            version_name="any-version-name",
            sdk_max="20",
            sdk_min="10",
            sdk_target="15",
            permissions=[
                "any-permission-1", "any-permission-2", "any-permission-0"
            ])

        manifest = self.sut.parse(filepath="any-file-path",
                                  binary=True,
                                  apk_path=None,
                                  extended_processing=False)

        assert_file_parser_called_once_with(mock_parser_instance,
                                            filepath="any-file-path",
                                            filename="AndroidManifest.xml")
        mock_file.assert_called_with("any-file-path", "rb")
        mock_axmlprinter.assert_called_with(ANY)
        mock_minidom.parseString.assert_called_with("any-axml-raw-value")
        assert_file_equal(self, expected=file, actual=manifest)
        self.assert_manifest_equal(manifest=manifest,
                                   package_name="any-package-name",
                                   version=AppVersion(code=1,
                                                      name="any-version-name"),
                                   sdk=AppSdk(min_version="10",
                                              target_version="15",
                                              max_version="20"),
                                   permissions=[
                                       "any-permission-0", "any-permission-1",
                                       "any-permission-2"
                                   ],
                                   activities=[],
                                   services=[],
                                   receivers=[])

    @patch('ninjadroid.parsers.manifest.minidom')
    @patch('ninjadroid.parsers.manifest.AXMLPrinter')
    @patch('ninjadroid.parsers.manifest.FileParser')
    @patch("builtins.open", new_callable=mock_open)
    def test_parse_binary_when_axmlprinter_fails(self, mock_file,
                                                 mock_file_parser,
                                                 mock_axmlprinter,
                                                 mock_minidom):
        file = any_file(filename="AndroidManifest.xml")
        mock_parser_instance = any_file_parser(file=file)
        mock_file_parser.return_value = mock_parser_instance
        mock_axmlprinter.side_effect = IOError()

        with self.assertRaises(AndroidManifestParsingError):
            self.sut.parse(filepath="any-file-path",
                           binary=True,
                           apk_path=None,
                           extended_processing=False)

    @patch('ninjadroid.parsers.manifest.minidom')
    @patch('ninjadroid.parsers.manifest.FileParser')
    @patch("builtins.open", new_callable=mock_open)
    def test_parse_when_minidom_fails_without_apk_path(self, mock_file,
                                                       mock_file_parser,
                                                       mock_minidom):
        file = any_file(filename="AndroidManifest.xml")
        mock_parser_instance = any_file_parser(file=file)
        mock_file_parser.return_value = mock_parser_instance
        mock_minidom.parse.side_effect = ExpatError()

        with self.assertRaises(AndroidManifestParsingError):
            self.sut.parse(filepath="any-file-path",
                           binary=False,
                           apk_path=None,
                           extended_processing=False)

    @patch('ninjadroid.parsers.manifest.Aapt')
    @patch('ninjadroid.parsers.manifest.minidom')
    @patch('ninjadroid.parsers.manifest.FileParser')
    @patch("builtins.open", new_callable=mock_open)
    def test_parse_when_minidom_fails_with_apk_path(self, mock_file,
                                                    mock_file_parser,
                                                    mock_minidom, mock_aapt):
        file = any_file(filename="AndroidManifest.xml")
        mock_parser_instance = any_file_parser(file=file)
        mock_file_parser.return_value = mock_parser_instance
        mock_minidom.parse.side_effect = ExpatError()
        mock_aapt.get_apk_info.return_value = self.any_aapt_apk_info(
            package_name="any-package-name",
            version_code=1,
            version_name="any-version-name",
            sdk_max="20",
            sdk_min="10",
            sdk_target="15")
        mock_aapt.get_app_permissions.return_value = [
            "any-permission-0", "any-permission-1", "any-permission-2"
        ]

        manifest = self.sut.parse(filepath="any-file-path",
                                  binary=False,
                                  apk_path="any_apk_path",
                                  extended_processing=False)

        assert_file_parser_called_once_with(mock_parser_instance,
                                            filepath="any-file-path",
                                            filename="AndroidManifest.xml")
        mock_file.assert_called_with("any-file-path", "rb")
        mock_minidom.parse.assert_called_with("any-file-path")
        mock_aapt.get_apk_info.assert_called_with("any_apk_path")
        mock_aapt.get_app_permissions.assert_called_with("any_apk_path")
        assert_file_equal(self, expected=file, actual=manifest)
        self.assert_manifest_equal(manifest=manifest,
                                   package_name="any-package-name",
                                   version=AppVersion(code=1,
                                                      name="any-version-name"),
                                   sdk=AppSdk(min_version="10",
                                              target_version="15",
                                              max_version="20"),
                                   permissions=[
                                       "any-permission-0", "any-permission-1",
                                       "any-permission-2"
                                   ],
                                   activities=[],
                                   services=[],
                                   receivers=[])

    @patch('ninjadroid.parsers.manifest.Aapt')
    @patch('ninjadroid.parsers.manifest.minidom')
    @patch('ninjadroid.parsers.manifest.FileParser')
    @patch("builtins.open", new_callable=mock_open)
    def test_parse_when_minidom_fails_with_apk_path_and_extended_processing(
            self, mock_file, mock_file_parser, mock_minidom, mock_aapt):
        file = any_file(filename="AndroidManifest.xml")
        mock_parser_instance = any_file_parser(file=file)
        mock_file_parser.return_value = mock_parser_instance
        mock_minidom.parse.side_effect = ExpatError()
        mock_aapt.get_apk_info.return_value = self.any_aapt_apk_info(
            package_name="any-package-name",
            version_code=1,
            version_name="any-version-name",
            sdk_max="20",
            sdk_min="10",
            sdk_target="15")
        mock_aapt.get_app_permissions.return_value = [
            "any-permission-0", "any-permission-1", "any-permission-2"
        ]
        mock_aapt.get_manifest_info.return_value = self.any_aapt_manifest_info(
            activities=["any-activity-name"],
            services=["any-service-name"],
            receivers=["any-broadcast-receiver-name"])

        manifest = self.sut.parse(filepath="any-file-path",
                                  binary=False,
                                  apk_path="any_apk_path",
                                  extended_processing=True)

        assert_file_parser_called_once_with(mock_parser_instance,
                                            filepath="any-file-path",
                                            filename="AndroidManifest.xml")
        mock_file.assert_called_with("any-file-path", "rb")
        mock_minidom.parse.assert_called_with("any-file-path")
        mock_aapt.get_apk_info.assert_called_with("any_apk_path")
        mock_aapt.get_app_permissions.assert_called_with("any_apk_path")
        mock_aapt.get_manifest_info.assert_called_with("any_apk_path")
        assert_file_equal(self, expected=file, actual=manifest)
        self.assert_manifest_equal(
            manifest=manifest,
            package_name="any-package-name",
            version=AppVersion(code=1, name="any-version-name"),
            sdk=AppSdk(min_version="10", target_version="15",
                       max_version="20"),
            permissions=[
                "any-permission-0", "any-permission-1", "any-permission-2"
            ],
            activities=[AppActivity(name="any-activity-name")],
            services=[AppService(name="any-service-name")],
            receivers=[
                AppBroadcastReceiver(name="any-broadcast-receiver-name")
            ])

    @parameterized.expand([["AndroidManifest.xml", True],
                           ["AndroidManifest", False], ["Whatever.xml", False],
                           ["META-INF/CERT.RSA", False],
                           ["classes.dex", False], ["Example.apk", False]])
    def test_looks_like_manifest(self, filename, expected):
        result = AndroidManifestParser.looks_like_manifest(filename)

        self.assertEqual(expected, result)