123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645 |
- #!/usr/bin/env python
- # -*- encoding: utf-8 -*-
- '''
- @Contact : liuyuqi.gov@msn.cn
- @Time : 2022/11/23 05:24:06
- @License : Copyright © 2017-2022 liuyuqi. All Rights Reserved.
- @Desc :
- '''
- import argparse
- import os
- import pkg_resources
- import shutil
- import subprocess
- import sys
- import tempfile
- import xml.etree.ElementTree
- def main():
- checkDependencies()
- args = getArgs()
- pkgname = verifyPackageName(args.pkgname)
- apkpaths = getAPKPathsForPackage(pkgname)
- 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)
-
- #Save the APK if requested
- if args.save_apk is not None:
- print("Saving a copy of the APK to " + args.save_apk)
- print("")
- shutil.copy(apkfile, args.save_apk)
-
- #Patch the target APK with objection
- 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())
- 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)
- 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("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("")
-
- #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("")
-
- #Done
- print("Done, cleaning up temporary files.")
- ####################
- # Check that required dependencies are present:
- # -> Tools used
- # -> Android device connected
- # -> Keystore
- ####################
- def checkDependencies():
- deps = ["adb", "apktool", "jarsigner", "objection", "zipalign"]
- missing = []
- for dep in deps:
- if shutil.which(dep) is None:
- missing.append(dep)
- if len(missing) > 0:
- print("Error, missing dependencies, ensure the following commands are available on the PATH: " + (", ".join(missing)))
- sys.exit(1)
-
- #Verify that an Android device is connected
- proc = subprocess.run(["adb", "devices"], stdout=subprocess.PIPE)
- if proc.returncode != 0:
- print("Error: Failed to run 'adb devices'.")
- sys.exit(1)
- deviceOut = proc.stdout.decode("utf-8")
- if len(deviceOut.strip().split(os.linesep)) == 1:
- print("Error, no Android device connected (\"adb devices\"), connect a device first.")
- sys.exit(1)
-
- #Check that the included keystore exists
- if os.path.exists(os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore"))) == False:
- print("Error, the keystore was not found at " + os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore")) + ", please clone the repository or get the keystore file and place it at this location.")
- sys.exit(1)
- ####################
- # Grab command line parameters
- ####################
- def getArgs():
- #Only parse args once
- 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."
- )
- 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("--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).")
-
- #Store the parsed args
- getArgs.parsed_args = parser.parse_args()
-
- #Return the parsed command line args
- return getArgs.parsed_args
- ####################
- # Debug print
- ####################
- def dbgPrint(msg):
- if getArgs().debug_output == True:
- print(msg)
- ####################
- # Get the stdout target for subprocess calls. Set to DEVNULL unless debug output is enabled.
- ####################
- def getStdout():
- if getArgs().debug_output == True:
- return None
- else:
- return subprocess.DEVNULL
- ####################
- # Get objection version
- ####################
- def getObjectionVersion():
- proc = subprocess.run(["objection", "version"], stdout=subprocess.PIPE)
- return pkg_resources.parse_version(proc.stdout.decode("utf-8").strip().split(": ")[-1].strip())
- ####################
- # Get apktool version
- ####################
- def getApktoolVersion():
- output = ""
- if os.name == "nt":
- proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
- proc.communicate(b"\r\n")
- output = proc.stdout.decode("utf-8").strip()
- else:
- proc = subprocess.run(["apktool", "-version"], stdout=subprocess.PIPE)
- output = proc.stdout.decode("utf-8").strip()
- return pkg_resources.parse_version(output.split("-")[0].strip())
- ####################
- # Wrapper to run apktool platform-independently, complete with a dirty hack to fix apktool's dirty hack.
- ####################
- def runApkTool(params):
- if os.name == "nt":
- args = ["apktool.bat"]
- args.extend(params)
-
- #apktool.bat has a dirty hack that execute "pause", so we need a dirty hack to kill the pause command...
- proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=getStdout())
- proc.communicate(b"\r\n")
- return proc
- else:
- args = ["apktool"]
- args.extend(params)
- return subprocess.run(args, stdout=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
- # the user for selection.
- ####################
- def verifyPackageName(pkgname):
- #Get a list of installed packages matching the given name
- packages = []
- proc = subprocess.run(["adb", "shell", "pm", "list", "packages"], stdout=subprocess.PIPE)
- if proc.returncode != 0:
- print("Error: Failed to run 'adb shell pm list packages'.")
- sys.exit(1)
- out = proc.stdout.decode("utf-8")
- for line in out.split(os.linesep):
- if line.startswith("package:"):
- line = line[8:].strip()
- if pkgname.lower() in line.lower():
- packages.append(line)
-
- #Bail out if no matching packages were found
- if len(packages) == 0:
- print("Error, no packages found on the device matching the search term '" + pkgname + "'.")
- print("Run 'adb shell pm list packages' to verify installed package names.")
- sys.exit(1)
-
- #Return the target package name, offering a choice to the user if necessary
- if len(packages) == 1:
- return packages[0]
- else:
- print("Multiple matching packages installed, select the package to patch.")
- choice = -1
- while choice == -1:
- for i in range(len(packages)):
- print("[" + str(i + 1) + "] " + packages[i])
- choice = input("Choice: ")
- 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)
- paths = []
- proc = subprocess.run(["adb", "shell", "pm", "path", pkgname], stdout=subprocess.PIPE)
- if proc.returncode != 0:
- print("Error: Failed to run 'adb shell pm path " + pkgname + "'.")
- sys.exit(1)
- out = proc.stdout.decode("utf-8")
- for line in out.split(os.linesep):
- if line.startswith("package:"):
- line = line[8:].strip()
- print("[+] APK path: " + line)
- paths.append(line)
- print("")
- return paths
- ####################
- # 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):
- #Pull the APKs from the device
- print("Pulling APK file(s) from device.")
- localapks = []
- for remotepath in apkpaths:
- 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("")
-
- #Return the target APK path
- if len(localapks) == 1:
- return localapks[0]
- else:
- #Combine split APKs
- return combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack)
- ####################
- # Combine app bundles/split APKs into a single APK for patching.
- ####################
- def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack):
- print("App bundle/split APK detected, rebuilding as a single APK.")
- print("")
-
- #Extract the individual APKs
- print("Extracting individual APKs with apktool.")
- baseapkdir = os.path.join(tmppath, pkgname + "-base")
- baseapkfilename = pkgname + "-base.apk"
- splitapkpaths = []
- for apkpath in localapks:
- print("[+] Extracting: " + apkpath)
- apkdir = apkpath[:-4]
- ret = runApkTool(["d", apkpath, "-o", apkdir])
- if ret.returncode != 0:
- print("Error: Failed to run 'apktool d " + apkpath + " -o " + apkdir + "'.\nRun with --debug-output for more information.")
- sys.exit(1)
-
- #Record the destination paths of all but the base APK
- if apkpath.endswith("base.apk") == False:
- splitapkpaths.append(apkdir)
-
- #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)
-
- #Fix public resource identifiers
- fixPublicResourceIDs(baseapkdir, splitapkpaths)
-
- #Hack: Delete duplicate style resource entries.
- if disableStylesHack == False:
- hackRemoveDuplicateStyleEntries(baseapkdir)
-
- #Disable APK splitting in the base AndroidManifest.xml file
- disableApkSplitting(baseapkdir)
-
- #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)
-
-
- #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)
-
- #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)
- ####################
- # Attempt to detect ProGuard/AndResGuard.
- ####################
- def detectProGuard(extractedPath):
- if os.path.exists(os.path.join(extractedPath, "original", "META-INF", "proguard")) == True:
- return True
- if os.path.exists(os.path.join(extractedPath, "original", "META-INF", "MANIFEST.MF")) == True:
- fh = open(os.path.join(extractedPath, "original", "META-INF", "MANIFEST.MF"))
- d = fh.read()
- fh.close()
- if "proguard" in d.lower():
- return True
- return False
- ####################
- # 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.")
- for apkdir in splitapkpaths:
- for (root, dirs, files) in os.walk(apkdir):
- #Skip the original files directory
- if root.startswith(os.path.join(apkdir, "original")) == False:
- #Create any missing directories
- for d in dirs:
- #Translate directory path to base APK path and create the directory if it doesn't exist
- p = baseapkdir + os.path.join(root, d)[len(apkdir):]
- if os.path.exists(p) == False:
- dbgPrint("[+] Creating directory in base APK: " + p[len(baseapkdir):])
- os.mkdir(p)
-
- #Copy files into the base APK
- for f in files:
- #Skip the AndroidManifest.xml and apktool.yml in the APK root directory
- if apkdir == root and (f == "AndroidManifest.xml" or f == "apktool.yml"):
- continue
-
- #Translate path to base APK
- p = baseapkdir + os.path.join(root, f)[len(apkdir):]
-
- #Copy files into the base APK, except for XML files in the res directory
- if f.lower().endswith(".xml") and p.startswith(os.path.join(baseapkdir, "res")):
- 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.
- # Maps all APKTOOL_DUMMY_ resource IDs in the base APK to the proper resource names from the
- # split APKs, then updates references in other resource files in the base APK to use proper
- # resource names.
- ####################
- 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.")
-
- #Mappings of resource IDs and names
- idToDummyName = {}
- dummyNameToRealName = {}
-
- #Step 1) Find all resource IDs that apktool has assigned a name of APKTOOL_DUMMY_XXX to.
- # Load these into the lookup tables ready to resolve the real resource names from
- # the split APKs in step 2 below.
- baseXmlTree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, "res", "values", "public.xml"))
- for el in baseXmlTree.getroot():
- if "name" in el.attrib and "id" in el.attrib:
- if el.attrib["name"].startswith("APKTOOL_DUMMY_") and el.attrib["name"] not in idToDummyName:
- idToDummyName[el.attrib["id"]] = el.attrib["name"]
- dummyNameToRealName[el.attrib["name"]] = None
- print("[+] Resolving " + str(len(idToDummyName)) + " resource identifiers.")
-
- #Step 2) Parse the public.xml file from each split APK in search of resource IDs matching
- # those loaded during step 1. Each match gives the true resource name allowing us to
- # replace all APKTOOL_DUMMY_XXX resource names with the true resource names back in
- # the base APK.
- found = 0
- for splitdir in splitapkpaths:
- if os.path.exists(os.path.join(splitdir, "res", "values", "public.xml")):
- tree = xml.etree.ElementTree.parse(os.path.join(splitdir, "res", "values", "public.xml"))
- for el in tree.getroot():
- if "name" in el.attrib and "id" in el.attrib:
- if el.attrib["id"] in idToDummyName:
- dummyNameToRealName[idToDummyName[el.attrib["id"]]] = el.attrib["name"]
- found += 1
- print("[+] Located " + str(found) + " true resource names.")
-
- #Step 3) Update the base APK to replace all APKTOOL_DUMMY_XXX resource names with the true
- # resource name.
- updated = 0
- for el in baseXmlTree.getroot():
- if "name" in el.attrib and "id" in el.attrib:
- if el.attrib["name"] in dummyNameToRealName and dummyNameToRealName[el.attrib["name"]] is not None:
- el.attrib["name"] = dummyNameToRealName[el.attrib["name"]]
- updated += 1
- baseXmlTree.write(os.path.join(baseapkdir, "res", "values", "public.xml"), encoding="utf-8", xml_declaration=True)
- print("[+] Updated " + str(updated) + " dummy resource names with true names in the base APK.")
-
- #Step 4) Find all references to APKTOOL_DUMMY_XXX resources within other XML resource files
- # in the base APK and update them to refer to the true resource name.
- updated = 0
- for (root, dirs, files) in os.walk(os.path.join(baseapkdir, "res")):
- for f in files:
- if f.lower().endswith(".xml"):
- try:
- #Load the XML
- dbgPrint("[~] Parsing " + os.path.join(root, f))
- tree = xml.etree.ElementTree.parse(os.path.join(root, f))
-
- #Register the namespaces and get the prefix for the "android" namespace
- namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(baseapkdir, "AndroidManifest.xml"), events=["start-ns"])])
- for ns in namespaces:
- xml.etree.ElementTree.register_namespace(ns, namespaces[ns])
- ns = "{" + namespaces["android"] + "}"
-
- #Update references to APKTOOL_DUMMY_XXX resources
- changed = False
- for el in tree.iter():
- #Check for references to APKTOOL_DUMMY_XXX resources in attributes of this element
- for attr in el.attrib:
- val = el.attrib[attr]
- if val.startswith("@") and "/" in val and val.split("/")[1].startswith("APKTOOL_DUMMY_") and dummyNameToRealName[val.split("/")[1]] is not None:
- el.attrib[attr] = val.split("/")[0] + "/" + dummyNameToRealName[val.split("/")[1]]
- updated += 1
- changed = True
- elif val.startswith("APKTOOL_DUMMY_") and dummyNameToRealName[val] is not None:
- el.attrib[attr] = dummyNameToRealName[val]
- updated += 1
- changed = True
-
- #Check for references to APKTOOL_DUMMY_XXX resources in the element text
- val = el.text
- if val is not None and val.startswith("@") and "/" in val and val.split("/")[1].startswith("APKTOOL_DUMMY_") and dummyNameToRealName[val.split("/")[1]] is not None:
- el.text = val.split("/")[0] + "/" + dummyNameToRealName[val.split("/")[1]]
- updated += 1
- changed = True
-
- #Save the file if it was updated
- if changed == True:
- tree.write(os.path.join(root, f), encoding="utf-8", xml_declaration=True)
- 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.
- #
- # Possibly a bug in apktool affecting the Uber app (com.ubercab)
- # -> res/values/styles.xml has <style> elements where two child <item> elements had the same name e.g.
- # <item name="borderWarning">@color/ub__ui_core_v2_orange200</item>
- # <item name="borderWarning">@color/ub__ui_core_v2_orange400</item>
- # --> Doing an "apktool d com.ubercab.apk" then "apktool b com.ubercab" fails, so not a bug with patch-apk.py.
- # --> See: https://github.com/iBotPeaches/Apktool/issues/2240
- #
- # This hack parses res/values/styles.xml, finds all offending elements, removes them, then saves the result.
- ####################
- 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 <style> -> <item> elements and removing.")
- print("[~] Warning: this is a complete hack and may impact the visuals of the app, disable with --disable-styles-hack.")
-
- #Duplicates
- dupes = []
-
- #Parse styles.xml and find all <item> elements with duplicate names
- tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, "res", "values", "styles.xml"))
- for styleEl in tree.getroot().findall("style"):
- itemNames = []
- for itemEl in styleEl:
- if "name" in itemEl.attrib and itemEl.attrib["name"] in itemNames:
- dupes.append([styleEl, itemEl])
- else:
- itemNames.append(itemEl.attrib["name"])
-
- #Delete all duplicates from the tree
- for dupe in dupes:
- dupe[0].remove(dupe[1])
-
- #Save the result if any duplicates were found and removed
- if len(dupes) > 0:
- tree.write(os.path.join(baseapkdir, "res", "values", "styles.xml"), encoding="utf-8", xml_declaration=True)
- print("[+] Removed " + str(len(dupes)) + " duplicate entries from styles.xml.")
- print("")
- ####################
- # Update AndroidManifest.xml to disable APK splitting.
- # -> Removes the "isSplitRequired" attribute of the "application" element.
- # -> Sets the "extractNativeLibs" attribute of the "application" element.
- # -> Removes meta-data elements with the name "com.android.vending.splits" or "com.android.vending.splits.required"
- ####################
- def disableApkSplitting(baseapkdir):
- print("Disabling APK splitting in AndroidManifest.xml of base APK.")
-
- #Load AndroidManifest.xml
- tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, "AndroidManifest.xml"))
-
- #Register the namespaces and get the prefix for the "android" namespace
- namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(baseapkdir, "AndroidManifest.xml"), events=["start-ns"])])
- for ns in namespaces:
- xml.etree.ElementTree.register_namespace(ns, namespaces[ns])
- ns = "{" + namespaces["android"] + "}"
-
- #Disable APK splitting
- appEl = None
- elsToRemove = []
- for el in tree.iter():
- if el.tag == "application":
- appEl = el
- if ns + "isSplitRequired" in el.attrib:
- del el.attrib[ns + "isSplitRequired"]
- if ns + "extractNativeLibs" in el.attrib:
- el.attrib[ns + "extractNativeLibs"] = "true"
- elif appEl is not None and el.tag == "meta-data":
- if ns + "name" in el.attrib:
- if el.attrib[ns + "name"] == "com.android.vending.splits.required":
- elsToRemove.append(el)
- elif el.attrib[ns + "name"] == "com.android.vending.splits":
- elsToRemove.append(el)
- for el in elsToRemove:
- appEl.remove(el)
-
- #Save the updated AndroidManifest.xml
- tree.write(os.path.join(baseapkdir, "AndroidManifest.xml"), encoding="utf-8", xml_declaration=True)
- print("")
- ####################
- # Patch an APK to enable support for user-installed CA certs (e.g. Burp Suite CA cert).
- ####################
- def enableUserCerts(apkfile):
- #Create a separate temp directory to work from
- print("Patching APK to enable support for user-installed CA certificates.")
- with tempfile.TemporaryDirectory() as tmppath:
- #Extract the APK
- apkdir = os.path.join(tmppath, apkfile.split(os.sep)[-1][:-4])
- apkname = apkdir.split(os.sep)[-1] + ".apk"
- ret = runApkTool(["d", apkfile, "-o", apkdir])
- if ret.returncode != 0:
- print("Error: Failed to run 'apktool d " + apkfile + " -o " + apkdir + "'.\nRun with --debug-output for more information.")
- sys.exit(1)
-
- #Load AndroidManifest.xml and check for or create the networkSecurityConfig attribute
- tree = xml.etree.ElementTree.parse(os.path.join(apkdir, "AndroidManifest.xml"))
- namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(apkdir, "AndroidManifest.xml"), events=["start-ns"])])
- for ns in namespaces:
- xml.etree.ElementTree.register_namespace(ns, namespaces[ns])
- ns = "{" + namespaces["android"] + "}"
- for el in tree.findall("application"):
- el.attrib[ns + "networkSecurityConfig"] = "@xml/network_security_config"
- tree.write(os.path.join(apkdir, "AndroidManifest.xml"), encoding="utf-8", xml_declaration=True)
-
- #Create a network security config file
- fh = open(os.path.join(apkdir, "res", "xml", "network_security_config.xml"), "wb")
- fh.write("<?xml version=\"1.0\" encoding=\"utf-8\" ?><network-security-config><base-config><trust-anchors><certificates src=\"system\" /><certificates src=\"user\" /></trust-anchors></base-config></network-security-config>".encode("utf-8"))
- 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)
- 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("")
- if __name__ == "__main__":
- main()
|