diff --git a/src/frida/application.py b/src/frida/application.py index 376f3d7..32889d1 100755 --- a/src/frida/application.py +++ b/src/frida/application.py @@ -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): diff --git a/src/frida/discoverer.py b/src/frida/discoverer.py index fcf6c71..720b7bc 100644 --- a/src/frida/discoverer.py +++ b/src/frida/discoverer.py @@ -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() diff --git a/src/frida/repl.py b/src/frida/repl.py index e64a4f5..2fbfa63 100644 --- a/src/frida/repl.py +++ b/src/frida/repl.py @@ -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) diff --git a/src/frida/tracer.py b/src/frida/tracer.py index 26a0e5a..2d30a9f 100644 --- a/src/frida/tracer.py +++ b/src/frida/tracer.py @@ -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