diff --git a/.gitignore b/.gitignore index 270abb9..41918df 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ /src/frida/Makefile /src/frida/Makefile.in /stamp-h1 +.idea/ diff --git a/src/frida/repl.py b/src/frida/repl.py index f94746c..5e7b46f 100644 --- a/src/frida/repl.py +++ b/src/frida/repl.py @@ -2,8 +2,12 @@ def main(): from frida.application import ConsoleApplication from colorama import Fore, Style import json + import platform try: import readline + # Stupid hack to workaround oxs's shitty readline impl + if platform.system() == "Darwin": + import gnureadline as readline HAVE_READLINE = True except: HAVE_READLINE = False @@ -14,7 +18,16 @@ def main(): def __init__(self): if HAVE_READLINE: readline.parse_and_bind("tab: complete") + # force completor to run on first tab press + readline.parse_and_bind("set show-all-if-unmodified on") + readline.parse_and_bind("set show-all-if-ambiguous on") + # Set our custom completer + readline.set_completer(self.completer) self._idle = threading.Event() + self._cond = threading.Condition() + self._response = None + self._completor_locals = [] + super(REPLApplication, self).__init__(self._process_input) def _usage(self): @@ -24,6 +37,7 @@ def main(): return True def _start(self): + self._prompt_string = self._create_prompt() def on_message(message, data): self._reactor.schedule(lambda: self._process_message(message, data)) self._script = self._session.create_script(self._create_repl_script()) @@ -41,8 +55,7 @@ def main(): self._script = None def _create_repl_script(self): - return """\ - + return """ (function () { this.resume = function () { send({ name: '+resume' }); @@ -66,17 +79,27 @@ def main(): function onExpression(expression) { try { var result; - eval("result = " + expression + ";"); - var sentRaw = false; - if (result && result.hasOwnProperty('length')) { - try { - send({ name: '+result', payload: "OOB" }, result); - sentRaw = true; - } catch (e) { + // If we passed in an object + // TODO: Fix this gross nonsense + if (typeof(expression) == "object"){ + if (expression.eval){ + eval("result = " + expression.eval + ";"); + } + send({ name: '+silent_result', payload: result }); + // Otherwise expression is a string + } else { + eval("result = " + expression + ";"); + var sentRaw = false; + if (result && result.hasOwnProperty('length')) { + try { + send({ name: '+result', payload: "OOB" }, result); + sentRaw = true; + } catch (e) { + } + } + if (!sentRaw) { + send({ name: '+result', payload: result }); } - } - if (!sentRaw) { - send({ name: '+result', payload: result }); } } catch (e) { send({ name: '+error', payload: e.toString() }); @@ -87,11 +110,78 @@ def main(): }).call(this); """ + def _synchronous_evaluate(self, text): + self._reactor.schedule(lambda: self._send_expression({"eval": text})) + self._cond.acquire() + while self._response is None: + self._cond.wait() + response = self._response + self._response = None + self._cond.release() + # TODO: This is super gross, need to actually fix this and unify conventions + # once we know what we're doing with messages. + if "payload" not in response[0]: + response[0]['payload'] = None + return response[0]['payload'] + + def _create_prompt(self): + # Todo: Make this prompt less shitty and make sure all platforms are covered ;) + device_type = self._device.type + type_name = self._target[0] + target = self._target[1] + + if device_type == "local": + if self._target[0] == "name": + type_name = "ProcName" + elif self._target[0] == "pid": + type_name = "PID" + prompt_string = "%s::%s::%s" % ("Local", type_name, target) + + elif device_type == "tether" : + prompt_string = "%s::%s::%s" % ("USB", self._device.name, target) + + else: + prompt_string = "%s::%s::%s" % (self._device.name, self._device.name, target) + + return prompt_string + + def completer(self, prefix, index): + # If it's the first loop, do the init + if index == 0: + # If nothing is in the readline buffer + if not prefix: + # Give every key that doesn't start with a "_" + self._completor_locals = [key for key in self._synchronous_evaluate("Object.keys(this)") if not key.startswith('_')] + else: + if prefix.endswith("."): + thing_to_check = prefix.split(' ')[0][:-1] + self._completor_locals = self._synchronous_evaluate("Object.keys(" + thing_to_check + ")") + else: + if "." in prefix: + # grab the last statement + target = prefix.split(' ')[-1] + needle = target.split('.')[-1] + # Chop off everything + target = ".".join(target.split('.')[:-1]) + self._completor_locals = [target + "." + key for key in self._synchronous_evaluate("Object.keys(" + target + ")") if key.startswith(needle)] + # Do stuff + # Give every key that starts with the root prefix + else: + self._completor_locals = [key for key in self._synchronous_evaluate("Object.keys(this)") if key.startswith(prefix)] + + try: + return self._completor_locals[index] + except IndexError: + return None + def _process_input(self): if sys.version_info[0] >= 3: input_impl = input else: input_impl = raw_input + + self._print_startup_message() + while True: self._idle.wait() expression = "" @@ -99,10 +189,12 @@ def main(): while len(expression) == 0 or line.endswith("\\"): try: if len(expression) == 0: - line = input_impl(">>> ") + line = input_impl("[%s]" % self._prompt_string + "-> ") else: line = input_impl("... ") except EOFError: + # An extra newline after EOF to exit the REPL cleanly + print "\nThank you for using Frida!" return if len(line.strip()) > 0: if len(expression) > 0: @@ -111,8 +203,21 @@ def main(): if HAVE_READLINE: readline.add_history(expression) - self._idle.clear() - self._reactor.schedule(lambda: self._send_expression(expression)) + + if expression.endswith("?"): + # Help feature + self._print_help(expression) + elif expression.startswith("%"): + # "Magic" commands + self._do_magic(expression) + elif expression in ("quit", "q", "exit"): + print "Thank you for using Frida!" + return + elif expression == "help": + print "Frida help 1.0!" + else: + self._idle.clear() + self._reactor.schedule(lambda: self._send_expression(expression)) def _send_expression(self, expression): self._script.post_message(expression) @@ -136,18 +241,79 @@ def main(): output = Fore.RED + Style.BRIGHT + value + Style.RESET_ALL sys.stdout.write(output + "\n") sys.stdout.flush() - self._idle.set() - handled = True elif name == '+resume': self._resume() - self._idle.set() - handled = True + elif name == "+silent_result": + # TODO: This is hacky as shit, need to make it cleaner/better + self._cond.acquire() + self._response = (stanza, data) + self._cond.notify() + self._cond.release() + + self._idle.set() + handled = True if not handled: print("message:", message, "data:", data) + def _print_startup_message(self): + print """ _____ + (_____) + | | Frida v3.0 - A world-class dynamic instrumentation framework + | | + |`-'| Commands: + | | help -> Displays the help system + | | object? -> Display information about 'object' + | | exit/quit -> Exit + | | + | | More info at http://www.frida.re/docs/home/ + `._.' + +""" + + def _print_help(self, expression): + # TODO: Figure out docstrings and implement here. This is real jankaty right now. + help_text = "" + if expression.endswith(".?"): + expression = expression[:-2] + "?" + + obj_to_identify = [x for x in expression.split(' ') if x.endswith("?")][0][:-1] + obj_type = self._synchronous_evaluate("typeof(%s)" % obj_to_identify) + + if obj_type == "function": + + signature = self._synchronous_evaluate("%s.toString()" % obj_to_identify) + clean_signature = signature.split("{")[0][:-1].split('function ')[-1] + + if "[native code]" in signature: + help_text += "Type: Function (native)\n" + else: + help_text += "Type: Function\n" + + help_text += "Signature: %s\n" % clean_signature + help_text += "Docstring: #TODO :)" + elif obj_type == "object": + help_text += "Type: Object\n" + help_text += "Docstring: #TODO :)" + + elif obj_type == "boolean": + help_text += "Type: Boolean\n" + help_text += "Docstring: #TODO :)" + + elif obj_type == "string": + help_text += "Type: Boolean\n" + help_text += "Text: %s\n" % self._synchronous_evaluate("%s.toString()" % obj_to_identify) + help_text += "Docstring: #TODO :)" + + print help_text + + + def _do_magic(self, expression): + #TODO: add local file read capabilities i.e. %run /tmp/script.txt, and other stuff? + print "You thought I was sleeping, didn't you. Acting." + def hexdump(src, length=16): try: xrange @@ -171,6 +337,5 @@ def main(): app = REPLApplication() app.run() - if __name__ == '__main__': main() diff --git a/src/setup.py b/src/setup.py index fe6db07..0a414ec 100755 --- a/src/setup.py +++ b/src/setup.py @@ -60,7 +60,7 @@ setup( "frida-ps = frida.ps:main", "frida-discover = frida.discoverer:main", "frida-trace = frida.tracer:main", - "frida-repl = frida.repl:main" + "frida = frida.repl:main" ] }, ext_modules=[Extension('_frida', [])],