예제 #1
0
    def parse(self, filepath: str, filename: str = "") -> Cert:
        """
        :param filepath: path of the CERT file
        :param filename: name of the CERT file
        :return: the parsed CERT file
        :raise: FileParsingError if cannot parse the file
        :raise: CertParsingError if cannot parse the file as a CERT
        """
        self.logger.debug(
            "Parsing CERT file: filepath=\"%s\", filename=\"%s\"", filepath,
            filename)
        file = FileParser(self.logger).parse(filepath, filename)
        raw = self.parse_cert(filepath)

        return Cert(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(),
                    serial_number=self.__parse_string(
                        raw, pattern=r"^Serial number: (.*)$"),
                    validity=self.parse_validity(raw),
                    fingerprint=self.parse_fingerprint(raw),
                    owner=self.parse_participant(raw,
                                                 pattern=r"^Owner: (.*)$"),
                    issuer=self.parse_participant(raw,
                                                  pattern=r"^Issuer: (.*)$"))
예제 #2
0
 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
예제 #3
0
 def parse(
         self,
         filepath: str,
         binary: bool = False,
         apk_path: Optional[str] = None,
         extended_processing: bool = True
 ):
     """
     :param filepath: path of the AndroidManifest.xml file
     :param binary: (optional) whether the AndroidManifest.xml file is in binary format or not. False by default.
     :param apk_path: (optional) path of the APK package containing this AndroidManifest.xml file. None by default.
     :param extended_processing: (optional) whether should parse all information or only a summary. True by default.
     :return: the parsed AndroidManifest.xml file
     :raise: FileParsingError if cannot parse the file
     :raise: AndroidManifestParsingError if cannot parse the file as an AndroidManifest.xml
     """
     self.logger.debug("Parsing AndroidManifest.xml file: filepath=\"%s\"", filepath)
     file = FileParser(self.logger).parse(filepath, "AndroidManifest.xml")
     try:
         self.logger.debug("Parsing AndroidManifest.xml from DOM...")
         dom = self.parse_manifest_dom(filepath, binary)
     except AndroidManifestParsingError as error:
         self.logger.debug("Cannot parse AndroidManifest.xml from DOM!")
         if apk_path is None or apk_path == "":
             self.logger.debug("Cannot parse AndroidManifest.xml from APK!")
             raise error
         self.logger.debug("Parsing AndroidManifest.xml from APK: apk_path=%s", apk_path)
         return self.build_manifest_from_apk(file, extended_processing, apk_path)
     return self.build_manifest_from_dom(file, extended_processing, dom)
예제 #4
0
    def parse(self, filepath: str, filename: str) -> Dex:
        """
        :param filepath: path of the dex file
        :param filename: name of the dex file
        :return: the parsed dex file
        :raise: FileParsingError if cannot parse the file
        """
        self.logger.debug("Parsing dex file: filepath=\"%s\", filename=\"%s\"",
                          filepath, filename)
        file = FileParser(self.logger).parse(filepath, filename)

        self.logger.debug("Extracting strings...")
        strings = self.parse_strings(filepath)
        self.logger.debug("Strings extracted: %d", len(strings))

        self.logger.debug("Extracting URLs...")
        urls = self.parse_signatures(signature=UriSignature(),
                                     strings=strings,
                                     min_string_len=6)
        self.logger.debug("URLs extracted: %s ", len(urls))

        self.logger.debug("Extracting shell commands...")
        shell_commands = self.parse_signatures(signature=ShellSignature(),
                                               strings=strings)
        self.logger.debug("Shell commands extracted: %s", len(shell_commands))

        # TODO: improve custom signatures parsing performance (commented in the meanwhile because far too slow)
        # self.logger.debug("Extracting custom signatures...")
        custom_signatures = [
        ]  # self.extract_signatures(signature=Signature(), strings=self._strings)
        # self.logger.debug("Custom signatures extracted: %s", len(custom_signatures))

        return Dex(
            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(),
            strings=strings,
            urls=urls,
            shell_commands=shell_commands,
            custom_signatures=custom_signatures,
        )
예제 #5
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)
예제 #6
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)
예제 #7
0
 def looks_like_apk(filename: str) -> bool:
     return FileParser.is_zip_file(filename)
예제 #8
0
class TestFileParser(unittest.TestCase):
    """
    Test File parser.
    """

    ANY_FILE_SIZE = 70058
    ANY_FILE_MD5 = "c9504f487c8b51412ba4980bfe3cc15d"
    ANY_FILE_SHA1 = "482a28812495b996a92191fbb3be1376193ca59b"
    ANY_FILE_SHA256 = "8773441a656b60c5e18481fd5ba9c1bf350d98789b975987cb3b2b57ee44ee51"
    ANY_FILE_SHA512 = "559eab9840ff2f8507842605e60bb0730442ddf9ee7ca4ab4f386f715c1a4707766065d6f0b977816886692bf88b400643979e2fd13e6999358a21cabdfb3071"

    sut = FileParser()

    @patch('ninjadroid.parsers.file.sha512')
    @patch('ninjadroid.parsers.file.sha256')
    @patch('ninjadroid.parsers.file.sha1')
    @patch('ninjadroid.parsers.file.md5')
    @patch('ninjadroid.parsers.file.getsize')
    @patch('ninjadroid.parsers.file.access')
    @patch('ninjadroid.parsers.file.isfile')
    @patch("builtins.open", new_callable=mock_open)
    def test_parse(
            self,
            mock_file,
            mock_isfile,
            mock_access,
            mock_getsize,
            mock_md5,
            mock_sha1,
            mock_sha256,
            mock_sha512
    ):
        mock_isfile.return_value = True
        mock_access.return_value = True
        mock_getsize.return_value = self.ANY_FILE_SIZE
        mock_md5.return_value.hexdigest.return_value = self.ANY_FILE_MD5
        mock_sha1.return_value.hexdigest.return_value = self.ANY_FILE_SHA1
        mock_sha256.return_value.hexdigest.return_value = self.ANY_FILE_SHA256
        mock_sha512.return_value.hexdigest.return_value = self.ANY_FILE_SHA512

        file = self.sut.parse("any-file-path", "any-file-name")

        mock_file.assert_called_with("any-file-path", "rb")
        assert_file_equal(
            self,
            expected=any_file(
                filename="any-file-name",
                size=self.ANY_FILE_SIZE,
                md5=self.ANY_FILE_MD5,
                sha1=self.ANY_FILE_SHA1,
                sha256=self.ANY_FILE_SHA256,
                sha512=self.ANY_FILE_SHA512
            ),
            actual=file
        )

    @patch('ninjadroid.parsers.file.sha512')
    @patch('ninjadroid.parsers.file.sha256')
    @patch('ninjadroid.parsers.file.sha1')
    @patch('ninjadroid.parsers.file.md5')
    @patch('ninjadroid.parsers.file.getsize')
    @patch('ninjadroid.parsers.file.access')
    @patch('ninjadroid.parsers.file.isfile')
    @patch("builtins.open", new_callable=mock_open)
    def test_parse_without_filename(
            self,
            mock_file,
            mock_isfile,
            mock_access,
            mock_getsize,
            mock_md5,
            mock_sha1,
            mock_sha256,
            mock_sha512
    ):
        mock_isfile.return_value = True
        mock_access.return_value = True
        mock_getsize.return_value = self.ANY_FILE_SIZE
        mock_md5.return_value.hexdigest.return_value = self.ANY_FILE_MD5
        mock_sha1.return_value.hexdigest.return_value = self.ANY_FILE_SHA1
        mock_sha256.return_value.hexdigest.return_value = self.ANY_FILE_SHA256
        mock_sha512.return_value.hexdigest.return_value = self.ANY_FILE_SHA512

        file = self.sut.parse("any-file-path")

        mock_file.assert_called_with("any-file-path", "rb")
        assert_file_equal(
            self,
            expected=any_file(
                filename="any-file-path",
                size=self.ANY_FILE_SIZE,
                md5=self.ANY_FILE_MD5,
                sha1=self.ANY_FILE_SHA1,
                sha256=self.ANY_FILE_SHA256,
                sha512=self.ANY_FILE_SHA512
            ),
            actual=file
        )

    @patch('ninjadroid.parsers.file.access')
    @patch('ninjadroid.parsers.file.isfile')
    @patch("builtins.open", new_callable=mock_open)
    def test_parse_fails_when_open_fails(self, mock_file, mock_isfile, mock_access):
        mock_isfile.return_value = True
        mock_access.return_value = True
        mock_file.side_effect = OSError()

        with self.assertRaises(OSError):
            self.sut.parse("any-file-path")

    @patch('ninjadroid.parsers.file.access')
    @patch('ninjadroid.parsers.file.isfile')
    def test_parse_fails_with_non_existing_file(self, mock_isfile, mock_access):
        mock_isfile.return_value = False
        mock_access.return_value = True

        with self.assertRaises(FileParsingError):
            self.sut.parse("any-file-path")

    @patch('ninjadroid.parsers.file.access')
    @patch('ninjadroid.parsers.file.isfile')
    def test_parse_fails_with_non_readable_file(self, mock_isfile, mock_access):
        mock_isfile.return_value = True
        mock_access.return_value = False

        with self.assertRaises(FileParsingError):
            self.sut.parse("any-file-path")

    @parameterized.expand([
        [True],
        [False]
    ])
    @patch('ninjadroid.parsers.file.isfile')
    def test_is_file(self, expected, mock_isfile):
        mock_isfile.return_value = expected

        result = self.sut.is_file("any-path")

        self.assertEqual(expected, result)

    def test_is_file_with_empty_path(self):
        result = self.sut.is_file("")

        self.assertFalse(result)

    @parameterized.expand([
        [True, True, True],
        [True, False, False],
        [False, True, False],
        [False, False, False]
    ])
    @patch('ninjadroid.parsers.file.access')
    @patch('ninjadroid.parsers.file.isfile')
    def test_is_readable_file(self, is_file, is_readable, expected, mock_isfile, mock_access):
        mock_isfile.return_value = is_file
        mock_access.return_value = is_readable

        result = self.sut.is_readable_file("any-path")

        self.assertEqual(expected, result)

    def test_is_readable_file_with_empty_path(self):
        result = self.sut.is_readable_file("")

        self.assertFalse(result)

    @parameterized.expand([
        [True, True, True],
        [True, False, False],
        [False, True, False],
        [False, False, False]
    ])
    @patch('ninjadroid.parsers.file.is_zipfile')
    @patch('ninjadroid.parsers.file.isfile')
    def test_is_zip_file(self, is_file, is_zip, expected, mock_isfile, mock_is_zipfile):
        mock_isfile.return_value = is_file
        mock_is_zipfile.return_value = is_zip

        result = self.sut.is_zip_file("any-path")

        self.assertEqual(expected, result)

    def test_is_zip_file_with_empty_path(self):
        result = self.sut.is_zip_file("")

        self.assertFalse(result)

    @parameterized.expand([
        [True],
        [False]
    ])
    @patch('ninjadroid.parsers.file.isdir')
    def test_is_directory(self, expected, mock_isdir):
        mock_isdir.return_value = expected

        result = self.sut.is_directory("any-path")

        self.assertEqual(expected, result)

    def test_is_directory_with_empty_path(self):
        result = self.sut.is_directory("")

        self.assertFalse(result)