diff --git a/android_class_shadowing_scanner/__init__.py b/android_class_shadowing_scanner/__init__.py index 6f30b99..bb90c44 100644 --- a/android_class_shadowing_scanner/__init__.py +++ b/android_class_shadowing_scanner/__init__.py @@ -6,12 +6,11 @@ from pathlib import Path from getpass import getpass from .androzoo import download_apk +from .data import ApkData from .analysis import analyze def main(): - import pprint - parser = ArgumentParser( prog="Android Class Shadowing Scanner", description="Scan application for patern that could be reveling of class shadowing obfuscation", @@ -21,6 +20,11 @@ def main(): "--sha256", help="The sha256 hash of the APK to download", type=str ) apk_parser.add_argument("--apk", help="The APK to use", type=Path) + apk_parser.add_argument( + "--sha256-list", + help="A file containing a list of application sha256s (one by line)", + type=Path, + ) key_parser = parser.add_mutually_exclusive_group(required=False) key_parser.add_argument( "--api-key-file", @@ -30,6 +34,12 @@ def main(): key_parser.add_argument( "--api-key", help="The Androzoo API key (Usage NOT recommanded)", type=str ) + parser.add_argument( + "--output-dir", + help="The directory where to output results, when no set results are printed to stdout", + type=Path, + ) + SECRET_STORAGE_IMPORTED = False try: import secretstorage @@ -45,6 +55,11 @@ def main(): pass args = parser.parse_args() + if args.output_dir: + if not args.output_dir.is_dir(): + raise RuntimeError("--output-dir must be a directory") + args.output_dir.mkdir(parents=True, exist_ok=True) + if args.apk: with args.apk.open("rb") as file: with zipfile.ZipFile(file) as apk: @@ -55,6 +70,10 @@ def main(): sha256s = [] if args.sha256: sha256s.append(args.sha256) + if args.sha256_list: + with args.sha256_list.open("r") as file: + for line in file: + sha256s.append(line.strip()) api_key = "" if args.api_key: @@ -79,6 +98,12 @@ def main(): api_key = getpass(prompt="Androzoo API key: ").strip() for sha256 in sha256s: + if args.output_dir and (args.output_dir / sha256).exists(): + continue with zipfile.ZipFile(io.BytesIO(download_apk(sha256, api_key))) as apk: entry = analyze(apk, sha256) - pprint.pprint(entry) + if not args.output_dir: + print(entry.to_string()) + else: + with (args.output_dir / sha256).open("w") as file: + file.write(entry) diff --git a/android_class_shadowing_scanner/analysis.py b/android_class_shadowing_scanner/analysis.py index 5b9d1da..7286b7b 100644 --- a/android_class_shadowing_scanner/analysis.py +++ b/android_class_shadowing_scanner/analysis.py @@ -18,6 +18,7 @@ from .platform_classes import ( PLATFORM_34_CLASSES, SDK_34_CLASSES, ) +from .data import ApkData # Remove Androguard logs logger.remove() @@ -58,27 +59,6 @@ androguard.core.dex.HiddenApiClassDataItem.RestrictionApiFlag = ( ) -@dataclass -class ApkData: - sha256: str - nb_duplicate_classes: int - nb_platform_32_classes: int - nb_platform_non_sdk_32_classes: int - nb_sdk_32_classes: int - nb_platform_33_classes: int - nb_platform_non_sdk_33_classes: int - nb_sdk_33_classes: int - nb_platform_34_classes: int - nb_platform_non_sdk_34_classes: int - nb_sdk_34_classes: int - has_classes0_dex: bool - has_classes1_dex: bool - has_classes0X_dex: bool - has_classes_dex_over_10: bool - has_non_numeric_classes_dex: bool - has_non_consecutive_classes_dex: bool - - @dataclass class PlatformClassesData: nb_duplicate_classes: int diff --git a/android_class_shadowing_scanner/data.py b/android_class_shadowing_scanner/data.py new file mode 100644 index 0000000..781e780 --- /dev/null +++ b/android_class_shadowing_scanner/data.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass, astuple, fields +from typing import Self + + +@dataclass +class ApkData: + sha256: str + nb_duplicate_classes: int + nb_platform_32_classes: int + nb_platform_non_sdk_32_classes: int + nb_sdk_32_classes: int + nb_platform_33_classes: int + nb_platform_non_sdk_33_classes: int + nb_sdk_33_classes: int + nb_platform_34_classes: int + nb_platform_non_sdk_34_classes: int + nb_sdk_34_classes: int + has_classes0_dex: bool + has_classes1_dex: bool + has_classes0X_dex: bool + has_classes_dex_over_10: bool + has_non_numeric_classes_dex: bool + has_non_consecutive_classes_dex: bool + + def to_string(self) -> str: + return "|".join(map(str, astuple(self))) + + @staticmethod + def from_string(val: str) -> "ApkData": + return ApkData( + *(map(lambda f_v: f_v[1] == "True" if f_v[0].type is bool else f_v[0].type(f_v[1]), zip(fields(ApkData), val.strip().split("|")))) # type: ignore + )