patch_apk.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. #!/usr/bin/env python
  2. # -*- encoding: utf-8 -*-
  3. '''
  4. @Contact : liuyuqi.gov@msn.cn
  5. @Time : 2022/11/23 05:24:06
  6. @License : Copyright © 2017-2022 liuyuqi. All Rights Reserved.
  7. @Desc :
  8. '''
  9. import argparse
  10. import os
  11. import pkg_resources
  12. import shutil
  13. import subprocess
  14. import sys
  15. import tempfile
  16. import xml.etree.ElementTree
  17. def main():
  18. checkDependencies()
  19. args = getArgs()
  20. pkgname = verifyPackageName(args.pkgname)
  21. apkpaths = getAPKPathsForPackage(pkgname)
  22. with tempfile.TemporaryDirectory() as tmppath:
  23. #Get the APK to patch. Combine app bundles/split APKs into a single APK.
  24. apkfile = getTargetAPK(pkgname, apkpaths, tmppath, args.disable_styles_hack)
  25. #Save the APK if requested
  26. if args.save_apk is not None:
  27. print("Saving a copy of the APK to " + args.save_apk)
  28. print("")
  29. shutil.copy(apkfile, args.save_apk)
  30. #Patch the target APK with objection
  31. print("Patching " + apkfile.split(os.sep)[-1] + " with objection.")
  32. ret = None
  33. if getObjectionVersion() >= pkg_resources.parse_version("1.9.3"):
  34. ret = subprocess.run(["objection", "patchapk", "--skip-resources", "--ignore-nativelibs", "-s", apkfile], stdout=getStdout())
  35. else:
  36. ret = subprocess.run(["objection", "patchapk", "--skip-resources", "-s", apkfile], stdout=getStdout())
  37. if ret.returncode != 0:
  38. print("Error: Failed to run 'objection patchapk --skip-resources -s " + apkfile + "'.\nRun with --debug-output for more information.")
  39. sys.exit(1)
  40. os.remove(apkfile)
  41. shutil.move(apkfile[:-4] + ".objection.apk", apkfile)
  42. print("")
  43. #Enable support for user-installed CA certs (e.g. Burp Suite CA installed on device by user)
  44. if args.no_enable_user_certs == False:
  45. enableUserCerts(apkfile)
  46. #Uninstall the original package from the device
  47. print("Uninstalling the original package from the device.")
  48. ret = subprocess.run(["adb", "uninstall", pkgname], stdout=getStdout())
  49. if ret.returncode != 0:
  50. print("Error: Failed to run 'adb uninstall " + pkgname + "'.\nRun with --debug-output for more information.")
  51. sys.exit(1)
  52. print("")
  53. #Install the patched APK
  54. print("Installing the patched APK to the device.")
  55. ret = subprocess.run(["adb", "install", apkfile], stdout=getStdout())
  56. if ret.returncode != 0:
  57. print("Error: Failed to run 'adb install " + apkfile + "'.\nRun with --debug-output for more information.")
  58. sys.exit(1)
  59. print("")
  60. #Done
  61. print("Done, cleaning up temporary files.")
  62. ####################
  63. # Check that required dependencies are present:
  64. # -> Tools used
  65. # -> Android device connected
  66. # -> Keystore
  67. ####################
  68. def checkDependencies():
  69. deps = ["adb", "apktool", "jarsigner", "objection", "zipalign"]
  70. missing = []
  71. for dep in deps:
  72. if shutil.which(dep) is None:
  73. missing.append(dep)
  74. if len(missing) > 0:
  75. print("Error, missing dependencies, ensure the following commands are available on the PATH: " + (", ".join(missing)))
  76. sys.exit(1)
  77. #Verify that an Android device is connected
  78. proc = subprocess.run(["adb", "devices"], stdout=subprocess.PIPE)
  79. if proc.returncode != 0:
  80. print("Error: Failed to run 'adb devices'.")
  81. sys.exit(1)
  82. deviceOut = proc.stdout.decode("utf-8")
  83. if len(deviceOut.strip().split(os.linesep)) == 1:
  84. print("Error, no Android device connected (\"adb devices\"), connect a device first.")
  85. sys.exit(1)
  86. #Check that the included keystore exists
  87. if os.path.exists(os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore"))) == False:
  88. 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.")
  89. sys.exit(1)
  90. ####################
  91. # Grab command line parameters
  92. ####################
  93. def getArgs():
  94. #Only parse args once
  95. if not hasattr(getArgs, "parsed_args"):
  96. #Parse the command line
  97. parser = argparse.ArgumentParser(
  98. description="patch-apk - Pull and patch Android apps for use with objection/frida."
  99. )
  100. 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")
  101. parser.add_argument("--save-apk", help="Save a copy of the APK (or single APK) prior to patching for use with other tools.")
  102. parser.add_argument("--disable-styles-hack", help="Disable the styles hack that removes duplicate entries from res/values/styles.xml.", action="store_true")
  103. parser.add_argument("--debug-output", help="Enable debug output.", action="store_true")
  104. parser.add_argument("pkgname", help="The name, or partial name, of the package to patch (e.g. com.foo.bar).")
  105. #Store the parsed args
  106. getArgs.parsed_args = parser.parse_args()
  107. #Return the parsed command line args
  108. return getArgs.parsed_args
  109. ####################
  110. # Debug print
  111. ####################
  112. def dbgPrint(msg):
  113. if getArgs().debug_output == True:
  114. print(msg)
  115. ####################
  116. # Get the stdout target for subprocess calls. Set to DEVNULL unless debug output is enabled.
  117. ####################
  118. def getStdout():
  119. if getArgs().debug_output == True:
  120. return None
  121. else:
  122. return subprocess.DEVNULL
  123. ####################
  124. # Get objection version
  125. ####################
  126. def getObjectionVersion():
  127. proc = subprocess.run(["objection", "version"], stdout=subprocess.PIPE)
  128. return pkg_resources.parse_version(proc.stdout.decode("utf-8").strip().split(": ")[-1].strip())
  129. ####################
  130. # Get apktool version
  131. ####################
  132. def getApktoolVersion():
  133. output = ""
  134. if os.name == "nt":
  135. proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
  136. proc.communicate(b"\r\n")
  137. output = proc.stdout.decode("utf-8").strip()
  138. else:
  139. proc = subprocess.run(["apktool", "-version"], stdout=subprocess.PIPE)
  140. output = proc.stdout.decode("utf-8").strip()
  141. return pkg_resources.parse_version(output.split("-")[0].strip())
  142. ####################
  143. # Wrapper to run apktool platform-independently, complete with a dirty hack to fix apktool's dirty hack.
  144. ####################
  145. def runApkTool(params):
  146. if os.name == "nt":
  147. args = ["apktool.bat"]
  148. args.extend(params)
  149. #apktool.bat has a dirty hack that execute "pause", so we need a dirty hack to kill the pause command...
  150. proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=getStdout())
  151. proc.communicate(b"\r\n")
  152. return proc
  153. else:
  154. args = ["apktool"]
  155. args.extend(params)
  156. return subprocess.run(args, stdout=getStdout())
  157. ####################
  158. # Verify the package name - checks whether the target package is installed
  159. # on the device or if an exact match is not found presents the options to
  160. # the user for selection.
  161. ####################
  162. def verifyPackageName(pkgname):
  163. #Get a list of installed packages matching the given name
  164. packages = []
  165. proc = subprocess.run(["adb", "shell", "pm", "list", "packages"], stdout=subprocess.PIPE)
  166. if proc.returncode != 0:
  167. print("Error: Failed to run 'adb shell pm list packages'.")
  168. sys.exit(1)
  169. out = proc.stdout.decode("utf-8")
  170. for line in out.split(os.linesep):
  171. if line.startswith("package:"):
  172. line = line[8:].strip()
  173. if pkgname.lower() in line.lower():
  174. packages.append(line)
  175. #Bail out if no matching packages were found
  176. if len(packages) == 0:
  177. print("Error, no packages found on the device matching the search term '" + pkgname + "'.")
  178. print("Run 'adb shell pm list packages' to verify installed package names.")
  179. sys.exit(1)
  180. #Return the target package name, offering a choice to the user if necessary
  181. if len(packages) == 1:
  182. return packages[0]
  183. else:
  184. print("Multiple matching packages installed, select the package to patch.")
  185. choice = -1
  186. while choice == -1:
  187. for i in range(len(packages)):
  188. print("[" + str(i + 1) + "] " + packages[i])
  189. choice = input("Choice: ")
  190. if choice.isnumeric() == False or int(choice) < 1 or int(choice) > len(packages):
  191. print("Invalid choice.\n")
  192. choice = -1
  193. print("")
  194. return packages[int(choice) - 1]
  195. ####################
  196. # Get the APK path(s) on the device for the given package name.
  197. ####################
  198. def getAPKPathsForPackage(pkgname):
  199. print("Getting APK path(s) for package: " + pkgname)
  200. paths = []
  201. proc = subprocess.run(["adb", "shell", "pm", "path", pkgname], stdout=subprocess.PIPE)
  202. if proc.returncode != 0:
  203. print("Error: Failed to run 'adb shell pm path " + pkgname + "'.")
  204. sys.exit(1)
  205. out = proc.stdout.decode("utf-8")
  206. for line in out.split(os.linesep):
  207. if line.startswith("package:"):
  208. line = line[8:].strip()
  209. print("[+] APK path: " + line)
  210. paths.append(line)
  211. print("")
  212. return paths
  213. ####################
  214. # Pull the APK file(s) for the package and return the local file path to work with.
  215. # If the package is an app bundle/split APK, combine the APKs into a single APK.
  216. ####################
  217. def getTargetAPK(pkgname, apkpaths, tmppath, disableStylesHack):
  218. #Pull the APKs from the device
  219. print("Pulling APK file(s) from device.")
  220. localapks = []
  221. for remotepath in apkpaths:
  222. baseapkname = remotepath.split('/')[-1]
  223. localapks.append(os.path.join(tmppath, pkgname + "-" + baseapkname))
  224. print("[+] Pulling: " + pkgname + "-" + baseapkname)
  225. ret = subprocess.run(["adb", "pull", remotepath, localapks[-1]], stdout=getStdout())
  226. if ret.returncode != 0:
  227. print("Error: Failed to run 'adb pull " + remotepath + " " + localapks[-1] + "'.\nRun with --debug-output for more information.")
  228. sys.exit(1)
  229. print("")
  230. #Return the target APK path
  231. if len(localapks) == 1:
  232. return localapks[0]
  233. else:
  234. #Combine split APKs
  235. return combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack)
  236. ####################
  237. # Combine app bundles/split APKs into a single APK for patching.
  238. ####################
  239. def combineSplitAPKs(pkgname, localapks, tmppath, disableStylesHack):
  240. print("App bundle/split APK detected, rebuilding as a single APK.")
  241. print("")
  242. #Extract the individual APKs
  243. print("Extracting individual APKs with apktool.")
  244. baseapkdir = os.path.join(tmppath, pkgname + "-base")
  245. baseapkfilename = pkgname + "-base.apk"
  246. splitapkpaths = []
  247. for apkpath in localapks:
  248. print("[+] Extracting: " + apkpath)
  249. apkdir = apkpath[:-4]
  250. ret = runApkTool(["d", apkpath, "-o", apkdir])
  251. if ret.returncode != 0:
  252. print("Error: Failed to run 'apktool d " + apkpath + " -o " + apkdir + "'.\nRun with --debug-output for more information.")
  253. sys.exit(1)
  254. #Record the destination paths of all but the base APK
  255. if apkpath.endswith("base.apk") == False:
  256. splitapkpaths.append(apkdir)
  257. #Check for ProGuard/AndResGuard - this might b0rk decompile/recompile
  258. if detectProGuard(apkdir):
  259. print("\n[~] WARNING: Detected ProGuard/AndResGuard, decompile/recompile may not succeed.\n")
  260. print("")
  261. #Walk the extracted APK directories and copy files and directories to the base APK
  262. copySplitApkFiles(baseapkdir, splitapkpaths)
  263. #Fix public resource identifiers
  264. fixPublicResourceIDs(baseapkdir, splitapkpaths)
  265. #Hack: Delete duplicate style resource entries.
  266. if disableStylesHack == False:
  267. hackRemoveDuplicateStyleEntries(baseapkdir)
  268. #Disable APK splitting in the base AndroidManifest.xml file
  269. disableApkSplitting(baseapkdir)
  270. #Rebuild the base APK
  271. print("Rebuilding as a single APK.")
  272. if os.path.exists(os.path.join(baseapkdir, "res", "navigation")) == True:
  273. print("[+] Found res/navigation directory, rebuilding with 'apktool --use-aapt2'.")
  274. ret = runApkTool(["--use-aapt2", "b", baseapkdir])
  275. if ret.returncode != 0:
  276. print("Error: Failed to run 'apktool b " + baseapkdir + "'.\nRun with --debug-output for more information.")
  277. sys.exit(1)
  278. elif getApktoolVersion() > pkg_resources.parse_version("2.4.2"):
  279. print("[+] Found apktool version > 2.4.2, rebuilding with 'apktool --use-aapt2'.")
  280. ret = runApkTool(["--use-aapt2", "b", baseapkdir])
  281. if ret.returncode != 0:
  282. print("Error: Failed to run 'apktool b " + baseapkdir + "'.\nRun with --debug-output for more information.")
  283. sys.exit(1)
  284. else:
  285. print("[+] Building APK with apktool.")
  286. ret = runApkTool(["b", baseapkdir])
  287. if ret.returncode != 0:
  288. print("Error: Failed to run 'apktool b " + baseapkdir + "'.\nRun with --debug-output for more information.")
  289. sys.exit(1)
  290. #Sign the new APK
  291. print("[+] Signing new APK.")
  292. ret = subprocess.run([
  293. "jarsigner", "-sigalg", "SHA1withRSA", "-digestalg", "SHA1", "-keystore",
  294. os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore")),
  295. "-storepass", "patch-apk", os.path.join(baseapkdir, "dist", baseapkfilename), "patch-apk-key"],
  296. stdout=getStdout()
  297. )
  298. if ret.returncode != 0:
  299. print("Error: Failed to run 'jarsigner -sigalg SHA1withRSA -digestalg SHA1 -keystore " +
  300. os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore")) +
  301. "-storepass patch-apk " + os.path.join(baseapkdir, "dist", baseapkfilename) + " patch-apk-key'.\nRun with --debug-output for more information.")
  302. sys.exit(1)
  303. #Zip align the new APK
  304. print("[+] Zip aligning new APK.")
  305. ret = subprocess.run([
  306. "zipalign", "-f", "4", os.path.join(baseapkdir, "dist", baseapkfilename),
  307. os.path.join(baseapkdir, "dist", baseapkfilename[:-4] + "-aligned.apk")
  308. ],
  309. stdout=getStdout()
  310. )
  311. if ret.returncode != 0:
  312. print("Error: Failed to run 'zipalign -f 4 " + os.path.join(baseapkdir, "dist", baseapkfilename) +
  313. " " + os.path.join(baseapkdir, "dist", baseapkfilename[:-4] + "-aligned.apk") + "'.\nRun with --debug-output for more information.")
  314. sys.exit(1)
  315. shutil.move(os.path.join(baseapkdir, "dist", baseapkfilename[:-4] + "-aligned.apk"), os.path.join(baseapkdir, "dist", baseapkfilename))
  316. print("")
  317. #Return the new APK path
  318. return os.path.join(baseapkdir, "dist", baseapkfilename)
  319. ####################
  320. # Attempt to detect ProGuard/AndResGuard.
  321. ####################
  322. def detectProGuard(extractedPath):
  323. if os.path.exists(os.path.join(extractedPath, "original", "META-INF", "proguard")) == True:
  324. return True
  325. if os.path.exists(os.path.join(extractedPath, "original", "META-INF", "MANIFEST.MF")) == True:
  326. fh = open(os.path.join(extractedPath, "original", "META-INF", "MANIFEST.MF"))
  327. d = fh.read()
  328. fh.close()
  329. if "proguard" in d.lower():
  330. return True
  331. return False
  332. ####################
  333. # Copy files and directories from split APKs into the base APK directory.
  334. ####################
  335. def copySplitApkFiles(baseapkdir, splitapkpaths):
  336. print("Copying files and directories from split APKs into base APK.")
  337. for apkdir in splitapkpaths:
  338. for (root, dirs, files) in os.walk(apkdir):
  339. #Skip the original files directory
  340. if root.startswith(os.path.join(apkdir, "original")) == False:
  341. #Create any missing directories
  342. for d in dirs:
  343. #Translate directory path to base APK path and create the directory if it doesn't exist
  344. p = baseapkdir + os.path.join(root, d)[len(apkdir):]
  345. if os.path.exists(p) == False:
  346. dbgPrint("[+] Creating directory in base APK: " + p[len(baseapkdir):])
  347. os.mkdir(p)
  348. #Copy files into the base APK
  349. for f in files:
  350. #Skip the AndroidManifest.xml and apktool.yml in the APK root directory
  351. if apkdir == root and (f == "AndroidManifest.xml" or f == "apktool.yml"):
  352. continue
  353. #Translate path to base APK
  354. p = baseapkdir + os.path.join(root, f)[len(apkdir):]
  355. #Copy files into the base APK, except for XML files in the res directory
  356. if f.lower().endswith(".xml") and p.startswith(os.path.join(baseapkdir, "res")):
  357. continue
  358. dbgPrint("[+] Moving file to base APK: " + p[len(baseapkdir):])
  359. shutil.move(os.path.join(root, f), p)
  360. print("")
  361. ####################
  362. # Fix public resource identifiers that are shared across split APKs.
  363. # Maps all APKTOOL_DUMMY_ resource IDs in the base APK to the proper resource names from the
  364. # split APKs, then updates references in other resource files in the base APK to use proper
  365. # resource names.
  366. ####################
  367. def fixPublicResourceIDs(baseapkdir, splitapkpaths):
  368. #Bail if the base APK does not have a public.xml
  369. if os.path.exists(os.path.join(baseapkdir, "res", "values", "public.xml")) == False:
  370. return
  371. print("Found public.xml in the base APK, fixing resource identifiers across split APKs.")
  372. #Mappings of resource IDs and names
  373. idToDummyName = {}
  374. dummyNameToRealName = {}
  375. #Step 1) Find all resource IDs that apktool has assigned a name of APKTOOL_DUMMY_XXX to.
  376. # Load these into the lookup tables ready to resolve the real resource names from
  377. # the split APKs in step 2 below.
  378. baseXmlTree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, "res", "values", "public.xml"))
  379. for el in baseXmlTree.getroot():
  380. if "name" in el.attrib and "id" in el.attrib:
  381. if el.attrib["name"].startswith("APKTOOL_DUMMY_") and el.attrib["name"] not in idToDummyName:
  382. idToDummyName[el.attrib["id"]] = el.attrib["name"]
  383. dummyNameToRealName[el.attrib["name"]] = None
  384. print("[+] Resolving " + str(len(idToDummyName)) + " resource identifiers.")
  385. #Step 2) Parse the public.xml file from each split APK in search of resource IDs matching
  386. # those loaded during step 1. Each match gives the true resource name allowing us to
  387. # replace all APKTOOL_DUMMY_XXX resource names with the true resource names back in
  388. # the base APK.
  389. found = 0
  390. for splitdir in splitapkpaths:
  391. if os.path.exists(os.path.join(splitdir, "res", "values", "public.xml")):
  392. tree = xml.etree.ElementTree.parse(os.path.join(splitdir, "res", "values", "public.xml"))
  393. for el in tree.getroot():
  394. if "name" in el.attrib and "id" in el.attrib:
  395. if el.attrib["id"] in idToDummyName:
  396. dummyNameToRealName[idToDummyName[el.attrib["id"]]] = el.attrib["name"]
  397. found += 1
  398. print("[+] Located " + str(found) + " true resource names.")
  399. #Step 3) Update the base APK to replace all APKTOOL_DUMMY_XXX resource names with the true
  400. # resource name.
  401. updated = 0
  402. for el in baseXmlTree.getroot():
  403. if "name" in el.attrib and "id" in el.attrib:
  404. if el.attrib["name"] in dummyNameToRealName and dummyNameToRealName[el.attrib["name"]] is not None:
  405. el.attrib["name"] = dummyNameToRealName[el.attrib["name"]]
  406. updated += 1
  407. baseXmlTree.write(os.path.join(baseapkdir, "res", "values", "public.xml"), encoding="utf-8", xml_declaration=True)
  408. print("[+] Updated " + str(updated) + " dummy resource names with true names in the base APK.")
  409. #Step 4) Find all references to APKTOOL_DUMMY_XXX resources within other XML resource files
  410. # in the base APK and update them to refer to the true resource name.
  411. updated = 0
  412. for (root, dirs, files) in os.walk(os.path.join(baseapkdir, "res")):
  413. for f in files:
  414. if f.lower().endswith(".xml"):
  415. try:
  416. #Load the XML
  417. dbgPrint("[~] Parsing " + os.path.join(root, f))
  418. tree = xml.etree.ElementTree.parse(os.path.join(root, f))
  419. #Register the namespaces and get the prefix for the "android" namespace
  420. namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(baseapkdir, "AndroidManifest.xml"), events=["start-ns"])])
  421. for ns in namespaces:
  422. xml.etree.ElementTree.register_namespace(ns, namespaces[ns])
  423. ns = "{" + namespaces["android"] + "}"
  424. #Update references to APKTOOL_DUMMY_XXX resources
  425. changed = False
  426. for el in tree.iter():
  427. #Check for references to APKTOOL_DUMMY_XXX resources in attributes of this element
  428. for attr in el.attrib:
  429. val = el.attrib[attr]
  430. if val.startswith("@") and "/" in val and val.split("/")[1].startswith("APKTOOL_DUMMY_") and dummyNameToRealName[val.split("/")[1]] is not None:
  431. el.attrib[attr] = val.split("/")[0] + "/" + dummyNameToRealName[val.split("/")[1]]
  432. updated += 1
  433. changed = True
  434. elif val.startswith("APKTOOL_DUMMY_") and dummyNameToRealName[val] is not None:
  435. el.attrib[attr] = dummyNameToRealName[val]
  436. updated += 1
  437. changed = True
  438. #Check for references to APKTOOL_DUMMY_XXX resources in the element text
  439. val = el.text
  440. 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:
  441. el.text = val.split("/")[0] + "/" + dummyNameToRealName[val.split("/")[1]]
  442. updated += 1
  443. changed = True
  444. #Save the file if it was updated
  445. if changed == True:
  446. tree.write(os.path.join(root, f), encoding="utf-8", xml_declaration=True)
  447. except xml.etree.ElementTree.ParseError:
  448. print("[-] XML parse error in " + os.path.join(root, f) + ", skipping.")
  449. print("[+] Updated " + str(updated) + " references to dummy resource names in the base APK.")
  450. print("")
  451. ####################
  452. # Hack to remove duplicate style resource entries before rebuilding.
  453. #
  454. # Possibly a bug in apktool affecting the Uber app (com.ubercab)
  455. # -> res/values/styles.xml has <style> elements where two child <item> elements had the same name e.g.
  456. # <item name="borderWarning">@color/ub__ui_core_v2_orange200</item>
  457. # <item name="borderWarning">@color/ub__ui_core_v2_orange400</item>
  458. # --> Doing an "apktool d com.ubercab.apk" then "apktool b com.ubercab" fails, so not a bug with patch-apk.py.
  459. # --> See: https://github.com/iBotPeaches/Apktool/issues/2240
  460. #
  461. # This hack parses res/values/styles.xml, finds all offending elements, removes them, then saves the result.
  462. ####################
  463. def hackRemoveDuplicateStyleEntries(baseapkdir):
  464. #Bail if there is no styles.xml
  465. if os.path.exists(os.path.join(baseapkdir, "res", "values", "styles.xml")) == False:
  466. return
  467. print("Found styles.xml in the base APK, checking for duplicate <style> -> <item> elements and removing.")
  468. print("[~] Warning: this is a complete hack and may impact the visuals of the app, disable with --disable-styles-hack.")
  469. #Duplicates
  470. dupes = []
  471. #Parse styles.xml and find all <item> elements with duplicate names
  472. tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, "res", "values", "styles.xml"))
  473. for styleEl in tree.getroot().findall("style"):
  474. itemNames = []
  475. for itemEl in styleEl:
  476. if "name" in itemEl.attrib and itemEl.attrib["name"] in itemNames:
  477. dupes.append([styleEl, itemEl])
  478. else:
  479. itemNames.append(itemEl.attrib["name"])
  480. #Delete all duplicates from the tree
  481. for dupe in dupes:
  482. dupe[0].remove(dupe[1])
  483. #Save the result if any duplicates were found and removed
  484. if len(dupes) > 0:
  485. tree.write(os.path.join(baseapkdir, "res", "values", "styles.xml"), encoding="utf-8", xml_declaration=True)
  486. print("[+] Removed " + str(len(dupes)) + " duplicate entries from styles.xml.")
  487. print("")
  488. ####################
  489. # Update AndroidManifest.xml to disable APK splitting.
  490. # -> Removes the "isSplitRequired" attribute of the "application" element.
  491. # -> Sets the "extractNativeLibs" attribute of the "application" element.
  492. # -> Removes meta-data elements with the name "com.android.vending.splits" or "com.android.vending.splits.required"
  493. ####################
  494. def disableApkSplitting(baseapkdir):
  495. print("Disabling APK splitting in AndroidManifest.xml of base APK.")
  496. #Load AndroidManifest.xml
  497. tree = xml.etree.ElementTree.parse(os.path.join(baseapkdir, "AndroidManifest.xml"))
  498. #Register the namespaces and get the prefix for the "android" namespace
  499. namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(baseapkdir, "AndroidManifest.xml"), events=["start-ns"])])
  500. for ns in namespaces:
  501. xml.etree.ElementTree.register_namespace(ns, namespaces[ns])
  502. ns = "{" + namespaces["android"] + "}"
  503. #Disable APK splitting
  504. appEl = None
  505. elsToRemove = []
  506. for el in tree.iter():
  507. if el.tag == "application":
  508. appEl = el
  509. if ns + "isSplitRequired" in el.attrib:
  510. del el.attrib[ns + "isSplitRequired"]
  511. if ns + "extractNativeLibs" in el.attrib:
  512. el.attrib[ns + "extractNativeLibs"] = "true"
  513. elif appEl is not None and el.tag == "meta-data":
  514. if ns + "name" in el.attrib:
  515. if el.attrib[ns + "name"] == "com.android.vending.splits.required":
  516. elsToRemove.append(el)
  517. elif el.attrib[ns + "name"] == "com.android.vending.splits":
  518. elsToRemove.append(el)
  519. for el in elsToRemove:
  520. appEl.remove(el)
  521. #Save the updated AndroidManifest.xml
  522. tree.write(os.path.join(baseapkdir, "AndroidManifest.xml"), encoding="utf-8", xml_declaration=True)
  523. print("")
  524. ####################
  525. # Patch an APK to enable support for user-installed CA certs (e.g. Burp Suite CA cert).
  526. ####################
  527. def enableUserCerts(apkfile):
  528. #Create a separate temp directory to work from
  529. print("Patching APK to enable support for user-installed CA certificates.")
  530. with tempfile.TemporaryDirectory() as tmppath:
  531. #Extract the APK
  532. apkdir = os.path.join(tmppath, apkfile.split(os.sep)[-1][:-4])
  533. apkname = apkdir.split(os.sep)[-1] + ".apk"
  534. ret = runApkTool(["d", apkfile, "-o", apkdir])
  535. if ret.returncode != 0:
  536. print("Error: Failed to run 'apktool d " + apkfile + " -o " + apkdir + "'.\nRun with --debug-output for more information.")
  537. sys.exit(1)
  538. #Load AndroidManifest.xml and check for or create the networkSecurityConfig attribute
  539. tree = xml.etree.ElementTree.parse(os.path.join(apkdir, "AndroidManifest.xml"))
  540. namespaces = dict([node for _,node in xml.etree.ElementTree.iterparse(os.path.join(apkdir, "AndroidManifest.xml"), events=["start-ns"])])
  541. for ns in namespaces:
  542. xml.etree.ElementTree.register_namespace(ns, namespaces[ns])
  543. ns = "{" + namespaces["android"] + "}"
  544. for el in tree.findall("application"):
  545. el.attrib[ns + "networkSecurityConfig"] = "@xml/network_security_config"
  546. tree.write(os.path.join(apkdir, "AndroidManifest.xml"), encoding="utf-8", xml_declaration=True)
  547. #Create a network security config file
  548. fh = open(os.path.join(apkdir, "res", "xml", "network_security_config.xml"), "wb")
  549. 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"))
  550. fh.close()
  551. #Rebuild and sign the APK
  552. ret = runApkTool(["b", apkdir])
  553. if ret.returncode != 0:
  554. print("Error: Failed to run 'apktool b " + apkdir + "'.\nRun with --debug-output for more information.")
  555. sys.exit(1)
  556. ret = subprocess.run([
  557. "jarsigner", "-sigalg", "SHA1withRSA", "-digestalg", "SHA1", "-keystore",
  558. os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore")),
  559. "-storepass", "patch-apk", os.path.join(apkdir, "dist", apkname), "patch-apk-key"],
  560. stdout=getStdout()
  561. )
  562. if ret.returncode != 0:
  563. print("Error: Failed to run 'jarsigner -sigalg SHA1withRSA -digestalg SHA1 -keystore " +
  564. os.path.realpath(os.path.join(os.path.realpath(__file__), "..", "data", "patch-apk.keystore")) +
  565. "-storepass patch-apk " + os.path.join(apkdir, "dist", apkname) + "patch-apk-key'.\nRun with --debug-output for more information.")
  566. sys.exit(1)
  567. #Zip align the new APK
  568. os.remove(apkfile)
  569. ret = subprocess.run(["zipalign", "4", os.path.join(apkdir, "dist", apkname), apkfile], stdout=getStdout())
  570. if ret.returncode != 0:
  571. print("Error: Failed to run 'zipalign 4 " + os.path.join(apkdir, "dist", apkname) + " " + apkfile + "'.\nRun with --debug-output for more information.")
  572. sys.exit(1)
  573. print("")
  574. if __name__ == "__main__":
  575. main()