From 0b1aaa25dd5233b92584b6ec1e5302ae44570042 Mon Sep 17 00:00:00 2001 From: Mir Saman Tajbakhsh Date: Thu, 23 Dec 2021 13:42:53 +0330 Subject: [PATCH 1/4] Android App Bundle Support Added --- README.md | 55 +++--- src/obfuscapk/main.py | 2 + src/obfuscapk/obfuscation.py | 117 ++++++++---- src/obfuscapk/toolbundledecompiler.py | 250 ++++++++++++++++++++++++++ 4 files changed, 369 insertions(+), 55 deletions(-) mode change 100644 => 100755 README.md create mode 100644 src/obfuscapk/toolbundledecompiler.py diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 8d2a9f03..b61490ea --- a/README.md +++ b/README.md @@ -80,6 +80,14 @@ plugins bundled with the tool). +## ❱ Android App Bundle Support + +In this version, Obfuscapk is supporting obfuscation of [Bundle files](https://developer.android.com/guide/app-bundle) (aab) by a tool from [TamilanPeriyasamy](https://github.com/TamilanPeriyasamy) named [BundleDecompiler](https://github.com/TamilanPeriyasamy/BundleDecompiler). This repository is for decompiling bundle to smali and resource files. Since obfuscapk is working with smali and other resource files, it does not matter how the files are decompiled. + +**The BundleDecompiler works only in Linux. Therefore, this version of obfuscapk will only work in Linux.** You should download the `BundleDecompiler-x.x.x.jar` and save it as `BundleDecompiler.jar` in `/usr/bin` directory (or anywhere else and set the `BUNDLE_DECOMPILER_PATH` local variable). + +This version is the first release. If you have faced any problems, please refer to **Contributing** section. + ## ❱ Installation There are two ways of getting a working copy of Obfuscapk on your own computer: either @@ -175,9 +183,8 @@ Copyright (C) 2009 The Android Open Source Project ``` To install and use `apktool` you need a recent version of Java. -`zipalign` and `apksigner` are included in the Android SDK. The location of the -executables can also be specified through the following environment variables: -`APKTOOL_PATH`, `APKSIGNER_PATH` and `ZIPALIGN_PATH` (e.g., in Ubuntu, run +`zipalign` and `apksigner` are included in the Android SDK. `bundledecompiler` is a tool created by [TamilanPeriyasamy](https://github.com/TamilanPeriyasamy) which you can download it from [BundleDecompiler](https://github.com/TamilanPeriyasamy/BundleDecompiler) repository. The downloaded file should be placed in `/usr/bin` with the name of `BundleDecompiler.jar` if `BUNDLE_DECOMPILER_PATH` variable is not set. The location of the executables can also be specified through the following environment variables: +`APKTOOL_PATH`, `APKSIGNER_PATH` , `ZIPALIGN_PATH`, and `BUNDLE_DECOMPILER_PATH` (e.g., in Ubuntu, run `export APKTOOL_PATH=/custom/location/apktool` before running Obfuscapk in the same terminal). @@ -209,7 +216,7 @@ $ cd src/ $ # The following command has to be executed always from Obfuscapk/src/ directory $ # or by adding Obfuscapk/src/ directory to PYTHONPATH environment variable. $ python3 -m obfuscapk.cli --help -usage: python3 -m obfuscapk.cli [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK] +usage: python3 -m obfuscapk.cli [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK_OR_AAB] ... ``` @@ -223,9 +230,9 @@ information. From now on, Obfuscapk will be considered as an executable available as `obfuscapk`, so you need to adapt the commands according to how you installed the tool: -* **Docker image**: a local directory containing the application to obfuscate has to be -mounted to `/workdir` in the container (e.g., the current directory `"${PWD}"`), so the -command: +* **Docker image**: a local directory containing the application to obfuscate has to be mounted to `/workdir` in the container (e.g., the current directory `"${PWD}"`), so the + command: + ```Shell $ obfuscapk [params...] ``` @@ -235,8 +242,9 @@ command: ``` * **From source**: every instruction has to be executed from the `Obfuscapk/src/` -directory (or by adding `Obfuscapk/src/` directory to `PYTHONPATH` environment -variable) and the command: + directory (or by adding `Obfuscapk/src/` directory to `PYTHONPATH` environment + variable) and the command: + ```Shell $ obfuscapk [params...] ``` @@ -249,14 +257,14 @@ Let's start by looking at the help message: ```Shell $ obfuscapk --help -obfuscapk [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK] [-i] [-p] [-k VT_API_KEY] +obfuscapk [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK_OR_AAB] [-i] [-p] [-k VT_API_KEY] [--keystore-file KEYSTORE_FILE] [--keystore-password KEYSTORE_PASSWORD] [--key-alias KEY_ALIAS] [--key-password KEY_PASSWORD] [--use-aapt2] - + ``` -There are two mandatory parameters: ``, the path (relative or absolute) to -the apk file to obfuscate and the list with the names of the obfuscation techniques to +There are two mandatory parameters: ``, the path (relative or absolute) to +the apk or bundle file to obfuscate and the list with the names of the obfuscation techniques to apply (specified with a `-o` option that can be used multiple times, e.g., `-o Rebuild -o NewAlignment -o NewSignature`). The other optional arguments are as follows: @@ -267,8 +275,8 @@ is created in the same directory as the input application. This can be useful fo debugging purposes, but if it's not needed it can be set to a temporary directory (e.g., `-w /tmp/`). -* `-d OUT_APK` is used to set the path of the destination file: the apk file generated -by the obfuscation process (e.g., `-d /home/user/Desktop/obfuscated.apk`). If not +* `-d OUT_APK_OR_AAB` is used to set the path of the destination file: the apk file generated +by the obfuscation process (e.g., `-d /home/user/Desktop/obfuscated.apk` or `-d /home/user/Desktop/obfuscated.aab`). If not specified, the final obfuscated file will be saved inside the working directory. Note: existing files will be overwritten without any warning. @@ -295,14 +303,15 @@ By default (when `--keystore-file` is not specified), a is used for the signing operations. * `--ignore-packages-file IGNORE_PACKAGES_FILE` is a path to a file which includes -package names to be ignored. All the classes inside those packages will not be -obfuscated when this option is used. The file should have one package name per line as -shown in the example below: + package names to be ignored. All the classes inside those packages will not be + obfuscated when this option is used. The file should have one package name per line as + shown in the example below: + ``` com.mycompany.dontobfuscate com.mycompany.ignore ... - ``` + ``` * `--use-aapt2` is a flag for use aapt2 option to rebuild app when using apktool. Let's consider now a simple working example to see how Obfuscapk works: @@ -325,14 +334,14 @@ available and ready to be used (in order) one by one until there's no obfuscator left or until an error is encountered - when running the first obfuscator, `original.apk` is decompiled with `apktool` - and the results are stored into the working directory + and the results are stored into the working directory - since the first obfuscator is `RandomManifest`, the entries in the decompiled - Android manifest are reordered randomly (without breaking the `xml` structures) + Android manifest are reordered randomly (without breaking the `xml` structures) - `Rebuild` obfuscator simply rebuilds the application (now with the modified - manifest) using `apktool`, and since no output file was specified, the resulting - apk file is saved in the working directory created before + manifest) using `apktool`, and since no output file was specified, the resulting + apk file is saved in the working directory created before - `NewAlignment` obfuscator uses `zipalign` tool to align the resulting apk file diff --git a/src/obfuscapk/main.py b/src/obfuscapk/main.py index aaa7ddf9..a31a2cf7 100644 --- a/src/obfuscapk/main.py +++ b/src/obfuscapk/main.py @@ -8,6 +8,7 @@ from obfuscapk.obfuscation import Obfuscation from obfuscapk.obfuscator_manager import ObfuscatorManager from obfuscapk.tool import Apktool, Zipalign, ApkSigner +from obfuscapk.toolbundledecompiler import BundleDecompiler if "LOG_LEVEL" in os.environ: log_level = os.environ["LOG_LEVEL"] @@ -38,6 +39,7 @@ def check_external_tool_dependencies(): # an exception will be thrown by the corresponding constructor. logger.debug("Checking external tool dependencies") Apktool() + BundleDecompiler() ApkSigner() Zipalign() diff --git a/src/obfuscapk/obfuscation.py b/src/obfuscapk/obfuscation.py index 59b77deb..26e93207 100644 --- a/src/obfuscapk/obfuscation.py +++ b/src/obfuscapk/obfuscation.py @@ -7,7 +7,8 @@ from typing import List, Union from obfuscapk import util -from obfuscapk.tool import Apktool, Zipalign, ApkSigner +from obfuscapk.toolbundledecompiler import BundleDecompiler, JarsignerBundle +from obfuscapk.tool import Apktool, ApkSigner, Zipalign class Obfuscation(object): @@ -31,6 +32,7 @@ def __init__( key_password: str = None, ignore_packages_file: str = None, use_aapt2: bool = False, + is_bundle: bool = False, ): self.logger = logging.getLogger(__name__) @@ -46,6 +48,10 @@ def __init__( self.key_password: str = key_password self.ignore_packages_file: str = ignore_packages_file self.use_aapt2 = use_aapt2 + if apk_path.endswith('aab'): + self.is_bundle = True + else: + self.is_bundle = False # Random string (32 chars long) generation with ASCII letters and digits self.encryption_secret = "".join( @@ -114,12 +120,20 @@ def __init__( # If the path of the output obfuscated apk is not specified, save it in the # working directory. if not self.obfuscated_apk_path: - self.obfuscated_apk_path = "{0}_obfuscated.apk".format( - os.path.join( - self.working_dir_path, - os.path.splitext(os.path.basename(self.apk_path))[0], + if (self.is_bundle): + self.obfuscated_apk_path = "{0}_obfuscated.aab".format( + os.path.join( + self.working_dir_path, + os.path.splitext(os.path.basename(self.apk_path))[0], + ) + ) + else: + self.obfuscated_apk_path = "{0}_obfuscated.apk".format( + os.path.join( + self.working_dir_path, + os.path.splitext(os.path.basename(self.apk_path))[0], + ) ) - ) self.logger.debug( "No obfuscated apk path provided, the result will be saved " 'as "{0}"'.format(self.obfuscated_apk_path) @@ -329,8 +343,9 @@ def decode_apk(self) -> None: if not self._is_decoded: - # The input apk will be decoded with apktool. + # The input apk will be decoded with apktool or BundleDecompiler. apktool: Apktool = Apktool() + bundledecompiler: BundleDecompiler = BundleDecompiler() # // self._decoded_apk_path = os.path.join( @@ -338,15 +353,24 @@ def decode_apk(self) -> None: os.path.splitext(os.path.basename(self.apk_path))[0], ) try: - apktool.decode(self.apk_path, self._decoded_apk_path, force=True) + if (self.is_bundle): + bundledecompiler.decode(self.apk_path, self._decoded_apk_path, force=False) + else: + apktool.decode(self.apk_path, self._decoded_apk_path, force=True) + # Path to the decoded manifest file. - self._manifest_file = os.path.join( + if (self.is_bundle): + self._manifest_file = os.path.join( + self._decoded_apk_path, "base", "manifest", "AndroidManifest.xml", + ) + else: + self._manifest_file = os.path.join( self._decoded_apk_path, "AndroidManifest.xml" ) # A list containing the paths to all the smali files obtained with - # apktool. + # apktool or bundledecompiler. self._smali_files = [ os.path.join(root, file_name) for root, dir_names, file_names in os.walk(self._decoded_apk_path) @@ -389,19 +413,31 @@ def decode_apk(self) -> None: self._smali_files.sort() # Check if multidex. - if os.path.isdir( - os.path.join(self._decoded_apk_path, "smali_classes2") - ): - self._is_multidex = True - + if (self.is_bundle): + if os.path.isdir( + os.path.join(self._decoded_apk_path, "base", "dex", "smali_classes2") + ): + self._is_multidex = True + else: + if os.path.isdir( + os.path.join(self._decoded_apk_path, "smali_classes2") + ): + self._is_multidex = True + + if (self._is_multidex): smali_directories = ["smali"] for i in range(2, 15): smali_directories.append("smali_classes{0}".format(i)) for smali_directory in smali_directories: - current_directory = os.path.join( - self._decoded_apk_path, smali_directory, "" - ) + if (self.is_bundle): + current_directory = os.path.join( + self._decoded_apk_path, "base", "dex", smali_directory, "" + ) + else: + current_directory = os.path.join( + self._decoded_apk_path, smali_directory, "" + ) if os.path.isdir(current_directory): self._multidex_smali_files.append( [ @@ -499,13 +535,15 @@ def build_obfuscated_apk(self) -> None: if not self._is_decoded: self.decode_apk() - # The obfuscated apk will be built with apktool. + # The obfuscated apk will be built with apktool or BundleDecompiler. apktool: Apktool = Apktool() + bundledecompiler: BundleDecompiler = BundleDecompiler() try: - apktool.build( - self._decoded_apk_path, self.obfuscated_apk_path, self.use_aapt2 - ) + if (self.is_bundle): + bundledecompiler.build(self._decoded_apk_path, self.obfuscated_apk_path) + else: + apktool.build(self._decoded_apk_path, self.obfuscated_apk_path) except Exception as e: self.logger.error("Error during apk building: {0}".format(e)) raise @@ -514,7 +552,8 @@ def sign_obfuscated_apk(self) -> None: # This method must be called AFTER the obfuscated apk has been built. - # The obfuscated apk will be signed with apksigner. + # The obfuscated apk will be signed with APKSigner or BundleDecompiler. + aabsigner: AABSigner = AABSigner() apksigner: ApkSigner = ApkSigner() # If a custom keystore file is not provided, use the default one bundled with @@ -541,13 +580,18 @@ def sign_obfuscated_apk(self) -> None: ) try: - apksigner.resign( - self.obfuscated_apk_path, - self.keystore_file, - self.keystore_password, - self.key_alias, - self.key_password, - ) + if (self.is_bundle): + aabsigner.sign( + self.obfuscated_apk_path, + ) + else: + apksigner.resign( + self.obfuscated_apk_path, + self.keystore_file, + self.keystore_password, + self.key_alias, + self.key_password, + ) except Exception as e: self.logger.error("Error during apk signing: {0}".format(e)) raise @@ -558,6 +602,8 @@ def align_obfuscated_apk(self) -> None: # The obfuscated apk will be aligned with zipalign. zipalign: Zipalign = Zipalign() + if (self.is_bundle): + return try: zipalign.align(self.obfuscated_apk_path) @@ -607,7 +653,10 @@ def get_assets_directory(self) -> str: self.decode_apk() # '.join(x, "")' is used to add a trailing slash. - return os.path.join(self._decoded_apk_path, "assets", "") + if (self.is_bundle): + return os.path.join(self._decoded_apk_path, "base", "assets", "") + else: + return os.path.join(self._decoded_apk_path, "assets", "") def get_resource_directory(self) -> str: @@ -615,7 +664,11 @@ def get_resource_directory(self) -> str: self.decode_apk() # '.join(x, "")' is used to add a trailing slash. - return os.path.join(self._decoded_apk_path, "res", "") + if (self.is_bundle): + return os.path.join(self._decoded_apk_path, "base", "res", "") + else: + return os.path.join(self._decoded_apk_path, "res", "") + def get_ignore_package_names(self) -> List[str]: ignore_package_list = [] diff --git a/src/obfuscapk/toolbundledecompiler.py b/src/obfuscapk/toolbundledecompiler.py new file mode 100644 index 00000000..45f9e439 --- /dev/null +++ b/src/obfuscapk/toolbundledecompiler.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 + +import io +import logging +import os +import shutil +import subprocess +import tempfile +import zipfile +from typing import List + + +class BundleDecompiler(object): + def __init__(self): + self.logger = logging.getLogger( + "{0}.{1}".format(__name__, self.__class__.__name__) + ) + + if "BUNDLE_DECOMPILER_PATH" in os.environ: + self.bundledecompiler_path: str = os.environ["BUNDLE_DECOMPILER_PATH"] + else: + self.bundledecompiler_path: str = "BundleDecompiler.jar" + + full_bundledecompiler_path = "/usr/bin/BundleDecompiler.jar" #shutil.which(self.bundledecompiler_path) + + # Make sure bundle decompiler is available + if not os.path.isfile(full_bundledecompiler_path): + raise RuntimeError( + 'Cannot find BundleDecompiler with executable "{0}"'.format(self.full_bundledecompiler_path) + ) + + # Make sure to use the full path of the executable (needed for cross-platform + # compatibility). + if full_bundledecompiler_path is None: + raise RuntimeError( + 'Something is wrong with executable "{0}"'.format(self.bundledecompiler_path) + ) + else: + self.bundledecompiler_path = full_bundledecompiler_path + + def decode( + self, aab_path: str, output_dir_path: str = None, force: bool = False + ) -> str: + + # Check if the aab file to decode is a valid file. + if not os.path.isfile(aab_path): + self.logger.error('Unable to find file "{0}"'.format(aab_path)) + raise FileNotFoundError('Unable to find file "{0}"'.format(aab_path)) + + # If no output directory is specified, use a new directory in the same + # directory as the aab file to decode. + if not output_dir_path: + output_dir_path = os.path.join( + os.path.dirname(aab_path), + os.path.splitext(os.path.basename(aab_path))[0], + ) + self.logger.debug( + "No output directory provided, the result will be saved in the " + "same directory as the input file, in a directory with the same " + 'name as the input file: "{0}"'.format(output_dir_path) + ) + + # If an output directory is provided, make sure that the path to that + # directory exists (the final directory will be created by aabtool). + elif not os.path.isdir(os.path.dirname(output_dir_path)): + self.logger.error( + 'Unable to find output directory "{0}", aabtool won\'t be able to ' + 'create the directory "{1}"'.format( + os.path.dirname(output_dir_path), output_dir_path + ) + ) + raise NotADirectoryError( + 'Unable to find output directory "{0}", aabtool won\'t be able to ' + 'create the directory "{1}"'.format( + os.path.dirname(output_dir_path), output_dir_path + ) + ) + + # Inform the user if an existing output directory is provided without the + # "force" flag. + if os.path.isdir(output_dir_path) and not force: + self.logger.error( + 'Output directory "{0}" already exists, use the "force" flag ' + "to overwrite".format(output_dir_path) + ) + raise FileExistsError( + 'Output directory "{0}" already exists, use the "force" flag ' + "to overwrite".format(output_dir_path) + ) + + decode_cmd: List[str] = [ + "java", + "-jar", + self.bundledecompiler_path, + "d", + "--in=" + aab_path, + "--out=" + output_dir_path, + ] + + if force: + self.logger.warning( + 'Bundle Decompiler does not support force' + ) + + try: + self.logger.info( + 'Running decode command "{0}"'.format(" ".join(decode_cmd)) + ) + # A new line character is sent as input since newer versions of aabtool + # have an interactive prompt on Windows where the user should press a key. + output = subprocess.check_output( + decode_cmd, stderr=subprocess.STDOUT, input=b"\n" + ).strip() + if b"Exception in thread " in output: + # Report exception raised in aabtool. + raise subprocess.CalledProcessError(1, decode_cmd, output) + return output.decode(errors="replace") + except subprocess.CalledProcessError as e: + self.logger.error( + "Error during decode command: {0}".format( + e.output.decode(errors="replace") if e.output else e + ) + ) + raise + except Exception as e: + self.logger.error("Error during decoding: {0}".format(e)) + raise + + def build(self, source_dir_path: str, output_aab_path: str = None) -> str: + + # Check if the input directory exists. + if not os.path.isdir(source_dir_path): + self.logger.error( + 'Unable to find source directory "{0}"'.format(source_dir_path) + ) + raise NotADirectoryError( + 'Unable to find source directory "{0}"'.format(source_dir_path) + ) + + # If no output aab path is specified, the new aab will be saved in the + # default path: /dist/.aab + if not output_aab_path: + output_aab_path = os.path.join( + source_dir_path, + "output", + "{0}.aab".format(os.path.basename(source_dir_path)), + ) + self.logger.debug( + "No output aab path provided, the new aab will be saved in the " + 'default path: "{0}"'.format(output_aab_path) + ) + + build_cmd: List[str] = [ + "java", + "-jar", + self.bundledecompiler_path, + "b", + "--in=" + source_dir_path, + "--out=" + output_aab_path, + ] + + try: + self.logger.info('Running build command "{0}"'.format(" ".join(build_cmd))) + # A new line character is sent as input since newer versions of aabtool + # have an interactive prompt on Windows where the user should press a key. + output = subprocess.check_output( + build_cmd, stderr=subprocess.STDOUT, input=b"\n" + ).strip() + if ( + b"brut.directory.PathNotExist: " in output + or b"Exception in thread " in output + ): + # Report exception raised in aabtool. + raise subprocess.CalledProcessError(1, build_cmd, output) + + if not os.path.isfile(output_aab_path): + raise FileNotFoundError( + '"{0}" was not built correctly. aabtool output:\n{1}'.format( + output_aab_path, output.decode(errors="replace") + ) + ) + + return output.decode(errors="replace") + except subprocess.CalledProcessError as e: + self.logger.error( + "Error during build command: {0}".format( + e.output.decode(errors="replace") if e.output else e + ) + ) + raise + except Exception as e: + self.logger.error("Error during building: {0}".format(e)) + raise + + +class AABSigner(object): + def __init__(self): + self.logger = logging.getLogger( + "{0}.{1}".format(__name__, self.__class__.__name__) + ) + + if "BUNDLE_DECOMPILER_PATH" in os.environ: + self.aabsigner_path: str = os.environ["BUNDLE_DECOMPILER_PATH"] + else: + self.aabsigner_path: str = "BundleDecompiler.jar" + + full_aabsigner_path = "/usr/bin/BundleDecompiler.jar" #shutil.which(self.aabsigner_path) + + # Make sure to use the full path of the executable (needed for cross-platform + # compatibility). + if full_aabsigner_path is None: + raise RuntimeError( + 'Something is wrong with executable "{0}"'.format(self.aabsigner_path) + ) + else: + self.aabsigner_path = full_aabsigner_path + + def sign( + self, + aab_path: str, + ) -> str: + + # Check if the aab file to sign is a valid file. + if not os.path.isfile(aab_path): + self.logger.error('Unable to find file "{0}"'.format(aab_path)) + raise FileNotFoundError('Unable to find file "{0}"'.format(aab_path)) + + sign_cmd: List[str] = [ + "java", + "-jar", + self.aabsigner_path, + "sign-bundle", + "--in=" + aab_path, + "--out=" + aab_path.replace(".aab","_signed.aab") + ] + + try: + self.logger.info('Running sign command "{0}"'.format(" ".join(sign_cmd))) + output = subprocess.check_output(sign_cmd, stderr=subprocess.STDOUT).strip() + return output.decode(errors="replace") + except subprocess.CalledProcessError as e: + self.logger.error( + "Error during sign command: {0}".format( + e.output.decode(errors="replace") if e.output else e + ) + ) + raise + except Exception as e: + self.logger.error("Error during signing: {0}".format(e)) + raise From 45a823382863cf55d67c0efc6eff6edf9e5f989b Mon Sep 17 00:00:00 2001 From: Mir Saman Tajbakhsh Date: Thu, 23 Dec 2021 13:55:16 +0330 Subject: [PATCH 2/4] error fixed --- src/obfuscapk/obfuscation.py | 2 +- src/obfuscapk/toolbundledecompiler.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/obfuscapk/obfuscation.py b/src/obfuscapk/obfuscation.py index 26e93207..8839affb 100644 --- a/src/obfuscapk/obfuscation.py +++ b/src/obfuscapk/obfuscation.py @@ -7,7 +7,7 @@ from typing import List, Union from obfuscapk import util -from obfuscapk.toolbundledecompiler import BundleDecompiler, JarsignerBundle +from obfuscapk.toolbundledecompiler import BundleDecompiler, AABSigner from obfuscapk.tool import Apktool, ApkSigner, Zipalign diff --git a/src/obfuscapk/toolbundledecompiler.py b/src/obfuscapk/toolbundledecompiler.py index 45f9e439..07628354 100644 --- a/src/obfuscapk/toolbundledecompiler.py +++ b/src/obfuscapk/toolbundledecompiler.py @@ -26,7 +26,7 @@ def __init__(self): # Make sure bundle decompiler is available if not os.path.isfile(full_bundledecompiler_path): raise RuntimeError( - 'Cannot find BundleDecompiler with executable "{0}"'.format(self.full_bundledecompiler_path) + 'Cannot find BundleDecompiler with executable "{0}"'.format(full_bundledecompiler_path) ) # Make sure to use the full path of the executable (needed for cross-platform From 2e4ff078879e215c0336a764ab95ed480fd04e68 Mon Sep 17 00:00:00 2001 From: Gabriel Claudiu Georgiu Date: Fri, 24 Dec 2021 12:11:43 +0100 Subject: [PATCH 3/4] Fix tests and add warning when using BundleDecompiler on Windows --- .github/workflows/macos.yml | 3 +++ .github/workflows/ubuntu.yml | 2 ++ src/obfuscapk/toolbundledecompiler.py | 24 ++++++++++++++++++------ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 89ee08f6..903b6408 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -39,6 +39,9 @@ jobs: sudo chmod a+x /usr/local/bin/apktool sudo wget -q "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_${APKTOOL_VERSION}.jar" -O /usr/local/bin/apktool.jar sudo chmod a+x /usr/local/bin/apktool.jar + # Install BundleDecompiler. + sudo wget -q https://github.com/TamilanPeriyasamy/BundleDecompiler/raw/master/build/libs/BundleDecompiler-0.0.2.jar -O /usr/local/bin/BundleDecompiler.jar + sudo chmod a+x /usr/local/bin/BundleDecompiler.jar - name: Run tests run: | diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index b21b11ae..f8d95f3f 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -39,6 +39,8 @@ jobs: sudo chmod a+x /usr/local/bin/apktool sudo wget -q "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_${APKTOOL_VERSION}.jar" -O /usr/local/bin/apktool.jar sudo chmod a+x /usr/local/bin/apktool.jar + sudo wget -q https://github.com/TamilanPeriyasamy/BundleDecompiler/raw/master/build/libs/BundleDecompiler-0.0.2.jar -O /usr/local/bin/BundleDecompiler.jar + sudo chmod a+x /usr/local/bin/BundleDecompiler.jar - name: Run tests run: | diff --git a/src/obfuscapk/toolbundledecompiler.py b/src/obfuscapk/toolbundledecompiler.py index 07628354..8fc450ed 100644 --- a/src/obfuscapk/toolbundledecompiler.py +++ b/src/obfuscapk/toolbundledecompiler.py @@ -1,12 +1,10 @@ #!/usr/bin/env python3 -import io import logging import os +import platform import shutil import subprocess -import tempfile -import zipfile from typing import List @@ -16,14 +14,18 @@ def __init__(self): "{0}.{1}".format(__name__, self.__class__.__name__) ) + if platform.system() == "Windows": + self.logger.warning("BundleDecompiler is not yet available on Windows platform") + return + if "BUNDLE_DECOMPILER_PATH" in os.environ: self.bundledecompiler_path: str = os.environ["BUNDLE_DECOMPILER_PATH"] else: self.bundledecompiler_path: str = "BundleDecompiler.jar" - full_bundledecompiler_path = "/usr/bin/BundleDecompiler.jar" #shutil.which(self.bundledecompiler_path) + full_bundledecompiler_path = shutil.which(self.bundledecompiler_path) - # Make sure bundle decompiler is available + # Make sure bundle decompiler is available if not os.path.isfile(full_bundledecompiler_path): raise RuntimeError( 'Cannot find BundleDecompiler with executable "{0}"'.format(full_bundledecompiler_path) @@ -41,6 +43,8 @@ def __init__(self): def decode( self, aab_path: str, output_dir_path: str = None, force: bool = False ) -> str: + if platform.system() == "Windows": + raise NotImplementedError("BundleDecompiler is not yet available on Windows platform") # Check if the aab file to decode is a valid file. if not os.path.isfile(aab_path): @@ -127,6 +131,8 @@ def decode( raise def build(self, source_dir_path: str, output_aab_path: str = None) -> str: + if platform.system() == "Windows": + raise NotImplementedError("BundleDecompiler is not yet available on Windows platform") # Check if the input directory exists. if not os.path.isdir(source_dir_path): @@ -199,12 +205,16 @@ def __init__(self): "{0}.{1}".format(__name__, self.__class__.__name__) ) + if platform.system() == "Windows": + self.logger.warning("BundleDecompiler is not yet available on Windows platform") + return + if "BUNDLE_DECOMPILER_PATH" in os.environ: self.aabsigner_path: str = os.environ["BUNDLE_DECOMPILER_PATH"] else: self.aabsigner_path: str = "BundleDecompiler.jar" - full_aabsigner_path = "/usr/bin/BundleDecompiler.jar" #shutil.which(self.aabsigner_path) + full_aabsigner_path = shutil.which(self.aabsigner_path) # Make sure to use the full path of the executable (needed for cross-platform # compatibility). @@ -219,6 +229,8 @@ def sign( self, aab_path: str, ) -> str: + if platform.system() == "Windows": + raise NotImplementedError("BundleDecompiler is not yet available on Windows platform") # Check if the aab file to sign is a valid file. if not os.path.isfile(aab_path): From 9e8a53b526131240cefe21fdf086083b5e8cbdbf Mon Sep 17 00:00:00 2001 From: Gabriel Claudiu Georgiu Date: Fri, 24 Dec 2021 14:19:04 +0100 Subject: [PATCH 4/4] Update readme --- README.md | 97 +++++++++++++++++++++--------------- src/obfuscapk/cli.py | 13 ++--- src/obfuscapk/obfuscation.py | 3 +- 3 files changed, 64 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index b61490ea..2199d887 100755 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Windows Build Status](https://github.com/ClaudiuGeorgiu/Obfuscapk/workflows/Windows/badge.svg)](https://github.com/ClaudiuGeorgiu/Obfuscapk/actions?query=workflow%3AWindows) [![MacOS Build Status](https://github.com/ClaudiuGeorgiu/Obfuscapk/workflows/MacOS/badge.svg)](https://github.com/ClaudiuGeorgiu/Obfuscapk/actions?query=workflow%3AMacOS) [![Docker Hub](https://img.shields.io/docker/cloud/build/claudiugeorgiu/obfuscapk)](https://hub.docker.com/r/claudiugeorgiu/obfuscapk) -[![Python Version](https://img.shields.io/badge/Python-3.6%2B-green.svg?logo=python&logoColor=white)](https://www.python.org/downloads/) +[![Python Version](https://img.shields.io/badge/Python-3.7%2B-green.svg?logo=python&logoColor=white)](https://www.python.org/downloads/) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ClaudiuGeorgiu/Obfuscapk/blob/master/LICENSE) @@ -20,6 +20,22 @@ obfuscated app retains the same functionality as the original one, but the diffe under the hood sometimes make the new application very different from the original (e.g., to signature-based antivirus software). +### :new: Android App Bundle support :new: + +Obfuscapk is adding support for +[Android App Bundles](https://developer.android.com/guide/app-bundle) (aab files) by +using [BundleDecompiler](https://github.com/TamilanPeriyasamy/BundleDecompiler) (see +[#121](https://github.com/ClaudiuGeorgiu/Obfuscapk/pull/121)). In order to use this new +feature, download the latest version of BundleDecompiler available from +[here](https://github.com/TamilanPeriyasamy/BundleDecompiler/tree/master/build/libs) and +save it as `BundleDecompiler.jar` in a directory included in `PATH` (e.g., in Ubuntu, +`/usr/local/bin` or `/usr/bin`). + +`NOTE:` BundleDecompiler doesn't work on Windows yet, so app bundle obfuscation is not +supported by Obfuscapk on Windows platform. Also, app bundle support is still in early +development, so if you faced any problems or if you want to help us improve, please see +[contributing](#-contributing). + ## ❱ Publication @@ -80,14 +96,6 @@ plugins bundled with the tool). -## ❱ Android App Bundle Support - -In this version, Obfuscapk is supporting obfuscation of [Bundle files](https://developer.android.com/guide/app-bundle) (aab) by a tool from [TamilanPeriyasamy](https://github.com/TamilanPeriyasamy) named [BundleDecompiler](https://github.com/TamilanPeriyasamy/BundleDecompiler). This repository is for decompiling bundle to smali and resource files. Since obfuscapk is working with smali and other resource files, it does not matter how the files are decompiled. - -**The BundleDecompiler works only in Linux. Therefore, this version of obfuscapk will only work in Linux.** You should download the `BundleDecompiler-x.x.x.jar` and save it as `BundleDecompiler.jar` in `/usr/bin` directory (or anywhere else and set the `BUNDLE_DECOMPILER_PATH` local variable). - -This version is the first release. If you have faced any problems, please refer to **Contributing** section. - ## ❱ Installation There are two ways of getting a working copy of Obfuscapk on your own computer: either @@ -117,7 +125,7 @@ Docker version 19.03.0, build aeac949 #### Official Docker Hub image The [official Obfuscapk Docker image](https://hub.docker.com/r/claudiugeorgiu/obfuscapk) -is available on Docker Hub (automatically built from this repository): +is available on Docker Hub: ```Shell $ # Download the Docker image. @@ -144,7 +152,7 @@ installed correctly: ```Shell $ docker run --rm -it obfuscapk --help -usage: python3 -m obfuscapk.cli [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK] +usage: python3 -m obfuscapk.cli [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK_OR_AAB] ... ``` @@ -182,14 +190,22 @@ Copyright (C) 2009 The Android Open Source Project ... ``` -To install and use `apktool` you need a recent version of Java. -`zipalign` and `apksigner` are included in the Android SDK. `bundledecompiler` is a tool created by [TamilanPeriyasamy](https://github.com/TamilanPeriyasamy) which you can download it from [BundleDecompiler](https://github.com/TamilanPeriyasamy/BundleDecompiler) repository. The downloaded file should be placed in `/usr/bin` with the name of `BundleDecompiler.jar` if `BUNDLE_DECOMPILER_PATH` variable is not set. The location of the executables can also be specified through the following environment variables: -`APKTOOL_PATH`, `APKSIGNER_PATH` , `ZIPALIGN_PATH`, and `BUNDLE_DECOMPILER_PATH` (e.g., in Ubuntu, run -`export APKTOOL_PATH=/custom/location/apktool` before running Obfuscapk in the same -terminal). +To support app bundles obfuscation you need +[BundleDecompiler](https://github.com/TamilanPeriyasamy/BundleDecompiler), so download +the latest available version from +[here](https://github.com/TamilanPeriyasamy/BundleDecompiler/tree/master/build/libs) and +save it as `BundleDecompiler.jar` in a directory included in `PATH` (e.g., in Ubuntu, +`/usr/local/bin` or `/usr/bin`). + +To use BundleDecompiler and `apktool` you also need a recent version of Java. +`zipalign` and `apksigner` are included in the Android SDK. The location of the +executables can also be specified through the following environment variables: +`APKTOOL_PATH`, `BUNDLE_DECOMPILER_PATH`, `APKSIGNER_PATH` and `ZIPALIGN_PATH` (e.g., +in Ubuntu, run `export APKTOOL_PATH=/custom/location/apktool` before running Obfuscapk +in the same terminal). Apart from the above tools, the only requirement of this project is a working -`Python 3` (at least `3.6`) installation (along with its package manager `pip`). +`Python 3` (at least `3.7`) installation (along with its package manager `pip`). #### Install @@ -230,9 +246,9 @@ information. From now on, Obfuscapk will be considered as an executable available as `obfuscapk`, so you need to adapt the commands according to how you installed the tool: -* **Docker image**: a local directory containing the application to obfuscate has to be mounted to `/workdir` in the container (e.g., the current directory `"${PWD}"`), so the - command: - +* **Docker image**: a local directory containing the application to obfuscate has to be +mounted to `/workdir` in the container (e.g., the current directory `"${PWD}"`), so the +command: ```Shell $ obfuscapk [params...] ``` @@ -242,9 +258,8 @@ so you need to adapt the commands according to how you installed the tool: ``` * **From source**: every instruction has to be executed from the `Obfuscapk/src/` - directory (or by adding `Obfuscapk/src/` directory to `PYTHONPATH` environment - variable) and the command: - +directory (or by adding `Obfuscapk/src/` directory to `PYTHONPATH` environment +variable) and the command: ```Shell $ obfuscapk [params...] ``` @@ -263,11 +278,11 @@ obfuscapk [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK_OR_AAB] [-i] [-p] [-k VT_API_K ``` -There are two mandatory parameters: ``, the path (relative or absolute) to -the apk or bundle file to obfuscate and the list with the names of the obfuscation techniques to -apply (specified with a `-o` option that can be used multiple times, e.g., -`-o Rebuild -o NewAlignment -o NewSignature`). The other optional arguments are as -follows: +There are two mandatory parameters: ``, the path (relative or +absolute) to the apk or app bundle file to obfuscate and the list with the names of the +obfuscation techniques to apply (specified with a `-o` option that can be used multiple +times, e.g., `-o Rebuild -o NewAlignment -o NewSignature`). The other optional arguments +are as follows: * `-w DIR` is used to set the working directory where to save the intermediate files (generated by `apktool`). If not specified, a directory named `obfuscation_working_dir` @@ -275,10 +290,11 @@ is created in the same directory as the input application. This can be useful fo debugging purposes, but if it's not needed it can be set to a temporary directory (e.g., `-w /tmp/`). -* `-d OUT_APK_OR_AAB` is used to set the path of the destination file: the apk file generated -by the obfuscation process (e.g., `-d /home/user/Desktop/obfuscated.apk` or `-d /home/user/Desktop/obfuscated.aab`). If not -specified, the final obfuscated file will be saved inside the working directory. -Note: existing files will be overwritten without any warning. +* `-d OUT_APK_OR_AAB` is used to set the path of the destination file: the apk file +generated by the obfuscation process (e.g., `-d /home/user/Desktop/obfuscated.apk` or +`-d /home/user/Desktop/obfuscated.aab`). If not specified, the final obfuscated file +will be saved inside the working directory. Note: existing files will be overwritten +without any warning. * `-i` is a flag for ignoring known third party libraries during the obfuscation process, to use fewer resources, to increase performances and to reduce the risk of @@ -303,16 +319,15 @@ By default (when `--keystore-file` is not specified), a is used for the signing operations. * `--ignore-packages-file IGNORE_PACKAGES_FILE` is a path to a file which includes - package names to be ignored. All the classes inside those packages will not be - obfuscated when this option is used. The file should have one package name per line as - shown in the example below: - +package names to be ignored. All the classes inside those packages will not be +obfuscated when this option is used. The file should have one package name per line as +shown in the example below: ``` com.mycompany.dontobfuscate com.mycompany.ignore ... ``` -* `--use-aapt2` is a flag for use aapt2 option to rebuild app when using apktool. +* `--use-aapt2` is a flag for using aapt2 option when rebuilding an app with `apktool`. Let's consider now a simple working example to see how Obfuscapk works: @@ -334,14 +349,14 @@ available and ready to be used (in order) one by one until there's no obfuscator left or until an error is encountered - when running the first obfuscator, `original.apk` is decompiled with `apktool` - and the results are stored into the working directory + and the results are stored into the working directory - since the first obfuscator is `RandomManifest`, the entries in the decompiled - Android manifest are reordered randomly (without breaking the `xml` structures) + Android manifest are reordered randomly (without breaking the `xml` structures) - `Rebuild` obfuscator simply rebuilds the application (now with the modified - manifest) using `apktool`, and since no output file was specified, the resulting - apk file is saved in the working directory created before + manifest) using `apktool`, and since no output file was specified, the resulting + apk file is saved in the working directory created before - `NewAlignment` obfuscator uses `zipalign` tool to align the resulting apk file diff --git a/src/obfuscapk/cli.py b/src/obfuscapk/cli.py index bad56459..a380bd7b 100644 --- a/src/obfuscapk/cli.py +++ b/src/obfuscapk/cli.py @@ -21,13 +21,14 @@ def get_cmd_args(args: list = None): parser = argparse.ArgumentParser( prog="python3 -m obfuscapk.cli", - description="Obfuscate an application (.apk) without needing its source code.", + description="Obfuscate an application (.apk/.aab) without needing its " + "source code.", ) parser.add_argument( "apk_file", type=str, - metavar="", - help="The path to the application (.apk) to obfuscate", + metavar="", + help="The path to the application (.apk/.aab) to obfuscate", ) parser.add_argument( "-o", @@ -54,9 +55,9 @@ def get_cmd_args(args: list = None): "-d", "--destination", type=str, - metavar="OUT_APK", - help="The path where to save the obfuscated .apk file. By default the file " - "will be saved in the working directory", + metavar="OUT_APK_OR_AAB", + help="The path where to save the obfuscated .apk/.aab file. By default the " + "file will be saved in the working directory", ) parser.add_argument( "-i", diff --git a/src/obfuscapk/obfuscation.py b/src/obfuscapk/obfuscation.py index 8839affb..ca7ce11b 100644 --- a/src/obfuscapk/obfuscation.py +++ b/src/obfuscapk/obfuscation.py @@ -7,8 +7,8 @@ from typing import List, Union from obfuscapk import util -from obfuscapk.toolbundledecompiler import BundleDecompiler, AABSigner from obfuscapk.tool import Apktool, ApkSigner, Zipalign +from obfuscapk.toolbundledecompiler import BundleDecompiler, AABSigner class Obfuscation(object): @@ -32,7 +32,6 @@ def __init__( key_password: str = None, ignore_packages_file: str = None, use_aapt2: bool = False, - is_bundle: bool = False, ): self.logger = logging.getLogger(__name__)