diff --git a/README.md b/README.md index 17cb3fd0..f0ad232f 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ information. Make sure to have a recent version of [`apktool`](https://ibotpeaches.github.io/Apktool/), -[`jarsigner`](https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jarsigner.html) +[`apksigner`](https://developer.android.com/studio/command-line/apksigner) and [`zipalign`](https://developer.android.com/studio/command-line/zipalign) installed and available from the command line: @@ -161,9 +161,10 @@ Apktool v2.5.0 - a tool for reengineering Android apk files ... ``` ```Shell -$ jarsigner -Usage: jarsigner [options] jar-file alias - jarsigner -verify [options] jar-file [alias...] +$ apksigner +Usage: apksigner [options] + apksigner --version + apksigner --help ... ``` ```Shell @@ -173,10 +174,10 @@ Copyright (C) 2009 The Android Open Source Project ... ``` -To install and use `apktool` you need a recent version of Java, which should also have -`jarsigner` bundled. `zipalign` is included in the Android SDK. The location of the +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`, `JARSIGNER_PATH` and `ZIPALIGN_PATH` (e.g., in Ubuntu, run +`APKTOOL_PATH`, `APKSIGNER_PATH` and `ZIPALIGN_PATH` (e.g., in Ubuntu, run `export APKTOOL_PATH=/custom/location/apktool` before running Obfuscapk in the same terminal). @@ -257,7 +258,7 @@ obfuscapk [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK] [-i] [-p] [-k VT_API_KEY] 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 apply (specified with a `-o` option that can be used multiple times, e.g., -`-o Rebuild -o NewSignature -o NewAlignment`). The other optional arguments are as +`-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 @@ -307,7 +308,7 @@ Let's consider now a simple working example to see how Obfuscapk works: ```Shell $ # original.apk is a valid Android apk file. -$ obfuscapk -o RandomManifest -o Rebuild -o NewSignature -o NewAlignment original.apk +$ obfuscapk -o RandomManifest -o Rebuild -o NewAlignment -o NewSignature original.apk ``` When running the above command, this is what happens behind the scenes: @@ -332,18 +333,18 @@ available and ready to be used manifest) using `apktool`, and since no output file was specified, the resulting apk file is saved in the working directory created before - - `NewSignature` obfuscator signs the newly created apk file with a custom - certificate contained in a - [keystore bundled with Obfuscapk](https://github.com/ClaudiuGeorgiu/Obfuscapk/blob/master/src/obfuscapk/resources/obfuscation_keystore.jks) - (though a different keystore can be specified with the `--keystore-file` parameter) - - `NewAlignment` obfuscator uses `zipalign` tool to align the resulting apk file + + - `NewSignature` obfuscator signs the newly created apk file with a custom + certificate contained in a + [keystore bundled with Obfuscapk](https://github.com/ClaudiuGeorgiu/Obfuscapk/blob/master/src/obfuscapk/resources/obfuscation_keystore.jks) + (though a different keystore can be specified with the `--keystore-file` parameter) * when all the obfuscators have been executed without errors, the resulting obfuscated apk file can be found in `obfuscation_working_dir/original_obfuscated.apk`, signed, aligned and ready to be installed into a device/emulator -As seen in the previous example, `Rebuild`, `NewSignature` and `NewAlignment` +As seen in the previous example, `Rebuild`, `NewAlignment` and `NewSignature` obfuscators are always needed to complete an obfuscation operation, to build the final obfuscated apk. They are not actual obfuscation techniques, but they are needed in the build process and so they are included in the list of obfuscators to keep the overall diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 32c2de60..15c37101 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -30,7 +30,7 @@ from Docker Hub. If you are not using the Docker image, make sure to install and setup properly the additional tools needed for Obfuscapk to work: [`apktool`](https://ibotpeaches.github.io/Apktool/), -[`jarsigner`](https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jarsigner.html) +[`apksigner`](https://developer.android.com/studio/command-line/apksigner) and [`zipalign`](https://developer.android.com/studio/command-line/zipalign). Please ensure to be using a recent release of [`apktool`](https://ibotpeaches.github.io/Apktool/) (some systems, like Kali Linux, diff --git a/src/obfuscapk/cli.py b/src/obfuscapk/cli.py index ccad8ad8..d02e71dc 100644 --- a/src/obfuscapk/cli.py +++ b/src/obfuscapk/cli.py @@ -125,7 +125,7 @@ def main(): -o AssetEncryption -o MethodOverload -o ConstStringEncryption \ -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 Rebuild -o NewAlignment -o NewSignature \ -o VirusTotal -k virus_total_key \ /path/to/original.apk """ diff --git a/src/obfuscapk/main.py b/src/obfuscapk/main.py index a0a800ba..d5083fb8 100644 --- a/src/obfuscapk/main.py +++ b/src/obfuscapk/main.py @@ -7,7 +7,7 @@ from obfuscapk import util from obfuscapk.obfuscation import Obfuscation from obfuscapk.obfuscator_manager import ObfuscatorManager -from obfuscapk.tool import Apktool, Jarsigner, Zipalign +from obfuscapk.tool import Apktool, Zipalign, ApkSigner if "LOG_LEVEL" in os.environ: log_level = os.environ["LOG_LEVEL"] @@ -32,13 +32,13 @@ def check_external_tool_dependencies(): """ Make sure all the external needed tools are available and ready to be used. """ - # APKTOOL_PATH, JARSIGNER_PATH and ZIPALIGN_PATH environment variables can be + # APKTOOL_PATH, APKSIGNER_PATH and ZIPALIGN_PATH environment variables can be # used to specify the location of the external tools (make sure they have the # execute permission). If there is a problem with any of the executables below, # an exception will be thrown by the corresponding constructor. logger.debug("Checking external tool dependencies") Apktool() - Jarsigner() + ApkSigner() Zipalign() diff --git a/src/obfuscapk/obfuscation.py b/src/obfuscapk/obfuscation.py index 05883734..6c235ac7 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.tool import Apktool, Jarsigner, Zipalign +from obfuscapk.tool import Apktool, Zipalign, ApkSigner class Obfuscation(object): @@ -510,8 +510,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 jarsigner. - jarsigner: Jarsigner = Jarsigner() + # The obfuscated apk will be signed with apksigner. + apksigner: ApkSigner = ApkSigner() # If a custom keystore file is not provided, use the default one bundled with # the tool. Otherwise check that the keystore password and a key alias are @@ -537,7 +537,7 @@ def sign_obfuscated_apk(self) -> None: ) try: - jarsigner.resign( + apksigner.resign( self.obfuscated_apk_path, self.keystore_file, self.keystore_password, diff --git a/src/obfuscapk/tool.py b/src/obfuscapk/tool.py index caf7d781..6ff49304 100644 --- a/src/obfuscapk/tool.py +++ b/src/obfuscapk/tool.py @@ -188,27 +188,98 @@ def build(self, source_dir_path: str, output_apk_path: str = None) -> str: raise -class Jarsigner(object): +class Zipalign(object): def __init__(self): self.logger = logging.getLogger( "{0}.{1}".format(__name__, self.__class__.__name__) ) - if "JARSIGNER_PATH" in os.environ: - self.jarsigner_path: str = os.environ["JARSIGNER_PATH"] + if "ZIPALIGN_PATH" in os.environ: + self.zipalign_path: str = os.environ["ZIPALIGN_PATH"] else: - self.jarsigner_path: str = "jarsigner" + self.zipalign_path: str = "zipalign" - full_jarsigner_path = shutil.which(self.jarsigner_path) + full_zipalign_path = shutil.which(self.zipalign_path) # Make sure to use the full path of the executable (needed for cross-platform # compatibility). - if full_jarsigner_path is None: + if full_zipalign_path is None: raise RuntimeError( - 'Something is wrong with executable "{0}"'.format(self.jarsigner_path) + 'Something is wrong with executable "{0}"'.format(self.zipalign_path) ) else: - self.jarsigner_path = full_jarsigner_path + self.zipalign_path = full_zipalign_path + + def align(self, apk_path: str) -> str: + + # Check if the apk file to align is a valid file. + if not os.path.isfile(apk_path): + self.logger.error('Unable to find file "{0}"'.format(apk_path)) + raise FileNotFoundError('Unable to find file "{0}"'.format(apk_path)) + + # Since zipalign cannot be run inplace, a temp file will be created. + apk_copy_path = "{0}.copy.apk".format( + os.path.join( + os.path.dirname(apk_path), + os.path.splitext(os.path.basename(apk_path))[0], + ) + ) + + try: + apk_copy_path = shutil.copy2(apk_path, apk_copy_path) + + align_cmd = [ + self.zipalign_path, + "-p", + "-v", + "-f", + "4", + apk_copy_path, + apk_path, + ] + + self.logger.info('Running align command "{0}"'.format(" ".join(align_cmd))) + output = subprocess.check_output( + align_cmd, stderr=subprocess.STDOUT + ).strip() + return output.decode(errors="replace") + except subprocess.CalledProcessError as e: + self.logger.error( + "Error during align command: {0}".format( + e.output.decode(errors="replace") if e.output else e + ) + ) + raise + except Exception as e: + self.logger.error("Error during aligning: {0}".format(e)) + raise + finally: + # Remove the temp file used for zipalign. + if os.path.isfile(apk_copy_path): + os.remove(apk_copy_path) + + +class ApkSigner(object): + def __init__(self): + self.logger = logging.getLogger( + "{0}.{1}".format(__name__, self.__class__.__name__) + ) + + if "APKSIGNER_PATH" in os.environ: + self.apksigner_path: str = os.environ["APKSIGNER_PATH"] + else: + self.apksigner_path: str = "apksigner" + + full_apksigner_path = shutil.which(self.apksigner_path) + + # Make sure to use the full path of the executable (needed for cross-platform + # compatibility). + if full_apksigner_path is None: + raise RuntimeError( + 'Something is wrong with executable "{0}"'.format(self.apksigner_path) + ) + else: + self.apksigner_path = full_apksigner_path def sign( self, @@ -225,24 +296,21 @@ def sign( raise FileNotFoundError('Unable to find file "{0}"'.format(apk_path)) sign_cmd: List[str] = [ - self.jarsigner_path, - "-tsa", - "http://timestamp.comodoca.com/rfc3161", - "-sigalg", - "SHA1withRSA", - "-digestalg", - "SHA1", - "-keystore", + self.apksigner_path, + "sign", + "-v", + "--ks", keystore_file_path, - "-storepass", - keystore_password, - apk_path, + "--ks-key-alias", key_alias, + "--ks-pass", + f"pass:{keystore_password}", + apk_path, ] if key_password: - sign_cmd.insert(-2, "-keypass") - sign_cmd.insert(-2, key_password) + sign_cmd.insert(-1, "--key-pass") + sign_cmd.insert(-1, f"pass:{key_password}") try: self.logger.info('Running sign command "{0}"'.format(" ".join(sign_cmd))) @@ -310,66 +378,3 @@ def resign( return self.sign( apk_path, keystore_file_path, keystore_password, key_alias, key_password ) - - -class Zipalign(object): - def __init__(self): - self.logger = logging.getLogger( - "{0}.{1}".format(__name__, self.__class__.__name__) - ) - - if "ZIPALIGN_PATH" in os.environ: - self.zipalign_path: str = os.environ["ZIPALIGN_PATH"] - else: - self.zipalign_path: str = "zipalign" - - full_zipalign_path = shutil.which(self.zipalign_path) - - # Make sure to use the full path of the executable (needed for cross-platform - # compatibility). - if full_zipalign_path is None: - raise RuntimeError( - 'Something is wrong with executable "{0}"'.format(self.zipalign_path) - ) - else: - self.zipalign_path = full_zipalign_path - - def align(self, apk_path: str) -> str: - - # Check if the apk file to align is a valid file. - if not os.path.isfile(apk_path): - self.logger.error('Unable to find file "{0}"'.format(apk_path)) - raise FileNotFoundError('Unable to find file "{0}"'.format(apk_path)) - - # Since zipalign cannot be run inplace, a temp file will be created. - apk_copy_path = "{0}.copy.apk".format( - os.path.join( - os.path.dirname(apk_path), - os.path.splitext(os.path.basename(apk_path))[0], - ) - ) - - try: - apk_copy_path = shutil.copy2(apk_path, apk_copy_path) - - align_cmd = [self.zipalign_path, "-v", "-f", "4", apk_copy_path, apk_path] - - self.logger.info('Running align command "{0}"'.format(" ".join(align_cmd))) - output = subprocess.check_output( - align_cmd, stderr=subprocess.STDOUT - ).strip() - return output.decode(errors="replace") - except subprocess.CalledProcessError as e: - self.logger.error( - "Error during align command: {0}".format( - e.output.decode(errors="replace") if e.output else e - ) - ) - raise - except Exception as e: - self.logger.error("Error during aligning: {0}".format(e)) - raise - finally: - # Remove the temp file used for zipalign. - if os.path.isfile(apk_copy_path): - os.remove(apk_copy_path) diff --git a/src/test/test_cli.py b/src/test/test_cli.py index 2d219c7d..42e490fe 100644 --- a/src/test/test_cli.py +++ b/src/test/test_cli.py @@ -25,7 +25,7 @@ def test_valid_basic_command_without_quotes( # Mock the command line parser. arguments = cli.get_cmd_args( "-w {working_dir} -d {destination} " - "-o Rebuild -o NewSignature -o NewAlignment {apk_file}".format( + "-o Rebuild -o NewAlignment -o NewSignature {apk_file}".format( working_dir=tmp_working_directory_path, destination=obfuscated_apk_path, apk_file=tmp_demo_apk_v10_original_path, diff --git a/src/test/test_obfuscation.py b/src/test/test_obfuscation.py index a584ce6b..9c9f3174 100644 --- a/src/test/test_obfuscation.py +++ b/src/test/test_obfuscation.py @@ -49,8 +49,8 @@ def test_perform_full_obfuscation_valid_apk( "Reorder", "RandomManifest", "Rebuild", - "NewSignature", "NewAlignment", + "NewSignature", ], tmp_working_directory_path, obfuscated_apk_path, diff --git a/src/test/test_tool.py b/src/test/test_tool.py index 9b48d282..e02b1783 100644 --- a/src/test/test_tool.py +++ b/src/test/test_tool.py @@ -5,7 +5,7 @@ import pytest -from obfuscapk.tool import Apktool, Jarsigner, Zipalign +from obfuscapk.tool import Apktool, ApkSigner, Zipalign # noinspection PyUnresolvedReferences from test.test_fixtures import ( @@ -109,23 +109,23 @@ def mock(*args, **kwargs): Apktool().build(tmp_demo_apk_v10_decoded_files_directory_path) -class TestJarsigner(object): - def test_jarsigner_valid_path(self): - jarsigner = Jarsigner() - assert os.path.isfile(jarsigner.jarsigner_path) +class TestApkSigner(object): + def test_apksigner_valid_path(self): + apksigner = ApkSigner() + assert os.path.isfile(apksigner.apksigner_path) output = subprocess.check_output( - jarsigner.jarsigner_path, stderr=subprocess.STDOUT + apksigner.apksigner_path, stderr=subprocess.STDOUT ).decode() - assert "usage: jarsigner" in output.lower() + assert "usage: apksigner" in output.lower() - def test_jarsigner_wrong_path(self, monkeypatch): - monkeypatch.setenv("JARSIGNER_PATH", "invalid.jarsigner.path") + def test_apksigner_wrong_path(self, monkeypatch): + monkeypatch.setenv("APKSIGNER_PATH", "invalid.apksigner.path") with pytest.raises(RuntimeError): - Jarsigner() + ApkSigner() def test_resign_valid_apk(self, tmp_demo_apk_v10_rebuild_path: str): - output = Jarsigner().resign( + output = ApkSigner().resign( tmp_demo_apk_v10_rebuild_path, os.path.join( os.path.dirname(__file__), @@ -137,7 +137,7 @@ def test_resign_valid_apk(self, tmp_demo_apk_v10_rebuild_path: str): "obfuscation_password", "obfuscation_key", ) - assert "jar signed" in output.lower() + assert "signed" in output.lower() def test_resign_error_generic( self, tmp_demo_apk_v10_original_path: str, monkeypatch @@ -148,7 +148,7 @@ def mock(*args, **kwargs): monkeypatch.setattr("subprocess.check_output", mock) with pytest.raises(Exception): - Jarsigner().resign( + ApkSigner().resign( tmp_demo_apk_v10_original_path, "ignore", "ignore", "ignore" ) @@ -161,13 +161,13 @@ def mock(*args, **kwargs): monkeypatch.setattr("zipfile.ZipFile", mock) with pytest.raises(Exception): - Jarsigner().resign( + ApkSigner().resign( tmp_demo_apk_v10_original_path, "ignore", "ignore", "ignore" ) def test_sign_error_invalid_apk_path(self): with pytest.raises(FileNotFoundError): - Jarsigner().sign("invalid.apk.path", "ignore", "ignore", "ignore") + ApkSigner().sign("invalid.apk.path", "ignore", "ignore", "ignore") def test_sign_error_invalid_file(self, tmp_working_directory_path: str): invalid_file_path = os.path.join(tmp_working_directory_path, "invalid.apk") @@ -176,11 +176,11 @@ def test_sign_error_invalid_file(self, tmp_working_directory_path: str): invalid_file.write("This is not an apk file\n") with pytest.raises(subprocess.CalledProcessError): - Jarsigner().sign(invalid_file_path, "ignore", "ignore", "ignore") + ApkSigner().sign(invalid_file_path, "ignore", "ignore", "ignore") def test_sign_error_invalid_key_password(self, tmp_demo_apk_v10_rebuild_path: str): with pytest.raises(subprocess.CalledProcessError): - Jarsigner().sign( + ApkSigner().sign( tmp_demo_apk_v10_rebuild_path, os.path.join( os.path.dirname(__file__),