diff --git a/.gitignore b/.gitignore index db3f06e6..bc2c477c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,10 @@ obj src/drozer/modules/**/*.class src/drozer/modules/**/*.apk src/drozer.egg-info +src/drozer/lib/standard-agent mercury.log deb_dist/* *.tar.gz -.vs/ \ No newline at end of file +.vs/ +.idea/ +.venv/ diff --git a/README.md b/README.md index 1729f33b..966ed43b 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,6 @@ drozer is open source software, maintained by WithSecure, and can be downloaded This is an BETA release of a rewritten drozer version; this version is updated to support python3. -Currently, the following known issues are present: - -- Building of custom agents functionality will crash the drozer client. This functionality is considered out of scope for the beta release of the revived drozer project. - ## Docker Container To help with making sure drozer can be run on all systems, a Docker container was created that has a working build of drozer. @@ -92,6 +88,12 @@ drozer can be installed using Android Debug Bridge (adb). Download the latest drozer Agent [here](https://github.com/WithSecureLabs/drozer-agent/releases/latest). +Or create a custom agent with +``` +drozer agent set-apk --latest +drozer agent interactive +``` +Then install with ```shell adb install drozer-agent.apk ``` diff --git a/pyproject.toml b/pyproject.toml index 6eab2ecd..a403d3f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,9 @@ dependencies = ["protobuf>=4.25.2", "twisted>=18.9.0", "service-identity", "distro", - "pyyaml"] + "pyyaml", + "readchar", + "ansi"] requires-python = ">=3.8" authors = [ { name = "WithSecure", email = "drozer@withsecure.com" }, diff --git a/src/WithSecure/common/cli.py b/src/WithSecure/common/cli.py index e5a159c3..e3c014f2 100644 --- a/src/WithSecure/common/cli.py +++ b/src/WithSecure/common/cli.py @@ -94,7 +94,7 @@ def run(self, argv=None): def do_commands(self, arguments): """shows a list of all console commands""" - print("usage: %s", self.__doc__.strip(), end="\n") + print("usage:", self.__doc__.strip(), end="\n") print("available commands:") print(self.__get_commands_help()) @@ -121,7 +121,7 @@ def __get_commands_help(self): commands = {} for command in self.__commands(): - commands[command.replace("do_", "")] = getattr(self, command).__doc__.strip() + commands[command.replace("do_", "").replace("_", "-")] = getattr(self, command).__doc__.strip() return console.format_dict(commands, left_margin=2) @@ -131,7 +131,7 @@ def __invoke_command(self, arguments): """ try: - command = arguments.command + command = arguments.command.replace("-", "_") if "do_" + command in dir(self): getattr(self, "do_" + command)(arguments) diff --git a/src/WithSecure/common/cli_fancy.py b/src/WithSecure/common/cli_fancy.py new file mode 100644 index 00000000..1b5e2ea0 --- /dev/null +++ b/src/WithSecure/common/cli_fancy.py @@ -0,0 +1,226 @@ +import os +from WithSecure.common import cli +import readchar +from ansi import * + + +class OT: + def __init__(self, string, children=None): + if children is None: + children = [] + self.string = string + self.children = list(children) + + +class FancyBase(cli.Base): + def __init__(self, add_help=True): + cli.Base.__init__(self, add_help=add_help) + + @staticmethod + def __ansi_print(*args, end=''): + print(*args, sep='', end=end) + + @staticmethod + def __print_init(prompt, max_options): + print('\n' * max_options, end='') + FancyBase.__ansi_print(cursor.prev_line(max_options), prompt) + + @staticmethod + def __print(input_str, options, max_options, selected_line, offset_x): + for i in range(max_options): + FancyBase.__ansi_print(cursor.next_line(1), cursor.erase_line(0)) + if i < len(options): + if i + 1 == selected_line: + FancyBase.__ansi_print(color.fg.blue, '-', cursor.goto_x(offset_x + 1), options[i], + color.fx.reset) + else: + FancyBase.__ansi_print(cursor.goto_x(offset_x + 1), options[i]) + + FancyBase.__ansi_print(cursor.prev_line(max_options), cursor.goto_x(offset_x + 1), cursor.erase_line(0), + input_str) + + # printing built string last ensures that cursor is left at correct pos + + @staticmethod + def __print__cleanup(input_str, max_options, offset_x): + for i in range(max_options): + FancyBase.__ansi_print(cursor.next_line(1), cursor.erase_line(2)) + + FancyBase.__ansi_print(cursor.prev_line(max_options), cursor.goto_x(offset_x + 1), cursor.erase_line(0), + input_str, end='\n') + + # Levenshtein string distance calculator + # modified to assign 0 cost to appending or prepending to string a + # seems to underestimate cost oops + # TODO: On second thoughts this might not be the best approach, lev may work with longer a strings but may need to use a diffrent algo for short strings + + @staticmethod + def lev(a, b): + len_a, len_b = len(a), len(b) + if len_a == 0: + return len_b + if len_b == 0: + return len_a + + a = a.lower() + b = b.lower() + + dist_m = [[0] * (len_b + 1) for _ in range(len_a + 1)] + + for itr_a in range(1, len_a + 1): + dist_m[itr_a][0] = itr_a + for itr_b in range(1, len_b + 1): + dist_m[0][itr_b] = 0 if itr_b <= len_b else itr_b # No cost for insertions at the start of s1 + + for itr_a in range(1, len_a + 1): + for itr_b in range(1, len_b + 1): + if a[itr_a - 1] == b[itr_b - 1]: + dist_m[itr_a][itr_b] = dist_m[itr_a - 1][itr_b - 1] + else: + ins_cost = (0 if itr_a == len_a or itr_a == 1 else 1) # Free if at start or end of s1 + dist_m[itr_a][itr_b] = min( + dist_m[itr_a - 1][itr_b] + 1, # Deletion + dist_m[itr_a][itr_b - 1] + ins_cost, # Insertion, + dist_m[itr_a - 1][itr_b - 1] + 1 # Substitution + ) + + if len_a <= len_b: + return min(dist_m[len_a][len_a:len_b + 1]) + else: + return dist_m[len_a][len_b] + + """ + finds all options in the options tree that begins with the given string + """ + + @staticmethod + def __matches(options, string): # not super efficient but was easy to write + def __impl(_segments, _option, _built, _valid): + if len(_segments) == 1: # base case, last segment + if _segments[0] == _option.string: + for _child in _option.children: # show only children if segment matches exactly + _valid.append((' '.join(_built + [_option.string, _child.string]), 0)) + elif _segments[0] == "": + valid.append((' '.join(_built + [_option.string]), 0)) + else: + _valid.append((' '.join(_built + [_option.string]), + FancyBase.lev(_segments[0], _option.string))) + return + + if _option.string != _segments[0]: # option body does must match non-last segment + return + + if len(_option.children) == 0: + return + + _built.append(_option.string) + for _child in _option.children: + __impl(_segments[1:], _child, _built, _valid) + _built.pop() + + segments = string.split(' ') + valid = [] + for option in options: + __impl(segments, option, [], valid) + valid.sort(key=lambda x: x[1]) + return list(map(lambda x: x[0], + filter(lambda x: x[1] < 4, # max lev distance before the option is not suggested + valid))) + + @staticmethod + def __strict_match(options, string): + def __impl(_segments, _option): + if _option.string != _segments[0]: + return False + + if len(_option.children) == 0 and len(_segments) == 1: + return True + + if len(_option.children) == 0 or len(_segments) == 1: + return False + + return any(map(lambda x: __impl(_segments[1:], x), _option.children)) + + segments = string.split(' ') + for option in options: + if __impl(segments, option): + return True + return False + + @staticmethod + def choose_fill(options, strict=False, head=None, prompt="> ", max_options=5): + if os.name == 'nt': # fix ansi if we are running on Windows + # TODO: detect if windows version does not support ansi flag and fall back to base choose + from ctypes import windll + k = windll.kernel32 + k.SetConsoleMode(k.GetStdHandle(-11), 7) + + if head is not None: + print(head) + + while True: + choice = FancyBase.__choose_fill_impl(options, prompt, max_options) + + if not strict or FancyBase.__strict_match(options, choice): + return choice + + print("input was not recognised") + + @staticmethod + def __choose_fill_impl(options, prompt, max_options): + cursor_x = len(prompt) + + FancyBase.__print_init(prompt, max_options) + matching_options = list(map(lambda x: x.string, options)) + FancyBase.__print("", matching_options, max_options, 0, cursor_x) + + stringbuilder = "" + selected_line = 0 + max_line = min(max_options, len(matching_options)) + virtual_space = False + + while True: + print('', end='', flush=True) + char = readchar.readkey() + + if char == readchar.key.ENTER: + if selected_line == 0: + FancyBase.__print__cleanup(stringbuilder, max_options, cursor_x) + return stringbuilder + else: + FancyBase.__print__cleanup(matching_options[selected_line - 1], max_options, cursor_x) + return matching_options[selected_line - 1] + + if char == readchar.key.BACKSPACE and len(stringbuilder) > 0: + virtual_space = False + stringbuilder = stringbuilder[:-1] + elif char.isprintable(): + if virtual_space: + virtual_space = False + if char != " ": + stringbuilder += " " + stringbuilder += char + elif char == readchar.key.UP and selected_line > 0: + selected_line -= 1 + elif char == readchar.key.DOWN and selected_line < max_line: + selected_line += 1 + elif char == readchar.key.TAB and max_line > 0: + virtual_space = True + stringbuilder = matching_options[max(0, selected_line - 1)] + selected_line = 0 + elif char == readchar.key.CTRL_W: + space_index = stringbuilder.rfind(' ') + selected_line = 0 + if space_index == -1: + virtual_space = False + stringbuilder = "" + else: + stringbuilder = stringbuilder[:space_index] + + matching_options = FancyBase.__matches(options, stringbuilder if not virtual_space else stringbuilder + " ") + + max_line = min(max_options, len(matching_options)) + if selected_line > max_line: + selected_line = max_line + + FancyBase.__print(stringbuilder, matching_options, max_options, selected_line, cursor_x) diff --git a/src/WithSecure/common/command_wrapper.py b/src/WithSecure/common/command_wrapper.py index 60ba498e..785e0913 100644 --- a/src/WithSecure/common/command_wrapper.py +++ b/src/WithSecure/common/command_wrapper.py @@ -4,8 +4,8 @@ class Wrapper(object): - - def _execute(self, argv): + @classmethod + def _execute(cls, argv): if platform.system() != "Windows": return os.spawnve(os.P_WAIT, argv[0], argv, os.environ) else: diff --git a/src/WithSecure/common/console.py b/src/WithSecure/common/console.py index 16667386..498ad058 100644 --- a/src/WithSecure/common/console.py +++ b/src/WithSecure/common/console.py @@ -10,7 +10,7 @@ # src: http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python def format_dict(values, left_margin=0): - width = {'gutter': 2, 'left_margin': left_margin, 'total': get_size()[0] - left_margin} + width = {'gutter': 2, 'left_margin': left_margin, 'total': get_size()[0] - left_margin - 1} width['key'] = min([max([len(k) for k in values.keys()] + [0]), width['total'] / 3]) width['value'] = width['total'] - (width['gutter'] + width['key']) diff --git a/src/WithSecure/common/path_completion.py b/src/WithSecure/common/path_completion.py index 75d56fb2..b373d06c 100644 --- a/src/WithSecure/common/path_completion.py +++ b/src/WithSecure/common/path_completion.py @@ -11,7 +11,7 @@ def complete(path, include_files=True): folder, search_path = get_folder_and_search_path(path, os.path.sep) folders = os.listdir(folder) - return [s.replace(" ", "\ ") for s in get_suggestions(folder, search_path, folders, os.path.sep, include_files)] + return [s.replace(" ", r"\ ") for s in get_suggestions(folder, search_path, folders, os.path.sep, include_files)] def get_folder_and_search_path(path, sep): diff --git a/src/WithSecure/common/stream.py b/src/WithSecure/common/stream.py index 2db87a7e..4c13a6bf 100644 --- a/src/WithSecure/common/stream.py +++ b/src/WithSecure/common/stream.py @@ -105,7 +105,7 @@ def replace_color(m): return "%s%s%s" % (Colors[m.group(1)], m.group(2), Colors['end']) - text = re.sub("\[color\s*([a-z]+)\](.*?)\[\/color\]", replace_color, text) + text = re.sub(r"\[color\s*([a-z]+)\](.*?)\[\/color\]", replace_color, text) return text @@ -122,6 +122,6 @@ def remove_color(m): return "%s" % (m.group(2)) - text = re.sub("\[color\s*([a-z]+)\](.*?)\[\/color\]", remove_color, text) + text = re.sub(r"\[color\s*([a-z]+)\](.*?)\[\/color\]", remove_color, text) return text diff --git a/src/drozer/__init__.py b/src/drozer/__init__.py index f5f41e56..d539d50c 100644 --- a/src/drozer/__init__.py +++ b/src/drozer/__init__.py @@ -1 +1 @@ -__version__ = "3.1.0" +__version__ = "3.1.1" diff --git a/src/drozer/agent/builder.py b/src/drozer/agent/builder.py index 9d74a5a2..998d968c 100644 --- a/src/drozer/agent/builder.py +++ b/src/drozer/agent/builder.py @@ -1,66 +1,136 @@ import os +from pathlib import Path import platform +import yaml +import shutil +import tempfile from WithSecure.common import command_wrapper - from drozer.configuration import Configuration +from drozer.agent import manifest class Packager(command_wrapper.Wrapper): - __aapt = Configuration.library("aapt") - __aapt_osx = Configuration.library("aapt-osx") - __aapt_exe = Configuration.library("aapt.exe") __apk_tool = Configuration.library("apktool.jar") __certificate = Configuration.library("certificate.pem") __key = Configuration.library("key.pk8") __java = Configuration.executable("java") - __sign_apk = Configuration.library("signapk.jar") + __sign_apk = Configuration.library("apksigner.jar") - __endpoint = "endpoint.txt" + __endpoint = "config.txt" __manifest = "AndroidManifest.xml" __apktool_yml = "apktool.yml" + __aapt = Configuration.library("aapt") + __zipalign = Configuration.library("zipalign") - def __init__(self): - self.__wd = self._get_wd() - - def apk_path(self, signed=True): - if signed: - return os.path.join(self.__wd, "agent.apk") + def __init__(self, wd=None): + if wd is None: + self.__tmp_dir_object = tempfile.TemporaryDirectory() + self.__wd = Path(self.__tmp_dir_object.name) else: - return os.path.join(self.__wd, "agent-unsigned.apk") + self.__tmp_dir_object = None + self.__wd = wd + + self.__manifest_file = None + self.__config_file = None + self.__apktool_file = None + + @classmethod + def init_from_tmp_folder(cls, folder_path): + p = Packager(wd=folder_path) + p._init_components() + return p + + @classmethod + def init_from_folder(cls, folder_path): + p = Packager() + print("Copying files to working directory, this may take some time...") + shutil.copytree(folder_path, p.source_dir()) + p._init_components() + return p + + @classmethod + def init_from_apk(cls, apk_path): + p = Packager() + cls.unpack_apk(apk_path, p.source_dir()) + p._init_components() + return p + + def _init_components(self): + self.__manifest_file = manifest.Manifest(self.manifest_path()) + self.__config_file = manifest.Endpoint(self.endpoint_path()) + with open(self.apktool_yml_path(), 'r') as file: + self.__apktool_file = yaml.safe_load(file) + + def close(self): + if self.__tmp_dir_object is not None: + self.__tmp_dir_object.cleanup() + + def source_dir(self): + return os.path.join(self.__wd, "agent") + + def apk_path(self, name): + return os.path.join(self.__wd, name + ".apk") def endpoint_path(self): - return os.path.join(self.__wd, "agent", "res", "raw", self.__endpoint) + return os.path.join(self.source_dir(), "res", "raw", self.__endpoint) def manifest_path(self): - return os.path.join(self.__wd, "agent", self.__manifest) + return os.path.join(self.source_dir(), self.__manifest) def apktool_yml_path(self): - return os.path.join(self.__wd, "agent", self.__apktool_yml) + return os.path.join(self.source_dir(), self.__apktool_yml) - def package(self): - platform_name = platform.system() + def get_config_file(self): + return self.__config_file - if platform_name == "Darwin": - aapt = self.__aapt_osx - elif platform_name == "Windows": - aapt = self.__aapt_exe - else: - aapt = self.__aapt + def get_manifest_file(self): + return self.__manifest_file + + def get_apktool_file(self): + return self.__apktool_file - if self._execute( - [self.__java, "-jar", self.__apk_tool, "build", self.source_dir(), "-o", self.apk_path(False)]) != 0: + def rename_package(self, name): + app_name = "com.withsecure." + name + + self.__apktool_file["packageInfo"]["renameManifestPackage"] = app_name + self.__manifest_file.set_name(name) + + def package(self): + with open(self.apktool_yml_path(), 'w') as file: + yaml.dump(self.__apktool_file, file) + self.__manifest_file.write() + self.__config_file.write() + + if self._execute([self.__java, "-jar", self.__apk_tool, "build", + self.source_dir(), "-o", self.apk_path("agent-unsigned")]) != 0: raise RuntimeError("could not repack the agent sources") - if self._execute([self.__java, "-jar", self.__sign_apk, self.__certificate, self.__key, self.apk_path(False), - self.apk_path(True)]) != 0: - raise RuntimeError("could not sign the agent package") - return os.path.join(self.__wd, "agent.apk") + if self._execute([self.__zipalign, "4", self.apk_path("agent-unsigned"), + self.apk_path("agent-unsigned-aligned")]) != 0: + raise RuntimeError("Could not align apk") - def source_dir(self): - return os.path.join(self.__wd, "agent") + if self._execute([self.__java, "-jar", self.__sign_apk, "sign", "-key", self.__key, "-cert", self.__certificate, + "--in", self.apk_path("agent-unsigned-aligned"), "--out", self.apk_path("agent")]) != 0: + raise RuntimeError("could not sign the agent package") + + return self.apk_path("agent") def unpack(self, name): - if self._execute([self.__java, "-jar", self.__apk_tool, "decode", Configuration.library(name + ".apk"), - "-o", self.source_dir()]) != 0: - raise RuntimeError("could not unpack " + name) + """ + Depreciated + use init_from_apk() instead to ensure Packager object is always in a valid state + """ + + apk_path = Configuration.library(name + ".apk") + if apk_path is None: + raise RuntimeError("could not locate " + name + ".apk in library") + + self.unpack_apk(apk_path, self.source_dir()) + self._init_components() + + @classmethod + def unpack_apk(cls, in_path, out_path): + if cls._execute([cls.__java, "-jar", cls.__apk_tool, "decode", in_path, + "-o", out_path]) != 0: + raise RuntimeError("could not unpack " + in_path) diff --git a/src/drozer/agent/manager.py b/src/drozer/agent/manager.py index 08c72aef..09632e32 100644 --- a/src/drozer/agent/manager.py +++ b/src/drozer/agent/manager.py @@ -1,77 +1,333 @@ -import itertools -from WithSecure.common import cli -import yaml +import shutil +import os +from urllib.error import HTTPError + +from WithSecure.common.cli_fancy import * from drozer import android, meta from drozer.agent import builder, manifest +from drozer.configuration import Configuration +from urllib.request import urlopen +from tempfile import TemporaryDirectory -class AgentManager(cli.Base): +class AgentManager(FancyBase): """ drozer agent COMMAND [OPTIONS] - + A utility for building custom drozer Agents. """ - + def __init__(self): - cli.Base.__init__(self) - + FancyBase.__init__(self) + self._parser.add_argument("--rogue", action="store_true", default=False, help="create a rogue agent with no GUI") self._parser.add_argument("--no-gui", action="store_true", default=False, help="deprecated: rather use --rogue. create an agent with no GUI") self._parser.add_argument("--granular", action="store_true", default=False, help="don't request all permissions when building GUI-less agent") self._parser.add_argument("--permission", "-p", nargs="+", help="add permissions to the Agent manifest") - self._parser.add_argument("--define-permission", "-d", metavar="name protectionLevel", nargs="+", help="define a permission and protectionLevel in the Agent manifest") + self._parser.add_argument("--define-permission", "-d", metavar="name:protectionLevel", nargs="+", help="define a permission and protectionLevel in the Agent manifest") self._parser.add_argument("--server", default=None, metavar="HOST[:PORT]", help="specify the address and port of the drozer server") - + self._parser.add_argument("--name", "-n", default=None, help="set package name to allow multiple instances") + self._parser.add_argument("--theme", "-t", default=None, help="set app theme (red/blue/purple)") + self._parser.add_argument("--out", "-o", default=None, help="set output file") + self._parser.add_argument("--latest", "-l", action="store_true", help="for use with set_apk, download the lates drozer agent from WithSecureLabs repository") + self._parser.add_argument("--file", "-f", default=None, help="for use with set_apk, set a local file as the base for custom drozer agents") + self._parser.add_argument("--version", "-v", default=None, help="for use with set_apk, specify the apk version to install") + self._parser.add_argument("--url", "-u", default=None, help="for use with set_apk, download apk from url") + + def do_interactive(self, arguments): + """build drozer agents in an interactive shell""" + + presets = { + "red": (android.permissions, [], "drozer_red", "red"), + "purple": ([ + "android.permission.INTERNET", + "android.permission.SYSTEM_ALERT_WINDOW", + "android.permission.FOREGROUND_SERVICE" + ], [], "drozer_purple", "purple") + } + + # options_tree = [ + # OT("standard-agent"), + # OT("rogue-agent") + # ] + # agent_type = FancyBase.choose_fill(options_tree, strict=True, head="Select drozer agent type", + # max_options=len(options_tree)) + + base_apk = Configuration.library("standard-agent") + if base_apk is None: + print("Could not find base apk, has it been set with \"set-apk\"") + return + packager = builder.Packager.init_from_folder(base_apk) + + permissions = set(packager.get_manifest_file().permissions()) + security_permissions = set(packager.get_manifest_file().security_permissions()) + + built = None + name = None + theme = None + port = 31415 + + print("set drozer options:\n") + while True: + options_tree = [ # maps must be re-created to be consumed again :c + OT("add", map(lambda x: OT(x.split('.')[-1]), android.permissions)), + OT("remove", map(lambda x: OT(x.split('.')[-1]), permissions)), + OT("preset", [OT("red"), OT("purple")]), + OT("define"), + OT("list", [OT("set"), OT("all")]), + OT("set", [OT("name"), + OT("theme", [OT("purple"), OT("red"), OT("green"), OT("blue")]), + OT("port")]), + OT("config"), + OT("build"), + OT("help"), + OT("exit") + ] + + choice = (FancyBase.choose_fill(options_tree) + .split(' ')) + num_segments = len(choice) + + help_str = """" + add PERMISSION_NAME add permission to manifest + define PERMISSION_NAME PROTECTION_LEVEL + define security permission + remove PERMISSION_NAME remove permission from manifest + list all list all available permissions + list set list all set permissions + set name NAME set the package name of the output apk + set theme THEME set the application theme + set port PORT set the default listening port for drozer server + config print currently set configuration + build [OUTPUT] build the apk with an optional name + exit exit tool + """ + + match choice[0].lower(): + case "help": + print(help_str) + case "add": + if num_segments == 1: + print("permission name required after \"add\"") + continue + perm_full_name = "android.permission." + choice[1] + if perm_full_name in android.permissions: + permissions.append(perm_full_name) + else: + print("permission " + perm_full_name + " is not valid") + case "preset": + if num_segments == 1: + print("preset requires a value") + continue + try: + preset = presets[choice[1].lower()] + permissions, security_permissions, name, theme = preset + except KeyError: + print(f"unknown preset {choice[1]}") + continue + case "remove": + if num_segments == 1: + print("permission name required after \"remove\"") + continue + perm_full_name = "android.permission." + choice[1] + try: + permissions.remove(perm_full_name) + except ValueError: + pass + case "define": + if num_segments < 3: + print("define requires two arguments") + pass + security_permissions.append((choice[1], choice[2])) + case "list": + if num_segments == 1: + print("list requires a option (all, set)") + continue + match choice[1]: + case "all": + print("android permissions:\n" + '\n'.join(android.permissions)) + case "set": + print("set permissions:\n" + '\n'.join(permissions)) + case "set": + if num_segments < 3: + print("set requires a name and value") + match choice[1]: + case "name": + name = choice[2] + print(f"name => {choice[2]}") + case "theme": + theme = choice[2] + print(f"theme => {choice[2]}") + case "port": + try: + int(choice[2]) + packager.get_config_file().put("server-port", choice[2]) + print(f"port => {choice[2]}") + except ValueError: + print("port must be an integer") + case _: + print(f"unrecognised key \"{choice[1]}\"") + case "config": + print("---Drozer Configuration---") + if name is not None: + print(f"package name: {name}") + if theme is not None: + print(f"package theme: {theme}") + print(f"default port: {port}") + print("set permissions:") + print("\n".join(map(lambda x: f"\t{x}", permissions))) + print("security permission:") + print("\n".join(map(lambda x: f"\t{x[0]}\t{x[1]}", security_permissions))) + case "build": + built = self.build_std(packager, permissions=permissions, name=name, theme=theme) + out_name = choice[1] if num_segments > 1 else "." + out = shutil.copy(built, out_name) + print("Done:", out) + case "exit": + break + print("cleaning up working directory...") + packager.close() + def do_build(self, arguments): """build a drozer Agent""" - source = (arguments.rogue or arguments.no_gui) and "rogue-agent" or "standard-agent" - packager = builder.Packager() - packager.unpack(source) - - if arguments.rogue or arguments.no_gui: - e = manifest.Endpoint(packager.endpoint_path()) - if arguments.server != None: - e.put_server(arguments.server) - e.write() - - if not arguments.granular: - permissions = set(android.permissions) - else: - permissions = set([]) + source = "rogue-agent" if (arguments.rogue or arguments.no_gui) else "standard-agent" + base_apk = Configuration.library("standard-agent") + if base_apk is None: + print("Could not find base apk, has it been set with \"set-apk\"") + return + packager = builder.Packager.init_from_folder(base_apk) + + if arguments.define_permission is not None: + define_permission = set(map(lambda x: tuple(x.split(':', 1)), arguments.define_permission)) else: - permissions = set([]) - - if arguments.permission != None: - permissions = permissions.union(arguments.permission) - - defined_permissions = {} - if arguments.define_permission != None: - defined_permissions = dict(itertools.izip_longest(*[iter(arguments.define_permission)] * 2, fillvalue="")) - - # add extra permissions to the Manifest file - m = manifest.Manifest(packager.manifest_path()) - - # Apktool v2.2.4 generates a malformed YAML file when unpacking apks - # See https://github.com/iBotPeaches/Apktool/issues/1610 - # This workaround generates a valid YAML document and prevents agent building from failing - yaml_doc = yaml.load(open(packager.apktool_yml_path()).read().replace('!!brut.androlib.meta.MetaInfo','')) - m_ver = yaml_doc['versionInfo']['versionName'] - #m_ver = m.version() + define_permission = None + built = self.build_std(packager, permissions=arguments.permission, define_permission=define_permission, + name=arguments.name, theme=arguments.theme) + + if arguments.out is not None: + out = shutil.copy(built, arguments.out) + else: + out = shutil.copy(built, ".") + packager.close() + print("Done:", out) + + @staticmethod + def build_std(packager, permissions=None, define_permission=None, name=None, theme=None): + if permissions is not None: + permissions = set(permissions) + else: + permissions = set() + if define_permission is not None: + define_permission = set(define_permission) + else: + define_permission = set() + + # ensure minimal permissions + permissions.add("android.permission.INTERNET") + permissions.add("com.withsecure.dz.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION") + define_permission.add(("com.withsecure.dz.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION", "signature")) + + m_ver = packager.get_apktool_file()['versionInfo']['versionName'] c_ver = meta.version.__str__() - + if m_ver != c_ver: print("Version Mismatch: Consider updating your build(s)") print("Agent Version: %s" % m_ver) print("drozer Version: %s" % c_ver) - for p in permissions: - m.add_permission(p) + man = packager.get_manifest_file() + for p in permissions: # add our perms + man.add_permission(p) + + for permission_name, protection_level in define_permission: + man.define_permission(permission_name, protection_level) + + if name is not None: + man.set_name(name) + + if theme is not None: + packager.get_config_file().put("theme", theme) + + return packager.package() + + def build_rogue(self, packager, server=None, granular=False): + if server is not None: + packager.get_config_file().put_server(server) + + if not granular: + permissions = set(android.permissions) + else: + permissions = set([]) + pass + + def do_set_apk(self, arguments): + """globally set the base apk which build commands will use""" + + out_path = os.path.join(Configuration.library_path(), "standard-agent") + if arguments.file is not None: + self._set_apk(arguments.file, out_path) + elif arguments.url is not None or arguments.version is not None or arguments.latest: + if arguments.url is not None: + url = arguments.url + elif arguments.version is not None: + url = f"{Configuration.ws_dz_agent_url}download/{arguments.version}/drozer-agent.apk" + else: + url = f"{Configuration.ws_dz_agent_url}latest/download/drozer-agent.apk" + + self._download_apk(url, out_path) + else: + print("You must specify an apk with a local file, url, or use the latest flag") + + @classmethod + def _download_apk(cls, source, out_path): + with TemporaryDirectory() as tmp: + apk_path = os.path.join(tmp, "agent.apk") + + print(f"Downloading latest apk from: {source}") + + try: + request = urlopen(source) + except HTTPError as e: + if e.code == 404: + print(f"Release does not appear to exist, if you specified a custom version or url verify that it exists") + else: + print(f"Unexpected HTTP error occurred, verify you can access the repository at: {source}") + raise e + + with open(apk_path, "wb") as f: + f.write(request.read()) + print("Download finished") + + cls._set_apk_from_tmp(apk_path, out_path, tmp) + print("Cleaning up working directory") + print("done!") + + @classmethod + def _set_apk(cls, apk_path, out_path): + with TemporaryDirectory() as tmp: + cls._set_apk_from_tmp(apk_path, out_path, tmp) + + @classmethod + def _set_apk_from_tmp(cls, apk_path, out_path, tmp): + unpack_path = os.path.join(tmp, "agent") + print("Unpacking apk") + + try: + builder.Packager.unpack_apk(apk_path, unpack_path) + except Exception as e: + print("Unable to unpack apk, it may be corrupt") + raise e + + print("Unpack finished") - for name, protectionLevel in defined_permissions.iteritems(): - m.define_permission(name, protectionLevel) + p = builder.Packager.init_from_tmp_folder(tmp) - m.write() + man = p.get_manifest_file() # clear all uses-perms and perms from manifest + man.remove_all_perms() + man.write() - built = packager.package() - - print("Done:", built) + if os.path.exists(out_path): + print("Removing existing agent base") + shutil.rmtree(out_path) + print("Copying to library, this may take some time...") + shutil.copytree(unpack_path, out_path) diff --git a/src/drozer/agent/manifest.py b/src/drozer/agent/manifest.py index 6b935d1a..683b4d9e 100644 --- a/src/drozer/agent/manifest.py +++ b/src/drozer/agent/manifest.py @@ -4,65 +4,107 @@ class Endpoint(object): def __init__(self, path): self.__path = path - - lines = open(self.__path).read().split("\n") - data = dict(map(lambda l: l.split(":"), filter(lambda l: l.find(":") > -1, lines))) - - self.host = data['host'] - self.password = data['password'] - self.port = int(data['port']) - self.ssl = data['ssl'].startswith("t") - self.ts_password = data['ts_password'] - self.ts_path = data['ts_path'] + + try: + with open(self.__path, 'r') as file: + lines = file.readlines() + self.data = dict(map(lambda x: x.split(":"), filter(lambda p: p.find(":") > -1, lines))) + except FileNotFoundError: + self.data = dict() def put_server(self, server): if isinstance(server, tuple): - self.host, self.port = server + self.data["host"], self.data["self.port"] = server else: if server.find(":") > -1: - self.host, self.port = server.split(":") + self.data["host"], self.data["self.port"] = server.split(":") else: - self.host = server + self.data["host"] = server + + def put(self, key, value): + self.data[key] = value + + def get(self, key): + return self.data[key] def write(self): - h = open(self.__path, 'w') - - h.write("drozer Endpoint\n") - h.write("---------------\n") - h.write("host:" + self.host + "\n") - h.write("port:" + str(self.port) + "\n") - h.write("password:" + self.password + "\n") - h.write("ssl:" + str(self.ssl).lower() + "\n") - h.write("ts_path:" + self.ts_path + "\n") - h.write("ts_password:" + self.ts_password) + with open(self.__path, 'w') as file: + file.write("drozer Config\n") + file.write("-------------\n") + for key, value in self.data.items(): + file.write(f"{key}:{value}\n") class Manifest(object): def __init__(self, path): self.__path = path - self.__doc = xml.fromstring(open(self.__path).read()) - + + with open(self.__path, 'r') as file: + self.__doc = xml.fromstring(file.read()) def add_permission(self, name): node = xml.Element('uses-permission') - node.attrib["ns0:name"] = name + node.attrib["{http://schemas.android.com/apk/res/android}name"] = name self.__doc.insert(len(list(self.__doc)) - 1, node) - def define_permission(self, name, protectionLevel): + def remove_permission(self, name): + node = self.__doc.find(f"uses-permission[@{{http://schemas.android.com/apk/res/android}}name='{name}']") + if node is not None: + self.__doc.remove(node) + + def define_permission(self, name, protection_level): node = xml.Element('permission') - node.attrib["ns0:name"] = name - node.attrib["ns0:protectionLevel"] = protectionLevel + node.attrib["{http://schemas.android.com/apk/res/android}name"] = name + node.attrib["{http://schemas.android.com/apk/res/android}protectionLevel"] = protection_level self.__doc.insert(len(list(self.__doc)) - 1, node) + + def set_name(self, name): + full_name = "com.withsecure." + name + dr = "DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" + + # set manifest package name + self.__doc.attrib["package"] = full_name + + # rename uses-permission for DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION + na_attrib_name = "{http://schemas.android.com/apk/res/android}name" + for node in filter(lambda p: dr in p.attrib[na_attrib_name], + self.__doc.findall(f".//*[@{na_attrib_name}]")): + node.attrib[na_attrib_name] = full_name + "." + dr + + # replace all authorities + au_attrib_name = "{http://schemas.android.com/apk/res/android}authorities" + for node in self.__doc.findall(f".//*[@{au_attrib_name}]"): + val = node.attrib[au_attrib_name] + node.attrib[au_attrib_name] = val.replace("com.withsecure.dz", full_name) + + # set launcher name + launcher_activity = self.__doc.find("application/activity[@{http://schemas.android.com/apk/res/android}name='com.WithSecure.dz.activities.MainActivity']") + if launcher_activity is not None: + launcher_activity.attrib["{http://schemas.android.com/apk/res/android}label"] = name def permissions(self): - return self.__doc.findall('uses-permission') + return list(map(lambda x: x.attrib['{http://schemas.android.com/apk/res/android}name'], + self.__doc.findall('uses-permission'))) + + def security_permissions(self): + return list(map(lambda x: (x.attrib['{http://schemas.android.com/apk/res/android}name'], + x.attrib['{http://schemas.android.com/apk/res/android}protectionLevel']), + self.__doc.findall('permission'))) + + def remove_all_perms(self): + for permission_node in self.__doc.findall('uses-permission'): + self.__doc.remove(permission_node) + for defined_permission_node in self.__doc.findall('permission'): + self.__doc.remove(defined_permission_node) def write(self): xml.ElementTree(self.__doc).write(self.__path) def version(self): return self.__doc.attrib['{http://schemas.android.com/apk/res/android}versionName'] - + + def tree(self): + return self.__doc diff --git a/src/drozer/android.py b/src/drozer/android.py index c556465f..95f081cc 100644 --- a/src/drozer/android.py +++ b/src/drozer/android.py @@ -1,131 +1,354 @@ import re; import base64; -permissions = [ 'android.permission.ACCESS_CHECKIN_PROPERTIES', +permissions = [ 'android.permission.ACCESS_ALL_DOWNLOADS', + 'android.permission.ACCESS_BLUETOOTH_SHARE', + 'android.permission.ACCESS_CACHE_FILESYSTEM', + 'android.permission.ACCESS_CHECKIN_PROPERTIES', 'android.permission.ACCESS_COARSE_LOCATION', + 'android.permission.ACCESS_CONTENT_PROVIDERS_EXTERNALLY', + 'android.permission.ACCESS_DOWNLOAD_MANAGER', + 'android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED', + 'android.permission.ACCESS_DRM_CERTIFICATES', + 'android.permission.ACCESS_EPHEMERAL_APPS', 'android.permission.ACCESS_FINE_LOCATION', + 'android.permission.ACCESS_FM_RADIO', + 'android.permission.ACCESS_INPUT_FLINGER', + 'android.permission.ACCESS_KEYGUARD_SECURE_STORAGE', 'android.permission.ACCESS_LOCATION_EXTRA_COMMANDS', 'android.permission.ACCESS_MOCK_LOCATION', + 'android.permission.ACCESS_MTP', + 'android.permission.ACCESS_NETWORK_CONDITIONS', 'android.permission.ACCESS_NETWORK_STATE', + 'android.permission.ACCESS_NOTIFICATIONS', + 'android.permission.ACCESS_NOTIFICATION_POLICY', + 'android.permission.ACCESS_PDB_STATE', 'android.permission.ACCESS_SURFACE_FLINGER', + 'android.permission.ACCESS_VOICE_INTERACTION_SERVICE', + 'android.permission.ACCESS_VR_MANAGER', 'android.permission.ACCESS_WIFI_STATE', + 'android.permission.ACCESS_WIMAX_STATE', 'android.permission.ACCOUNT_MANAGER', 'android.permission.ADD_VOICEMAIL', + 'android.permission.ALLOW_ANY_CODEC_FOR_PLAYBACK', + 'android.permission.ASEC_ACCESS', + 'android.permission.ASEC_CREATE', + 'android.permission.ASEC_DESTROY', + 'android.permission.ASEC_MOUNT_UNMOUNT', + 'android.permission.ASEC_RENAME', 'android.permission.AUTHENTICATE_ACCOUNTS', + 'android.permission.BACKUP', 'android.permission.BATTERY_STATS', 'android.permission.BIND_ACCESSIBILITY_SERVICE', 'android.permission.BIND_APPWIDGET', + 'android.permission.BIND_CARRIER_MESSAGING_SERVICE', + 'android.permission.BIND_CARRIER_SERVICES', + 'android.permission.BIND_CHOOSER_TARGET_SERVICE', + 'android.permission.BIND_CONDITION_PROVIDER_SERVICE', + 'android.permission.BIND_CONNECTION_SERVICE', 'android.permission.BIND_DEVICE_ADMIN', + 'android.permission.BIND_DIRECTORY_SEARCH', + 'android.permission.BIND_DREAM_SERVICE', + 'android.permission.BIND_INCALL_SERVICE', 'android.permission.BIND_INPUT_METHOD', + 'android.permission.BIND_INTENT_FILTER_VERIFIER', + 'android.permission.BIND_JOB_SERVICE', + 'android.permission.BIND_KEYGUARD_APPWIDGET', + 'android.permission.BIND_MIDI_DEVICE_SERVICE', + 'android.permission.BIND_NFC_SERVICE', + 'android.permission.BIND_NOTIFICATION_LISTENER_SERVICE', + 'android.permission.BIND_NOTIFICATION_RANKER_SERVICE', + 'android.permission.BIND_PACKAGE_VERIFIER', + 'android.permission.BIND_PRINT_RECOMMENDATION_SERVICE', + 'android.permission.BIND_PRINT_SERVICE', + 'android.permission.BIND_PRINT_SPOOLER_SERVICE', + 'android.permission.BIND_QUICK_SETTINGS_TILE', 'android.permission.BIND_REMOTEVIEWS', + 'android.permission.BIND_REMOTE_DISPLAY', + 'android.permission.BIND_ROUTE_PROVIDER', + 'android.permission.BIND_RUNTIME_PERMISSION_PRESENTER_SERVICE', + 'android.permission.BIND_SCREENING_SERVICE', + 'android.permission.BIND_TELECOM_CONNECTION_SERVICE', 'android.permission.BIND_TEXT_SERVICE', + 'android.permission.BIND_TRUST_AGENT', + 'android.permission.BIND_TV_INPUT', + 'android.permission.BIND_TV_REMOTE_SERVICE', + 'android.permission.BIND_VOICE_INTERACTION', 'android.permission.BIND_VPN_SERVICE', + 'android.permission.BIND_VR_LISTENER_SERVICE', 'android.permission.BIND_WALLPAPER', 'android.permission.BLUETOOTH', 'android.permission.BLUETOOTH_ADMIN', + 'android.permission.BLUETOOTH_MAP', + 'android.permission.BLUETOOTH_PRIVILEGED', + 'android.permission.BLUETOOTH_STACK', + 'android.permission.BRICK', + 'android.permission.BROADCAST_CALLLOG_INFO', + 'android.permission.BROADCAST_NETWORK_PRIVILEGED', 'android.permission.BROADCAST_PACKAGE_REMOVED', + 'android.permission.BROADCAST_PHONE_ACCOUNT_REGISTRATION', 'android.permission.BROADCAST_SMS', 'android.permission.BROADCAST_STICKY', 'android.permission.BROADCAST_WAP_PUSH', + 'android.permission.CACHE_CONTENT', 'android.permission.CALL_PHONE', 'android.permission.CALL_PRIVILEGED', 'android.permission.CAMERA', + 'android.permission.CAMERA_DISABLE_TRANSMIT_LED', + 'android.permission.CAMERA_SEND_SYSTEM_EVENTS', + 'android.permission.CAPTURE_AUDIO_HOTWORD', + 'android.permission.CAPTURE_AUDIO_OUTPUT', + 'android.permission.CAPTURE_SECURE_VIDEO_OUTPUT', + 'android.permission.CAPTURE_TV_INPUT', + 'android.permission.CAPTURE_VIDEO_OUTPUT', + 'android.permission.CARRIER_FILTER_SMS', + 'android.permission.CHANGE_APP_IDLE_STATE', + 'android.permission.CHANGE_BACKGROUND_DATA_SETTING', 'android.permission.CHANGE_COMPONENT_ENABLED_STATE', 'android.permission.CHANGE_CONFIGURATION', + 'android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST', 'android.permission.CHANGE_NETWORK_STATE', 'android.permission.CHANGE_WIFI_MULTICAST_STATE', 'android.permission.CHANGE_WIFI_STATE', + 'android.permission.CHANGE_WIMAX_STATE', 'android.permission.CLEAR_APP_CACHE', + 'android.permission.CLEAR_APP_GRANTED_URI_PERMISSIONS', 'android.permission.CLEAR_APP_USER_DATA', + 'android.permission.CONFIGURE_DISPLAY_COLOR_TRANSFORM', + 'android.permission.CONFIGURE_WIFI_DISPLAY', + 'android.permission.CONFIRM_FULL_BACKUP', + 'android.permission.CONNECTIVITY_INTERNAL', + 'android.permission.CONTROL_INCALL_EXPERIENCE', + 'android.permission.CONTROL_KEYGUARD', 'android.permission.CONTROL_LOCATION_UPDATES', + 'android.permission.CONTROL_VPN', + 'android.permission.CONTROL_WIFI_DISPLAY', + 'android.permission.COPY_PROTECTED_DATA', + 'android.permission.CREATE_USERS', + 'android.permission.CRYPT_KEEPER', 'android.permission.DELETE_CACHE_FILES', 'android.permission.DELETE_PACKAGES', 'android.permission.DEVICE_POWER', 'android.permission.DIAGNOSTIC', 'android.permission.DISABLE_KEYGUARD', + 'android.permission.DISPATCH_NFC_MESSAGE', + 'android.permission.DISPATCH_PROVISIONING_MESSAGE', + 'android.permission.DOWNLOAD_CACHE_NON_PURGEABLE', 'android.permission.DUMP', + 'android.permission.DVB_DEVICE', 'android.permission.EXPAND_STATUS_BAR', 'android.permission.FACTORY_TEST', + 'android.permission.FILTER_EVENTS', 'android.permission.FLASHLIGHT', 'android.permission.FORCE_BACK', + 'android.permission.FORCE_STOP_PACKAGES', + 'android.permission.FRAME_STATS', + 'android.permission.FREEZE_SCREEN', 'android.permission.GET_ACCOUNTS', + 'android.permission.GET_ACCOUNTS_PRIVILEGED', + 'android.permission.GET_APP_GRANTED_URI_PERMISSIONS', + 'android.permission.GET_APP_OPS_STATS', + 'android.permission.GET_DETAILED_TASKS', + 'android.permission.GET_INTENT_SENDER_INTENT', + 'android.permission.GET_PACKAGE_IMPORTANCE', 'android.permission.GET_PACKAGE_SIZE', + 'android.permission.GET_PASSWORD', + 'android.permission.GET_PROCESS_STATE_AND_OOM_SCORE', 'android.permission.GET_TASKS', + 'android.permission.GET_TOP_ACTIVITY_INFO', 'android.permission.GLOBAL_SEARCH', + 'android.permission.GLOBAL_SEARCH_CONTROL', + 'android.permission.GRANT_RUNTIME_PERMISSIONS', 'android.permission.HARDWARE_TEST', + 'android.permission.HDMI_CEC', 'android.permission.INJECT_EVENTS', + 'android.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS', 'android.permission.INSTALL_LOCATION_PROVIDER', 'android.permission.INSTALL_PACKAGES', + 'android.permission.INTENT_FILTER_VERIFICATION_AGENT', + 'android.permission.INTERACT_ACROSS_USERS', + 'android.permission.INTERACT_ACROSS_USERS_FULL', 'android.permission.INTERNAL_SYSTEM_WINDOW', 'android.permission.INTERNET', + 'android.permission.INVOKE_CARRIER_SETUP', 'android.permission.KILL_BACKGROUND_PROCESSES', + 'android.permission.KILL_UID', + 'android.permission.LAUNCH_TRUST_AGENT_SETTINGS', + 'android.permission.LOCAL_MAC_ADDRESS', + 'android.permission.LOCATION_HARDWARE', + 'android.permission.LOOP_RADIO', 'android.permission.MANAGE_ACCOUNTS', + 'android.permission.MANAGE_ACTIVITY_STACKS', + 'android.permission.MANAGE_APP_OPS_RESTRICTIONS', 'android.permission.MANAGE_APP_TOKENS', + 'android.permission.MANAGE_CA_CERTIFICATES', + 'android.permission.MANAGE_DEVICE_ADMINS', + 'android.permission.MANAGE_DOCUMENTS', + 'android.permission.MANAGE_FINGERPRINT', + 'android.permission.MANAGE_MEDIA_PROJECTION', + 'android.permission.MANAGE_NETWORK_POLICY', + 'android.permission.MANAGE_NOTIFICATIONS', + 'android.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS', + 'android.permission.MANAGE_SOUND_TRIGGER', + 'android.permission.MANAGE_USB', + 'android.permission.MANAGE_USERS', + 'android.permission.MANAGE_VOICE_KEYPHRASES', 'android.permission.MASTER_CLEAR', + 'android.permission.MEDIA_CONTENT_CONTROL', + 'android.permission.MODIFY_APPWIDGET_BIND_PERMISSIONS', + 'android.permission.MODIFY_AUDIO_ROUTING', 'android.permission.MODIFY_AUDIO_SETTINGS', + 'android.permission.MODIFY_CELL_BROADCASTS', + 'android.permission.MODIFY_DAY_NIGHT_MODE', + 'android.permission.MODIFY_NETWORK_ACCOUNTING', + 'android.permission.MODIFY_PARENTAL_CONTROLS', 'android.permission.MODIFY_PHONE_STATE', 'android.permission.MOUNT_FORMAT_FILESYSTEMS', 'android.permission.MOUNT_UNMOUNT_FILESYSTEMS', + 'android.permission.MOVE_PACKAGE', + 'android.permission.NET_ADMIN', + 'android.permission.NET_TUNNELING', 'android.permission.NFC', + 'android.permission.NFC_HANDOVER_STATUS', + 'android.permission.NOTIFY_PENDING_SYSTEM_UPDATE', + 'android.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS', + 'android.permission.OEM_UNLOCK_STATE', + 'android.permission.OVERRIDE_WIFI_CONFIG', + 'android.permission.PACKAGE_USAGE_STATS', + 'android.permission.PACKAGE_VERIFICATION_AGENT', + 'android.permission.PACKET_KEEPALIVE_OFFLOAD', + 'android.permission.PEERS_MAC_ADDRESS', + 'android.permission.PERFORM_CDMA_PROVISIONING', + 'android.permission.PERFORM_SIM_ACTIVATION', 'android.permission.PERSISTENT_ACTIVITY', + 'android.permission.PROCESS_CALLLOG_INFO', 'android.permission.PROCESS_OUTGOING_CALLS', + 'android.permission.PROCESS_PHONE_ACCOUNT_REGISTRATION', + 'android.permission.PROVIDE_TRUST_AGENT', + 'android.permission.QUERY_DO_NOT_ASK_CREDENTIALS_ON_BOOT', + 'android.permission.READ_BLOCKED_NUMBERS', 'android.permission.READ_CALENDAR', 'android.permission.READ_CALL_LOG', 'android.permission.READ_CONTACTS', + 'android.permission.READ_DREAM_STATE', 'android.permission.READ_EXTERNAL_STORAGE', 'android.permission.READ_FRAME_BUFFER', 'android.permission.READ_HISTORY_BOOKMARKS', 'android.permission.READ_INPUT_STATE', + 'android.permission.READ_INSTALL_SESSIONS', 'android.permission.READ_LOGS', + 'android.permission.READ_NETWORK_USAGE_HISTORY', + 'android.permission.READ_OEM_UNLOCK_STATE', 'android.permission.READ_PHONE_STATE', + 'android.permission.READ_PRECISE_PHONE_STATE', + 'android.permission.READ_PRIVILEGED_PHONE_STATE', 'android.permission.READ_PROFILE', + 'android.permission.READ_SEARCH_INDEXABLES', 'android.permission.READ_SMS', 'android.permission.READ_SOCIAL_STREAM', 'android.permission.READ_SYNC_SETTINGS', 'android.permission.READ_SYNC_STATS', 'android.permission.READ_USER_DICTIONARY', + 'android.permission.READ_WIFI_CREDENTIAL', + 'android.permission.REAL_GET_TASKS', 'android.permission.REBOOT', + 'android.permission.RECEIVE_BLUETOOTH_MAP', 'android.permission.RECEIVE_BOOT_COMPLETED', + 'android.permission.RECEIVE_DATA_ACTIVITY_CHANGE', + 'android.permission.RECEIVE_EMERGENCY_BROADCAST', + 'android.permission.RECEIVE_MEDIA_RESOURCE_USAGE', 'android.permission.RECEIVE_MMS', 'android.permission.RECEIVE_SMS', + 'android.permission.RECEIVE_STK_COMMANDS', 'android.permission.RECEIVE_WAP_PUSH', + 'android.permission.RECEIVE_WIFI_CREDENTIAL_CHANGE', 'android.permission.RECORD_AUDIO', + 'android.permission.RECOVERY', + 'android.permission.REGISTER_CALL_PROVIDER', + 'android.permission.REGISTER_CONNECTION_MANAGER', + 'android.permission.REGISTER_SIM_SUBSCRIPTION', + 'android.permission.REGISTER_WINDOW_MANAGER_LISTENERS', + 'android.permission.REMOTE_AUDIO_PLAYBACK', + 'android.permission.REMOVE_DRM_CERTIFICATES', + 'android.permission.REMOVE_TASKS', 'android.permission.REORDER_TASKS', + 'android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS', + 'android.permission.REQUEST_INSTALL_PACKAGES', + 'android.permission.RESET_FINGERPRINT_LOCKOUT', + 'android.permission.RESET_SHORTCUT_MANAGER_THROTTLING', 'android.permission.RESTART_PACKAGES', + 'android.permission.RETRIEVE_WINDOW_CONTENT', + 'android.permission.RETRIEVE_WINDOW_TOKEN', + 'android.permission.REVOKE_RUNTIME_PERMISSIONS', + 'android.permission.SCORE_NETWORKS', + 'android.permission.SEND_CALL_LOG_CHANGE', + 'android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS', + 'android.permission.SEND_RESPOND_VIA_MESSAGE', 'android.permission.SEND_SMS', + 'android.permission.SEND_SMS_NO_CONFIRMATION', + 'android.permission.SERIAL_PORT', 'android.permission.SET_ACTIVITY_WATCHER', 'android.permission.SET_ALARM', 'android.permission.SET_ALWAYS', + 'android.permission.SET_ALWAYS_FINISH', 'android.permission.SET_ANIMATION_SCALE', 'android.permission.SET_DEBUG_APP', + 'android.permission.SET_INPUT_CALIBRATION', + 'android.permission.SET_KEYBOARD_LAYOUT', 'android.permission.SET_ORIENTATION', 'android.permission.SET_POINTER_SPEED', 'android.permission.SET_PREFERRED_APPLICATIONS', 'android.permission.SET_PROCESS_LIMIT', + 'android.permission.SET_SCREEN_COMPATIBILITY', 'android.permission.SET_TIME', 'android.permission.SET_TIME_ZONE', 'android.permission.SET_WALLPAPER', + 'android.permission.SET_WALLPAPER_COMPONENT', 'android.permission.SET_WALLPAPER_HINTS', + 'android.permission.SHUTDOWN', 'android.permission.SIGNAL_PERSISTENT_PROCESSES', + 'android.permission.START_ANY_ACTIVITY', + 'android.permission.START_PRINT_SERVICE_CONFIG_ACTIVITY', + 'android.permission.START_TASKS_FROM_RECENTS', 'android.permission.STATUS_BAR', + 'android.permission.STATUS_BAR_SERVICE', + 'android.permission.STOP_APP_SWITCHES', + 'android.permission.STORAGE_INTERNAL', 'android.permission.SUBSCRIBED_FEEDS_READ', 'android.permission.SUBSCRIBED_FEEDS_WRITE', + 'android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME', 'android.permission.SYSTEM_ALERT_WINDOW', + 'android.permission.TABLET_MODE', + 'android.permission.TEMPORARY_ENABLE_ACCESSIBILITY', + 'android.permission.TETHER_PRIVILEGED', + 'android.permission.TRANSMIT_IR', + 'android.permission.TRUST_LISTENER', + 'android.permission.TV_INPUT_HARDWARE', + 'android.permission.TV_VIRTUAL_REMOTE_CONTROLLER', + 'android.permission.UPDATE_APP_OPS_STATS', + 'android.permission.UPDATE_CONFIG', 'android.permission.UPDATE_DEVICE_STATS', + 'android.permission.UPDATE_LOCK', + 'android.permission.UPDATE_LOCK_TASK_PACKAGES', + 'android.permission.USER_ACTIVITY', 'android.permission.USE_CREDENTIALS', 'android.permission.USE_SIP', 'android.permission.VIBRATE', 'android.permission.WAKE_LOCK', 'android.permission.WRITE_APN_SETTINGS', + 'android.permission.WRITE_BLOCKED_NUMBERS', 'android.permission.WRITE_CALENDAR', 'android.permission.WRITE_CALL_LOG', 'android.permission.WRITE_CONTACTS', + 'android.permission.WRITE_DREAM_STATE', 'android.permission.WRITE_EXTERNAL_STORAGE', 'android.permission.WRITE_GSERVICES', 'android.permission.WRITE_HISTORY_BOOKMARKS', + 'android.permission.WRITE_MEDIA_STORAGE', 'android.permission.WRITE_PROFILE', 'android.permission.WRITE_SECURE_SETTINGS', 'android.permission.WRITE_SETTING', + 'android.permission.WRITE_SETTINGS', 'android.permission.WRITE_SMS', 'android.permission.WRITE_SOCIAL_STREAM', 'android.permission.WRITE_SYNC_SETTINGS', diff --git a/src/drozer/configuration.py b/src/drozer/configuration.py index dc011b33..0463a28a 100644 --- a/src/drozer/configuration.py +++ b/src/drozer/configuration.py @@ -2,6 +2,7 @@ import os import platform import sys +import pathlib from WithSecure.common import system @@ -12,6 +13,7 @@ class Configuration(object): """ __config = None + ws_dz_agent_url = "https://github.com/WithSecureLabs/drozer-agent/releases/" @classmethod def executable(cls, name): @@ -101,19 +103,34 @@ def has_section(cls, section): cls.__ensure_config() return cls.__config.has_section(section) + + @classmethod + def library_path(cls): + """ + Returns the path to the drozer Library + """ + return os.path.join(os.path.dirname(__file__), "lib") @classmethod def library(cls, name): """ - Returns the path to a drozer Library + Returns the path to a drozer Library file + Library search order is + 1) lib/name + 2) lib/platform/name + 3) lib/platform/name.* """ - path = os.path.join(os.path.dirname(__file__), "lib", name) - - if os.path.exists(path): - return path - else: - return None + file_path = os.path.join(cls.library_path(), name) + if os.path.exists(file_path): + return file_path + + file_path = os.path.join(cls.library_path(), platform.system(), name) + if os.path.exists(file_path): + return file_path + + dir_path = pathlib.Path(os.path.join(cls.library_path(), platform.system())) + return next(dir_path.glob(f"{name}.*"), None) @classmethod def path(cls): diff --git a/src/drozer/console/session.py b/src/drozer/console/session.py index 58c11caa..d119a02b 100644 --- a/src/drozer/console/session.py +++ b/src/drozer/console/session.py @@ -298,7 +298,10 @@ def do_list(self, args): include_unsupported = False if "--unsupported" in argv: argv.remove("--unsupported") - + include_unsupported = True + + if "--all" in argv: + argv.remove("--all") include_unsupported = True term = len(argv) > 0 and argv[0] or None @@ -419,7 +422,7 @@ def complete_run(self, text, line, begidx, endidx): method defined on the specified module. """ - _line = re.match("(run\s+)([^\s]*)(\s*)", line) + _line = re.match(r"(run\s+)([^\s]*)(\s*)", line) # figure out where the module name starts in the string cmdidx = len(_line.group(1)) @@ -701,6 +704,8 @@ def __print_banner(self): self.__print_banner2() elif x == 42: self.__print_banner3() + elif x == 84: + self.__print_banner4() else: self.__print_banner1() @@ -766,6 +771,39 @@ def __print_banner3(self): print("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠄⢀⣀⡰⠀⠂⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀") print("") + def __print_banner4(self): + if console.get_size()[0] < 100: + self.__print_banner1() + return + + print(" ▓▓████████████░░▓▓██") + print(" ~dozer~ ▓▓░░░░░░░░░░▒▒░░░░░░▒▒▓▓") + print(" ██▓▓████▓▓██████▓▓▓▓▓▓██") + print(" ██░░░░░░░░██▓▓██░░██▓▓██") + print(" ██░░░░░░░░██▓▓██░░░░██▓▓██") + print(" ██░░░░░░░░██▓▓██░░░░██▓▓██") + print(" ▓▓░░░░░░▒▒██▓▓██░░▒▒██▓▓▓▓") + print(" ▓▓▓▓▓▓ ▓▓▒▒░░░░▒▒██▓▓██▒▒▒▒▒▒██▒▒▓▓▓▓▓▓▓▓") + print(" ██░░ ██ ████ ██▒▒████████▓▓██▒▒▒▒▒▒▓▓▓▓▓▓██░░░░████████") + print(" ██ ░░████▓▓▓▓░░░░▓▓▓▓▓▓████░░░░░░░░██▒▒▒▒▒▒▓▓▓▓▓▓▓▓██░░░░░░▒▒░░▒▒██") + print(" ██ ░░██░░░░██░░░░░░████░░░░░░░░░░░░██▒▒▒▒██▓▓▓▓▓▓▓▓██░░░░░░░░░░░░██") + print(" ██ ░░░░▓▓████░░░░██░░░░░░░░░░▓▓██▓▓██▓▓▓▓████▓▓████░░░░░░░░░░░░░░██") + print(" ████░░░░░░██▒▒██░░██ ░░░░░░████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██") + print(" ▓▓░░██░░░░░░██▒▒▒▒██░░░░░░░░▓▓████▓▓▓▓▓▓▒▒▒▒░░░░░░░░░░░░░░▓▓██▓▓▒▒▓▓░░░░██") + print(" ██ ░░░░██░░░░██▒▒██ ░░░░░░██▒▒▒▒▒▒▒▒▒▒▒▒██▒▒▒▒░░░░░░░░░░██▓▓▒▒▒▒▒▒▒▒██░░██") + print(" ██ ░░░░▒▒▒▒██░░░░██ ░░░░░░██▒▒▒▒▒▒▓▓▓▓▓▓▓▓▒▒██▒▒▒▒▒▒▒▒▒▒░░██▓▓▓▓▓▓▓▓▓▓▒▒████") + print(" ██▓▓██████████████████▓▓████░░░░░░▒▒██▒▒▒▒▓▓▓▓▓▓██▓▓▓▓██▒▒▒▒░░░░░░░░██▓▓▓▓████▓▓▓▓██") + print(" ██░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓░░▒▒██░░▒▒██▒▒▒▒▒▒▓▓██▒▒▒▒██▓▓▓▓██▒▒▒▒▒▒░░░░▓▓▓▓██▒▒▒▒██▓▓▓▓██") + print(" ██░░▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓░░▒▒▒▒██▒▒██▒▒▒▒▒▒▓▓██▒▒▒▒██▓▓▓▓██▒▒░░░░░░░░▓▓▓▓██▒▒▒▒██▓▓▓▓██") + print(" ██░░▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓██░░▒▒▒▒██████▒▒▒▒▒▒▓▓██▒▒▒▒██▓▓▓▓██▒▒▒▒▒▒▒▒░░██▓▓██▒▒▒▒██▓▓▓▓██") + print(" ██░░▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓██░░▒▒▒▒▒▒██ ██▒▒▒▒▓▓▓▓██▒▒░░██▓▓▓▓████░░░░░░░░██▓▓██▒▒░░██▓▓▓▓██") + print(" ██░░▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓██░░▒▒▒▒▓▓██ ██▓▓▓▓▓▓▓▓██░░░░██▓▓▓▓██ ████████▓▓▓▓██░░░░██▓▓▓▓██") + print(" ██░░▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓██░░▒▒▒▒▒▒▓▓██ ██▓▓▓▓▓▓▓▓████▓▓▓▓██ ██▓▓▓▓▓▓▓▓████▓▓▓▓██") + print(" ████░░▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓██░░▒▒▒▒▒▒▒▒▓▓██ ██▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██ ██▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██") + print("█░░░░░░░░░░░░▒▒▒▒▒▒░░░░░░░░▒▒▒▒▒▒▒▒▓▓██ ██▓▓▓▓▓▓▓▓▓▓▓▓██ ██▓▓▓▓▓▓▓▓▓▓▓▓██") + print("█████████████████████████████████████ ████████████ ████████████") + print("") + def __setBase(self, base): """ Changes the user's namespace. diff --git a/src/drozer/lib/aapt-osx b/src/drozer/lib/Darwin/aapt similarity index 100% rename from src/drozer/lib/aapt-osx rename to src/drozer/lib/Darwin/aapt diff --git a/src/drozer/lib/Darwin/zipalign b/src/drozer/lib/Darwin/zipalign new file mode 100644 index 00000000..5d6cdb5b Binary files /dev/null and b/src/drozer/lib/Darwin/zipalign differ diff --git a/src/drozer/lib/aapt b/src/drozer/lib/Linux/aapt old mode 100644 new mode 100755 similarity index 100% rename from src/drozer/lib/aapt rename to src/drozer/lib/Linux/aapt diff --git a/src/drozer/lib/Linux/libc++.so b/src/drozer/lib/Linux/libc++.so new file mode 100644 index 00000000..dd5837db Binary files /dev/null and b/src/drozer/lib/Linux/libc++.so differ diff --git a/src/drozer/lib/Linux/zipalign b/src/drozer/lib/Linux/zipalign new file mode 100755 index 00000000..76a5e9a1 Binary files /dev/null and b/src/drozer/lib/Linux/zipalign differ diff --git a/src/drozer/lib/aapt.exe b/src/drozer/lib/Windows/aapt.exe similarity index 100% rename from src/drozer/lib/aapt.exe rename to src/drozer/lib/Windows/aapt.exe diff --git a/src/drozer/lib/Windows/zipalign.exe b/src/drozer/lib/Windows/zipalign.exe new file mode 100644 index 00000000..092ef599 Binary files /dev/null and b/src/drozer/lib/Windows/zipalign.exe differ diff --git a/src/drozer/lib/apksigner.jar b/src/drozer/lib/apksigner.jar new file mode 100644 index 00000000..37ea4b24 Binary files /dev/null and b/src/drozer/lib/apksigner.jar differ diff --git a/src/drozer/lib/signapk.jar b/src/drozer/lib/signapk.jar deleted file mode 100644 index 967e07cc..00000000 Binary files a/src/drozer/lib/signapk.jar and /dev/null differ diff --git a/src/drozer/main.py b/src/drozer/main.py index dff343b4..fbfd5773 100644 --- a/src/drozer/main.py +++ b/src/drozer/main.py @@ -1,5 +1,5 @@ #!/usr/bin/env python - +import importlib import sys import os import distro diff --git a/src/drozer/modules/collection.py b/src/drozer/modules/collection.py index 133b5914..23edb7a5 100644 --- a/src/drozer/modules/collection.py +++ b/src/drozer/modules/collection.py @@ -22,7 +22,8 @@ def all(self, contains=None, permissions=None, prefix=None,exploit=None, module_ if contains != None: modules = filter(lambda m: m.find(contains.lower()) >= 0, modules) if permissions != None: - modules = filter(lambda m: len(set(self.get(m).permissions).difference(permissions)) == 0, modules) + p = set(permissions) + modules = filter(lambda m: set(self.get(m).permissions).issubset(p), modules) if prefix != None: modules = filter(lambda m: m.startswith(prefix), modules) if module_type =="payload" and exploit is not None: diff --git a/src/drozer/modules/common/exploit.py b/src/drozer/modules/common/exploit.py index 798549f4..e3b56dd1 100644 --- a/src/drozer/modules/common/exploit.py +++ b/src/drozer/modules/common/exploit.py @@ -14,7 +14,7 @@ class Exploit(object): def __init__(self, loader): self.__loader = loader - def build_agent(self, arguments): + def build_agent(self, arguments): # TODO: update to new agent build proscess source = arguments.no_gui and "rogue-agent" or "standard-agent" packager = builder.Packager() diff --git a/src/drozer/modules/common/path_completion.py b/src/drozer/modules/common/path_completion.py index 794b8dfe..bcd2560b 100644 --- a/src/drozer/modules/common/path_completion.py +++ b/src/drozer/modules/common/path_completion.py @@ -10,5 +10,5 @@ def on_agent(path, context): folder, search_path = get_folder_and_search_path(path, "/") folders = context.listFiles(folder) - return [s.replace(" ", "\ ") for s in get_suggestions(folder, search_path, map(lambda f: str(f), folders), "/", True)] + return [s.replace(" ", r"\ ") for s in get_suggestions(folder, search_path, map(lambda f: str(f), folders), "/", True)]