From 0399aaa09bace48a85554aea283484644268abd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20Andr=C3=A9=20Vadla=20Ravn=C3=A5s?= Date: Sun, 5 Jan 2014 21:35:28 +0100 Subject: [PATCH] Improve Tracer so handlers can be defined on the file-system Also fix Py3k issues. --- src/frida/tracer.py | 318 +++++++++++++++++++++++++++++++++++++------ tests/test_tracer.py | 18 ++- 2 files changed, 290 insertions(+), 46 deletions(-) diff --git a/src/frida/tracer.py b/src/frida/tracer.py index 7ca402e..ccc414f 100644 --- a/src/frida/tracer.py +++ b/src/frida/tracer.py @@ -1,3 +1,5 @@ +from frida.core import ModuleFunction +import os import fnmatch import sys @@ -79,24 +81,46 @@ class TracerProfile(object): r.append(export) return set(r) - class Tracer(object): - def __init__(self, profile): + def __init__(self, reactor, repository, profile): + self._reactor = reactor + self._repository = repository self._profile = profile self._script = None - def start_trace(self, process, sink): - def on_message(message, data): - sink.on_update([ message['payload'] ]) + def start_trace(self, process, ui): + def on_create(*args): + ui.on_trace_handler_create(*args) + self._repository.on_create(on_create) + def on_load(*args): + ui.on_trace_handler_load(*args) + self._repository.on_load(on_load) + + def on_message(message, data): + self._reactor.schedule(lambda: self._process_message(message, data, ui)) + + ui.on_trace_progress('resolve') working_set = self._profile.resolve(process) source = self._create_trace_script() + ui.on_trace_progress('upload') self._script = process.session.create_script(source) self._script.on("message", on_message) self._script.load() for chunk in [working_set[i:i+1000] for i in range(0, len(working_set), 1000)]: - targets = [{ 'absolute_address': hex(export.absolute_address), 'name': export.name } for export in chunk] - self._script.post_message(targets) + targets = [{ + 'name': export.name, + 'absolute_address': hex(export.absolute_address), + 'handler': self._repository.ensure_handler(export) + } for export in chunk] + self._script.post_message({ + 'to': "/targets", + 'name': '+add', + 'payload': { + 'items': targets + } + }) + ui.on_trace_progress('ready') return working_set @@ -113,6 +137,9 @@ class Tracer(object): var started = new Date(); var pending = []; var timer = null; +function log(message) { + send([new Date().getTime() - started.getTime(), message]); +} function processNext() { timer = null; @@ -127,42 +154,185 @@ function scheduleNext() { timer = setTimeout(processNext, 0); } }; -function onStanza(targets) { - targets.forEach(function (target) { - pending.push(function () { - Interceptor.attach(ptr(target.absolute_address), { - onEnter: function onEnter(args) { - send([new Date().getTime() - started.getTime(), target.name]); - } +function onStanza(stanza) { + if (stanza.to === "/targets") { + if (stanza.name === '+add') { + var targets = stanza.payload.items; + targets.forEach(function (target) { + pending.push(function () { + eval("var handler = " + target.handler); + var state = {}; + Interceptor.attach(ptr(target.absolute_address), { + onEnter: function onEnter(args) { + handler.onEnter(log, args, state); + }, + onLeave: function onLeave(retval) { + handler.onLeave(log, retval, state); + } + }); + }); }); - }); - }); - scheduleNext(); + scheduleNext(); + } + } recv(onStanza); }; recv(onStanza); """ - def end_trace(self): + def _process_message(self, message, data, ui): + if message['type'] == 'send': + ui.on_trace_events([ message['payload'] ]) + else: + print(message) + +class Repository(object): + def __init__(self): + self._on_create_callback = None + self._on_load_callback = None + + def ensure_handler(self, function): + raise NotImplementedError("not implemented") + + def on_create(self, callback): + self._on_create_callback = callback + + def on_load(self, callback): + self._on_load_callback = callback + + def _notify_create(self, function, handler, source): + if self._on_create_callback is not None: + self._on_create_callback(function, handler, source) + + def _notify_load(self, function, handler, source): + if self._on_load_callback is not None: + self._on_load_callback(function, handler, source) + + def _create_stub_handler(self, function): + return """\ +/* + * Auto-generated by Frida — please modify to match the signature of %(name)s. + * This stub is somewhat dumb. Future verions of Frida could auto-generate + * based on OS API references, manpages, etc. (Pull-requests appreciated!) + * + * For full API reference, see: https://github.com/frida/frida-gum/wiki/Reference:-Script + */ + +{ + /** + * Called synchronously when about to call %(name)s. + * + * @this {object} - Object allowing you to store state for use in onLeave. + * @param {function} log - Call this function with a string to be presented to the user. + * @param {array} args - Function arguments represented as an array of NativePointer objects. + * For example use Memory.readUtf8String(args[0]) if the first argument is a pointer to a C string encoded as UTF-8. + * It is also possible to modify arguments by assigning a NativePointer object to an element of this array. + * @param {object} state - Object allowing you to keep state across function calls. + * Only one JavaScript function will execute at a time, so do not worry about race-conditions. + * However, do not use this to store function arguments across onEnter/onLeave, but instead + * use "this" which is an object for keeping state local to an invocation. + */ + onEnter: function onEnter(log, args, state) { + log("%(name)s()"); + }, + + /** + * Called synchronously when about to return from %(name)s. + * + * See onEnter for details. + * + * @this {object} - Object allowing you to access state stored in onEnter. + * @param {function} log - Call this function with a string to be presented to the user. + * @param {NativePointer} retval - Return value represented as a NativePointer object. + * @param {object} state - Object allowing you to keep state across function calls. + */ + onLeave: function onLeave(log, retval, state) { + } +} +""" % { 'name': function.name } + +class MemoryRepository(Repository): + def __init__(self): + super(MemoryRepository, self).__init__() + self._handlers = {} + + def ensure_handler(self, function): + handler = self._handlers.get(function) + if handler is None: + handler = self._create_stub_handler(function) + self._handlers[function] = handler + self._notify_create(function, handler, "memory") + else: + self._notify_load(function, handler, "memory") + return handler + +class FileRepository(Repository): + def __init__(self): + super(FileRepository, self).__init__() + self._handlers = {} + self._repo_dir = os.path.join(os.getcwd(), "__frida_handlers__") + + def ensure_handler(self, function): + handler = self._handlers.get(function) + if handler is not None: + return handler + + handler_files_to_try = [] + + if isinstance(function, ModuleFunction): + module_dir = os.path.join(self._repo_dir, function.module.name) + module_handler_file = os.path.join(module_dir, function.name + ".js") + handler_files_to_try.append(module_handler_file) + + any_module_handler_file = os.path.join(self._repo_dir, function.name + ".js") + handler_files_to_try.append(any_module_handler_file) + + for handler_file in handler_files_to_try: + if os.path.isfile(handler_file): + with open(handler_file, 'r') as f: + handler = f.read() + self._notify_load(function, handler, handler_file) + break + + if handler is None: + handler = self._create_stub_handler(function) + handler_file = handler_files_to_try[0] + handler_dir = os.path.dirname(handler_file) + if not os.path.isdir(handler_dir): + os.makedirs(handler_dir) + with open(handler_file, 'w') as f: + f.write(handler) + self._notify_create(function, handler, handler_file) + + self._handlers[function] = handler + + return handler + +class UI(object): + def on_trace_progress(self, operation): pass -class IOSink(object): - def __init__(self, stream): - self._stream = stream + def on_trace_events(self, events): + pass - def on_update(self, invocation_events): - for timestamp, function_name in invocation_events: - self._stream.write("%6d ms\t%s\n" % (timestamp, function_name)) + def on_trace_handler_create(self, function, handler, source): + pass -STDOUT_SINK = IOSink(sys.stdout) -STDERR_SINK = IOSink(sys.stderr) + def on_trace_handler_load(self, function, handler, source): + pass def main(): + import colorama + from colorama import Fore, Back, Style import frida + from frida.core import Reactor from optparse import OptionParser + import sys + + colorama.init(autoreset=True) tp = TracerProfileBuilder() def process_builder_arg(option, opt_str, value, parser, method, **kwargs): @@ -181,26 +351,92 @@ def main(): (options, args) = parser.parse_args() if len(args) != 1: parser.error("process name or id must be specified") - - t = Tracer(tp.build()) try: target = int(args[0]) except: target = args[0] - try: - p = frida.attach(target) - except Exception as e: - print >> sys.stderr, "Failed to attach: %s" % e - sys.exit(1) - targets = t.start_trace(p, STDOUT_SINK) - print("Started tracing %d functions" % len(targets)) - print("Press ENTER to stop") - raw_input() - print("Stopping...") - t.stop() - p.detach() + profile = tp.build() + + class Application(UI): + def __init__(self, target, profile): + self._target = target + self._process = None + self._tracer = None + self._profile = profile + self._status_updated = False + self._exit_status = 0 + self._reactor = Reactor(await_enter) + self._reactor.schedule(self._start) + + def run(self): + self._reactor.run() + self._stop() + return self._exit_status + + def _start(self): + try: + self._update_status("Attaching...") + self._process = frida.attach(self._target) + except Exception as e: + self._update_status("Failed to attach: %s" % e) + self._exit_status = 1 + self._reactor.schedule(self._stop) + return + self._tracer = Tracer(self._reactor, FileRepository(), self._profile) + targets = self._tracer.start_trace(self._process, self) + if len(targets) == 1: + plural = "" + else: + plural = "s" + self._update_status("Started tracing %d function%s. Press ENTER to stop." % (len(targets), plural)) + + def _stop(self): + if self._tracer is not None: + print("Stopping...") + self._tracer.stop() + self._tracer = None + if self._process is not None: + self._process.detach() + self._process = None + self._reactor.stop() + + def on_trace_progress(self, operation): + if operation == 'resolve': + self._update_status("Resolving functions...") + elif operation == 'upload': + self._update_status("Uploading data...") + elif operation == 'ready': + self._update_status("Ready!") + + def on_trace_events(self, events): + self._status_updated = False + for timestamp, message in events: + print("%6d ms\t%s" % (timestamp, message)) + + def on_trace_handler_create(self, function, handler, source): + print("%s: Auto-generated handler at \"%s\"" % (function, source)) + + def on_trace_handler_load(self, function, handler, source): + print("%s: Loaded handler at \"%s\"" % (function, source)) + + def _update_status(self, message): + if self._status_updated: + cursor_position = "\033[A" + else: + cursor_position = "" + print("%-80s" % (cursor_position + Style.BRIGHT + message,)) + self._status_updated = True + + def await_enter(): + if sys.version_info[0] >= 3: + input() + else: + raw_input() + + app = Application(target, profile) + status = app.run() frida.shutdown() - sys.exit(0) + sys.exit(status) if __name__ == '__main__': diff --git a/tests/test_tracer.py b/tests/test_tracer.py index 6c85b05..ada2ada 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -1,7 +1,9 @@ import frida -from frida.tracer import Tracer, TracerProfileBuilder, STDOUT_SINK +from frida.core import Reactor +from frida.tracer import Tracer, TracerProfileBuilder, MemoryRepository, UI import platform import subprocess +import threading try: import unittest2 as unittest except: @@ -24,10 +26,16 @@ class TestTracer(unittest.TestCase): cls.target.terminate() def test_basics(self): - tp = TracerProfileBuilder().include("open*") - t = Tracer(tp.build()) - targets = t.start_trace(self.process, STDOUT_SINK) - t.stop() + never = threading.Event() + reactor = Reactor(never.wait) + def start(): + tp = TracerProfileBuilder().include("open*") + t = Tracer(reactor, MemoryRepository(), tp.build()) + targets = t.start_trace(self.process, UI()) + t.stop() + reactor.stop() + reactor.schedule(start) + reactor.run() if __name__ == '__main__':