From a3a6a5bf42a2a7168c58834fee89e0d659ab1722 Mon Sep 17 00:00:00 2001 From: Ivan Gotovchits Date: Fri, 4 May 2018 14:41:51 -0400 Subject: [PATCH 01/13] adds a robust sexp parser --- plugins/bap/utils/sexp.py | 44 +++++++++++++++++++++++++++++++++++++++ tests/test_sexp.py | 19 +++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 plugins/bap/utils/sexp.py create mode 100644 tests/test_sexp.py diff --git a/plugins/bap/utils/sexp.py b/plugins/bap/utils/sexp.py new file mode 100644 index 0000000..4c00993 --- /dev/null +++ b/plugins/bap/utils/sexp.py @@ -0,0 +1,44 @@ +from shlex import shlex + + +class Parser(object): + def __init__(self, instream=None, infile=None): + self.lexer = shlex(instream, infile) + self.lexer.wordchars += ":-/@#$%^&*+`\\'" + self.lexer.commenters = ";" + + def __iter__(self): + return self + + def __next__(self): + return self.next() + + def next(self): + token = self.lexer.get_token() + if token == self.lexer.eof: + raise StopIteration + elif token == '(': + return self._parse_list() + else: + return token + + def _parse_list(self): + elts = [] + for token in self.lexer: + if token == ')': + break + elif token == '(': + elts.append(self._parse_list()) + else: + elts.append(token) + return elts + + +def loads(ins): + parser = Parser(ins) + return [x for x in parser] + + +def parse(ins): + parser = Parser(ins) + return parser.next() diff --git a/tests/test_sexp.py b/tests/test_sexp.py new file mode 100644 index 0000000..7d8a187 --- /dev/null +++ b/tests/test_sexp.py @@ -0,0 +1,19 @@ +from bap.utils.sexp import parse + + +def test_parse(): + assert parse('()') == [] + assert parse('hello') == 'hello' + assert parse('"hello world"') == '"hello world"' + assert parse('(hello world)') == ['hello', 'world'] + assert parse('(() () ())') == [[], [], []] + assert parse("hi'") == "hi'" + assert parse('hello"') == 'hello"' + assert parse('(hello\" cruel world\")') == ['hello"', 'cruel', 'world"'] + assert parse('(a (b c) c (d (e f) g) h') == [ + 'a', + ['b', 'c'], + 'c', + ['d', ['e', 'f'], 'g'], + 'h' + ] From f5da8a425d7e117639c3d5eca6f402427607ad20 Mon Sep 17 00:00:00 2001 From: Ivan Gotovchits Date: Fri, 4 May 2018 14:42:59 -0400 Subject: [PATCH 02/13] adds the tracing framework The framework for reading and processing Primus observation files --- plugins/bap/utils/trace.py | 294 +++++++++++++++++++++++++++++++++++++ tests/test_trace.py | 271 ++++++++++++++++++++++++++++++++++ 2 files changed, 565 insertions(+) create mode 100644 plugins/bap/utils/trace.py create mode 100644 tests/test_trace.py diff --git a/plugins/bap/utils/trace.py b/plugins/bap/utils/trace.py new file mode 100644 index 0000000..474c692 --- /dev/null +++ b/plugins/bap/utils/trace.py @@ -0,0 +1,294 @@ +from .sexp import Parser + +handlers = {} +filters = {} + + +class Loader(object): + + def __init__(self, *args): + self.parser = Parser(*args) + self.state = {} + # the following are private, as we need to maintain + # several invariants on them. + self._handlers = [] + self._filters = [] + self._filter_reqs = set() + + def __iter__(self): + return self + + def __next__(self): + return self.next() + + def enable_handlers(self, names): + """enables the given trace event handler and all its requirements. + Example: + + >>> loader.enable_handler('regs') + """ + self._handlers = satisfy_requirements(self._handlers + names) + for name in self._handlers: + handlers[name].init(self.state) + + def enable_filter(self, filter_name, *args, **kwargs): + """turns on the specified filter. + + Passes all arguments to the filter with the given name. The + returned predicate is checked on each event and if it returns + ``False`` then event handlers will not be called for that + event. + + If a filter has requirements then those requirements are + invoked before the filter, and are not affected by the + predicates of other filters. + + + Example: + >>> loader.enable_filter('filter-machine', id=3) + + Or: + >>> loader.enable_filter('filter-range', lo=0x400000, hi=0x400100) + + """ + filter = filters[filter_name] + requires = satisfy_requirements(filter.requires) + self.enable_handlers(requires) + for req in requires: + self._filter_reqs.add(req) + self._filters.append(filter(*args, **kwargs)) + + def next(self): + event = self.parser.next() + print('Parser event {}'.format(repr(event))) + if len(event) != 2: + raise ValueError('Malformed Observation {}'.format(event)) + event, payload = event + completed = set() + self.state['event'] = event + + # first run handlers that are required by filters + for h in self._handlers: + if h in self._filter_reqs and event in handlers[h].events: + completed.add(h) + handlers[h](self.state, payload) + + # now we can run all filters, + for accept in self._filters: + if not accept(self.state): + break + else: # and if nobody complains then run the rest of handlers + for h in self._handlers: + if h not in completed and event in handlers[h].events: + handlers[h](self.state, payload) + return self.state + + def run(self): + for _ in next(): + pass + + +def attach_meta_attributes(h, **kwargs): + """attaches meta attributes to the provided callable object ``h`` + + The following attributes are attached to the ``__dict__`` + namespace (also available through the ``func_dict`` name): + + - ``'name'`` a normalized human readable name, + computed from a function name with underscores substituted + by dashes. If ``'name'`` is in ``kwargs``, then the provided + name will be used instead. + + - ``'requires'`` a list handler dependencies. Will be empty if + the ``requires`` keyword argument is not provided. Otherwise it + will be intialized from the argument value, that could be a list + or a string. + + - all other attributes from the ``kwargs`` argument. + """ + if 'name' not in kwargs: + name = h.__name__.replace('_', '-') + h.__dict__['name'] = name + req = kwargs.get('requires', []) + if 'requires' in kwargs: + del kwargs['requires'] + h.__dict__['requires'] = req if isinstance(req, list) else [req] + h.__dict__.update(kwargs) + + +def handler(*args, **kwargs): + """a decorator that creates a trace event handler + + Registers the provided function as an event handler for the + specified list of events. If enabled the function will be called + every time one of the events occurs with two arguments - the + trace loader state (which is a dictionary) and the payload of the + occurred event, which is an s-expression represented as a list. + + The loader state is guaranteed to have the ``'event'`` attribute + that will contain the name of the current event. + + Example: + ``` + @handler('switch', 'fork') + def machine_id(state, fromto): + state['machine-id'] = fromto[1] + + ``` + """ + def make_handler(f): + f.__dict__['events'] = args + if 'init' in kwargs: + default = kwargs['init'] + f.__dict__['init'] = lambda s: s.update(default) + del kwargs['init'] + else: + f.__dict__['init'] = lambda x: None + attach_meta_attributes(f, **kwargs) + handlers[f.name] = f + return make_handler + + +def filter(**kwargs): + """a decorator that creates a trace event filter + + The decorated function must accept the state dictionary + and zero or more user provided arguments and return ``True`` + or ``False`` depending on whether the current event should be + accepted or, correspondingly rejected. + + If the ``requires`` argument is passed to the filter decorator + then the loader will ensure that all event handlers in ``requires`` + are run before the filter is called. + + Note: if a handler is required by any filter, then it will be + always invoked, no matter whether its event is filtered or not. + + The decorator will also add several meta attributes to the + decorated function (as described in ``attach_meta_attributes``) + and update the global dictionary of available filters. + + Example: + ``` + @filter(requires='pc') + def filter_range(state, lo, hi): + return lo <= state['pc'] <= hi + ``` + """ + def make_filter(f): + def init(**kwargs): + return lambda state: f(state, **kwargs) + attach_meta_attributes(f, **kwargs) + attach_meta_attributes(init, name=f.name, **kwargs) + filters[init.name] = init + return make_filter + + +@handler('machine-switch', 'machine-fork', init={'machine-id': 0}) +def machine_id(state, fromto): + """tracks machine identifier + + Maintains the 'machine-id' field in the state. + """ + state['machine-id'] = int(fromto[1]) + + +@handler('pc-changed', init={'pc': 0}) +def pc(state, data): + """tracks program counter + + Maintains the 'pc' field in the state. + """ + state['pc'] = word(data)['value'] + + +@handler('enter-term', init={'term-id': None}) +def term_id(state, data): + """tracks term identifier + + Maintaints the 'term-id' field in the state. + """ + state['term-id'] = data + + +@handler('pc-changed', 'written', init={'regs': {}}) +def regs(state, data): + """"tracks register assignments + + Provides the 'regs' field, which is a mapping from + register names to values. + """ + if state['event'] == 'pc-changed': + state['regs'] = {} + else: + state['regs'][data[0]] = value(data[1]) + + +@handler('pc-changed', 'stored', init={'mems': {}}) +def mems(state, data): + """tracks memory writes + + Provides the 'mems' field that represents all updates made by + the current instruction to the memory in a form of a mapping + from addresses to bytes. Both are represented with the Python + int type + """ + if state['event'] == 'pc-changed': + state['mems'] = {} + else: + state['mems'][word(data[0])['value']] = value(data[1]) + + +@filter(requires='pc') +def filter_range(state, lo, hi): + """masks events that do not fall into the specified region. + + interval bounds are included. + """ + return lo <= state['pc'] <= hi + + +@filter(requires='machine-id') +def filter_machine(state, id): + "masks events that do not belong to the specified machine identifier" + cur = state['machine-id'] + return cur == id if isinstance(id, int) else cur in id + + +def word(x): + "parses a Primus word into a ``value``, ``type`` dictionary" + w, t = x.split(':') + return { + 'value': int(w, 0), + 'type': t + } + + +def value(x): + "parses a Primus value into a ``value``, ``type``, ``id`` dictionary" + w, id = x.split('#') + w = word(w) + w['id'] = int(id) + return w + + +def satisfy_requirements(requests): + """ensures that each request gets what it ``requires``. + + Accepts a list of handler names and returns a list of handler + names that guarantees that if a handler has a non-empty + ``requires`` field, then all names in this list will precede the + name of this handler. It also guarantees that each handler will + occur at most once. + """ + solution = [] + for name in requests: + solution += satisfy_requirements(handlers[name].requires) + solution.append(name) + + # now we need to dedup the solution - a handler must occur at most once + result = [] + for h in solution: + if h not in result: + result.append(h) + return result diff --git a/tests/test_trace.py b/tests/test_trace.py new file mode 100644 index 0000000..fbd162d --- /dev/null +++ b/tests/test_trace.py @@ -0,0 +1,271 @@ +from bap.utils import trace + +testdata = [ + { + 'input': '(pc-changed 0x10:64u)', + 'state': { + 'event': 'pc-changed', + 'pc': 0x10, + 'machine-id': 0, + 'regs': {}, + 'mems': {}, + } + }, + + { + 'input': '(written (RAX 1:64u#1))', + 'state': { + 'event': 'written', + 'pc': 0x10, + 'machine-id': 0, + 'regs': { + 'RAX': {'value': 1, 'type': '64u', 'id': 1} + }, + 'mems': {}, + } + }, + + { + 'input': '(written (RBX 2:64u#4))', + 'state': { + 'event': 'written', + 'pc': 0x10, + 'machine-id': 0, + 'regs': { + 'RAX': {'value': 1, 'type': '64u', 'id': 1}, + 'RBX': {'value': 2, 'type': '64u', 'id': 4} + }, + 'mems': {}, + } + }, + + { + 'input': '(machine-fork (0 1))', + 'state': { + 'event': 'machine-fork', + 'pc': 0x10, + 'machine-id': 1, + 'regs': { + 'RAX': {'value': 1, 'type': '64u', 'id': 1}, + 'RBX': {'value': 2, 'type': '64u', 'id': 4} + }, + 'mems': {}, + } + }, + + { + 'input': '(written (ZF 1:1u#32))', + 'state': { + 'event': 'written', + 'pc': 0x10, + 'machine-id': 1, + 'regs': { + 'RAX': {'value': 1, 'type': '64u', 'id': 1}, + 'RBX': {'value': 2, 'type': '64u', 'id': 4}, + 'ZF': {'value': 1, 'type': '1u', 'id': 32} + }, + 'mems': {}, + } + }, + + { + 'input': '(machine-switch (1 0))', + 'state': { + 'event': 'machine-switch', + 'pc': 0x10, + 'machine-id': 0, + 'regs': { + 'RAX': {'value': 1, 'type': '64u', 'id': 1}, + 'RBX': {'value': 2, 'type': '64u', 'id': 4}, + 'ZF': {'value': 1, 'type': '1u', 'id': 32} + }, + 'mems': {}, + } + }, + + { + 'input': '(pc-changed 0x11:64u)', + 'state': { + 'event': 'pc-changed', + 'pc': 0x11, + 'machine-id': 0, + 'regs': {}, + 'mems': {}, + } + }, + + { + 'input': '(stored (0x400:64u#42 0xDE:8u#706))', + 'state': { + 'event': 'stored', + 'pc': 0x11, + 'machine-id': 0, + 'regs': {}, + 'mems': { + 0x400: {'value': 0xDE, 'type': '8u', 'id': 706} + }, + } + }, + + { + 'input': """ + (incident-location (2602 + (677:27ee3 677:27e85 677:27e74 677:27e6b 677:27e60 + 677:27edc 677:27ed0 677:27ee3 677:27e85 677:27e74 + 677:27e6b 677:27e60 677:27edc 677:27ed0 677:27ee3 + 677:27e85 677:27e74 677:27e6b 677:27e60 677:27edc))) + """, + 'state': { + 'event': 'incident-location', + 'pc': 0x11, + 'machine-id': 0, + 'regs': {}, + 'mems': { + 0x400: {'value': 0xDE, 'type': '8u', 'id': 706} + }, + } + }, + + { + 'input': '(machine-switch (0 1))', + 'state': { + 'event': 'machine-switch', + 'pc': 0x11, + 'machine-id': 1, + 'regs': {}, + 'mems': { + 0x400: {'value': 0xDE, 'type': '8u', 'id': 706} + }, + } + }, + + { + 'input': '(pc-changed 0x10:64u)', + 'state': { + 'event': 'pc-changed', + 'pc': 0x10, + 'machine-id': 1, + 'regs': {}, + 'mems': {}, + } + }, + + { + 'input': '(written (RAX 2:64u#3))', + 'state': { + 'event': 'written', + 'pc': 0x10, + 'machine-id': 1, + 'regs': { + 'RAX': {'value': 2, 'type': '64u', 'id': 3} + }, + 'mems': {}, + } + }, + + { + 'input': '(pc-changed 0x11:64u)', + 'state': { + 'event': 'pc-changed', + 'pc': 0x11, + 'machine-id': 1, + 'regs': {}, + 'mems': {}, + } + }, + + + { + 'input': '(call (malloc 2:64u#12))', + 'state': { + 'event': 'call', + 'pc': 0x11, + 'machine-id': 1, + 'regs': {}, + 'mems': {}, + } + }, + + { + 'input': '(pc-changed 0x1:64u)', + 'state': { + 'event': 'pc-changed', + 'pc': 0x1, + 'machine-id': 1, + 'regs': {}, + 'mems': {}, + } + }, + + { + 'input': '(written (RAX 2:64u#3))', + 'state': { + 'event': 'written', + 'pc': 0x1, + 'machine-id': 1, + 'regs': {}, + 'mems': {}, + } + }, + + { + 'input': '(pc-changed 0x12:64u)', + 'state': { + 'event': 'pc-changed', + 'pc': 0x12, + 'machine-id': 1, + 'regs': {}, + 'mems': {}, + } + }, + + { + 'input': '(written (RAX 2:64u#3))', + 'state': { + 'event': 'written', + 'pc': 0x12, + 'machine-id': 1, + 'regs': { + 'RAX': {'value': 2, 'type': '64u', 'id': 3} + }, + 'mems': {}, + } + }, + + { + 'input': '(machine-fork (1 2))', + 'state': { + 'event': 'machine-fork', + 'pc': 0x12, + 'machine-id': 2, + 'regs': { + 'RAX': {'value': 2, 'type': '64u', 'id': 3} + }, + 'mems': {}, + } + }, + + { + 'input': '(stored (0x600:64u#76 0xDE:64u#))', + 'state': { + 'event': 'stored', + 'pc': 0x12, + 'machine-id': 2, + 'regs': { + 'RAX': {'value': 2, 'type': '64u', 'id': 3} + }, + 'mems': {}, + } + } +] + + +def test_loader(): + loader = trace.Loader('\n'.join(s['input'] for s in testdata)) + loader.enable_handlers(['regs', 'mems']) + loader.enable_filter('filter-machine', id=[0, 1]) + loader.enable_filter('filter-range', lo=0x10, hi=0x20) + step = 0 + for state in loader: + assert step >= 0 and state == testdata[step]['state'] + step += 1 From b6c119159b453d3c1f105f963649f5a799182116 Mon Sep 17 00:00:00 2001 From: Ivan Gotovchits Date: Fri, 4 May 2018 15:33:12 -0400 Subject: [PATCH 03/13] adds an experimental support for IDA Debugger funny but it works, yes only three lines of code. --- plugins/bap/plugins/bap_trace.py | 8 ++++++++ plugins/bap/utils/trace.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 plugins/bap/plugins/bap_trace.py diff --git a/plugins/bap/plugins/bap_trace.py b/plugins/bap/plugins/bap_trace.py new file mode 100644 index 0000000..9234733 --- /dev/null +++ b/plugins/bap/plugins/bap_trace.py @@ -0,0 +1,8 @@ +import idaapi + +from bap.utils import trace + + +@trace.handler('pc-changed', requires=['machine-id', 'pc']) +def tev_insn(state, ev): + idaapi.dbg_add_tev(1, state['machine-id'], state['pc']) diff --git a/plugins/bap/utils/trace.py b/plugins/bap/utils/trace.py index 474c692..3bf5889 100644 --- a/plugins/bap/utils/trace.py +++ b/plugins/bap/utils/trace.py @@ -84,7 +84,7 @@ def next(self): return self.state def run(self): - for _ in next(): + for state in self: pass From 8408adcfadc684808af904aba5c571cda63cfab4 Mon Sep 17 00:00:00 2001 From: Ivan Gotovchits Date: Wed, 9 May 2018 17:11:38 -0400 Subject: [PATCH 04/13] adds rudimentary gui --- plugins/bap/plugins/bap_trace.py | 234 +++++++++++++++++++++++++++++++ plugins/bap/utils/trace.py | 6 +- 2 files changed, 237 insertions(+), 3 deletions(-) diff --git a/plugins/bap/plugins/bap_trace.py b/plugins/bap/plugins/bap_trace.py index 9234733..b8ebd2e 100644 --- a/plugins/bap/plugins/bap_trace.py +++ b/plugins/bap/plugins/bap_trace.py @@ -1,8 +1,242 @@ import idaapi +import os + + from bap.utils import trace +from PyQt5 import QtWidgets +from PyQt5 import QtGui + +from PyQt5.QtCore import ( + QFile, + QIODevice, + QCryptographicHash as QCrypto, + QRegExp, + QTimer, + pyqtSignal) @trace.handler('pc-changed', requires=['machine-id', 'pc']) def tev_insn(state, ev): + "stores each visted instruction to the IDA Trace Window" idaapi.dbg_add_tev(1, state['machine-id'], state['pc']) + + +@trace.handler('call', requires=['machine-id', 'pc']) +def tev_call(state, call): + "stores call events to the IDA Trace Window" + caller = state['pc'] + callee = idaapi.get_name_ea(0, call[0]) + idaapi.dbg_add_call_tev(state['machine-id'], caller, callee) + + +# we are using PyQt5 here, because IDAPython relies on a system +# openssl 0.9.8 which is quite outdated and not available on most +# modern installations +def md5sum(filename): + """computes md5sum of a file with the given ``filename`` + + The return value is a 32 byte hexadecimal ASCII representation of + the md5 sum (same value as returned by the ``md5sum filename`` command) + """ + stream = QFile(filename) + if not stream.open(QIODevice.ReadOnly | QIODevice.Text): + raise IOError("Can't open file: " + filename) + hasher = QCrypto(QCrypto.Md5) + if not hasher.addData(stream): + raise ValueError('Unable to hash file: ' + filename) + stream.close() + return str(hasher.result().toHex()) + + +class HandlerSelector(QtWidgets.QGroupBox): + def __init__(self, parent=None): + super(HandlerSelector, self).__init__("Trace Event Processors", parent) + self.setFlat(True) + box = QtWidgets.QVBoxLayout(self) + self.options = {} + for name in trace.handlers: + btn = QtWidgets.QCheckBox(name) + btn.setToolTip(trace.handlers[name].__doc__) + box.addWidget(btn) + self.options[name] = btn + box.addStretch(1) + self.setLayout(box) + + +class MachineSelector(QtWidgets.QWidget): + def __init__(self, parent=None): + super(MachineSelector, self).__init__(parent) + box = QtWidgets.QHBoxLayout(self) + label = MonitoringLabel('List of &machines (threads)') + self.is_ready = label.is_ready + self.updated = label.updated + box.addWidget(label) + self._machines = QtWidgets.QLineEdit('all') + self._machines.setToolTip('an integer, \ + a comma-separated list of integers, or "all"') + grammar = QRegExp(r'\s*(all|\d+\s*(,\s*\d+\s*)*)\s*') + valid = QtGui.QRegExpValidator(grammar) + self._machines.setValidator(valid) + label.setBuddy(self._machines) + box.addWidget(self._machines) + box.addStretch(1) + self.setLayout(box) + + def selected(self): + if not self._machines.hasAcceptableInput(): + raise ValueError('invalid input') + data = self._machines.text().strip() + if data == 'all': + return None + else: + return [int(x) for x in data.split(',')] + + +class MonitoringLabel(QtWidgets.QLabel): + "a label that will monitors the validity of its buddy" + + updated = pyqtSignal() + + def __init__(self, text='', buddy=None, parent=None): + super(MonitoringLabel, self).__init__(parent) + self.setText(text) + if buddy: + self.setBuddy(buddy) + + def setText(self, text): + super(MonitoringLabel, self).setText(text) + self.text = text + + def setBuddy(self, buddy): + super(MonitoringLabel, self).setBuddy(buddy) + buddy.textChanged.connect(lambda x: self.update()) + self.update() + + def is_ready(self): + return not self.buddy() or self.buddy().hasAcceptableInput() + + def update(self): + self.updated.emit() + if self.is_ready(): + super(MonitoringLabel, self).setText(self.text) + else: + super(MonitoringLabel, self).setText( + ''+self.text+'') + + +class ExistingFileValidator(QtGui.QValidator): + def __init__(self, parent=None): + super(ExistingFileValidator, self).__init__(parent) + + def validate(self, name, pos): + if os.path.isfile(name): + return (self.Acceptable, name, pos) + else: + return (self.Intermediate, name, pos) + + +class TraceFileSelector(QtWidgets.QWidget): + + def __init__(self, parent=None): + super(TraceFileSelector, self).__init__(parent) + box = QtWidgets.QHBoxLayout(self) + label = MonitoringLabel('Trace &file:') + self.is_ready = label.is_ready + self.updated = label.updated + box.addWidget(label) + self.location = QtWidgets.QLineEdit('incidents') + self.text = self.location.text + must_exist = ExistingFileValidator() + self.location.setValidator(must_exist) + label.setBuddy(self.location) + box.addWidget(self.location) + openfile = QtWidgets.QPushButton(self) + openfile.setIcon(self.style().standardIcon( + QtWidgets.QStyle.SP_DialogOpenButton)) + dialog = QtWidgets.QFileDialog(self) + openfile.clicked.connect(dialog.open) + dialog.fileSelected.connect(self.location.setText) + box.addWidget(openfile) + box.addStretch(1) + self.setLayout(box) + + +class TraceLoaderController(object): + def __init__(self, parent): + self.loader = None + box = QtWidgets.QVBoxLayout(parent) + self.location = TraceFileSelector(parent) + self.handlers = HandlerSelector(parent) + self.machines = MachineSelector(parent) + box.addWidget(self.location) + box.addWidget(self.handlers) + box.addWidget(self.machines) + self.load = QtWidgets.QPushButton('&Load') + self.load.setDefault(True) + self.load.setEnabled(self.location.is_ready()) + self.cancel = QtWidgets.QPushButton('&Stop') + self.cancel.setVisible(False) + hor = QtWidgets.QHBoxLayout() + hor.addWidget(self.load) + hor.addWidget(self.cancel) + self.progress = QtWidgets.QProgressBar() + self.progress.setVisible(False) + hor.addWidget(self.progress) + hor.addStretch(2) + box.addLayout(hor) + + def enable_load(): + self.load.setEnabled(self.location.is_ready() and + self.machines.is_ready()) + self.location.updated.connect(enable_load) + self.machines.updated.connect(enable_load) + enable_load() + self.processor = QTimer() + self.processor.timeout.connect(self.process) + self.load.clicked.connect(self.processor.start) + self.cancel.clicked.connect(self.stop) + parent.setLayout(box) + + def start(self): + self.cancel.setVisible(True) + self.load.setVisible(False) + filename = self.location.text() + self.loader = trace.Loader(file(filename)) + self.progress.setVisible(True) + stat = os.stat(filename) + self.progress.setRange(0, stat.st_size) + machines = self.machines.selected() + if machines is not None: + self.loader.enable_filter('filter-machine', id=machines) + + for name in self.handlers.options: + if self.handlers.options[name].isChecked(): + self.loader.enable_handlers([name]) + + def stop(self): + self.processor.stop() + self.progress.setVisible(False) + self.cancel.setVisible(False) + self.load.setVisible(True) + self.loader = None + + def process(self): + if not self.loader: + self.start() + try: + self.loader.next() + self.progress.setValue(self.loader.parser.lexer.instream.tell()) + except StopIteration: + self.stop() + + +class BapTraceMain(idaapi.PluginForm): + def OnCreate(self, form): + parent = self.FormToPyQtWidget(form) + self.control = TraceLoaderController(parent) + + +def bap_trace_test(): + main = BapTraceMain() + main.Show('Primus Observations') diff --git a/plugins/bap/utils/trace.py b/plugins/bap/utils/trace.py index 3bf5889..be20a46 100644 --- a/plugins/bap/utils/trace.py +++ b/plugins/bap/utils/trace.py @@ -60,7 +60,6 @@ def enable_filter(self, filter_name, *args, **kwargs): def next(self): event = self.parser.next() - print('Parser event {}'.format(repr(event))) if len(event) != 2: raise ValueError('Malformed Observation {}'.format(event)) event, payload = event @@ -240,12 +239,13 @@ def mems(state, data): @filter(requires='pc') -def filter_range(state, lo, hi): +def filter_range(state, lo=0, hi=None): """masks events that do not fall into the specified region. interval bounds are included. """ - return lo <= state['pc'] <= hi + pc = state['pc'] + return lo <= pc and (hi is None or pc <= hi) @filter(requires='machine-id') From 969e06f46d44fc9947d8e9202ba2d555b424a003 Mon Sep 17 00:00:00 2001 From: Ivan Gotovchits Date: Fri, 11 May 2018 11:20:04 -0400 Subject: [PATCH 05/13] adds incident view and model --- plugins/bap/plugins/bap_trace.py | 168 +++++++++++++++++++++++++++++-- 1 file changed, 159 insertions(+), 9 deletions(-) diff --git a/plugins/bap/plugins/bap_trace.py b/plugins/bap/plugins/bap_trace.py index b8ebd2e..97e9d0a 100644 --- a/plugins/bap/plugins/bap_trace.py +++ b/plugins/bap/plugins/bap_trace.py @@ -8,11 +8,15 @@ from PyQt5 import QtGui from PyQt5.QtCore import ( + Qt, QFile, QIODevice, QCryptographicHash as QCrypto, QRegExp, QTimer, + QAbstractItemModel, + QModelIndex, + QVariant, pyqtSignal) @@ -30,6 +34,21 @@ def tev_call(state, call): idaapi.dbg_add_call_tev(state['machine-id'], caller, callee) +incidents = [] +locations = {} + + +@trace.handler('incident') +def incident(state, data): + incidents.append(Incident(data[0], [int(x) for x in data[1:]])) + + +@trace.handler('incident-location') +def incident_location(state, data): + id = int(data[0]) + locations[id] = [parse_point(p) for p in data[1]] + + # we are using PyQt5 here, because IDAPython relies on a system # openssl 0.9.8 which is quite outdated and not available on most # modern installations @@ -61,6 +80,8 @@ def __init__(self, parent=None): box.addWidget(btn) self.options[name] = btn box.addStretch(1) + self.setCheckable(True) + self.setChecked(True) self.setLayout(box) @@ -162,13 +183,25 @@ def __init__(self, parent=None): self.setLayout(box) -class TraceLoaderController(object): - def __init__(self, parent): +class IncidentView(QtWidgets.QWidget): + def __init__(self, incidents, locations, parent=None): + super(IncidentView, self).__init__(parent) + self.model = IncidentModel(incidents, locations, parent) + self.view = QtWidgets.QTreeView() + self.view.setModel(self.model) + box = QtWidgets.QVBoxLayout() + box.addWidget(self.view) + self.setLayout(box) + + +class TraceLoaderController(QtWidgets.QWidget): + def __init__(self, parent=None): + super(TraceLoaderController, self).__init__(parent) self.loader = None - box = QtWidgets.QVBoxLayout(parent) - self.location = TraceFileSelector(parent) - self.handlers = HandlerSelector(parent) - self.machines = MachineSelector(parent) + box = QtWidgets.QVBoxLayout(self) + self.location = TraceFileSelector(self) + self.handlers = HandlerSelector(self) + self.machines = MachineSelector(self) box.addWidget(self.location) box.addWidget(self.handlers) box.addWidget(self.machines) @@ -196,7 +229,7 @@ def enable_load(): self.processor.timeout.connect(self.process) self.load.clicked.connect(self.processor.start) self.cancel.clicked.connect(self.stop) - parent.setLayout(box) + self.setLayout(box) def start(self): self.cancel.setVisible(True) @@ -231,12 +264,129 @@ def process(self): self.stop() +def index_level(idx): + if idx.isValid(): + return 1 + index_level(idx.parent()) + else: + return -1 + + +class IncidentModel(QAbstractItemModel): + def __init__(self, incidents, locations, parent=None): + super(IncidentModel, self).__init__(parent) + self.incidents = incidents + self.locations = locations + self.parents = {0: QModelIndex()} + self.child_ids = 0 + + def index(self, row, col, parent): + if parent.isValid(): + self.child_ids += 1 + index = self.createIndex(row, col, self.child_ids) + self.parents[self.child_ids] = parent + return index + else: + return self.createIndex(row, col, 0) + + def parent(self, child): + return self.parents[child.internalId()] + + def rowCount(self, parent): + level = index_level(parent) + if level == -1: + return len(self.incidents) + elif level == 0: + incident = self.incidents[parent.row()] + return len(incident.locations) + elif level == 1: + incident = self.incidents[parent.parent().row()] + location = incident.locations[parent.row()] + return len(self.locations[location]) + else: + return 0 + + def columnCount(self, index): + return 1 + + def data(self, index, role): + level = index_level(index) + + if role == Qt.DisplayRole: + if level == -1: + return QVariant() + elif level == 0: + incident = self.incidents[index.row()] + data = '{}#{}'.format(incident.name, index.row()) + return QVariant(data) + elif level == 1: + return QVariant('location-{}'.format(index.row())) + elif level == 2: + incident = self.incidents[index.parent().parent().row()] + location = incident.locations[index.parent().row()] + trace = self.locations[location] + point = trace[index.row()] + return QVariant('{:x}'.format(point.addr)) + else: + return QVariant() + else: + return QVariant() + + +class Incident(object): + __slots__ = ['name', 'locations'] + + def __init__(self, name, locations): + self.name = name + self.locations = locations + + def __repr__(self): + return 'Incident({}, {})'.format(repr(self.name), + repr(self.locations)) + + +class Point(object): + __slots__ = ['addr', 'machine'] + + def __init__(self, addr, machine=None): + self.addr = addr + self.machine = machine + + def __str__(self): + if self.machine: + return '{}:{}'.format(self.machine, self.addr) + else: + return str(self.addr) + + def __repr__(self): + if self.machine: + return 'Point({},{})'.format(self.machine, self.addr) + else: + return 'Point({})'.format(repr(self.addr)) + + +def parse_point(data): + parts = data.split(':') + if len(parts) == 1: + return Point(int(data, 16)) + else: + return Point(int(parts[1], 16), int(parts[0])) + + class BapTraceMain(idaapi.PluginForm): def OnCreate(self, form): - parent = self.FormToPyQtWidget(form) - self.control = TraceLoaderController(parent) + form = self.FormToPyQtWidget(form) + self.control = TraceLoaderController(form) + self.incidents = IncidentView(incidents, locations, form) + box = QtWidgets.QHBoxLayout() + box.addWidget(self.control) + box.addWidget(self.incidents) + form.setLayout(box) + + +main = None def bap_trace_test(): + global main main = BapTraceMain() main.Show('Primus Observations') From 8fc4e323082e20b3f93315db2bfec3d2aa9774d1 Mon Sep 17 00:00:00 2001 From: Ivan Gotovchits Date: Fri, 11 May 2018 13:01:48 -0400 Subject: [PATCH 06/13] refreshes the view after the trace is loaded --- plugins/bap/plugins/bap_trace.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/plugins/bap/plugins/bap_trace.py b/plugins/bap/plugins/bap_trace.py index 97e9d0a..cecef60 100644 --- a/plugins/bap/plugins/bap_trace.py +++ b/plugins/bap/plugins/bap_trace.py @@ -17,7 +17,8 @@ QAbstractItemModel, QModelIndex, QVariant, - pyqtSignal) + pyqtSignal, + QSortFilterProxyModel) @trace.handler('pc-changed', requires=['machine-id', 'pc']) @@ -186,15 +187,24 @@ def __init__(self, parent=None): class IncidentView(QtWidgets.QWidget): def __init__(self, incidents, locations, parent=None): super(IncidentView, self).__init__(parent) - self.model = IncidentModel(incidents, locations, parent) self.view = QtWidgets.QTreeView() - self.view.setModel(self.model) + self.view.setAllColumnsShowFocus(True) + self.view.setUniformRowHeights(True) box = QtWidgets.QVBoxLayout() box.addWidget(self.view) self.setLayout(box) + def display(self, incidents, locations): + model = IncidentModel(incidents, locations, self) + proxy = QSortFilterProxyModel(self) + proxy.setSourceModel(model) + self.view.setSortingEnabled(True) + self.view.setModel(proxy) + class TraceLoaderController(QtWidgets.QWidget): + finished = pyqtSignal() + def __init__(self, parent=None): super(TraceLoaderController, self).__init__(parent) self.loader = None @@ -253,6 +263,7 @@ def stop(self): self.cancel.setVisible(False) self.load.setVisible(True) self.loader = None + self.finished.emit() def process(self): if not self.loader: @@ -377,6 +388,10 @@ def OnCreate(self, form): form = self.FormToPyQtWidget(form) self.control = TraceLoaderController(form) self.incidents = IncidentView(incidents, locations, form) + + def display(): + self.incidents.display(incidents, locations) + self.control.finished.connect(display) box = QtWidgets.QHBoxLayout() box.addWidget(self.control) box.addWidget(self.incidents) From 9ab1157b5c86f7dc904392c8158ad220a9d95805 Mon Sep 17 00:00:00 2001 From: Ivan Gotovchits Date: Fri, 11 May 2018 15:52:20 -0400 Subject: [PATCH 07/13] adds incident loading to the trace view --- plugins/bap/plugins/bap_trace.py | 62 ++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/plugins/bap/plugins/bap_trace.py b/plugins/bap/plugins/bap_trace.py index cecef60..b0237be 100644 --- a/plugins/bap/plugins/bap_trace.py +++ b/plugins/bap/plugins/bap_trace.py @@ -185,21 +185,48 @@ def __init__(self, parent=None): class IncidentView(QtWidgets.QWidget): - def __init__(self, incidents, locations, parent=None): + def __init__(self, parent=None): super(IncidentView, self).__init__(parent) self.view = QtWidgets.QTreeView() self.view.setAllColumnsShowFocus(True) self.view.setUniformRowHeights(True) box = QtWidgets.QVBoxLayout() box.addWidget(self.view) + self.load_trace = QtWidgets.QPushButton('&Trace') + self.load_trace.setToolTip('Load into the Trace Window') + self.load_trace.setEnabled(False) + self.view.activated.connect(lambda _: self.update_controls_state()) + self.load_trace.clicked.connect(self.load_current_trace) + box.addWidget(self.load_trace) self.setLayout(box) + self.model = None def display(self, incidents, locations): - model = IncidentModel(incidents, locations, self) - proxy = QSortFilterProxyModel(self) - proxy.setSourceModel(model) - self.view.setSortingEnabled(True) - self.view.setModel(proxy) + self.model = IncidentModel(incidents, locations, self) + self.view.setModel(self.model) + + def update_controls_state(self): + curr = self.view.currentIndex() + self.load_trace.setEnabled(curr.isValid() and + curr.parent().isValid()) + + def load_current_trace(self): + idx = self.view.currentIndex() + if not idx.isValid() or index_level(idx) not in (1, 2): + raise ValueError('load_current_trace: invalid index') + + if index_level(idx) == 2: + idx = idx.parent() + + incident = self.model.incidents[idx.parent().row()] + location = incident.locations[idx.row()] + trace = self.model.locations[location] + + for p in trace: + self.load_trace_point(p) + + def load_trace_point(self, p): + idaapi.dbg_add_tev(1, p.machine, p.addr) class TraceLoaderController(QtWidgets.QWidget): @@ -303,6 +330,8 @@ def parent(self, child): return self.parents[child.internalId()] def rowCount(self, parent): + if parent.column() > 0: + return 0 level = index_level(parent) if level == -1: return len(self.incidents) @@ -316,27 +345,32 @@ def rowCount(self, parent): else: return 0 - def columnCount(self, index): - return 1 + def columnCount(self, parent): + return 2 if not parent.isValid() or parent.column() == 0 else 0 def data(self, index, role): level = index_level(index) - if role == Qt.DisplayRole: if level == -1: return QVariant() elif level == 0: incident = self.incidents[index.row()] - data = '{}#{}'.format(incident.name, index.row()) - return QVariant(data) + if index.column() == 0: + return QVariant(incident.name) + else: + return QVariant(str(index.row())) elif level == 1: - return QVariant('location-{}'.format(index.row())) + if index.column() == 0: + return QVariant('location-{}'.format(index.row())) elif level == 2: incident = self.incidents[index.parent().parent().row()] location = incident.locations[index.parent().row()] trace = self.locations[location] point = trace[index.row()] - return QVariant('{:x}'.format(point.addr)) + if index.column() == 0: + return QVariant('{:x}'.format(point.addr)) + else: + return QVariant(int(point.machine)) else: return QVariant() else: @@ -387,7 +421,7 @@ class BapTraceMain(idaapi.PluginForm): def OnCreate(self, form): form = self.FormToPyQtWidget(form) self.control = TraceLoaderController(form) - self.incidents = IncidentView(incidents, locations, form) + self.incidents = IncidentView(form) def display(): self.incidents.display(incidents, locations) From 7613402cd8ef1a0523a9536734e05f6c826d4b34 Mon Sep 17 00:00:00 2001 From: Ivan Gotovchits Date: Mon, 14 May 2018 16:58:00 -0400 Subject: [PATCH 08/13] adds filtering, jumping, and other nifty stuff --- plugins/bap/plugins/bap_trace.py | 80 +++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/plugins/bap/plugins/bap_trace.py b/plugins/bap/plugins/bap_trace.py index b0237be..102c85f 100644 --- a/plugins/bap/plugins/bap_trace.py +++ b/plugins/bap/plugins/bap_trace.py @@ -21,10 +21,24 @@ QSortFilterProxyModel) +def add_insn_to_trace_view(ea, tid=1): + idaapi.dbg_add_tev(1, tid, ea) + + @trace.handler('pc-changed', requires=['machine-id', 'pc']) def tev_insn(state, ev): - "stores each visted instruction to the IDA Trace Window" - idaapi.dbg_add_tev(1, state['machine-id'], state['pc']) + "stores each visited instruction to the IDA Trace Window" + add_insn_to_trace_view(1, state['pc'], tid=state['machine-id']) + + +@trace.handler('pc-changed', requires=['pc']) +def tev_insn0(state, ev): + """stores each visited instruction to the IDA Trace Window. + + But doesn't set the pid/tid field, and keep it equal to 0 + (This enables interoperation with the debugger) + """ + add_insn_to_trace_view(state['pc']) @trace.handler('call', requires=['machine-id', 'pc']) @@ -195,15 +209,34 @@ def __init__(self, parent=None): self.load_trace = QtWidgets.QPushButton('&Trace') self.load_trace.setToolTip('Load into the Trace Window') self.load_trace.setEnabled(False) - self.view.activated.connect(lambda _: self.update_controls_state()) + for activation_signal in [ + self.view.activated, + self.view.entered, + self.view.pressed]: + activation_signal.connect(lambda _: self.update_controls_state()) self.load_trace.clicked.connect(self.load_current_trace) + self.view.doubleClicked.connect(self.jump_to_index) + self.filter = QtWidgets.QLineEdit() + self.filter.textChanged.connect(self.filter_model) + box.addWidget(self.filter) box.addWidget(self.load_trace) self.setLayout(box) self.model = None + self.proxy = None def display(self, incidents, locations): self.model = IncidentModel(incidents, locations, self) - self.view.setModel(self.model) + self.proxy = QSortFilterProxyModel(self) + self.proxy.setSourceModel(self.model) + self.proxy.setFilterRole(self.model.incident_name_role) + self.proxy.setFilterRegExp(QRegExp(self.filter.text())) + self.proxy.setSortRole(self.model.incident_name_role) + self.view.setSortingEnabled(True) + self.view.setModel(self.proxy) + + def filter_model(self, txt): + if self.proxy: + self.proxy.setFilterRegExp(QRegExp(txt)) def update_controls_state(self): curr = self.view.currentIndex() @@ -211,7 +244,7 @@ def update_controls_state(self): curr.parent().isValid()) def load_current_trace(self): - idx = self.view.currentIndex() + idx = self.proxy.mapToSource(self.view.currentIndex()) if not idx.isValid() or index_level(idx) not in (1, 2): raise ValueError('load_current_trace: invalid index') @@ -220,13 +253,28 @@ def load_current_trace(self): incident = self.model.incidents[idx.parent().row()] location = incident.locations[idx.row()] - trace = self.model.locations[location] + backtrace = self.model.locations[location] - for p in trace: + for p in reversed(backtrace): self.load_trace_point(p) + def jump_to_index(self, idx): + idx = self.proxy.mapToSource(idx) + if index_level(idx) != 2: + # don't mess with parents, they are used to create children + return + grandpa = idx.parent().parent() + incident = self.model.incidents[grandpa.row()] + location = incident.locations[idx.parent().row()] + trace = self.model.locations[location] + point = trace[idx.row()] + self.show_trace_point(point) + def load_trace_point(self, p): - idaapi.dbg_add_tev(1, p.machine, p.addr) + add_insn_to_trace_view(p.addr) + + def show_trace_point(self, p): + idaapi.jumpto(p.addr) class TraceLoaderController(QtWidgets.QWidget): @@ -310,6 +358,8 @@ def index_level(idx): class IncidentModel(QAbstractItemModel): + incident_name_role = Qt.UserRole + def __init__(self, incidents, locations, parent=None): super(IncidentModel, self).__init__(parent) self.incidents = incidents @@ -350,7 +400,7 @@ def columnCount(self, parent): def data(self, index, role): level = index_level(index) - if role == Qt.DisplayRole: + if role in (Qt.DisplayRole, self.incident_name_role): if level == -1: return QVariant() elif level == 0: @@ -360,15 +410,23 @@ def data(self, index, role): else: return QVariant(str(index.row())) elif level == 1: - if index.column() == 0: + if role == self.incident_name_role: + incident = self.incidents[index.parent().row()] + return QVariant(incident.name) + elif role == Qt.DisplayRole and index.column() == 0: return QVariant('location-{}'.format(index.row())) + else: + return QVariant() elif level == 2: incident = self.incidents[index.parent().parent().row()] location = incident.locations[index.parent().row()] trace = self.locations[location] point = trace[index.row()] if index.column() == 0: - return QVariant('{:x}'.format(point.addr)) + if role == self.incident_name_role: + return QVariant(incident.name) + else: + return QVariant('{:x}'.format(point.addr)) else: return QVariant(int(point.machine)) else: From 7a6d7c5e8dc2edc20c40e3683a1bf8ae6f855faf Mon Sep 17 00:00:00 2001 From: Ivan Gotovchits Date: Tue, 15 May 2018 16:27:36 -0400 Subject: [PATCH 09/13] refactors methods into clos style methods --- plugins/bap/plugins/bap_trace.py | 195 ++++++++++++++++++++++--------- 1 file changed, 143 insertions(+), 52 deletions(-) diff --git a/plugins/bap/plugins/bap_trace.py b/plugins/bap/plugins/bap_trace.py index 102c85f..f5ef2ce 100644 --- a/plugins/bap/plugins/bap_trace.py +++ b/plugins/bap/plugins/bap_trace.py @@ -1,3 +1,5 @@ +# noqa: ignore=F811 + import idaapi import os @@ -216,10 +218,15 @@ def __init__(self, parent=None): activation_signal.connect(lambda _: self.update_controls_state()) self.load_trace.clicked.connect(self.load_current_trace) self.view.doubleClicked.connect(self.jump_to_index) + hbox = QtWidgets.QHBoxLayout() self.filter = QtWidgets.QLineEdit() self.filter.textChanged.connect(self.filter_model) - box.addWidget(self.filter) - box.addWidget(self.load_trace) + filter_label = QtWidgets.QLabel('&Search') + filter_label.setBuddy(self.filter) + hbox.addWidget(filter_label) + hbox.addWidget(self.filter) + hbox.addWidget(self.load_trace) + box.addLayout(hbox) self.setLayout(box) self.model = None self.proxy = None @@ -228,10 +235,8 @@ def display(self, incidents, locations): self.model = IncidentModel(incidents, locations, self) self.proxy = QSortFilterProxyModel(self) self.proxy.setSourceModel(self.model) - self.proxy.setFilterRole(self.model.incident_name_role) + self.proxy.setFilterRole(self.model.filter_role) self.proxy.setFilterRegExp(QRegExp(self.filter.text())) - self.proxy.setSortRole(self.model.incident_name_role) - self.view.setSortingEnabled(True) self.view.setModel(self.proxy) def filter_model(self, txt): @@ -357,8 +362,59 @@ def index_level(idx): return -1 +def index_up(idx, level=0): + if level == 0: + return idx + else: + return index_up(idx.parent(), level=level-1) + + +class IncidentIndex(object): + def __init__(self, model, index): + self.model = model + self.index = index + + @property + def incidents(self): + return self.model.incidents + + @property + def level(self): + return index_level(self.index) + + @property + def column(self): + return self.index.column() + + @property + def row(self): + return self.index.row() + + @property + def incident(self): + top = index_up(self.index, self.level) + return self.incidents[top.row()] + + @property + def location(self): + if self.level in (1, 2): + top = self.index + if self.level == 2: + top = index_up(self.index, 1) + location_id = self.incident.locations[top.row()] + return self.model.locations[location_id] + + @property + def point(self): + if self.level == 2: + return self.location[self.index.row()] + + class IncidentModel(QAbstractItemModel): - incident_name_role = Qt.UserRole + filter_role = Qt.UserRole + sort_role = Qt.UserRole + 1 + + handlers = [] def __init__(self, incidents, locations, parent=None): super(IncidentModel, self).__init__(parent) @@ -367,6 +423,22 @@ def __init__(self, incidents, locations, parent=None): self.parents = {0: QModelIndex()} self.child_ids = 0 + def dispatch(self, role, index): + for handler in self.handlers: + def sat(c, v): + if c == 'roles': + return role in v + if c == 'level': + return index.level == v + if c == 'column': + return index.column == v + + for (c, v) in handler['constraints'].items(): + if not sat(c, v): + break + else: + return handler['accept'](index) + def index(self, row, col, parent): if parent.isValid(): self.child_ids += 1 @@ -380,61 +452,80 @@ def parent(self, child): return self.parents[child.internalId()] def rowCount(self, parent): - if parent.column() > 0: - return 0 - level = index_level(parent) - if level == -1: - return len(self.incidents) - elif level == 0: - incident = self.incidents[parent.row()] - return len(incident.locations) - elif level == 1: - incident = self.incidents[parent.parent().row()] - location = incident.locations[parent.row()] - return len(self.locations[location]) - else: - return 0 + n = self.dispatch('row-count', IncidentIndex(self, parent)) + return 0 if n is None else n def columnCount(self, parent): return 2 if not parent.isValid() or parent.column() == 0 else 0 def data(self, index, role): - level = index_level(index) - if role in (Qt.DisplayRole, self.incident_name_role): - if level == -1: - return QVariant() - elif level == 0: - incident = self.incidents[index.row()] - if index.column() == 0: - return QVariant(incident.name) - else: - return QVariant(str(index.row())) - elif level == 1: - if role == self.incident_name_role: - incident = self.incidents[index.parent().row()] - return QVariant(incident.name) - elif role == Qt.DisplayRole and index.column() == 0: - return QVariant('location-{}'.format(index.row())) - else: - return QVariant() - elif level == 2: - incident = self.incidents[index.parent().parent().row()] - location = incident.locations[index.parent().row()] - trace = self.locations[location] - point = trace[index.row()] - if index.column() == 0: - if role == self.incident_name_role: - return QVariant(incident.name) - else: - return QVariant('{:x}'.format(point.addr)) - else: - return QVariant(int(point.machine)) - else: - return QVariant() + role = { + Qt.DisplayRole: 'display', + self.sort_role: 'sort', + self.filter_role: 'filter' + }.get(role) + + if role: + return QVariant(self.dispatch(role, IncidentIndex(self, index))) else: return QVariant() +def defmethod(*args, **kwargs): + def register(method): + kwargs['roles'] = args + IncidentModel.handlers.append({ + 'name': method.__name__, + 'constraints': kwargs, + 'accept': method}) + return register + + +@defmethod('display', level=2, column=0) +def display_point(msg): + return '{:x}'.format(msg.point.addr) + + +@defmethod('display', level=2, column=1) +def display_point_machine(msg): + return msg.point.machine + + +@defmethod('display', level=1, column=0) +def display_incident_location(msg): + return 'location-{}'.format(msg.row) + + +@defmethod('display', level=0, column=0) +def display_incident_name(msg): + return msg.incident.name + + +@defmethod('display', level=0, column=1) +def display_incident_id(msg): + return msg.row + + +@defmethod('sort', 'filter', column=0) +def incident_name(msg): + return msg.incident.name + + +@defmethod('row-count', level=-1) +def number_of_incidents(msg): + return len(msg.incidents) + + +@defmethod('row-count', level=0, column=0) +def number_of_locations(msg): + return len(msg.incident.locations) + + +@defmethod('row-count', level=1, column=0) +def backtrace_length(msg): + return len(msg.location) + + class Incident(object): __slots__ = ['name', 'locations'] From c421a06c1a20e11c559fbbeb086f700b26501aa4 Mon Sep 17 00:00:00 2001 From: Ivan Gotovchits Date: Tue, 15 May 2018 16:53:55 -0400 Subject: [PATCH 10/13] packs everything into a proper plugin --- plugins/bap/plugins/bap_trace.py | 33 ++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/plugins/bap/plugins/bap_trace.py b/plugins/bap/plugins/bap_trace.py index f5ef2ce..25a5ec9 100644 --- a/plugins/bap/plugins/bap_trace.py +++ b/plugins/bap/plugins/bap_trace.py @@ -576,11 +576,40 @@ def display(): self.incidents.display(incidents, locations) self.control.finished.connect(display) box = QtWidgets.QHBoxLayout() - box.addWidget(self.control) - box.addWidget(self.incidents) + split = QtWidgets.QSplitter() + split.addWidget(self.control) + split.addWidget(self.incidents) + box.addWidget(split) form.setLayout(box) +class BapTracePlugin(idaapi.plugin_t): + wanted_name = 'BAP: Load Observations' + wanted_hotkey = '' + flags = 0 + comment = 'Load Primus Observations' + help = """ + Loads Primus Observations into IDA for further analysis + """ + + def __init__(self): + self.form = None + + def init(self): + return idaapi.PLUGIN_KEEP + + def term(self): + pass + + def run(self, arg): + self.form = BapTraceMain() + self.form.Show('Primus Observations') + + +def PLUGIN_ENTRY(): + return BapTracePlugin() + + main = None From 890bbdd1dfd5f81f7446f074c4b16194842cbcdc Mon Sep 17 00:00:00 2001 From: Ivan Gotovchits Date: Wed, 16 May 2018 11:26:01 -0400 Subject: [PATCH 11/13] makes the form persistent still kind of ugly with the debugger, but at least usable now --- plugins/bap/plugins/bap_taint.py | 1 + plugins/bap/plugins/bap_trace.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/bap/plugins/bap_taint.py b/plugins/bap/plugins/bap_taint.py index a2be4b0..08e75ce 100644 --- a/plugins/bap/plugins/bap_taint.py +++ b/plugins/bap/plugins/bap_taint.py @@ -140,6 +140,7 @@ def install_callback(cls, callback_fn, ptr_or_reg=None): idc.Fatal("Invalid ptr_or_reg value passed {}". format(repr(ptr_or_reg))) + class BapTaintStub(DoNothing): pass diff --git a/plugins/bap/plugins/bap_trace.py b/plugins/bap/plugins/bap_trace.py index 25a5ec9..401d766 100644 --- a/plugins/bap/plugins/bap_trace.py +++ b/plugins/bap/plugins/bap_trace.py @@ -594,6 +594,7 @@ class BapTracePlugin(idaapi.plugin_t): def __init__(self): self.form = None + self.name = 'Primus Observations' def init(self): return idaapi.PLUGIN_KEEP @@ -602,8 +603,11 @@ def term(self): pass def run(self, arg): - self.form = BapTraceMain() - self.form.Show('Primus Observations') + if not self.form: + self.form = BapTraceMain() + return self.form.Show(self.name, options=( + self.form.FORM_PERSIST | + self.form.FORM_SAVE)) def PLUGIN_ENTRY(): From cf8d33a3dcbeaadf2fe989457b41e04064b215ac Mon Sep 17 00:00:00 2001 From: Ivan Gotovchits Date: Thu, 13 Sep 2018 10:10:34 -0400 Subject: [PATCH 12/13] fixes tev-insn even handler it was passing more parameters than the receiver was expecting --- plugins/bap/plugins/bap_trace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/bap/plugins/bap_trace.py b/plugins/bap/plugins/bap_trace.py index 401d766..975d749 100644 --- a/plugins/bap/plugins/bap_trace.py +++ b/plugins/bap/plugins/bap_trace.py @@ -30,7 +30,7 @@ def add_insn_to_trace_view(ea, tid=1): @trace.handler('pc-changed', requires=['machine-id', 'pc']) def tev_insn(state, ev): "stores each visited instruction to the IDA Trace Window" - add_insn_to_trace_view(1, state['pc'], tid=state['machine-id']) + add_insn_to_trace_view(state['pc'], tid=state['machine-id']) @trace.handler('pc-changed', requires=['pc']) From 087129b5e4c10d94b306d0832cc821ebf3f2e62e Mon Sep 17 00:00:00 2001 From: Ivan Gotovchits Date: Thu, 13 Sep 2018 10:53:41 -0400 Subject: [PATCH 13/13] kills an artifact of a merge conflict this code shall not be here --- plugins/bap/utils/bap_taint.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/plugins/bap/utils/bap_taint.py b/plugins/bap/utils/bap_taint.py index 85651f0..0187422 100644 --- a/plugins/bap/utils/bap_taint.py +++ b/plugins/bap/utils/bap_taint.py @@ -166,11 +166,3 @@ def install_callback(cls, callback_fn, ptr_or_reg=None): else: idc.Fatal("Invalid ptr_or_reg value passed {}". format(repr(ptr_or_reg))) - - -class BapTaintStub(DoNothing): - pass - - -def PLUGIN_ENTRY(): - return BapTaintStub()