-
Notifications
You must be signed in to change notification settings - Fork 16
/
repackage_apk_for_burp.py
313 lines (264 loc) · 12 KB
/
repackage_apk_for_burp.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
#!/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)"
__date__ = "20190705"
__version__ = "0.01"
__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_dir(name) or is_in_path(name):
return True
else:
return False
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) is not None
def is_in_dir(name, directory='.'):
"""Checks whether a file exists in a specified directory."""
files = (os.listdir(directory))
for file in files:
if file.lower() == name.lower():
if os.path.isfile(file):
return True
def apktool_decompile(filename):
"""Uses APKTool to decompile an APK"""
output = subprocess.getoutput("apktool d {} -o {}".format(filename, filename.replace('.apk', '_out')))
if 'Exception in' in output:
print('[-] An error occurred when decompiling the APK.')
print(output)
try:
os.rmdir(filename.replace('.apk', '_out'))
except:
pass
return False
else:
return True
def apktool_build(filepath):
"""Uses APKTool to create a new APK"""
output = subprocess.getoutput("apktool b {}".format(filepath))
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):
"""Uses keytool to generate a key."""
newline = os.linesep
'''Ugly hack to get around answering interactive prompts
The output would have been something like:
Generating 2,048 bit RSA key pair and self-signed certificate (SHA256withRSA) with a validity of 10,000 days
for: CN=Y, OU=Unknown, O=Y, L=Unknown, ST=Y, C=Unknown
[Storing test.keystore]'''
commands = ['Y', 'Y', 'Y', 'Y', 'Y', 'Y']
FNULL = open(os.devnull, 'w')
p = subprocess.Popen(['keytool', '-genkey', '-v', '-keystore',
keystore_name, '-storepass', 'password',
'-alias', 'android', '-keypass', 'password',
'-keyalg', 'RSA', '-keysize', '2048', '-validity',
'10000'],
stdin=subprocess.PIPE, stderr=subprocess.STDOUT,
stdout=FNULL, universal_newlines=True)
p.communicate(newline.join(commands))
keystore_present = True
def do_jarsigner(filepath, keystore):
"""Uses APKTool to create a new APK"""
output = subprocess.getoutput("jarsigner -verbose -keystore {} -storepass password -keypass password {} android".format(keystore, filepath))
if 'jar signed.' not in output:
print("[-] An error occurred during jarsigner: \n{}".format(output))
else:
print("[*] Signed!")
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...')
required_tools = ("apktool", "keytool", "jarsigner")
missing_tools = []
for tool in required_tools:
if not check_for_tools(tool):
missing_tools.append(tool)
if missing_tools:
for tool in missing_tools:
print("[-] {} could not be found in the current directory or in your PATH. Please ensure either of these conditions are met.".format(tool))
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:
# Decompile the app with APKTool
print("[*] Decompiling {}...".format(file))
if not apktool_decompile(file):
continue
project_dir = file.replace('.apk', '_out')
# 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):
exit()
new_apk = os.path.join(project_dir, 'dist', os.listdir(project_dir + os.sep + 'dist')[0])
# Sign the APK
print("[*] Signing the APK...")
if not keystore_present:
print("[*] Generating keystore...")
do_keytool(keystore_filename)
do_jarsigner(new_apk, keystore_filename)
print('[+] Repackaging complete. 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"
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()