Skip to content

Instantly share code, notes, and snippets.

@floyd-fuh
Last active January 22, 2024 16:41
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save floyd-fuh/7f7408b560672ece3ea78348559d47b6 to your computer and use it in GitHub Desktop.
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
#!/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