diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3322d58e..e3e32258 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -26,7 +26,7 @@ Before opening this issue, I tried the following steps: * [ ] Installed the tool in a way [described in the readme](https://github.com/ClaudiuGeorgiu/Obfuscapk#-installation) and ran `python3 -m obfuscapk.cli --help` without any errors -* [ ] Ran the tool using only `Rebuild`, `NewSignature` and `NewAlignment` obfuscators to verify that the app is not using anti-repackaging techniques +* [ ] Ran the tool using: `python3 -m obfuscapk.cli -o Rebuild -o NewSignature -o NewAlignment ` to verify that the app is not using anti-repackaging techniques * [ ] Ran the tool using `--ignore-libs` flag to exclude third party libraries from the obfuscation diff --git a/src/obfuscapk/cli.py b/src/obfuscapk/cli.py index 8f971c4e..fba456a1 100644 --- a/src/obfuscapk/cli.py +++ b/src/obfuscapk/cli.py @@ -74,7 +74,7 @@ def get_cmd_args(args: list = None): parser.add_argument( "-k", "--virus-total-key", - action="append", + type=str, metavar="VT_API_KEY", help="When using Virus Total obfuscator, a valid API key has to be provided. " "Can be specified multiple times to use a different API key for each request " @@ -121,7 +121,7 @@ def main(): -o ResStringEncryption -o ArithmeticBranch -o FieldRename -o Nop -o Goto \ -o ClassRename -o Reflection -o AdvancedReflection -o Reorder -o RandomManifest \ -o Rebuild -o NewSignature -o NewAlignment \ - -o VirusTotal -k virus_total_key_1 -k virus_total_key_2 \ + -o VirusTotal -k virus_total_key \ /path/to/original.apk """ @@ -142,9 +142,7 @@ def main(): arguments.destination = arguments.destination.strip(" '\"") if arguments.virus_total_key: - arguments.virus_total_key = [ - key.strip(" '\"") for key in arguments.virus_total_key - ] + arguments.virus_total_key = arguments.virus_total_key.strip(" '\"") if arguments.keystore_file: arguments.keystore_file = arguments.keystore_file.strip(" '\"") diff --git a/src/obfuscapk/main.py b/src/obfuscapk/main.py index 3eadbf8e..ece1cf98 100644 --- a/src/obfuscapk/main.py +++ b/src/obfuscapk/main.py @@ -49,7 +49,7 @@ def perform_obfuscation( obfuscated_apk_path: str = None, ignore_libs: bool = False, interactive: bool = False, - virus_total_api_key: List[str] = None, + virus_total_api_key: str = None, keystore_file: str = None, keystore_password: str = None, key_alias: str = None, @@ -71,7 +71,7 @@ def perform_obfuscation( :param ignore_libs: If True, exclude known third party libraries from the obfuscation operations. :param interactive: If True, show a progress bar with the obfuscation progress. - :param virus_total_api_key: A list containing Virus Total API keys, needed only + :param virus_total_api_key: A string containing Virus Total API keys, needed only when using Virus Total obfuscator. :param keystore_file: The path to a custom keystore file to be used for signing the resulting obfuscated application. If not provided, a default diff --git a/src/obfuscapk/obfuscation.py b/src/obfuscapk/obfuscation.py index 1ba82a59..b4e91ad1 100644 --- a/src/obfuscapk/obfuscation.py +++ b/src/obfuscapk/obfuscation.py @@ -24,7 +24,7 @@ def __init__( obfuscated_apk_path: str = None, ignore_libs: bool = False, interactive: bool = False, - virus_total_api_key: List[str] = None, + virus_total_api_key: str = None, keystore_file: str = None, keystore_password: str = None, key_alias: str = None, @@ -37,7 +37,7 @@ def __init__( self.obfuscated_apk_path: str = obfuscated_apk_path self.ignore_libs: bool = ignore_libs self.interactive: bool = interactive - self.virus_total_api_key: List[str] = virus_total_api_key + self.virus_total_api_key: str = virus_total_api_key self.keystore_file: str = keystore_file self.keystore_password: str = keystore_password self.key_alias: str = key_alias diff --git a/src/obfuscapk/obfuscators/virus_total/virus_total.py b/src/obfuscapk/obfuscators/virus_total/virus_total.py index e81db4b4..f5be671a 100644 --- a/src/obfuscapk/obfuscators/virus_total/virus_total.py +++ b/src/obfuscapk/obfuscators/virus_total/virus_total.py @@ -4,11 +4,10 @@ import logging import os import time -from http import HTTPStatus -from itertools import cycle -from pprint import pformat +from typing import Optional, Dict -from virus_total_apis import PublicApi as VirusTotalPublicApi +import vt +from pprint import pformat from obfuscapk import obfuscator_category from obfuscapk.obfuscation import Obfuscation @@ -21,85 +20,34 @@ def __init__(self): "{0}.{1}".format(__name__, self.__class__.__name__) ) super().__init__() + self.vt_session = None - self.vt_sessions = None - - def get_vt_session(self): - # Cycle through the Virus Total sessions (one session for each key): - return next(self.vt_sessions) - - def get_report(self, sha256_hash: str) -> dict: - sleep_interval = 8 # In seconds. - attempt = 1 - max_attempts = 16 - - for attempt in range(1, max_attempts + 1): - report = self.get_vt_session().get_file_report(sha256_hash) - - if "response_code" in report and report["response_code"] == HTTPStatus.OK: - report_results_rc = report["results"]["response_code"] - if report_results_rc == 1: - return report - - # Exponential backoff. - sleep_interval *= attempt - self.logger.warning( - "Attempt {0}/{1} (retrying in {2} s), complete result not yet " - "available: {3}".format(attempt, max_attempts, sleep_interval, report) - ) - time.sleep(sleep_interval) + @staticmethod + def get_positives(report: Dict) -> int: + return report['data']['attributes']['last_analysis_stats']['malicious'] - self.logger.error( - 'Maximum number of {0} attempts reached for "{1}"'.format( - attempt, sha256_hash - ) - ) - raise Exception("Maximum number of attempts reached") + def get_report_or_none(self, sha256_hash: str) -> Optional[dict]: + try: + report = self.vt_session.get_json(f'/files/{sha256_hash}') + return report + except vt.error.APIError: + return None def scan_apk_file(self, apk_file_path: str) -> dict: self.logger.info('Scanning file "{0}"'.format(apk_file_path)) - - report = self.get_vt_session().get_file_report(sha256sum(apk_file_path)) - if "error" in report: - self.logger.error( - 'Error while retrieving scan for file "{0}": {1}'.format( - apk_file_path, report - ) - ) - raise Exception( - 'Error while retrieving scan for file "{0}"'.format(apk_file_path) - ) - - rc = report["results"]["response_code"] - - if rc == 0: - # The requested resource is not among the finished, queued or pending scans. - # The apk file needs to be uploaded to Virus Total. - scan_report = self.get_vt_session().scan_file(apk_file_path) - if scan_report["response_code"] != HTTPStatus.OK: - self.logger.error( - 'Error while uploading file "{0}": {1}'.format( - apk_file_path, scan_report - ) - ) - raise Exception( - 'Error while uploading file "{0}"'.format(apk_file_path) - ) - - report = self.get_report(scan_report["results"]["sha256"]) - - elif rc != 1: - # response_code is 1 when Virus Total has a complete result. - err_msg = report["results"]["verbose_msg"] - self.logger.error( - 'Error while retrieving scan for file "{0}": {1}'.format( - apk_file_path, err_msg - ) - ) - raise Exception( - 'Error while retrieving scan for file "{0}"'.format(apk_file_path) - ) - + sha256_hash = sha256sum(apk_file_path) + report = self.get_report_or_none(sha256_hash) + if report is not None: + return report + + with open(apk_file_path, "rb") as f: + self.logger.info(f"Uploading '{apk_file_path}' to VirusTotal...") + analysis = self.vt_session.scan_file(f, wait_for_completion=True) + assert analysis.status == 'completed' + + report = self.get_report_or_none(sha256_hash) + if report is None: + raise Exception('Error while retrieving scan for file "{0}"'.format(apk_file_path)) return report def obfuscate(self, obfuscation_info: Obfuscation): @@ -112,29 +60,24 @@ def obfuscate(self, obfuscation_info: Obfuscation): "obfuscator?" ) - if not obfuscation_info.virus_total_api_key: + if obfuscation_info.virus_total_api_key is None: raise ValueError( "A valid API key has to be provided in order to submit the " "obfuscated application to Virus Total" ) - self.vt_sessions = iter( - cycle( - VirusTotalPublicApi(key) - for key in obfuscation_info.virus_total_api_key - ) - ) + self.vt_session = vt.Client(obfuscation_info.virus_total_api_key) original_report = self.scan_apk_file(obfuscation_info.apk_path) self.logger.info( "Original apk scan result ({0} positives): {1}".format( - original_report["results"]["positives"], pformat(original_report) + self.get_positives(original_report), pformat(original_report) ) ) obfuscated_report = self.scan_apk_file(obfuscation_info.obfuscated_apk_path) self.logger.info( "Obfuscated apk scan result ({0} positives): {1}".format( - obfuscated_report["results"]["positives"], + self.get_positives(obfuscated_report), pformat(obfuscated_report), ) ) @@ -181,4 +124,5 @@ def obfuscate(self, obfuscation_info: Obfuscation): raise finally: + self.vt_session.close() obfuscation_info.used_obfuscators.append(self.__class__.__name__) diff --git a/src/requirements.txt b/src/requirements.txt index 7b9a75cc..45befef6 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,5 +1,5 @@ pycryptodome==3.9.9 pytest-cov==2.10.1 tqdm==4.54.1 -virustotal-api==1.1.11 -Yapsy==1.12.2 +vt-py==0.6.1 +Yapsy==1.12.2 \ No newline at end of file