Merge branch 'feature/spawn'

This commit is contained in:
Ole André Vadla Ravnås
2015-02-22 03:57:44 +01:00
4 changed files with 283 additions and 188 deletions
+72 -13
View File
@@ -2,6 +2,7 @@
import collections
from optparse import OptionParser
import os
import sys
import threading
import time
@@ -27,6 +28,17 @@ class ConsoleApplication(object):
action='store_const', const='tether', dest="device_type", default='local')
parser.add_option("-R", "--remote", help="connect to remote device",
action='store_const', const='remote', dest="device_type", default='local')
if self._needs_target():
def store_target(option, opt_str, target_value, parser, target_type, *args, **kwargs):
if target_type == 'file':
target_value = [target_value]
setattr(parser.values, 'target', (target_type, target_value))
parser.add_option("-f", "--file", help="spawn FILE", metavar="FILE",
type='string', action='callback', callback=store_target, callback_args=('file',))
parser.add_option("-n", "--attach-name", help="attach to NAME", metavar="NAME",
type='string', action='callback', callback=store_target, callback_args=('name',))
parser.add_option("-p", "--attach-pid", help="attach to PID", metavar="PID",
type='int', action='callback', callback=store_target, callback_args=('pid',))
self._add_options(parser)
(options, args) = parser.parse_args()
@@ -34,25 +46,36 @@ class ConsoleApplication(object):
self._device_type = options.device_type
self._device = None
self._schedule_on_device_lost = lambda: self._reactor.schedule(self._on_device_lost)
self._target = None
self._spawned_pid = None
self._spawned_argv = None
self._process = None
self._schedule_on_process_detached = lambda: self._reactor.schedule(self._on_process_detached)
self._started = False
self._resumed = False
self._reactor = Reactor(run_until_return)
self._exit_status = None
self._status_updated = False
self._initialize(parser, options, args)
target_specifier = self._target_specifier(parser, options, args)
if target_specifier is not None:
try:
self._target = int(target_specifier)
except:
self._target = target_specifier
if self._needs_target():
target = getattr(options, 'target', None)
if target is None:
if len(args) < 1:
parser.error("target file, process name or pid must be specified")
target = infer_target(args[0])
args.pop(0)
target = expand_target(target)
if target[0] == 'file':
argv = target[1]
if not os.path.isfile(argv[0]):
parser.error("%s: file not found" % argv[0])
argv.extend(args)
args = []
self._target = target
else:
self._target = None
self._initialize(parser, options, args)
def run(self):
mgr = frida.get_device_manager()
on_devices_changed = lambda: self._reactor.schedule(self._try_start)
@@ -66,6 +89,8 @@ class ConsoleApplication(object):
self._process.off('detached', self._schedule_on_process_detached)
self._process.detach()
self._process = None
if self._spawned_pid is not None:
self._device.kill(self._spawned_pid)
if self._device is not None:
self._device.off('lost', self._schedule_on_device_lost)
mgr.off('changed', on_devices_changed)
@@ -78,8 +103,8 @@ class ConsoleApplication(object):
def _initialize(self, parser, options, args):
pass
def _target_specifier(self, parser, options, args):
return None
def _needs_target(self):
return False
def _start(self):
pass
@@ -87,6 +112,13 @@ class ConsoleApplication(object):
def _stop(self):
pass
def _resume(self):
if self._resumed:
return
if self._spawned_pid is not None:
self._device.resume(self._spawned_pid)
self._resumed = True
def _exit(self, exit_status):
self._exit_status = exit_status
self._reactor.stop()
@@ -100,8 +132,17 @@ class ConsoleApplication(object):
self._device.on('lost', self._schedule_on_device_lost)
if self._target is not None:
try:
self._update_status("Attaching...")
self._process = self._device.attach(self._target)
target_type, target_value = self._target
if target_type == 'file':
argv = target_value
self._update_status("Spawning `%s`..." % " ".join(argv))
self._spawned_pid = self._device.spawn(argv)
self._spawned_argv = argv
attach_target = self._spawned_pid
else:
attach_target = target_value
self._update_status("Attaching...")
self._process = self._device.attach(attach_target)
self._process.on('detached', self._schedule_on_process_detached)
except Exception as e:
self._update_status("Failed to attach: %s" % e)
@@ -138,6 +179,24 @@ def find_device(type):
return device
return None
def infer_target(target_value):
if target_value.startswith('.') or target_value.startswith(os.path.sep):
target_type = 'file'
target_value = [target_value]
else:
try:
target_value = int(target_value)
target_type = 'pid'
except:
target_type = 'name'
return (target_type, target_value)
def expand_target(target):
target_type, target_value = target
if target_type == 'file':
target_value = [os.path.abspath(target_value[0])]
return (target_type, target_value)
class Reactor(object):
def __init__(self, run_until_return):
+96 -95
View File
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
from frida.application import await_enter
from frida.core import ModuleFunction
import threading
class Discoverer(object):
@@ -13,9 +15,16 @@ class Discoverer(object):
self._reactor.schedule(lambda: self._process_message(message, data, process, ui))
source = self._create_discover_script()
self._script = process.session.create_script(source)
self._script.on("message", on_message)
self._script.on('message', on_message)
self._script.load()
def stop(self):
self._script.post_message({
'to': "/sampler",
'name': '+stop',
'payload': {}
})
def stop(self):
if self._script is not None:
try:
@@ -25,90 +34,74 @@ class Discoverer(object):
self._script = None
def _create_discover_script(self):
return """
var Sampler = function Sampler() {
var total = 0;
var pending = [];
var active = [];
var samples = {};
Process.enumerateThreads({
onMatch: function (thread) {
pending.push(thread);
},
onComplete: function () {
var currentThreadId = Process.getCurrentThreadId();
pending = pending.filter(function (thread) {
return thread.id !== currentThreadId;
});
total = pending.length;
var processNext = function processNext() {
active.forEach(function (thread) {
Stalker.unfollow(thread.id);
});
active = pending.splice(0, 4);
if (active.length > 0) {
var begin = total - pending.length - active.length;
send({
from: "/sampler",
name: '+progress',
payload: {
begin: begin,
end: begin + active.length - 1,
total: total
}
});
} else {
for (var address in samples) {
if (samples.hasOwnProperty(address)) {
var counts = samples[address].counts;
var sum = 0;
for (var i = 0; i !== counts.length; i++) {
sum += counts[i];
}
var callsPerSecond = Math.round(sum / (counts.length * 0.25));
samples[address] = callsPerSecond;
}
}
send({
from: "/sampler",
name: '+result',
payload: {
samples: samples
}
});
samples = null;
}
active.forEach(function (thread) {
Stalker.follow(thread.id, {
return """\
function Sampler() {
var threadIds = [];
var result = {};
function onStanza(stanza) {
if (stanza.to === "/sampler") {
if (stanza.name === '+stop') {
stop();
}
}
recv(onStanza);
}
this.start = function () {
threadIds = [];
Process.enumerateThreads({
onMatch: function (thread) {
threadIds.push(thread.id);
},
onComplete: function () {
threadIds.forEach(function (threadId) {
Stalker.follow(threadId, {
events: { call: true },
onCallSummary: function (summary) {
if (samples === null) {
return;
}
for (var address in summary) {
if (summary.hasOwnProperty(address)) {
var sample = samples[address];
if (sample === undefined) {
sample = { counts: [] };
samples[address] = sample;
}
sample.counts.push(summary[address]);
var count = result[address] || 0;
result[address] = count + summary[address];
}
}
}
});
});
if (active.length > 0) {
setTimeout(processNext, 2000);
setTimeout(Stalker.garbageCollect, 2100);
}
};
setTimeout(processNext, 0);
}
});
send({
from: "/sampler",
name: '+started',
payload: {
total: threadIds.length
}
});
}
});
}
function stop() {
threadIds.forEach(function (threadId) {
Stalker.unfollow(threadId);
});
threadIds = [];
send({
from: "/sampler",
name: '+stopped',
payload: {
result: result
}
});
result = {};
}
recv(onStanza);
};
sampler = new Sampler();
setTimeout(function () { sampler.start(); }, 0);
"""
def _process_message(self, message, data, process, ui):
@@ -117,21 +110,21 @@ sampler = new Sampler();
name = stanza['name']
payload = stanza['payload']
if stanza['from'] == "/sampler":
if name == '+progress':
ui.on_sample_progress(payload['begin'], payload['end'], payload['total'])
elif name == '+result':
if name == '+started':
ui.on_sample_start(payload['total'])
elif name == '+stopped':
module_functions = {}
dynamic_functions = []
for address, rate in payload['samples'].items():
for address, count in payload['result'].items():
address = int(address, 16)
function = process.ensure_function(address)
if isinstance(function, ModuleFunction):
functions = module_functions.get(function.module, [])
if len(functions) == 0:
module_functions[function.module] = functions
functions.append((function, rate))
functions.append((function, count))
else:
dynamic_functions.append((function, rate))
dynamic_functions.append((function, count))
ui.on_sample_result(module_functions, dynamic_functions)
else:
print(message, data)
@@ -141,7 +134,7 @@ sampler = new Sampler();
print(message, data)
class UI(object):
def on_sample_progress(self, begin, end, total):
def on_sample_start(self, total):
pass
def on_sample_result(self, module_functions, dynamic_functions):
@@ -152,16 +145,23 @@ def main():
from frida.application import ConsoleApplication
class DiscovererApplication(ConsoleApplication, UI):
def __init__(self):
self._results_received = threading.Event()
ConsoleApplication.__init__(self, self._await_keys)
def _await_keys(self):
await_enter()
self._reactor.schedule(lambda: self._discoverer.stop())
self._results_received.wait()
def _usage(self):
return "usage: %prog [options] process-name-or-id"
return "usage: %prog [options] target"
def _initialize(self, parser, options, args):
self._discoverer = None
def _target_specifier(self, parser, options, args):
if len(args) != 1:
parser.error("process name or id must be specified")
return args[0]
def _needs_target(self):
return True
def _start(self):
self._update_status("Injecting script...")
@@ -173,24 +173,25 @@ def main():
self._discoverer.stop()
self._discoverer = None
def on_sample_progress(self, begin, end, total):
self._update_status("Sampling %d threads: %d through %d..." % (total, begin, end))
def on_sample_start(self, total):
self._update_status("Tracing %d threads. Press ENTER to stop." % total)
self._resume()
def on_sample_result(self, module_functions, dynamic_functions):
for module, functions in module_functions.items():
print(module.name)
print("\t%-10s\t%s" % ("Rate", "Function"))
for function, rate in sorted(functions, key=lambda item: item[1], reverse=True):
print("\t%-10d\t%s" % (rate, function))
print("\t%-10s\t%s" % ("Calls", "Function"))
for function, count in sorted(functions, key=lambda item: item[1], reverse=True):
print("\t%-10d\t%s" % (count, function))
print("")
if len(dynamic_functions) > 0:
print("Dynamic functions:")
print("\t%-10s\t%s" % ("Rate", "Function"))
for function, rate in sorted(dynamic_functions, key=lambda item: item[1], reverse=True):
print("\t%-10d\t%s" % (rate, function))
print("\t%-10s\t%s" % ("Calls", "Function"))
for function, count in sorted(dynamic_functions, key=lambda item: item[1], reverse=True):
print("\t%-10d\t%s" % (count, function))
self._exit(0)
self._results_received.set()
app = DiscovererApplication()
app.run()
+70 -52
View File
@@ -17,12 +17,10 @@ def main():
super(REPLApplication, self).__init__(self._process_input)
def _usage(self):
return "usage: %prog [options]"
return "usage: %prog [options] target"
def _target_specifier(self, parser, options, args):
if len(args) != 1:
parser.error("process name or id must be specified")
return args[0]
def _needs_target(self):
return True
def _start(self):
def on_message(message, data):
@@ -30,50 +28,62 @@ def main():
self._script = self._process.session.create_script(self._create_repl_script())
self._script.on('message', on_message)
self._script.load()
if self._spawned_argv is not None:
self._update_status("Spawned `%s`. Call resume() to let the main thread start executing!" % " ".join(self._spawned_argv))
self._idle.set()
def _stop(self):
self._script.unload()
try:
self._script.unload()
except:
pass
self._script = None
def _create_repl_script(self):
return """\
Object.defineProperty(this, 'modules', {
enumerable: true,
get: function () {
var result = [];
Process.enumerateModules({
onMatch: function onMatch(mod) {
result.push(mod);
},
onComplete: function onComplete() {
}
});
return result;
}
});
(function () {
this.resume = function () {
send({ name: '+resume' });
};
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) {
Object.defineProperty(this, 'modules', {
enumerable: true,
get: function () {
var result = [];
Process.enumerateModules({
onMatch: function onMatch(mod) {
result.push(mod);
},
onComplete: function onComplete() {
}
});
return result;
}
});
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 (!sentRaw) {
send({ name: '+result', payload: result });
}
} catch (e) {
send({ name: '+error', payload: e.toString() });
}
if (!sentRaw) {
send({ name: '+result', payload: result });
}
} catch (e) {
send({ name: '+error', payload: e.toString() });
recv(onExpression);
}
recv(onExpression);
}
recv(onExpression);
}).call(this);
"""
def _process_input(self):
@@ -111,22 +121,30 @@ recv(onExpression);
if message['type'] == 'send' and 'payload' in message:
stanza = message['payload']
if isinstance(stanza, dict) and stanza.get('name') in ('+result', '+error'):
handled = True
if data is not None:
output = hexdump(data).rstrip("\n")
else:
if 'payload' in stanza:
value = stanza['payload']
if stanza['name'] == '+result':
output = json.dumps(value, sort_keys=True, indent=4, separators=(",", ": "))
else:
output = value
if isinstance(stanza, dict):
name = stanza.get('name')
if name in ('+result', '+error'):
if data is not None:
output = hexdump(data).rstrip("\n")
else:
output = "undefined"
sys.stdout.write(output + "\n")
sys.stdout.flush()
self._idle.set()
if 'payload' in stanza:
value = stanza['payload']
if stanza['name'] == '+result':
output = json.dumps(value, sort_keys=True, indent=4, separators=(",", ": "))
else:
output = value
else:
output = "undefined"
sys.stdout.write(output + "\n")
sys.stdout.flush()
self._idle.set()
handled = True
elif name == '+resume':
self._resume()
self._idle.set()
handled = True
if not handled:
print("message:", message, "data:", data)
+45 -28
View File
@@ -136,7 +136,7 @@ class Tracer(object):
ui.on_trace_progress('resolve')
working_set = self._profile.resolve(process)
source = self._create_trace_script()
ui.on_trace_progress('upload')
ui.on_trace_progress('instrument')
self._script = process.session.create_script(source)
self._script.on('message', on_message)
self._script.load()
@@ -152,7 +152,12 @@ class Tracer(object):
'items': targets
}
})
ui.on_trace_progress('ready')
self._script.post_message({
'to': "/targets",
'name': '+start',
'payload': {}
})
return working_set
@@ -176,6 +181,8 @@ function onStanza(stanza) {
add(stanza.payload.items);
} else if (stanza.name === '+update') {
update(stanza.payload.items);
} else if (stanza.name === '+start') {
start();
}
}
@@ -220,6 +227,16 @@ function update(targets) {
handlers[target.absolute_address][0] = handler;
});
}
function start() {
pending.push(function acknowledgeStart() {
send({
from: "/targets",
name: '+started',
payload: {}
});
});
scheduleNext();
}
function scheduleNext() {
if (timer === null) {
timer = setTimeout(processNext, 0);
@@ -238,22 +255,23 @@ recv(onStanza);
"""
def _process_message(self, message, data, ui):
handled = False
if message['type'] == 'send':
stanza = message['payload']
if stanza['from'] == "/events":
if stanza['name'] == '+add':
events = [(timestamp, int(target_address.rstrip("L"), 16), message) for timestamp, target_address, message in stanza['payload']['items']]
if stanza['from'] == "/events" and stanza['name'] == '+add':
events = [(timestamp, int(target_address.rstrip("L"), 16), message) for timestamp, target_address, message in stanza['payload']['items']]
ui.on_trace_events(events)
ui.on_trace_events(events)
target_addresses = set([target_address for timestamp, target_address, message in events])
for target_address in target_addresses:
self._repository.sync_handler(target_address)
else:
print(stanza)
else:
print(stanza)
else:
target_addresses = set([target_address for timestamp, target_address, message in events])
for target_address in target_addresses:
self._repository.sync_handler(target_address)
handled = True
elif stanza['from'] == "/targets" and stanza['name'] == '+started':
ui.on_trace_progress('ready')
handled = True
if not handled:
print(message)
class Repository(object):
@@ -449,25 +467,19 @@ def main():
self._profile_builder = pb
def _usage(self):
return "usage: %prog [options] process-name-or-id"
return "usage: %prog [options] target"
def _initialize(self, parser, options, args):
self._tracer = None
self._targets = None
self._profile = self._profile_builder.build()
def _target_specifier(self, parser, options, args):
if len(args) != 1:
parser.error("process name or id must be specified")
return args[0]
def _needs_target(self):
return True
def _start(self):
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))
self._targets = self._tracer.start_trace(self._process, self)
def _stop(self):
print("Stopping...")
@@ -477,10 +489,15 @@ def main():
def on_trace_progress(self, operation):
if operation == 'resolve':
self._update_status("Resolving functions...")
elif operation == 'upload':
self._update_status("Uploading data...")
elif operation == 'instrument':
self._update_status("Instrumenting functions...")
elif operation == 'ready':
self._update_status("Ready!")
if len(self._targets) == 1:
plural = ""
else:
plural = "s"
self._update_status("Started tracing %d function%s. Press ENTER to stop." % (len(self._targets), plural))
self._resume()
def on_trace_events(self, events):
self._status_updated = False