From 6571b66c091cba634f32f92f07d2747cfd28ab5f Mon Sep 17 00:00:00 2001 From: Jeroen Beckers Date: Wed, 23 Jun 2021 07:01:32 -0400 Subject: [PATCH 01/38] Various updates --- patch-apk.py | 46 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/patch-apk.py b/patch-apk.py index f666cd7..77dd656 100755 --- a/patch-apk.py +++ b/patch-apk.py @@ -12,11 +12,12 @@ # Main() #################### def main(): - #Check that dependencies are available - checkDependencies() #Grab argz args = getArgs() + + #Check that dependencies are available + checkDependencies(args.extract_only) #Verify the package name and ensure it's installed (also supports partial package names) pkgname = verifyPackageName(args.pkgname) @@ -27,13 +28,18 @@ def main(): #Create a temp directory to work from with tempfile.TemporaryDirectory() as tmppath: #Get the APK to patch. Combine app bundles/split APKs into a single APK. - apkfile = getTargetAPK(pkgname, apkpaths, tmppath, args.disable_styles_hack) + apkfile = getTargetAPK(pkgname, apkpaths, tmppath, args.disable_styles_hack, args.extract_only) #Save the APK if requested - if args.save_apk is not None: - print("Saving a copy of the APK to " + args.save_apk) + if args.save_apk is not None or args.extract_only: + targetName = args.save_apk if args.save_apk is not None else pkgname + ".apk" + print("Saving a copy of the APK to " + targetName) print("") - shutil.copy(apkfile, args.save_apk) + shutil.copy(apkfile, targetName) + + if args.extract_only: + os.remove(apkfile) + return #Patch the target APK with objection print("Patching " + apkfile.split(os.sep)[-1] + " with objection.") @@ -78,8 +84,12 @@ def main(): # -> Android device connected # -> Keystore #################### -def checkDependencies(): - deps = ["adb", "apktool", "jarsigner", "objection", "zipalign"] +def checkDependencies(extract_only): + deps = ["adb", "apktool"] + + if not extract_only: + deps += ["objection", "aapt", "jarsigner", "zipalign"] + missing = [] for dep in deps: if shutil.which(dep) is None: @@ -111,10 +121,11 @@ def getArgs(): if not hasattr(getArgs, "parsed_args"): #Parse the command line parser = argparse.ArgumentParser( - description="patch-apk - Pull and patch Android apps for use with objection/frida." + description="patch-apk - Pull and patch Android apps for use with objection/frida. Supports split APKs." ) parser.add_argument("--no-enable-user-certs", help="Prevent patch-apk from enabling user-installed certificate support via network security config in the patched APK.", action="store_true") - parser.add_argument("--save-apk", help="Save a copy of the APK (or single APK) prior to patching for use with other tools.") + parser.add_argument("--save-apk", help="Save a copy of the APK (or single APK) prior to patching for use with other tools. APK will be saved under the given name.") + parser.add_argument("--extract-only", help="Disable including objection and pushing modified APK to device.", action="store_true") parser.add_argument("--disable-styles-hack", help="Disable the styles hack that removes duplicate entries from res/values/styles.xml.", action="store_true") parser.add_argument("--debug-output", help="Enable debug output.", action="store_true") parser.add_argument("pkgname", help="The name, or partial name, of the package to patch (e.g. com.foo.bar).") @@ -236,7 +247,7 @@ def getAPKPathsForPackage(pkgname): # Pull the APK file(s) for the package and return the local file path to work with. # If the package is an app bundle/split APK, combine the APKs into a single APK. #################### -def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack): +def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack, extract_only): #Pull the APKs from the device print("Pulling APK file(s) from device.") localapks = [] @@ -255,12 +266,12 @@ def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack): return localapks[0] else: #Combine split APKs - return combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack) + return combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_only) #################### # Combine app bundles/split APKs into a single APK for patching. #################### -def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack): +def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_only): print("App bundle/split APK detected, rebuilding as a single APK.") print("") @@ -320,6 +331,10 @@ def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack): print("Error: Failed to run 'apktool b " + baseapkdir + "'.\nRun with --debug-output for more information.") sys.exit(1) + # If only extracting, no need to sign / zipalign + if extract_only: + return os.path.join(baseapkdir, "dist", baseapkfilename) + #Sign the new APK print("[+] Signing new APK.") @@ -643,5 +658,8 @@ def enableUserCerts(apkfile): # Main #################### if __name__ == "__main__": - main() + try: + main() + except KeyboardInterrupt: + sys.exit() From 2a7e55733bb68faa428e2f8abb1864865c56f931 Mon Sep 17 00:00:00 2001 From: Jack Seigel Date: Thu, 25 Nov 2021 00:06:29 -0500 Subject: [PATCH 02/38] Fix issue #30 with build function --- patch-apk.py | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/patch-apk.py b/patch-apk.py index 77dd656..424b98f 100755 --- a/patch-apk.py +++ b/patch-apk.py @@ -183,6 +183,29 @@ def runApkTool(params): args.extend(params) return subprocess.run(args, stdout=getStdout()) +#################### +# Build the APK +#################### +def build(baseapkdir): + if os.path.exists(os.path.join(baseapkdir, "res", "navigation")) == True: + print("[+] Found res/navigation directory, rebuilding with 'apktool --use-aapt2'.") + ret = runApkTool(["--use-aapt2", "b", baseapkdir]) + if ret.returncode != 0: + print("Error: Failed to run 'apktool b " + baseapkdir + "'.\nRun with --debug-output for more information.") + sys.exit(1) + elif getApktoolVersion() > pkg_resources.parse_version("2.4.2"): + print("[+] Found apktool version > 2.4.2, rebuilding with 'apktool --use-aapt2'.") + ret = runApkTool(["--use-aapt2", "b", baseapkdir]) + if ret.returncode != 0: + print("Error: Failed to run 'apktool b " + baseapkdir + "'.\nRun with --debug-output for more information.") + sys.exit(1) + else: + print("[+] Building APK with apktool.") + ret = runApkTool(["b", baseapkdir]) + if ret.returncode != 0: + print("Error: Failed to run 'apktool b " + baseapkdir + "'.\nRun with --debug-output for more information.") + sys.exit(1) + #################### # Verify the package name - checks whether the target package is installed # on the device or if an exact match is not found presents the options to @@ -312,24 +335,7 @@ def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_onl #Rebuild the base APK print("Rebuilding as a single APK.") - if os.path.exists(os.path.join(baseapkdir, "res", "navigation")) == True: - print("[+] Found res/navigation directory, rebuilding with 'apktool --use-aapt2'.") - ret = runApkTool(["--use-aapt2", "b", baseapkdir]) - if ret.returncode != 0: - print("Error: Failed to run 'apktool b " + baseapkdir + "'.\nRun with --debug-output for more information.") - sys.exit(1) - elif getApktoolVersion() > pkg_resources.parse_version("2.4.2"): - print("[+] Found apktool version > 2.4.2, rebuilding with 'apktool --use-aapt2'.") - ret = runApkTool(["--use-aapt2", "b", baseapkdir]) - if ret.returncode != 0: - print("Error: Failed to run 'apktool b " + baseapkdir + "'.\nRun with --debug-output for more information.") - sys.exit(1) - else: - print("[+] Building APK with apktool.") - ret = runApkTool(["b", baseapkdir]) - if ret.returncode != 0: - print("Error: Failed to run 'apktool b " + baseapkdir + "'.\nRun with --debug-output for more information.") - sys.exit(1) + build(baseapkdir) # If only extracting, no need to sign / zipalign if extract_only: @@ -630,10 +636,7 @@ def enableUserCerts(apkfile): fh.close() #Rebuild and sign the APK - ret = runApkTool(["b", apkdir]) - if ret.returncode != 0: - print("Error: Failed to run 'apktool b " + apkdir + "'.\nRun with --debug-output for more information.") - sys.exit(1) + build(apkdir) #Fix https://github.com/NickstaDB/patch-apk/issues/30 ret = subprocess.run([ "jarsigner", "-sigalg", "SHA1withRSA", "-digestalg", "SHA1", "-keystore", os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore")), From 4c7cd49d5d5c86d450a3e3de768cd1a7048deaff Mon Sep 17 00:00:00 2001 From: Jack Seigel Date: Thu, 25 Nov 2021 00:07:36 -0500 Subject: [PATCH 03/38] Add regex file patching and fix issue #31 --- patch-apk.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/patch-apk.py b/patch-apk.py index 424b98f..70934dc 100755 --- a/patch-apk.py +++ b/patch-apk.py @@ -7,6 +7,7 @@ import sys import tempfile import xml.etree.ElementTree +import re #################### # Main() @@ -341,6 +342,8 @@ def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_onl if extract_only: return os.path.join(baseapkdir, "dist", baseapkfilename) + #Fix https://github.com/NickstaDB/patch-apk/issues/31 + rawREReplace(os.path.join(baseapkdir, "apktool.yml"), r"(?<=targetSdkVersion: ')\d+", lambda m: '29' if int(m.group()) >= 30 else m.group()) #Sign the new APK print("[+] Signing new APK.") @@ -605,6 +608,20 @@ def disableApkSplitting(baseapkdir): tree.write(os.path.join(baseapkdir, "AndroidManifest.xml"), encoding="utf-8", xml_declaration=True) print("") +#################### +# Replace occurrences of a pattern in a file with a replacement pattern or function (a la re.sub) +#################### +def rawREReplace(path, pattern, replacement): + if os.path.exists(path): + contents = "" + with open(path, 'r') as file: + contents = file.read() + with open(path, 'w') as file: + file.write(re.sub(pattern, replacement, contents)) + else: + print("Error: Failed to find file at " + path + " for pattern replacement") + sys.exit(1) + #################### # Patch an APK to enable support for user-installed CA certs (e.g. Burp Suite CA cert). #################### From 99c40d5c08b85e74e366a004ae957e51afd30497 Mon Sep 17 00:00:00 2001 From: Jack Seigel Date: Thu, 25 Nov 2021 00:08:09 -0500 Subject: [PATCH 04/38] Fix untracked apktool xml strings escape bug --- patch-apk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/patch-apk.py b/patch-apk.py index 70934dc..9f68dd6 100755 --- a/patch-apk.py +++ b/patch-apk.py @@ -333,6 +333,9 @@ def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_onl #Disable APK splitting in the base AndroidManifest.xml file disableApkSplitting(baseapkdir) + + #Fix untracked apktool bug where improperly escaped ampersands (& instead of &) appear in strings.xml + rawREReplace(os.path.join(baseapkdir, "res", "values", "strings.xml"), r'(&)([^;])', r'\1;\2') #Rebuild the base APK print("Rebuilding as a single APK.") From dd61e398832c5d049fa59f65c24f5d75da2991d2 Mon Sep 17 00:00:00 2001 From: Jack Seigel Date: Thu, 25 Nov 2021 01:29:44 -0500 Subject: [PATCH 05/38] Add apksigner to deps --- patch-apk.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/patch-apk.py b/patch-apk.py index 9f68dd6..dafe1b6 100755 --- a/patch-apk.py +++ b/patch-apk.py @@ -89,7 +89,7 @@ def checkDependencies(extract_only): deps = ["adb", "apktool"] if not extract_only: - deps += ["objection", "aapt", "jarsigner", "zipalign"] + deps += ["objection", "aapt", "jarsigner", "zipalign", "apksigner"] missing = [] for dep in deps: @@ -620,7 +620,9 @@ def rawREReplace(path, pattern, replacement): with open(path, 'r') as file: contents = file.read() with open(path, 'w') as file: - file.write(re.sub(pattern, replacement, contents)) + output = re.sub(pattern, replacement, contents) + print(output) + file.write(output) else: print("Error: Failed to find file at " + path + " for pattern replacement") sys.exit(1) From fc905a04a91d9f6847d33e46c085cf6f12fb7af0 Mon Sep 17 00:00:00 2001 From: Jack Seigel Date: Thu, 25 Nov 2021 01:37:43 -0500 Subject: [PATCH 06/38] disclaimer --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3710c88..e99d879 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# ONLY TESTED WITH APKTOOL 2.5.0 # + # patch-apk - App Bundle/Split APK Aware Patcher for Objection # An APK patcher, for use with [objection](https://github.com/sensepost/objection), that supports Android app bundles/split APKs. It automates the following: From 2e4cb1fe033b5c6e093e3bf23ce35e51a874ad0c Mon Sep 17 00:00:00 2001 From: Jack Seigel Date: Thu, 25 Nov 2021 02:35:22 -0500 Subject: [PATCH 07/38] Fix aapt dependency --- patch-apk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patch-apk.py b/patch-apk.py index dafe1b6..f52e38d 100755 --- a/patch-apk.py +++ b/patch-apk.py @@ -86,10 +86,10 @@ def main(): # -> Keystore #################### def checkDependencies(extract_only): - deps = ["adb", "apktool"] + deps = ["adb", "apktool", "aapt"] if not extract_only: - deps += ["objection", "aapt", "jarsigner", "zipalign", "apksigner"] + deps += ["objection", "jarsigner", "zipalign", "apksigner"] missing = [] for dep in deps: From 96a8c8214f4d88af3d6a3d7c8cb40294937aaf21 Mon Sep 17 00:00:00 2001 From: Jack Seigel Date: Thu, 25 Nov 2021 05:05:51 -0500 Subject: [PATCH 08/38] small cosmetic changes --- README.md | 2 +- patch-apk.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e99d879..1196fba 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ONLY TESTED WITH APKTOOL 2.5.0 # +# MODIFIED FOR APKTOOL 2.5.0 # # patch-apk - App Bundle/Split APK Aware Patcher for Objection # An APK patcher, for use with [objection](https://github.com/sensepost/objection), that supports Android app bundles/split APKs. It automates the following: diff --git a/patch-apk.py b/patch-apk.py index f52e38d..3a0c875 100755 --- a/patch-apk.py +++ b/patch-apk.py @@ -620,9 +620,7 @@ def rawREReplace(path, pattern, replacement): with open(path, 'r') as file: contents = file.read() with open(path, 'w') as file: - output = re.sub(pattern, replacement, contents) - print(output) - file.write(output) + file.write(re.sub(pattern, replacement, contents)) else: print("Error: Failed to find file at " + path + " for pattern replacement") sys.exit(1) From 5270b31ee3aa4bf794afd66ae24c8762fed61889 Mon Sep 17 00:00:00 2001 From: Jack Seigel Date: Thu, 25 Nov 2021 06:29:55 -0500 Subject: [PATCH 09/38] Add subprocess run helper and remove some prints --- patch-apk.py | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/patch-apk.py b/patch-apk.py index 3a0c875..b312994 100755 --- a/patch-apk.py +++ b/patch-apk.py @@ -46,12 +46,9 @@ def main(): print("Patching " + apkfile.split(os.sep)[-1] + " with objection.") ret = None if getObjectionVersion() >= pkg_resources.parse_version("1.9.3"): - ret = subprocess.run(["objection", "patchapk", "--skip-resources", "--ignore-nativelibs", "-s", apkfile], stdout=getStdout()) + assertSubprocessSuccessfulRun(["objection", "patchapk", "--skip-resources", "--ignore-nativelibs", "-s", apkfile], getStdout()) else: - ret = subprocess.run(["objection", "patchapk", "--skip-resources", "-s", apkfile], stdout=getStdout()) - if ret.returncode != 0: - print("Error: Failed to run 'objection patchapk --skip-resources -s " + apkfile + "'.\nRun with --debug-output for more information.") - sys.exit(1) + assertSubprocessSuccessfulRun(["objection", "patchapk", "--skip-resources", "-s", apkfile], getStdout()) os.remove(apkfile) shutil.move(apkfile[:-4] + ".objection.apk", apkfile) print("") @@ -61,23 +58,16 @@ def main(): enableUserCerts(apkfile) #Uninstall the original package from the device - print("Uninstalling the original package from the device.") - ret = subprocess.run(["adb", "uninstall", pkgname], stdout=getStdout()) - if ret.returncode != 0: - print("Error: Failed to run 'adb uninstall " + pkgname + "'.\nRun with --debug-output for more information.") - sys.exit(1) - print("") + assertSubprocessSuccessfulRun(["adb", "uninstall", pkgname], getStdout()) #Install the patched APK - print("Installing the patched APK to the device.") - ret = subprocess.run(["adb", "install", apkfile], stdout=getStdout()) - if ret.returncode != 0: - print("Error: Failed to run 'adb install " + apkfile + "'.\nRun with --debug-output for more information.") - sys.exit(1) - print("") + assertSubprocessSuccessfulRun(["adb", "install", apkfile], getStdout()) #Done - print("Done, cleaning up temporary files.") +def assertSubprocessSuccessfulRun(args, stdout=subprocess.PIPE): + if subprocess.run(args, stdout).returncode != 0: + print(f"Error: Failed to run {' '.join(args)}.\nRun with --debug-output for more information.") + sys.exit(1) #################### # Check that required dependencies are present: @@ -279,11 +269,7 @@ def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack, extract_only): baseapkname = remotepath.split('/')[-1] localapks.append(os.path.join(tmppath, pkgname + "-" + baseapkname)) print("[+] Pulling: " + pkgname + "-" + baseapkname) - ret = subprocess.run(["adb", "pull", remotepath, localapks[-1]], stdout=getStdout()) - if ret.returncode != 0: - print("Error: Failed to run 'adb pull " + remotepath + " " + localapks[-1] + "'.\nRun with --debug-output for more information.") - sys.exit(1) - print("") + assertSubprocessSuccessfulRun(["adb", "pull", remotepath, localapks[-1]], getStdout()) #Return the target APK path if len(localapks) == 1: From 406fc530afd2e9a4a7645461849c62dd5682f5a6 Mon Sep 17 00:00:00 2001 From: Jack Seigel Date: Thu, 25 Nov 2021 06:32:51 -0500 Subject: [PATCH 10/38] signAndZipAlign, jarsigner -> apksigner, prints --- patch-apk.py | 70 +++++++++++++++------------------------------------- 1 file changed, 20 insertions(+), 50 deletions(-) diff --git a/patch-apk.py b/patch-apk.py index b312994..f023242 100755 --- a/patch-apk.py +++ b/patch-apk.py @@ -79,7 +79,7 @@ def checkDependencies(extract_only): deps = ["adb", "apktool", "aapt"] if not extract_only: - deps += ["objection", "jarsigner", "zipalign", "apksigner"] + deps += ["objection", "zipalign", "apksigner"] missing = [] for dep in deps: @@ -197,6 +197,23 @@ def build(baseapkdir): print("Error: Failed to run 'apktool b " + baseapkdir + "'.\nRun with --debug-output for more information.") sys.exit(1) +#################### +# Sign the APK with apksigner and zip align +# Fixes https://github.com/NickstaDB/patch-apk/issues/31 by no longer using jarsigner (V1 APK signatures) +#################### +def signAndZipAlign(baseapkdir, baseapkfilename): + #Sign the new APK + print("[+] Signing new APK.") + assertSubprocessSuccessfulRun(["apksigner", "sign", "--ks", + os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore")), "--ks-pass", + "pass:patch-apk", "--ks-key-alias", "patch-apk-key", os.path.join(baseapkdir, "dist", baseapkfilename)], + getStdout()) + + #Zip align the new APK + print("[+] Zip aligning new APK.") + assertSubprocessSuccessfulRun(["zipalign", "-f", "4", os.path.join(baseapkdir, "dist", baseapkfilename), + os.path.join(baseapkdir, "dist", baseapkfilename[:-4] + "-aligned.apk")], getStdout()) + #################### # Verify the package name - checks whether the target package is installed # on the device or if an exact match is not found presents the options to @@ -331,38 +348,9 @@ def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_onl if extract_only: return os.path.join(baseapkdir, "dist", baseapkfilename) - #Fix https://github.com/NickstaDB/patch-apk/issues/31 - rawREReplace(os.path.join(baseapkdir, "apktool.yml"), r"(?<=targetSdkVersion: ')\d+", lambda m: '29' if int(m.group()) >= 30 else m.group()) - - #Sign the new APK - print("[+] Signing new APK.") - ret = subprocess.run([ - "jarsigner", "-sigalg", "SHA1withRSA", "-digestalg", "SHA1", "-keystore", - os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore")), - "-storepass", "patch-apk", os.path.join(baseapkdir, "dist", baseapkfilename), "patch-apk-key"], - stdout=getStdout() - ) - if ret.returncode != 0: - print("Error: Failed to run 'jarsigner -sigalg SHA1withRSA -digestalg SHA1 -keystore " + - os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore")) + - "-storepass patch-apk " + os.path.join(baseapkdir, "dist", baseapkfilename) + " patch-apk-key'.\nRun with --debug-output for more information.") - sys.exit(1) - + signAndZipAlign(baseapkdir, baseapkfilename) - #Zip align the new APK - print("[+] Zip aligning new APK.") - ret = subprocess.run([ - "zipalign", "-f", "4", os.path.join(baseapkdir, "dist", baseapkfilename), - os.path.join(baseapkdir, "dist", baseapkfilename[:-4] + "-aligned.apk") - ], - stdout=getStdout() - ) - if ret.returncode != 0: - print("Error: Failed to run 'zipalign -f 4 " + os.path.join(baseapkdir, "dist", baseapkfilename) + - " " + os.path.join(baseapkdir, "dist", baseapkfilename[:-4] + "-aligned.apk") + "'.\nRun with --debug-output for more information.") - sys.exit(1) shutil.move(os.path.join(baseapkdir, "dist", baseapkfilename[:-4] + "-aligned.apk"), os.path.join(baseapkdir, "dist", baseapkfilename)) - print("") #Return the new APK path return os.path.join(baseapkdir, "dist", baseapkfilename) @@ -643,25 +631,7 @@ def enableUserCerts(apkfile): #Rebuild and sign the APK build(apkdir) #Fix https://github.com/NickstaDB/patch-apk/issues/30 - ret = subprocess.run([ - "jarsigner", "-sigalg", "SHA1withRSA", "-digestalg", "SHA1", "-keystore", - os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore")), - "-storepass", "patch-apk", os.path.join(apkdir, "dist", apkname), "patch-apk-key"], - stdout=getStdout() - ) - if ret.returncode != 0: - print("Error: Failed to run 'jarsigner -sigalg SHA1withRSA -digestalg SHA1 -keystore " + - os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore")) + - "-storepass patch-apk " + os.path.join(apkdir, "dist", apkname) + "patch-apk-key'.\nRun with --debug-output for more information.") - sys.exit(1) - - #Zip align the new APK - os.remove(apkfile) - ret = subprocess.run(["zipalign", "4", os.path.join(apkdir, "dist", apkname), apkfile], stdout=getStdout()) - if ret.returncode != 0: - print("Error: Failed to run 'zipalign 4 " + os.path.join(apkdir, "dist", apkname) + " " + apkfile + "'.\nRun with --debug-output for more information.") - sys.exit(1) - print("") + signAndZipAlign(apkdir, apkname) #################### # Main From 238ef47299a06cef2b8efeb9def2ba82b5645cde Mon Sep 17 00:00:00 2001 From: Jack Seigel Date: Thu, 25 Nov 2021 06:33:02 -0500 Subject: [PATCH 11/38] Fix last of the prints --- patch-apk.py | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/patch-apk.py b/patch-apk.py index f023242..21a8305 100755 --- a/patch-apk.py +++ b/patch-apk.py @@ -34,8 +34,7 @@ def main(): #Save the APK if requested if args.save_apk is not None or args.extract_only: targetName = args.save_apk if args.save_apk is not None else pkgname + ".apk" - print("Saving a copy of the APK to " + targetName) - print("") + print("\nSaving a copy of the APK to " + targetName) shutil.copy(apkfile, targetName) if args.extract_only: @@ -43,27 +42,29 @@ def main(): return #Patch the target APK with objection - print("Patching " + apkfile.split(os.sep)[-1] + " with objection.") - ret = None + print("\nPatching " + apkfile.split(os.sep)[-1] + " with objection.") if getObjectionVersion() >= pkg_resources.parse_version("1.9.3"): assertSubprocessSuccessfulRun(["objection", "patchapk", "--skip-resources", "--ignore-nativelibs", "-s", apkfile], getStdout()) else: assertSubprocessSuccessfulRun(["objection", "patchapk", "--skip-resources", "-s", apkfile], getStdout()) os.remove(apkfile) shutil.move(apkfile[:-4] + ".objection.apk", apkfile) - print("") #Enable support for user-installed CA certs (e.g. Burp Suite CA installed on device by user) if args.no_enable_user_certs == False: enableUserCerts(apkfile) #Uninstall the original package from the device + print("\nUninstalling the original package from the device.") assertSubprocessSuccessfulRun(["adb", "uninstall", pkgname], getStdout()) #Install the patched APK + print("\nInstalling the patched APK to the device.") assertSubprocessSuccessfulRun(["adb", "install", apkfile], getStdout()) #Done + print("\nDone, cleaning up temporary files.") + def assertSubprocessSuccessfulRun(args, stdout=subprocess.PIPE): if subprocess.run(args, stdout).returncode != 0: print(f"Error: Failed to run {' '.join(args)}.\nRun with --debug-output for more information.") @@ -252,14 +253,13 @@ def verifyPackageName(pkgname): if choice.isnumeric() == False or int(choice) < 1 or int(choice) > len(packages): print("Invalid choice.\n") choice = -1 - print("") return packages[int(choice) - 1] #################### # Get the APK path(s) on the device for the given package name. #################### def getAPKPathsForPackage(pkgname): - print("Getting APK path(s) for package: " + pkgname) + print("\nGetting APK path(s) for package: " + pkgname) paths = [] proc = subprocess.run(["adb", "shell", "pm", "path", pkgname], stdout=subprocess.PIPE) if proc.returncode != 0: @@ -271,7 +271,6 @@ def getAPKPathsForPackage(pkgname): line = line[8:].strip() print("[+] APK path: " + line) paths.append(line) - print("") return paths #################### @@ -280,7 +279,7 @@ def getAPKPathsForPackage(pkgname): #################### def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack, extract_only): #Pull the APKs from the device - print("Pulling APK file(s) from device.") + print("\nPulling APK file(s) from device.") localapks = [] for remotepath in apkpaths: baseapkname = remotepath.split('/')[-1] @@ -299,11 +298,10 @@ def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack, extract_only): # Combine app bundles/split APKs into a single APK for patching. #################### def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_only): - print("App bundle/split APK detected, rebuilding as a single APK.") - print("") + print("\nApp bundle/split APK detected, rebuilding as a single APK.") #Extract the individual APKs - print("Extracting individual APKs with apktool.") + print("\nExtracting individual APKs with apktool.") baseapkdir = os.path.join(tmppath, pkgname + "-base") baseapkfilename = pkgname + "-base.apk" splitapkpaths = [] @@ -322,7 +320,6 @@ def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_onl #Check for ProGuard/AndResGuard - this might b0rk decompile/recompile if detectProGuard(apkdir): print("\n[~] WARNING: Detected ProGuard/AndResGuard, decompile/recompile may not succeed.\n") - print("") #Walk the extracted APK directories and copy files and directories to the base APK copySplitApkFiles(baseapkdir, splitapkpaths) @@ -347,7 +344,7 @@ def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack, extract_onl # If only extracting, no need to sign / zipalign if extract_only: return os.path.join(baseapkdir, "dist", baseapkfilename) - + signAndZipAlign(baseapkdir, baseapkfilename) shutil.move(os.path.join(baseapkdir, "dist", baseapkfilename[:-4] + "-aligned.apk"), os.path.join(baseapkdir, "dist", baseapkfilename)) @@ -373,7 +370,7 @@ def detectProGuard(extractedPath): # Copy files and directories from split APKs into the base APK directory. #################### def copySplitApkFiles(baseapkdir, splitapkpaths): - print("Copying files and directories from split APKs into base APK.") + print("\nCopying files and directories from split APKs into base APK.") for apkdir in splitapkpaths: for (root, dirs, files) in os.walk(apkdir): #Skip the original files directory @@ -400,7 +397,6 @@ def copySplitApkFiles(baseapkdir, splitapkpaths): continue dbgPrint("[+] Moving file to base APK: " + p[len(baseapkdir):]) shutil.move(os.path.join(root, f), p) - print("") #################### # Fix public resource identifiers that are shared across split APKs. @@ -412,7 +408,7 @@ def fixPublicResourceIDs(baseapkdir, splitapkpaths): #Bail if the base APK does not have a public.xml if os.path.exists(os.path.join(baseapkdir, "res", "values", "public.xml")) == False: return - print("Found public.xml in the base APK, fixing resource identifiers across split APKs.") + print("\nFound public.xml in the base APK, fixing resource identifiers across split APKs.") #Mappings of resource IDs and names idToDummyName = {} @@ -500,7 +496,6 @@ def fixPublicResourceIDs(baseapkdir, splitapkpaths): except xml.etree.ElementTree.ParseError: print("[-] XML parse error in " + os.path.join(root, f) + ", skipping.") print("[+] Updated " + str(updated) + " references to dummy resource names in the base APK.") - print("") #################### # Hack to remove duplicate style resource entries before rebuilding. @@ -518,7 +513,7 @@ def hackRemoveDuplicateStyleEntries(baseapkdir): #Bail if there is no styles.xml if os.path.exists(os.path.join(baseapkdir, "res", "values", "styles.xml")) == False: return - print("Found styles.xml in the base APK, checking for duplicate