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: (.*)$"))
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
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)
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, )
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)
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)
def looks_like_apk(filename: str) -> bool: return FileParser.is_zip_file(filename)
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)