Improve Tracer so handlers can be defined on the file-system

Also fix Py3k issues.
This commit is contained in:
Ole André Vadla Ravnås
2014-01-05 21:35:28 +01:00
parent b0883ae28e
commit 0399aaa09b
2 changed files with 290 additions and 46 deletions
+277 -41
View File
@@ -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__':
+13 -5
View File
@@ -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__':