def run(self): Log.info("Dumping classes with otool") try: class_dump = otool_class_dump_to_dict(otool_class_dump( self.binary)) except Exception as e: Log.error("An error ocurred when trying to use otool") Log.debug(e) Log.info("Trying jtool") class_dump = jtool_class_dump_to_dict(jtool_class_dump( self.binary)) dump_name = self.binary.rsplit("/", 1)[-1].replace(" ", ".") result = { "{}_class_dump".format(dump_name.replace(".", "_")): class_dump } if hasattr(self, "output") and self.output: Log.info("Saving classes to file") dump_path = "{}/{}.class.dump".format(self.output, dump_name) # create output folder execute("mkdir -p {}".format(dump_path)) save_class_dump(class_dump, dump_path) result.update({ "{}_dump_path".format(dump_name.replace(".", "_")): dump_path, "print": "Dump saved in {}.".format(dump_path) }) return result
def _start_connection(self): """ Starts an SSH connection to the remote device """ from scrounger.utils.ssh import SSHClient from scrounger.utils.general import process from scrounger.utils.config import SSH_COMMAND_TIMEOUT from scrounger.utils.config import SSH_SESSION_TIMEOUT # setup if not self._ssh_session: # this was breaking #self._iproxy_process = process( # 'iproxy 2222 22 {}'.format(self._device_id)) self._iproxy_process = process("iproxy 2222 22") self._ssh_session = SSHClient("127.0.0.1", 2222, self._username, self._password, SSH_COMMAND_TIMEOUT) self._ssh_session.connect() # Log a new sessions _Log.debug("new ssh session started.") # start a tiemout for the connection from threading import Timer if self._timer: self._timer.cancel() # cancel old timer and start new one self._timer = Timer(SSH_SESSION_TIMEOUT, self._stop_connection) self._timer.start()
def _start_connection(self): """ Starts an SSH connection to the remote device """ from scrounger.utils.ssh import SSHClient from scrounger.utils.config import SSH_COMMAND_TIMEOUT from scrounger.utils.config import SSH_SESSION_TIMEOUT from scrounger.utils.config import _SCROUNGER_HOME from scrounger.lib.tcprelay import create_server # setup if not self._ssh_session: self._relay_process = create_server() self._ssh_session = SSHClient("127.0.0.1", 2222, self._username, self._password, SSH_COMMAND_TIMEOUT) self._ssh_session.connect() # Log a new sessions _Log.debug("new ssh session started.") # add scrounger's key key_path = "{}/bin/ios/scrounger.pub".format(_SCROUNGER_HOME) if not self._ssh_session.add_key(key_path): _Log.debug("Scrounger's ssh key not in authorized_keys") # start a tiemout for the connection from threading import Timer if self._timer: self._timer.cancel() # cancel old timer and start new one self._timer = Timer(SSH_SESSION_TIMEOUT, self._stop_connection) self._timer.start()
def execute(self, command): """ Executes a command on the target device :param str command: the command to be executed :return: stdout and stderr of the executed command """ # log command that is going to be run _Log.debug("Running: {}".format(command)) return _adb_command("-s {} shell {}".format(self._device_id, command))
def write(self, command): """ Writes a command into the interactive process :param str command: the command to be sent to the interactive process :return: nothing """ import os _Log.debug("Sending to process {}: {}".format( self._executable, command)) # add a new line and send the command to the process stdin os.write(self._process.stdin.fileno(), "{}\n".format(command))
def error(self): """ Reads from the process stderr :return: a str with the stderr result or None """ import os try: return self._process.stderr.read() except: _Log.debug("Nothing to read stderr {}".format(self._executable)) # there is nothing to read, return None return None
def execute(command): """ Executes a command on the local host. :param str command: the command to be executed :return: returns the output of the STDOUT or STDERR """ from subprocess import check_output, STDOUT command = "{}; exit 0".format(command) # log command that is going to be run _Log.debug("Shell Command: {}".format(command)) return check_output(command, stderr=STDOUT, shell=True)
def run(self): result = { "title": "Application Does Not Encrypt Shared Preferences", "details": "", "severity": "Medium", "report": False } if not self.device.installed(self.identifier): return {"print": "Application not installed"} Log.info("Starting the application") self.device.start(self.identifier) sleep(5) Log.info("Finding files in application's data") target_paths = [ "{}/shared_prefs".format(file_path) for file_path in self.device.data_paths(self.identifier) ] listed_files = [] report_files = [] for data_path in target_paths: listed_files += self.device.find_files(data_path) Log.info("Analysing application's data") for filename in listed_files: if filename: file_content = self.device.file_content(filename) lang = detect_langs(file_content)[0] Log.debug("{} language {}: {}".format(filename, lang.lang, lang.prob)) if lang.prob > float("0.{}".format(self.min_percentage)): report_files += [filename] if report_files: result.update({ "report": True, "details": "* Unencrypted Files:\n * {}".format( "\n * ".join(report_files)) }) return {"{}_result".format(self.name()): result}
def __decrypt_app_helper(app_id, decrypt_type): from socket import timeout scrounger_clutch_log_file = "/tmp/scrounger-clutch.log" try: output = self.execute("clutch -n {} {} &> {}".format( decrypt_type, app_id, scrounger_clutch_log_file))[0] except timeout: _Log.debug("ssh command timedout.") output = self._cat_file(scrounger_clutch_log_file) # cleanup log file self._rm_file(scrounger_clutch_log_file) return output
def run(self): results = [] exceptions = [] # run all modules Log.info("Running all Android analysis modules") for module in self._analysis_modules: instance = module.Module() for option in self.options: if hasattr(self, option["name"]): setattr(instance, option["name"], getattr(self, option["name"])) try: Log.debug("Validating and Running: {}".format(instance.name())) instance.validate_options() run_result = instance.run() for key in run_result: if key.endswith("_result") and validate_analysis_result( run_result[key]) and run_result[key]["report"]: results += [run_result[key]] except Exception as e: exceptions += [{"module": instance.name(), "exception": e}] # setup output folders Log.info("Creating output folders") output_directory = "{}{}".format(self.output, self._output_directory) execute("mkdir -p {}".format(output_directory)) output_file = "{}/results.json".format(output_directory) # write results to json file Log.info("Writing results to file") with open(output_file, "w") as fp: fp.write(dumps(results)) return { "android_analysis": results, "exceptions": exceptions, "print": "The following issues were found:\n* {}".format("\n* ".join( [result["title"] for result in results])) }
def put(self, local_file_path, remote_file_path): """ Copies a file to the remote device. :param str file_path: the local file path :param str remote_file_path: the remote path where to copy the file to (it needs to contain the file name too) :return: returns nothing """ # start a connection if there is none self._start_connection() # logging files _Log.debug("copying {} to {}.".format(local_file_path, remote_file_path)) # put file self._ssh_session.put_file(local_file_path, remote_file_path)
def _stop_connection(self): """ Stops the SSH connection with the remote device """ from scrounger.utils.general import execute # cleanup if self._timer: self._timer.cancel() self._timer = None if self._ssh_session: self._ssh_session.disconnect() self._relay_process.stop() self._relay_process = self._ssh_session = None # Log session stop _Log.debug("ssh session killed.")
def pid(self, package): """ Returns the PID of a running application :param str package: the identifier of the app to get the PID from :return int: a PID if the app with package is running or None if not """ apps = self.packages() if package not in apps: _Log.debug("App {} is not installed on the device".format(package)) return None processes = self.processes() for process in processes: if package.lower() in process["name"].lower(): return int(process["pid"]) return None
def __init__(self, device): """ Creates a new GDB object representiong a GDB instance on the remote device :param IOSDevice device: A device representing the remote device """ from scrounger.lib.tcprelay import create_server self._device = device host, port = device._ssh_session._ip, device._ssh_session._port key = "{}/bin/ios/scrounger.key".format(_SCROUNGER_HOME) _log.debug("Starting a new SSH connection to the remote device") self._process = InteractiveProcess("ssh -i {} -p {} root@{}".format( key, port, host)) _log.debug("Starting GDB on the remote device") self._running = self._start_gdb()
def _stop_connection(self): """ Stops the SSH connection with the remote device """ from scrounger.utils.general import execute # cleanup if self._timer: self._timer.cancel() self._timer = None if self._ssh_session: self._ssh_session.disconnect() #self._iproxy_process.kill() self._iproxy_process = self._ssh_session = None execute('killall iproxy') # make sure iproxy is killed # Log session stop _Log.debug("ssh session killed.")
def pid(self, app_id): """ Returns the PID of a running application :param str app_id: the identifier of the app to get the PID from :return int: a PID if the app with app_id is running or None if not """ apps = self.apps() if app_id not in apps: _Log.debug("App {} is not installed on the device".format(app_id)) return None install_path = apps[app_id]["application"] processes = self.processes() for process in processes: if install_path.rsplit("/", 1)[-1].lower() in \ process["name"].lower(): return int(process["pid"]) return None
def execute(self, command): """ Executes a command on the device and returns STDOUT and STDERR :param str command: the command to be executed :return: returns the STDOUT and STDERR of the executed command """ # start a connection if there is none self._start_connection() # log command that is going to be run _Log.debug("Running: {}".format(command)) # execute stdout, stderr = self._ssh_session.execute(command) # log result #_Log.debug("stdout: {}".format(stdout)) #_Log.debug("stderr: {}".format(stderr)) return stdout, stderr
def get(self, remote_file_path, local_file_path): """ Retrieves a file from the remote device :param str remote_file_path: the path on the remote device :param str local_file_path: the path on the local host to copy the file to (it needs to contain the file name too) :return: returns nothing """ from scrounger.utils.general import execute # start a connection if there is none self._start_connection() # logging files _Log.debug("copying {} to {}.".format(remote_file_path, local_file_path)) # create local file path if not exists execute("mkdir -p {}".format(local_file_path.split("/", 1)[0])) # get file self._ssh_session.get_file(remote_file_path, local_file_path)
def __init__(self, host, port): """ Creates a new JDB object representiong a JDB instance on the remote device :param str host: the host to connect JDB to :param int port: the port where JDB app is listening """ from time import sleep self._host, self._port = host, port _log.debug("Starting a new jdb process") self._process = InteractiveProcess("jdb -attach {}:{}".format( host, port)) sleep(5) # wait for it to start self._last_read = self._process.read() self._last_error = self._process.error() self._running = not self._last_error or (self._last_error and \ "unable to attach" not in self._last_error.lower())
def __init__(self, command): """ Creates an interactive process to interact with out of a command :param str command: the command to be executed """ from fcntl import fcntl, F_GETFL, F_SETFL from subprocess import Popen, PIPE import os self._command = command self._executable = command.split(" ", 1)[0] _Log.debug("Starting the interactive process: {}".format(command)) self._process = Popen(command, shell=True, stdout=PIPE, stdin=PIPE, stderr=PIPE) fcntl(self._process.stdin, F_SETFL, fcntl(self._process.stdin, F_GETFL) | os.O_NONBLOCK) fcntl(self._process.stdout, F_SETFL, fcntl(self._process.stdout, F_GETFL) | os.O_NONBLOCK) fcntl(self._process.stderr, F_SETFL, fcntl(self._process.stderr, F_GETFL) | os.O_NONBLOCK)
def run(self): result = { "title": "Application Does Not Use Obfuscation", "details": "", "severity": "Medium", "report": False } exceptions = [] # get class dump class_dump_module = ClDumpModule() class_dump_module.binary = self.binary class_dump_module.output = None class_dump_result, classes_dumped = class_dump_module.run(), None for key in class_dump_result: if key.endswith("_class_dump"): classes_dumped = class_dump_result[key] if key.endswith("exceptions"): exceptions += class_dump_result[key] Log.info("Analysing class dump") class_strings = [] for class_dumped in classes_dumped: # add to class name list class_strings += [class_dumped["name"]] # enumerate property names if "base_properties" in class_dumped: for property_dumped in class_dumped["base_properties"]: class_strings += [property_dumped.rsplit(" ", 1)[-1][:-1]] if "instance_property" in class_dumped: for property_dumped in class_dumped["instance_property"]: class_strings += [property_dumped.rsplit(" ", 1)[-1][:-1]] # enumerate method names if "base_methods" in class_dumped: for method_dumped in class_dumped["base_methods"]: class_strings += [ part.split(":", 1)[0].split(")", 1)[-1].replace(";", "") for part in method_dumped.split(" ") if ":" in part ] if "instance_methods" in class_dumped: for method_dumped in class_dumped["instance_methods"]: class_strings += [ part.split(":", 1)[0].split(")", 1)[-1].replace(";", "") for part in method_dumped.split(" ") if ":" in part ] # put them all together and get an analysis class_detect_lang = detect_langs(" ".join(class_strings))[0] class_small_strings = [ string for string in class_strings if len(string) < 4 ] # check if lang != expected or probability lower than required for str if class_detect_lang.lang != self.language or \ class_detect_lang.prob*100 < self.min_percentage: result.update({ "title": "Application shows evidence of obfuscation", "details": "Detected language {} with probability {}%.".format( class_detect_lang.lang, class_detect_lang.prob * 100), "report": True }) # check small_strings/total_strings >= min_percentage_small_strings sclass_percent = len(class_small_strings) * 1.0 / len( class_strings) * 100 if sclass_percent >= self.min_percentage_small_names: result.update({ "title": "Application shows evidence of obfuscation", "details": "{}\n\nDetected small strings: {}%".format( result["details"], sclass_percent), "report": True }) Log.debug("Strings detected {} with probability of {}%".format( class_detect_lang.lang, class_detect_lang.prob * 100)) Log.debug("Small len strings {}/{} = {}%".format( len(class_small_strings), len(class_strings), sclass_percent)) Log.debug("Unique small len classes {}/{} = {}%".format( len(set(class_small_strings)), len(set(class_strings)), len(set(class_small_strings)) * 1.0 / len(set(class_strings)) * 100)) if not result["report"]: result.update({ "details": "No evidence of obfuscation found.", "report": True }) return { "{}_result".format(self.name()): result, "exceptions": exceptions }
def _uncrypt_app_helper(self, app_id, decrypt_type): """ Decrypts an app using uncrypt11 and returns the result output :param str app_id: the id of the app to be decrypted :param str decrypt_type: the type of decryption to be done - either binary only (-b) or packed into ipa (-d) :return: returns the output of the decryption """ from time import sleep uncrypt_path = "/Library/MobileSubstrate/DynamicLibraries/\ uncrypt11.dylib" if not self.file_exists(uncrypt_path): _Log.debug("Uncrypt11 not found") return "FAIL: Uncrypt11 not installed." self.start(app_id) # start app - needs to be running sleep(5) # wait to start pid = self.pid(app_id) # get pid if not pid: _Log.debug("PID not found") return "FAIL: Could not get PID of {}".format(app_id) result = self.execute("/electra/inject_criticald {} {}".format( pid, uncrypt_path)) if "No error occured!" not in result[0] and \ "No error occured!" not in result[1]: _Log.debug("Not decrypted:\n{}\n{}".format(result[0], results[1])) return "FAIL: An error occured trying to decrypt {}".format(app_id) list_apps = self.apps() app_info = list_apps[app_id] decrypted_binary = "{}/Documents/{}\ decrypted".format( app_info["data"], app_info["binary_name"]) if not self.file_exists(decrypted_binary): _Log.debug("File {} does not exist".format(decrypted_binary)) return "FAIL: Could not decrypt {}".format(app_id) # move binary to tmp end_path = "/tmp/{}.decrypted".format(app_id) self.execute("mv {} {}".format(decrypted_binary, end_path)) if decrypt_type == "-b": _Log.debug("Dumpped binary") return "Finished dumping {} to {}\n".format(app_id, end_path) _Log.debug("Creating IPA") # create IPA scructure self.execute("rm -rf /tmp/scrounger-tmp/Payload") self.execute("mkdir -p /tmp/scrounger-tmp/Payload") # copy App to /tmp self.execute("cp -r {} /tmp/scrounger-tmp/Payload".format( app_info["application"])) # move decrypted binary to the Payload app_name = app_info["application"].rsplit("/", 1)[-1] self.execute("mv {} /tmp/scrounger-tmp/Payload/{}/{}".format( end_path, app_name, app_info["binary_name"])) # zip everything self.execute( "cd /tmp/scrounger-tmp; zip -r ../{}.ipa Payload/".format(app_id)) # cleanup self.execute("rm -rf /tmp/scrounger-tmp") # Success: DONE: /path/to/ipa\n # Success: Finished dumping app_id to /path/to/dump/binary\n return "DONE: /tmp/{}.ipa\n".format(app_id)
def run(self): result = { "title": "Application Does Not Use Obfuscation", "details": "", "severity": "Medium", "report": False } exceptions = [] # preparing variable to run ignore = [filepath.strip() for filepath in self.ignore.split(";")] # get identifier Log.info("Checking identifier package only") if self.check_package_only: manifest_module = ManifestModule() manifest_module.decompiled_apk = self.decompiled_apk self.manifest = manifest_module.run() if "print" not in self.manifest: identifier = self.manifest.popitem()[1].package() else: identifier = None exceptions += [Exception(self.manifest["print"])] Log.info("Identifying class names") class_names_list = class_names(self.decompiled_apk, ignore, identifier) Log.info("Identifying method names") method_names_list = method_names(self.decompiled_apk, ignore, identifier) Log.info("Identifying strings") strings_list = app_strings(self.decompiled_apk, ignore, identifier) Log.info("Identifying resources") resources_list = app_used_resources(self.decompiled_apk, ignore, identifier) Log.info("Analysing identified strings") # start by analysing class names class_detect_lang = detect_langs(" ".join(class_names_list))[0] class_small_names = [ class_name for class_name in class_names_list if len(class_name) < 4 ] # check if lang != expected or probability lower than required for class # names if class_detect_lang.lang != self.language or \ class_detect_lang.prob*100 < self.min_percentage: result.update({ "title": "Application shows evidence of obfuscation", "details": "Detected language {} with probability {}% on \ class names.".format(class_detect_lang.lang, class_detect_lang.prob * 100), "report": True }) # check small_classes/total_classes >= min_percent_class_names sclass_percent = len(class_small_names) * 1.0 / len( class_names_list) * 100 if sclass_percent >= self.min_percentage_small_names: result.update({ "title": "Application shows evidence of obfuscation", "details": "{}\n\nDetected small class names: {}%".format( result["details"], sclass_percent), "report": True }) Log.debug("Classes detected {} with probability of {}%".format( class_detect_lang.lang, class_detect_lang.prob * 100)) Log.debug("Small len classes {}/{} = {}%".format( len(class_small_names), len(class_names_list), sclass_percent)) Log.debug("Unique small len classes {}/{} = {}%".format( len(set(class_small_names)), len(set(class_names_list)), len(set(class_small_names)) * 1.0 / len(set(class_names_list)) * 100)) # analyse method names method_detect_lang = detect_langs(" ".join(method_names_list))[0] method_small_names = [ method_name for method_name in method_names_list if len(method_name) < 4 ] # check if lang != expected or probability lower than required for # method names if method_detect_lang.lang != self.language or \ method_detect_lang.prob*100 < self.min_percentage: result.update({ "title": "Application shows evidence of obfuscation", "details": "{}\n\nDetected language {} with probability {}% on \ method names.".format(result["details"], method_detect_lang.lang, method_detect_lang.prob * 100), "report": True }) # check small_methods/total_methods >= min_percent_mathod_names smthod_percent = len(method_small_names) * 1.0 / len( method_names_list) * 100 if smthod_percent >= self.min_percentage_small_names: result.update({ "title": "Application shows evidence of obfuscation", "details": "{}\n\nDetected small method names: {}%".format( result["details"], smthod_percent), "report": True }) Log.debug("Methods detected {} with probability of {}%".format( method_detect_lang.lang, method_detect_lang.prob * 100)) Log.debug("Small len methods {}/{} = {}%".format( len(method_small_names), len(method_names_list), smthod_percent)) Log.debug("Unique small len classes {}/{} = {}%".format( len(set(method_small_names)), len(set(method_names_list)), len(set(method_small_names)) * 1.0 / len(set(method_names_list)) * 100)) # analyse strings and resources strings_detect_lang = detect_langs(" ".join(strings_list + resources_list))[0] # check if lang != expected or probability lower than required for # strings and resources if strings_detect_lang.lang != self.language or \ strings_detect_lang.prob*100 < self.min_percentage: result.update({ "title": "Application shows evidence of obfuscation", "details": "{}\n\nDetected language {} with probability {}% on \ strings and resources.".format(result["details"], strings_detect_lang.lang, strings_detect_lang.prob * 100), "report": True }) Log.debug("Strings detected {} with probability of {}%".format( strings_detect_lang.lang, strings_detect_lang.prob * 100)) if not result["report"]: result.update({ "details": "No evidence of obfuscation found.", "report": True }) return { "{}_result".format(self.name()): result, "exceptions": exceptions }