This repository has been archived by the owner on Jan 16, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 81
/
patch-apk.py
executable file
·654 lines (579 loc) · 26.9 KB
/
patch-apk.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
#!/usr/bin/python3
import argparse
import os
import pkg_resources
import shutil
import subprocess
import sys
import tempfile
import xml.etree.ElementTree
####################
# Main()
####################
def main():
#Check that dependencies are available
checkDependencies()
#Grab argz
args = getArgs()
#Verify the package name and ensure it's installed (also supports partial package names)
pkgname = verifyPackageName(args.pkgname)
#Get the APK path(s) from the device
apkpaths = getAPKPathsForPackage(pkgname)
#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)
#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("")
####################
# Main
####################
if __name__ == "__main__":
main()