-
Notifications
You must be signed in to change notification settings - Fork 12
/
AutoDroid.py
436 lines (360 loc) · 18 KB
/
AutoDroid.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
import json
import os
import platform
import subprocess
import sys
import time
import frida as frida
from androguard.core.analysis.analysis import Analysis
from androguard.core.bytecodes.apk import APK
from androguard.core.bytecodes.dvm import DalvikVMFormat
from androguard.decompiler.decompiler import DecompilerJADX
from androguard.misc import AnalyzeAPK
class AndroidInterface():
"""
A class for automating interaction with Android devices.
"""
# A boolean on if device IDs are being used.
_using_devices = False
# A boolean defining if application ids are being used.
_using_apps = False
# A global variable defining the key value pairs, where the key is replaced by the value
_variables = {}
# A global variable defining the current device being used
_active_device_id = None
def __init__(self):
pass
def _clean_terminal_response(self, byte_to_clean):
"""
Used to clean a response. Primarily decodes a byte to string and removes uncesseary new line characters
:param byte_to_clean:
:return: a string without newline characters.
"""
return byte_to_clean.decode().replace("\r", "").replace("\n", "")
def _is_adb_available(self):
"""
Runs a simple adb command to check if a device is connected via adb.
:return: True if a device is connected via adb
"""
adb_available = True
result = self._execute_command("adb")
if result is None or str(result).startswith("error"):
adb_available = False
return adb_available
def _get_all_devices(self):
'''
A function for returning all connected device. Used if the config provides a '*' to the config.
:return: a list of device IDs
'''
devices = self._execute_command("adb devices")
device_ids = []
for device in devices:
device = self._clean_terminal_response(device)
device_split = device.split("\t")
id = device_split[0]
if not id.startswith("List"):
device_ids.append(id)
return device_ids
raise Exception("All devices not implemented")
def _get_list_of_device_packages(self):
"""
Runs a series of commands to identify which application packages are installed on a device.
:return: a list of the installed packages
"""
packages = self._execute_command("adb -s {} shell pm list packages".format(self._active_device_id))
iterator = 0
for package in packages:
package = self._clean_terminal_response(package)
packages[iterator] = package.replace("package:", "")
iterator = iterator + 1
return packages
def _get_paths(self, list_of_packages):
"""
Identifies the apk path of for a list of packages
:param list_of_packages: a list of packages
:return: a list of APK locations relating to the provided package names
"""
list_of_paths = []
for package in list_of_packages:
package = package.replace("package:", "")
paths = self._execute_command("adb -s {} shell pm path {}".format(self._active_device_id, package))
for path in paths:
path = self._clean_terminal_response(path)
path = path.replace("package:", "")
list_of_paths.append(path)
return list_of_paths
def _execute_blocks(self, blocks_dict):
'''
A function used for executing all blocks of commads. :param blocks_dict: a dictionary of key value pairs,
where the key is the block name and the value is the list of commands.
'''
for block in blocks_dict:
self._execute_block(blocks_dict[block], blocks_dict)
def _execute_block(self, block_list, blocks_dict):
'''
A function for executing a single block of commands.
:param block_list: a list of commads belonging to the block being executed.
:param blocks_dict: The dictionary of all blocks.
'''
for command in block_list:
try:
if command.startswith("block:"):
command = command.replace("block:", "")
command = command.strip()
self._execute_block(blocks_dict[command], blocks_dict)
elif command.startswith("sleep:"):
command = self._construct_command(command)
time_to_sleep = command.replace("sleep:", "").strip()
time.sleep(int(time_to_sleep))
elif command.startswith("print:"):
command = self._construct_command(command)
message = command.replace("print:", "").strip()
print("\nMessage:\t{}\n".format(message))
elif command.startswith("frida:"):
command = self._construct_command(command)
command_word = command.replace("frida:", "").strip()
java_script_code_path, package_name = command_word.split(";")
def my_message_handler(message, payload):
print(message)
print(payload)
# Opens a session with the device/ process/ gaget
session = frida.get_usb_device().attach(package_name)
elif command.startswith("write:"):
command = self._construct_command(command)
command_word = command.replace("write:", "").strip()
path, data_to_write = command_word.split(";")
file_to_write = open(path,"w")
file_to_write.write(data_to_write)
file_to_write.close()
elif command.startswith("append:"):
command = self._construct_command(command)
command_word = command.replace("append:", "").strip()
path, data_to_write = command_word.split(";")
file_to_write = open(path,"a")
file_to_write.write(data_to_write+"\n")
file_to_write.close()
elif command.startswith("find:"):
command = self._construct_command(command)
command = command.replace("find:", "")
apk_path, string_to_find = command.split(";")
apk_path = apk_path.strip()
a, d, dx = AnalyzeAPK(apk_path)
found_strings = dx.find_strings(string_to_find) #Find strings by regex
for found_string in found_strings:
found_string = str(found_string).strip()
print("'find' command hit target in app '{}' of value '{}'".format(apk_path,found_string))
file_name = "{}-find_output.txt".format(string_to_find)
if os.path.isfile(file_name):
file_to_write = open(file_name, "a")
file_to_write.write("'{}' | '{}' \n".format(apk_path,found_string))
file_to_write.close()
else:
file_to_write = open(file_name, "w")
file_to_write.write("'{}' | '{}' \n".format(apk_path,found_string))
file_to_write.close()
elif command.startswith("read:"):
command = self._construct_command(command)
command = command.replace("read:", "").strip()
path, variable = command.split(";")
file_to_read= open(path,"r")
contents = file_to_read.readlines()
contents = "\n".join(contents)
self._variables[variable] = contents
file_to_read.close()
elif command.startswith("reverse:"):
command = self._construct_command(command)
command = command.replace("reverse:", "").strip()
if ";" not in command:
apk_path = command
a, d, dx = AnalyzeAPK(apk_path)
a.new_zip("{}.zip".format(apk_path))
else:
path, *params = command.split(";")
path = path.strip()
a, d, dx = AnalyzeAPK(path)
for param in params:
param = param.strip()
if param == "info":
info_data = {
"package_name":a.get_app_name(),
"package":a.get_package(),
"icon":a.get_app_icon(),
"permissions":a.get_permissions(),
"activities":a.get_activities(),
"android_version_code":a.get_androidversion_code(),
"android_version_name":a.get_androidversion_name(),
"min_sdk_version":a.get_min_sdk_version(),
"max_sdk_version":a.get_max_sdk_version(),
"target_sdk_version":a.get_target_sdk_version(),
"effective_sdk_version":a.get_effective_target_sdk_version()
}
with open('{}-apk-info.json'.format(path), 'w') as outfile:
json.dump(info_data, outfile, indent=4)
if param == "decompile":
if platform.system() != "Windows":
apk_info_file = open("{}-decompiled.txt".format(path), "w")
a2 = APK(path)
# Create DalvikVMFormat Object
d = DalvikVMFormat(a2)
# Create Analysis Object
dx = Analysis(d)
decompiler = DecompilerJADX(d, dx)
# propagate decompiler and analysis back to DalvikVMFormat
d.set_decompiler(decompiler)
# Now you can do stuff like:
for m in d.get_methods():
apk_info_file.write("\n--\n{}\n{}".format(m,decompiler.get_source_method(m)))
apk_info_file.close()
else:
print("Warning: Decompile not available on Windows")
if param == "manifest":
manifest = a.get_android_manifest_axml().get_xml()
apk_info_file = open("{}-AndroidManifest.xml".format(path), "wb")
apk_info_file.write(manifest)
apk_info_file.close()
if param == "zip":
a.new_zip("{}.zip".format(path))
# use to set variables at runtime
elif command.startswith("?"):
variable, command = command.split(" ", 1)
variable = variable.replace("?","")
formatted_command = self._construct_command(command)
command_result = self._execute_command(formatted_command)
result = ""
for line in command_result:
result = result + line.decode()
command_result = result
if command_result == None or command_result == []:
command_result = formatted_command
self._variables[variable] = command_result
else:
formatted_command = self._construct_command(command)
self._execute_command(formatted_command)
except:
print("Command {} failed".format(command))
def _execute_command(self, command):
"""
Runs a command provided as a string with subprocess.popen.
:param command: a string representation of a shell command
:return: a list of the result of the provided command
"""
proc = subprocess.Popen(command, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
lines = proc.stdout.readlines()
print("Run command: '{}'".format(command))
if lines is not None and len(lines) > 0:
print("\tResult: '{}...".format(lines[0]))
else:
print("No response from command")
return lines
def _construct_command(self, command):
'''
A funaction for replacing all defined variable/ wildcards in the command
:param command:
:return: a command that has been replaced with the provided variables/ wildcards
'''
for variable in self._variables:
# replace the variable
command = command.replace(variable, self._variables[variable])
return command
def _initialise_commands(self, devices, apps, blocks):
'''
Initialises the commands
:param devices: a list of devices to target from the config.
:param apps: a list of applications being targeted.
:param blocks: a list of command blocks.
'''
# set default variales
if self._using_devices:
self._variables["!adb_connect"] = "adb -s !device_id"
# If using devices and apps, have a nested loop and set the variables
if self._using_devices and self._using_apps:
for device in devices:
if device == '':
continue
self._active_device_id = device
self._variables["!device_id"] = device
if apps[0] == "*":
self._using_apps = True
apps = self._get_list_of_device_packages()
elif len(apps) > 0:
self._using_apps = True
else:
self._using_apps = False
for app in apps:
self._variables["!app_id"] = app
paths = self._get_paths([app])
self._variables["!app_path"] = paths[0]
self._execute_blocks(blocks)
# If using just devices loop through them and set device variable
elif self._using_devices and not self._using_apps:
for device in devices:
self._active_device_id = device
self._variables["!device_id"] = device
self._execute_blocks(blocks)
# If using just apps loop through them and set the app variable
elif not self._using_devices and self._using_apps:
if apps[0] == "*":
self._using_apps = True
apps = self._get_list_of_device_packages()
elif len(apps) > 0:
self._using_apps = True
else:
self._using_apps = False
for app in apps:
self._variables["!app_id"] = app
self._execute_blocks(blocks)
# if not using devices or apps, just run blocks
else:
self._execute_blocks(blocks)
def _read_config(self, config_file_path):
'''
A function for reading the JSON config
:param config_file_path: the path to the config file
:return: devices, apps, blocks
'''
devices = [] # generates !device-id variable. loop for amount off devices selected
blocks = {}
commands = []
variables = {} # adb, block, frida, reverse, bash/ normal
apps = [] # none, all, class paths # generates !app variable. loop for amount off apps selected
with open(config_file_path) as json_file:
config_data = json.load(json_file)
# Set devices being used
devices = config_data["devices"]
if len(devices) > 0:
if devices[0] == "*":
self._using_devices = True
devices = self._get_all_devices()
self._using_devices = True
else:
self._using_devices = False
if "variables" in config_data:
self._variables = config_data["variables"]
# Set apps being used
apps = config_data["apps"]
if len(apps) > 0:
self._using_apps = True
else:
self._using_apps = False
# Set blocks
if type(config_data["commands"]) == dict:
blocks = config_data["commands"]
for block in blocks:
for command in block:
commands.append(command)
# create a main block if none given
elif type(config_data["commands"]) == list:
blocks["main"] = config_data["commands"]
return devices, apps, blocks
def run(self, conig_path):
if self._is_adb_available():
devices, apps, blocks = self._read_config(conig_path)
self._initialise_commands(devices, apps, blocks)
else:
raise Exception("ADB not available")
if __name__ == "__main__":
if len(sys.argv) > 1:
device_manager = AndroidInterface()
device_manager.run(sys.argv[1])
else:
raise Exception("Please provide a configuration json file")