Last active
August 12, 2024 21:59
-
-
Save floyd-fuh/7f7408b560672ece3ea78348559d47b6 to your computer and use it in GitHub Desktop.
Automatically repackage an Android apk and resign it for usage with Burp Proxy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import sys | |
if not sys.version.startswith('3'): | |
print('\n[-] This script will only work with Python3. Sorry!\n') | |
exit() | |
import subprocess | |
import os | |
import shutil | |
import argparse | |
import urllib.request | |
__author__ = "Jake Miller (@LaconicWolf), 2022 changes by floyd (@floyd_ch)" | |
__date__ = "20220215" | |
__version__ = "0.02" | |
__description__ = '''A script to repackage an APK file to allow a user-installed SSL certificate.''' | |
def check_for_tools(name): | |
"""Checks to see whether the tool name is in current directory or in the PATH""" | |
if is_in_path(name): | |
return name | |
elif is_in_dir(name): | |
return "./" + name | |
elif is_next_to_script(name): | |
return os.path.dirname(os.path.realpath(__file__)) + "/" + name | |
else: | |
return None | |
def is_next_to_script(name): | |
return os.path.isfile(os.path.dirname(os.path.realpath(__file__)) + "/" + name) | |
def is_in_path(name): | |
"""Check whether name is on PATH and marked as executable. | |
https://stackoverflow.com/questions/11210104/check-if-a-program-exists-from-a-python-script/34177358 | |
""" | |
return shutil.which(name) | |
def is_in_dir(name, directory='.'): | |
"""Checks whether a file exists in a specified directory.""" | |
return os.path.isfile(os.path.join(directory, name)) | |
def apktool_disassemble(filename, apktool): | |
"""Uses APKTool to disassemble an APK""" | |
cmd = "{} d {} -o {}".format(apktool, filename, filename.replace('.apk', '_out')) | |
print(cmd) | |
output = subprocess.getoutput(cmd) | |
if 'Exception in' in output: | |
print('[-] An error occurred when disassembling the APK.') | |
print(output) | |
try: | |
shutil.rmtree(filename.replace('.apk', '_out')) | |
except: | |
pass | |
return False | |
else: | |
return True | |
def apktool_build(filepath, apktool): | |
"""Uses APKTool to create a new APK""" | |
cmd = "{} b {}".format(apktool, filepath) | |
print(cmd) | |
output = subprocess.getoutput(cmd) | |
try: | |
os.listdir(filepath + os.sep + 'dist') | |
except FileNotFoundError: | |
print('[-] An error occurred when rebuilding the APK.') | |
print(output) | |
return False | |
return True | |
def do_keytool(keystore_name, keytool): | |
cmd = [keytool, '-genkey', '-v', '-keystore', | |
keystore_name, '-storepass', 'password', | |
'-alias', 'android', '-keypass', 'password', | |
'-keyalg', 'RSA', '-keysize', '2048', '-sigalg', 'SHA1withRSA', '-validity', | |
'9000', '-dname', 'CN=foo, OU=ID, O=bar, L=baz, S=bar, C=boo'] | |
print(" ".join(cmd)) | |
p = subprocess.Popen(cmd) | |
p.communicate() | |
keystore_present = True | |
def do_jarsigner(filepath, keystore, jarsigner): | |
"""Uses jarsigner to sign the old way (v1)""" | |
cmd = "{} -verbose -keystore {} -storepass password -keypass password {} android".format(jarsigner, keystore, filepath) | |
print(cmd) | |
output = subprocess.getoutput(cmd) | |
if 'jar signed.' not in output: | |
print("[-] An error occurred during jarsigner: \n{}".format(output)) | |
else: | |
print("[*] Signed!") | |
def do_apksigner(filepath, keystore, apksigner): | |
"""Uses apksigner to sign the new way (v2)""" | |
cmd = "{} sign --v4-signing-enabled true --ks {} --ks-pass pass:password --key-pass pass:password --ks-key-alias android {}".format(apksigner, keystore, filepath) | |
print(cmd) | |
output = subprocess.getoutput(cmd) | |
if output: | |
print("[-] An error occurred during apksigner: \n{}".format(output)) | |
exit() | |
else: | |
print("[*] Signed!") | |
# --min-sdk-version 1 | |
# | |
cmd = "{} verify -v4-signature-file {} -v {}".format(apksigner, filepath + ".idsig", filepath) | |
print(cmd) | |
output = subprocess.getoutput(cmd) | |
print(output) | |
def do_zipalign(filepath, zipalign): | |
"""Uses zipalign to create a new APK""" | |
if os.path.isfile("repacked.apk"): | |
os.remove("repacked.apk") | |
cmd = "{} -p -v 4 {} repacked.apk".format(zipalign, filepath) | |
print(cmd) | |
output = subprocess.getoutput(cmd) | |
if 'Verification succesful' not in output: | |
print("[-] An error occurred during zipalign: \n{}".format(output)) | |
else: | |
print("[*] zipalign!") | |
return "repacked.apk" | |
def add_network_security_config(basedir): | |
"""Adds a network security config file that allows user | |
certificates. | |
""" | |
data = '''\ | |
<network-security-config> | |
<base-config> | |
<trust-anchors> | |
<!-- Trust preinstalled CAs --> | |
<certificates src="system" /> | |
<!-- Trust user added CAs --> | |
<certificates src="user" /> | |
<!-- Trust any CA in this folder --> | |
<certificates src="@raw/cacert"/> | |
</trust-anchors> | |
</base-config> | |
</network-security-config>''' | |
with open(os.path.join(basedir, 'res', 'xml', 'network_security_config.xml'), 'w') as fh: | |
fh.write(data) | |
def do_network_security_config(directory): | |
"""Checks for a network security config file in the project. | |
If present, reads the file and adds a line to allow user certs. | |
If not present, creates one to allow user certs. | |
""" | |
# Still need to add the line if the file already exists | |
if 'xml' in os.listdir(os.path.join(directory, 'res')): | |
if 'network_security_config.xml' in os.listdir(os.path.join(directory, 'res', 'xml')): | |
filepath = os.path.join(directory, 'res', 'xml', 'network_security_config.xml') | |
with open(filepath) as fh: | |
contents = fh.read() | |
new_contents = contents.replace('<trust-anchors>', '<trust-anchors>\n <certificates src="user" />\n <certificates src="@raw/cacert"/>') | |
with open(filepath, 'w') as fh: | |
fh.write(new_contents) | |
return True | |
else: | |
print('[*] Adding network_security_config.xml to {}.'.format(os.path.join(directory, 'res', 'xml'))) | |
add_network_security_config(directory) | |
else: | |
print('[*] Creating {} and adding network_security_config.xml.'.format(os.path.join(directory, 'res', 'xml'))) | |
os.mkdir(os.path.join(directory, 'res', 'xml')) | |
add_network_security_config(directory) | |
def check_for_burp(host, port): | |
"""Checks to see if Burp is running.""" | |
url = ("http://{}:{}/".format(host, port)) | |
try: | |
resp = urllib.request.urlopen(url) | |
except Exception as e: | |
return False | |
if b"Burp Suite" in resp.read(): | |
return True | |
else: | |
return False | |
def download_burp_cert(host, port): | |
"""Downloads the Burp Suite certificate.""" | |
url = ("http://{}:{}/cert".format(host, port)) | |
file_name = 'cacert.der' | |
# Download the file from url and save it locally under file_name: | |
try: | |
with urllib.request.urlopen(url) as response, open(file_name, 'wb') as out_file: | |
data = response.read() # a bytes object | |
out_file.write(data) | |
cert_present = True | |
return file_name | |
except Exception as e: | |
print('[-] An error occurred: {}'.format(e)) | |
exit() | |
def edit_manifest(filepath): | |
'''Adds android:networkSecurityConfig="@xml/network_security_config" | |
to the manifest''' | |
with open(filepath) as fh: | |
contents = fh.read() | |
new_contents = contents.replace("<application ", '<application android:networkSecurityConfig="@xml/network_security_config" ') | |
with open(filepath, 'w') as fh: | |
fh.write(new_contents) | |
def main(): | |
"""Checks for tools, and repackages an APK to allow | |
a user-installed SSL certificate. | |
""" | |
# Check for required tools | |
print('[*] Checking for required tools...') | |
keytool = check_for_tools("keytool") | |
if not keytool: | |
print("[-] keytool could not be found in the current working directory, the directory of this script or in your PATH. Please ensure either of these conditions are met.") | |
exit() | |
jarsigner = check_for_tools("jarsigner") | |
if not jarsigner: | |
print("[-] jarsigner could not be found in the current working directory, the directory of this script or in your PATH. Please ensure either of these conditions are met.") | |
exit() | |
apksigner = check_for_tools("apksigner") | |
if not apksigner: | |
print("[-] apksigner could not be found in the current working directory, the directory of this script or in your PATH. Please ensure either of these conditions are met.") | |
exit() | |
zipalign = check_for_tools("zipalign") | |
if not zipalign: | |
print("[-] zipalign could not be found in the current working directory, the directory of this script or in your PATH. Please ensure either of these conditions are met.") | |
exit() | |
apktool = check_for_tools("apktool") | |
if not apktool: | |
apktool = check_for_tools("apktool.jar") | |
if apktool: | |
apktool = "java -jar " + apktool | |
else: | |
print("[-] {} could not be found in the current working directory, the directory of this script or in your PATH. Please ensure either of these conditions are met.".format(apktool)) | |
exit() | |
# Checks for Burp and adds the cert to the project | |
# Not sure why certname needs to be global. Kept getting an | |
# error saying I was referencing before defining it (shrug) | |
global certname | |
if not cert_present: | |
burp = check_for_burp(burp_host, burp_port) | |
if not burp: | |
print("[-] Burp not found on {}:{}. Please start Burp and specify ".format(burp_host, burp_port), | |
"the proxy host and port (-pr 127.0.0.1:8080), or specify the ", | |
"path to the self-signed burp cert (-c path/to/cacert.der).") | |
exit() | |
# Download the burp cert | |
print("[*] Downloading Burp cert from http://{}:{}".format(burp_host, burp_port)) | |
certname = download_burp_cert(burp_host, burp_port) | |
# Iterate through the APKs | |
for file in args.apk_input_file: | |
project_dir = file.replace('.apk', '_out') | |
if os.path.isdir(project_dir): | |
print("Looks like {} exists".format(project_dir)) | |
print("***") | |
print("*** Attention: Please delete {} if you want to do the modifications again".format(project_dir)) | |
print("***") | |
print("This script will now only repack the contents of the {} directory".format(project_dir)) | |
else: | |
# Disassemble the app with APKTool | |
print("[*] Disassembling {}...".format(file)) | |
if not apktool_disassemble(file, apktool): | |
continue | |
# Create or add to network_security_config.xml | |
config_exists = do_network_security_config(project_dir) | |
# Add the certificate to the project | |
print("[*] Adding the cert to {}".format(project_dir)) | |
cert_dest_path = os.path.join(project_dir, 'res', 'raw', certname) | |
os.makedirs(os.path.join(project_dir, 'res', 'raw'), exist_ok=True) | |
shutil.copy2(certname, cert_dest_path) | |
print("[*] {} copied to {}".format(certname, cert_dest_path)) | |
# Edit the manifest if there wasn't already a config | |
if not config_exists: | |
print('[*] Changing the manifest...') | |
manifest_filepath = project_dir + os.sep + 'AndroidManifest.xml' | |
edit_manifest(manifest_filepath) | |
# Repackage the APK | |
print('[*] Rebuilding the APK...') | |
if not apktool_build(project_dir, apktool): | |
exit() | |
new_apk = os.path.join(project_dir, 'dist', os.listdir(project_dir + os.sep + 'dist')[0]) | |
# Caution: You must use zipalign at one of two specific points in the app-building process, depending on which app-signing tool you use: | |
#If you use apksigner, zipalign must only be performed before the APK file has been signed. If you sign your APK using apksigner and make further changes to the APK, its signature is invalidated. | |
#If you use jarsigner, zipalign must only be performed after the APK file has been signed. | |
print("[*] Signing the APK with jarsigner...") | |
if not keystore_present: | |
print("[*] Generating keystore...") | |
do_keytool(keystore_filename, keytool) | |
do_jarsigner(new_apk, keystore_filename, jarsigner) | |
# zipalign the APK | |
print("[*] Zipaligning the APK...") | |
new_apk = do_zipalign(new_apk, zipalign) | |
# Sign the APK | |
print("[*] Signing the APK with apksigner...") | |
do_apksigner(new_apk, keystore_filename, apksigner) | |
#print("[*] Removing unpacked directory...") | |
#shutil.rmtree(project_dir) | |
print('[+] Repackaging complete') | |
print("[*] If you get an error while installing, try uninstall app on mobile first") | |
print('[+] Upload both, {} and {}.idsig to the storage of your phone to install from storage'.format(new_apk, new_apk)) | |
print('[+] Or Install using "adb install {}"'.format(new_apk)) | |
if __name__ == '__main__': | |
parser = argparse.ArgumentParser() | |
parser.add_argument('apk_input_file', | |
nargs='+', | |
help='Specify the APK file(s) to repackage.') | |
parser.add_argument('-c', '--cert_path', | |
help='Specify the path to either a PEM or DER formatted file.') | |
parser.add_argument('-k', '--keystore_path', | |
help='Specify the path to an existing keystore.') | |
parser.add_argument("-pr", "--proxy", | |
nargs='?', | |
const="127.0.0.1:8080", | |
default="127.0.0.1:8080", | |
help="Specify the host and port where burp is listening (default 127.0.0.1:8080)") | |
args = parser.parse_args() | |
keystore_present = False | |
if args.keystore_path: | |
if not os.path.exists(args.keystore_path): | |
print("[-] The file, {}, cannot be found, or you do not have permission to open the file. Please check the file path and try again.".format(file)) | |
exit() | |
keystore_filename = args.keystore_path | |
keystore_present = True | |
else: | |
keystore_filename = "my_keystore.keystore" | |
keystore_present = os.path.exists(keystore_filename) | |
cert_present = False | |
if args.cert_path: | |
if not os.path.exists(args.cert_path): | |
print("[-] The file, {}, cannot be found, or you do not have permission to open the file. Please check the file path and try again.".format(file)) | |
exit() | |
certname = args.cert_path | |
cert_present = True | |
else: | |
certname = '' | |
for file in args.apk_input_file: | |
if not os.path.exists(file): | |
print("[-] The file, {}, cannot be found, or you do not have permission to open the file. Please check the file path and try again.".format(file)) | |
exit() | |
if not file.endswith('.apk'): | |
print("[-] Please verify that the file, {}, is in apk file. If it is, just add .apk to the filename.".format(file)) | |
exit() | |
if args.proxy.startswith('http'): | |
if '://' not in args.proxy: | |
print("[-] Unknown format for proxy. Please specify only a host and port (-pr 127.0.0.1:8080") | |
exit() | |
args.proxy = ''.join(args.proxy.split("//")[1:]) | |
burp_host = args.proxy.split(":")[0] | |
burp_port = int(args.proxy.split(":")[1]) | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment