def PopulateModel(self, trace): self.CleanModel() w = NotifyProgress() w.show() ctr = 0 max = len(trace) for line in trace: assert isinstance(line, Traceline) tid = QtGui.QStandardItem('%s' % line.thread_id) addr = QtGui.QStandardItem('%x' % line.addr) disasm = QtGui.QStandardItem(line.disasm_str()) comment = QtGui.QStandardItem(''.join(c for c in line.comment if line.comment is not None)) context = QtGui.QStandardItem(''.join('%s:%s ' % (c, line.ctx[c]) for c in line.ctx.keys() if line.ctx is not None)) ctr += 1 w.pbar_set(int(float(ctr) / float(max) * 100)) self.sim.appendRow([tid, addr, disasm, comment, context]) w.close() self.treeView.resizeColumnToContents(0) self.treeView.resizeColumnToContents(1) self.treeView.resizeColumnToContents(2) self.treeView.resizeColumnToContents(3) self.treeView.resizeColumnToContents(4)
def PopulateForm(self): ### init widgets # model self.sim = QtGui.QStandardItemModel() # tree view self.treeView = QtGui.QTreeView() self.treeView.setExpandsOnDoubleClick(True) self.treeView.setSortingEnabled(False) self.treeView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) self.treeView.setToolTip( 'Filter instructions/clusters/basic blocks from trace by double clicking on them.' ) # Context menus self.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.treeView.customContextMenuRequested.connect( self.OnCustomContextMenu) self.treeView.doubleClicked.connect(self.ItemDoubleClickSlot) self.treeView.setModel(self.sim) ### populate widgets # fill model with data self.PopulateModel() # self.treeView.setFirstColumnSpanned(0, self.treeView.rootIndex(), True) # finalize layout layout = QtGui.QGridLayout() layout.addWidget(self.treeView) self.parent.setLayout(layout)
def PopulateForm(self): ### init widgets # model self.sim = QtGui.QStandardItemModel() self.sim.setHorizontalHeaderLabels( ['ThreadId', 'Address', 'Disasm', 'Stack Comment', 'CPU Context']) # toolbar self.utb = QtGui.QToolBar() self.ltb = QtGui.QToolBar() # tree view self.treeView = QtGui.QTreeView() self.treeView.setExpandsOnDoubleClick(True) self.treeView.setSortingEnabled(False) self.treeView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) self.treeView.setToolTip( 'Highlights:\n Rust red - Input\n Violet - Output\n Olive - Both') ### populate widgets # fill model with data self.PopulateModel() # fill toolbar with data self.PopulateUpperToolbar() self.PopulateLowerToolbar() self.treeView.setModel(self.sim) # finalize layout layout = QtGui.QGridLayout() layout.addWidget(self.utb) layout.addWidget(self.treeView) layout.addWidget(self.ltb) self.parent.setLayout(layout)
def PopulateForm(self): ### init widgets # model self.sim = QtGui.QStandardItemModel() self.sim.setHorizontalHeaderLabels( ['ThreadId', 'Address', 'Disasm', 'Stack Comment', 'CPU Context']) # toolbar self.ftb = QtGui.QToolBar() self.stb = QtGui.QToolBar() # tree view self.treeView = QtGui.QTreeView() self.treeView.setToolTip('Double click a grade to filter') self.treeView.setExpandsOnDoubleClick(True) self.treeView.setSortingEnabled(False) self.treeView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) # Context menus self.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.treeView.customContextMenuRequested.connect( self.OnCustomContextMenu) self.treeView.doubleClicked.connect(self.ItemDoubleClickSlot) self.treeView.setModel(self.sim) ### populate widgets # fill model with data self.PopulateModel(0) # finalize layout layout = QtGui.QGridLayout() layout.addWidget(self.treeView) self.parent.setLayout(layout)
def OnCustomContextMenu(self, point): menu = QtGui.QMenu() # Actions action_set_t = QtGui.QAction('Set grade threshold...', self.treeView, triggered=lambda: self.SetThreshold()) action_restore = QtGui.QAction('Show All', self.treeView, triggered=lambda: self.Restore()) action_export_trace = QtGui.QAction('Export this trace...', self.treeView, triggered=lambda: self.SaveTrace()) action_close_viewer = QtGui.QAction('Close Viewer', self.treeView, triggered=lambda: self.Close(4)) # add actions to menu menu.addAction(action_set_t) menu.addAction(action_restore) menu.addAction(action_export_trace) menu.addSeparator() menu.addAction(action_close_viewer) menu.exec_(self.treeView.viewport().mapToGlobal(point))
def PopulateForm(self): ### init widgets # model self.sim = QtGui.QStandardItemModel() self.sim.setHorizontalHeaderLabels([ 'Stack Address', 'Address Mapped to CPU Reg', 'Value Changes during Execution' ]) # tree view self.treeView = QtGui.QTreeView() self.treeView.setExpandsOnDoubleClick(True) self.treeView.setSortingEnabled(False) self.treeView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) ### populate widgets # fill model with data self.PopulateModel() self.treeView.setModel(self.sim) # finalize layout layout = QtGui.QGridLayout() layout.addWidget(self.treeView) self.parent.setLayout(layout)
def PopulateUpperToolbar(self): assert isinstance(self.input, set) self.utb.addWidget( QtGui.QLabel('Input values found (check to highlight in trace): ')) for value in self.input: self.ucb_map.append(QtGui.QCheckBox(value)) self.ucb_map[-1].stateChanged.connect( lambda: self.OnValueChecked()) self.utb.addWidget(self.ucb_map[-1]) self.utb.addSeparator()
def PopulateModel(self): for key in self.sorted: sa = QtGui.QStandardItem('%s' % key) chg = QtGui.QStandardItem('%s' % self.stack_changes[key]) if key in self.vr.values(): reg = QtGui.QStandardItem( '%s' % [k for k in self.vr.keys() if self.vr[k] == key][0]) else: reg = QtGui.QStandardItem(' ') self.sim.appendRow([sa, reg, chg]) self.treeView.resizeColumnToContents(0) self.treeView.resizeColumnToContents(1) self.treeView.resizeColumnToContents(2)
def save(trace): """ 保存到trace文件 """ try: fd = QtGui.QFileDialog() fd.setFileMode(QtGui.QFileDialog.AnyFile) fd.setFilter('JSON Files (*.json)') fd.setWindowTitle('Save Trace ...') if fd.exec_(): path = fd.selectedFiles()[0] else: path = None except: path = os.getcwd() + get_root_filename( ) + '_trace_%s.json' % time.time() if path is not None: if path.endswith('.json'): path = path[:-5] with open(path + '.json', 'w') as f: if trace: obj = { i: [ '%x' % trace[i].thread_id, '%x' % trace[i].addr, trace[i].disasm, trace[i].ctx, trace[i].comment, trace[i].grade ] for i in range(len(trace)) } f.write(json.dumps(obj)) msg('[*] Trace saved!\n') else: raise Exception("[*] Trace seems to be None:\n %s" % trace)
def PopulateSelectiveRegsToolbar(self): self.stb.addWidget(QtGui.QLabel('Selective Register Folding: ')) assert isinstance(self.trace, Trace) if self.trace.ctx_reg_size == 32: for i in range(8): self.foldable_regs.append( QtGui.QCheckBox(get_reg_by_size(i, self.trace.ctx_reg_size))) self.foldable_regs[-1].stateChanged.connect( lambda: self.FoldRegs()) self.stb.addWidget(self.foldable_regs[-1]) self.stb.addSeparator() elif self.trace.ctx_reg_size == 64: for i in range(16): self.foldable_regs.append( QtGui.QCheckBox(get_reg_by_size(i, self.trace.ctx_reg_size))) self.foldable_regs[-1].stateChanged.connect( lambda: self.FoldRegs()) self.stb.addWidget(self.foldable_regs[-1]) self.stb.addSeparator()
def PopulateOptimizationsToolbar(self): self.ftb.addWidget( QtGui.QLabel('Available Optimizations (check to run on trace): ')) self.cpcb = QtGui.QCheckBox(optimization_names[0]) self.cpcb.stateChanged.connect(lambda: self.OptimizeTrace(self.cpcb)) self.ftb.addWidget(self.cpcb) self.ftb.addSeparator() self.sacb = QtGui.QCheckBox(optimization_names[1]) self.sacb.stateChanged.connect(lambda: self.OptimizeTrace(self.sacb)) self.ftb.addWidget(self.sacb) self.ftb.addSeparator() self.oscb = QtGui.QCheckBox(optimization_names[2]) self.oscb.stateChanged.connect(lambda: self.OptimizeTrace(self.oscb)) self.ftb.addWidget(self.oscb) self.ftb.addSeparator() self.uocb = QtGui.QCheckBox(optimization_names[3]) self.uocb.stateChanged.connect(lambda: self.OptimizeTrace(self.uocb)) self.ftb.addWidget(self.uocb) self.ftb.addSeparator() self.pcb = QtGui.QCheckBox(optimization_names[4]) self.pcb.stateChanged.connect(lambda: self.OptimizeTrace(self.pcb)) self.ftb.addWidget(self.pcb) self.ftb.addSeparator()
def OnCustomContextMenu(self, point): menu = QtGui.QMenu() # Actions action_undo = QtGui.QAction('Undo', self.treeView, triggered=lambda: self.Undo()) action_restore = QtGui.QAction('Restore original trace', self.treeView, triggered=lambda: self.Restore()) action_forward_to_clustering = QtGui.QAction( "Open in Clustering Analysis", self.treeView, triggered=lambda: self.ClusterForward()) action_export_trace = QtGui.QAction('Export this trace...', self.treeView, triggered=lambda: self.SaveTrace()) action_close_viewer = QtGui.QAction('Close Viewer', self.treeView, triggered=lambda: self.Close(4)) # add actions to menu menu.addAction(action_undo) menu.addAction(action_restore) menu.addAction(action_forward_to_clustering) menu.addAction(action_export_trace) menu.addSeparator() menu.addAction(action_close_viewer) menu.exec_(self.treeView.viewport().mapToGlobal(point))
def OnCustomContextMenu(self, point): menu = QtGui.QMenu() init_index = self.treeView.indexAt(point) index = self.treeView.indexAt(point) level = 0 while index.parent().isValid(): index = index.parent() level += 1 text = 'Remove Line' if level == 0: text = "Remove Cluster / Line" elif level == 1 and get_vmr().bb: text = "Remove Basic Block" elif level == 2: text = "Remove Line" try: action_remove = QtGui.QAction( text, self.treeView, triggered=lambda: self.ItemDoubleClickSlot(init_index)) menu.addAction(action_remove) menu.addSeparator() except: print '[*] An Exception occured, remove action could not be added to the menu!' # Actions action_remove_threshold = QtGui.QAction( 'Remove several clusters...', self.treeView, triggered=lambda: self.ClusterRemoval()) action_undo = QtGui.QAction('Undo', self.treeView, triggered=lambda: self.Undo()) action_restore = QtGui.QAction('Restore original trace', self.treeView, triggered=lambda: self.Restore()) action_export_trace = QtGui.QAction('Export this trace ...', self.treeView, triggered=lambda: self.SaveTrace()) action_close_viewer = QtGui.QAction('Close Viewer', self.treeView, triggered=lambda: self.Close(4)) # add actions to menu menu.addAction(action_remove_threshold) menu.addAction(action_undo) menu.addAction(action_restore) menu.addAction(action_export_trace) menu.addSeparator() menu.addAction(action_close_viewer) menu.exec_(self.treeView.viewport().mapToGlobal(point))
def PopulateModel(self): self.CleanModel() assert isinstance(self.ctx, dict) for key in self.ctx.keys(): if get_reg_class(key) is not None: node = QtGui.QStandardItem('Register %s' % key) node_brush = set() for line in self.ctx[key]: assert isinstance(line, Traceline) tid = QtGui.QStandardItem('%s' % line.thread_id) addr = QtGui.QStandardItem('%x' % line.addr) disasm = QtGui.QStandardItem(line.disasm_str()) comment = QtGui.QStandardItem(''.join( c for c in line.comment if line.comment is not None)) context = QtGui.QStandardItem(''.join( '%s:%s ' % (c, line.ctx[c]) for c in line.ctx.keys() if line.ctx is not None)) ci = 0 co = 0 for selector in self.selection[ 'upper']: # check input values if line.to_str_line().__contains__( selector) or line.to_str_line().__contains__( selector.lower()): ci = 1 for selector in self.selection[ 'lower']: # check output values if line.to_str_line().__contains__( selector) or line.to_str_line().__contains__( selector.lower()): co = 2 node_brush.add(ci + co) tid.setBackground(self.brush_map[ci + co]) addr.setBackground(self.brush_map[ci + co]) disasm.setBackground(self.brush_map[ci + co]) comment.setBackground(self.brush_map[ci + co]) context.setBackground(self.brush_map[ci + co]) node.appendRow([tid, addr, disasm, comment, context]) try: if len(node_brush) == 3: color = 3 else: color = max(node_brush) node.setBackground(self.brush_map[color]) except: pass self.sim.appendRow(node) self.treeView.resizeColumnToContents(0) self.treeView.resizeColumnToContents(1) self.treeView.resizeColumnToContents(2) self.treeView.resizeColumnToContents(3) self.treeView.resizeColumnToContents(4)
def __init__(self, input_set, output_set, output_ctx, title='Input/Output Analysis (legacy)'): # context should be a dictionary containing the backward traced result of each relevant register super(VMInputOuputViewer, self).__init__(title) self.input = input_set self.output = output_set self.ctx = output_ctx self.selection = {'upper': [], 'lower': []} self.ucb_map = [] self.lcb_map = [] # brush map self.brush_map = { 0: QtGui.QBrush(QtCore.Qt.white), # unselected values 1: QtGui.QBrush(QtGui.QColor(228, 153, 105)), # input values color 2: QtGui.QBrush(QtGui.QColor(183, 166, 173)), # output values color 3: QtGui.QBrush(QtGui.QColor(157, 151, 84)) } # BOTH values, mix of both colors
def load(): """ 从文件加载trace Load a trace from file. Supported are IDAs txt trace files and VMAttacks json files. Further OllyDBG and ImmunityDBG traces are supported but have slightly limited analysis capabilities. :param path: system path to trace file :return: trace object """ path = '' try: fd = QtGui.QFileDialog() fd.setFileMode(QtGui.QFileDialog.AnyFile) fd.setFilters(["Text files (*.txt)", "JSON files (*.json)"]) fd.setWindowTitle('Load Trace ...') if fd.exec_(): path = fd.selectedFiles()[0] else: path = None except: msg('A Problem occured with the file selector dialog, first *.txt file in the current working directory was choosen!' ) for f in os.listdir(os.getcwd()): if f.endswith('txt'): path = f if path == '': path = asktext(40, '', 'Please provide the full path to the trace file: ') if path is not None: get_log().log('[TRC] Loaded the trace at %s\n' % path) if path.endswith('.txt'): with open(path, 'r') as f: lines = f.readlines() elif path.endswith('.json'): with open(path) as f: lines = json.load(f) else: return None trace = Trace() functions = { SegName(addr): { GetFunctionName(ea): ea for ea in Functions(SegStart(addr), SegEnd(addr)) } for addr in Segments() } try: context = defaultdict(lambda: False) # framework json trace if isinstance(lines, dict) or path.endswith('.json'): get_log().log('[TRC] The trace seems to be a VMAttack trace\n') for index in range(len(lines.keys())): line = lines[str(index)] t = Traceline(thread_id=line[0], addr=line[1], disasm=line[2], ctx=line[3], comment=line[4]) t.grade = line[5] trace.append(t) # ida trace via Win32Dbg elif lines[0].startswith('Thread '): for i in lines[3:]: if i.startswith('Thread'): break values = i.split('\t') # thread id thread_id = int(values[0], 16) # addr addr = BADADDR func_name = values[1].strip(' ').split(':') if len(func_name) == 2: try: # .segment:addr addr = int(func_name[1], 16) except: try: # .segment:func_name+offset offset = int(func_name[1].split('+')[1], 16) name = func_name[1].split('+')[0] addr = functions[func_name[0]][name] + offset except: try: # .segment:func_name-offset offset = int( i.split('-')[1].split(' ')[0], 16) name = func_name[1].split('-')[0] addr = functions[ func_name[0]][name] - offset except: if not func_name[1].startswith( 'loc_'): # .segment:func_name addr = functions[func_name[0]][ func_name[1]] else: # .segment:jmp_location addr = int(func_name[1][4:], 16) elif len(func_name) == 3: addr = int(func_name[2][4:], 16) # disasm disasm = values[2].strip(' ').lower() disasm = disasm.split(' ') disasm = [x.lstrip() for x in disasm] disasm = filter(None, disasm) if len(disasm) > 1 and disasm[1].__contains__(', '): temp = disasm.pop(1) for elem in temp.split(', '): disasm.append( elem.lstrip().lstrip('0').rstrip('h')) # remove [ebp+0] for dis in disasm: if dis.__contains__('[ebp+0]'): dis.replace('[ebp+0]', '[ebp]') # context ida_ctx = values[3].strip(' ').split(' ') for value in ida_ctx: try: a, b = value.split('=') if len(b) > 1: b = ''.join( c.rstrip('\r\n') for c in b.lstrip('0')) if b == '': b = '0' context[a.lower()] = b except: pass trace.append( Traceline(thread_id=thread_id, addr=addr, disasm=disasm, ctx=deepcopy(context))) # immunity trace elif lines[0].startswith('Address '): for i in lines[1:]: if i.__contains__('Run trace closed') or i.__contains__( 'Process terminated'): break values = i.split('\t') try: # thread_id thread_id = sum( ord(c) for c in values[1]) # immunity uses names, e.g. main # addr try: addr = int(values[0], 16) except: addr = BADADDR # disasm disasm = values[2].lower().rstrip('\r\n') disasm = disasm.split(' ', 1) if len(disasm) > 1 and disasm[1].__contains__(','): temp = disasm.pop(1) for elem in temp.split(','): disasm.append(elem.lstrip('0')) disasm = [ x.split('dword ptr ')[1] if x.__contains__('dword ptr ') else x for x in disasm ] if len(disasm) == 2 and len( re.findall(r'.*\[.*[\+\-\*].*[\+\-\*].*\].*', disasm[1])) > 0: disasm[1] = ida_offset(disasm[1]) # context if len(values) > 3: olly_ctx = values[3].lstrip(' ').rstrip( '\r\n').split(',') for value in olly_ctx: try: a, b = value.split('=') if len(b) > 1: b = ''.join(c for c in b.lstrip('0') if c not in '\n\r\t') if b == '': b = '0' context[a.lower()] = b except: pass trace.append( Traceline(thread_id=thread_id, addr=addr, disasm=disasm, ctx=deepcopy(context))) except: if i.__contains__('terminated') or i.__contains__( 'entry point'): pass # olly trace elif lines[1].startswith('main '): for i in lines[1:]: if i.__contains__('Logging stopped'): break values = i.split('\t') # thread_id thread_id = sum( ord(c) for c in values[0]) # olly uses names, e.g. main # addr try: addr = int(values[1], 16) except: addr = BADADDR # disasm disasm = values[2].lower().rstrip('\r\n') disasm = disasm.split(' ', 1) if len(disasm) > 1 and disasm[1].__contains__(','): temp = disasm.pop(1) for elem in temp.split(','): disasm.append(elem.lstrip('0')) disasm = [ x.split('dword ptr ')[1] if x.__contains__('dword ptr ') else x for x in disasm ] if len(disasm) == 2 and len( re.findall(r'.*\[.*[\+\-\*].*[\+\-\*].*\].*', disasm[1])) > 0: disasm[1] = ida_offset(disasm[1]) # context if len(values) > 3: olly_ctx = values[3].lstrip(' ').rstrip('\r\n').split( ',') for value in olly_ctx: try: a, b = value.split('=') if len(b) > 1: b = ''.join(c for c in b.lstrip('0') if c not in '\n\r\t') if b == '': b = '0' context[a.lower()] = b except: pass trace.append( Traceline(thread_id=thread_id, addr=addr, disasm=disasm, ctx=deepcopy(context))) if 'rax' in trace[-1].ctx.keys(): trace.ctx_reg_size = 64 elif 'eax' in trace[-1].ctx.keys( ) and 'rax' not in trace[-1].ctx.keys(): trace.ctx_reg_size = 32 msg("[*] Trace Loaded!\n") return trace except Exception, e: raise Exception('[*] Exception occured: \n%s\n' % (e.message))
def PopulateModel(self, threshold): self.CleanModel() w = NotifyProgress() ctr = 0 max = len(self.trace) prev = None for line in self.trace: assert isinstance(line, Traceline) if line.grade >= threshold: if prev is not None and threshold > 2: grade = QtGui.QStandardItem(' ') tid = QtGui.QStandardItem(' ') addr = QtGui.QStandardItem(' ') disasm = QtGui.QStandardItem('previous CPU context:') comment = QtGui.QStandardItem(' ') context = QtGui.QStandardItem(''.join( '%s:%s ' % (c, prev.ctx[c]) for c in prev.ctx.keys() if prev.ctx is not None)) self.sim.appendRow( [grade, tid, addr, disasm, comment, context]) grade = QtGui.QStandardItem('%s' % line.grade) tid = QtGui.QStandardItem('%s' % line.thread_id) addr = QtGui.QStandardItem('%x' % line.addr) disasm = QtGui.QStandardItem(line.disasm_str()) comment = QtGui.QStandardItem(''.join( c for c in line.comment if line.comment is not None)) context = QtGui.QStandardItem(''.join( '%s:%s ' % (c, line.ctx[c]) for c in line.ctx.keys() if line.ctx is not None)) self.sim.appendRow( [grade, tid, addr, disasm, comment, context]) ctr += 1 w.pbar_set(int(float(ctr) / float(max) * 100)) prev = line w.close() self.treeView.resizeColumnToContents(0) self.treeView.resizeColumnToContents(1) self.treeView.resizeColumnToContents(2) self.treeView.resizeColumnToContents(3) self.treeView.resizeColumnToContents(4) self.treeView.resizeColumnToContents(5)
def PopulateModel(self): self.Clean() vmr = get_vmr() w = NotifyProgress() w.show() ctr = 0 max = len(self.trace) # present clustering analysis in viewer prev_ctx = defaultdict(lambda: 0) for line in self.trace: ctr += 1 w.pbar_set(int(float(ctr) / float(max) * 100)) if isinstance(line, Traceline): tid = QtGui.QStandardItem('%s' % line.thread_id) addr = QtGui.QStandardItem('%x' % line.addr) disasm = QtGui.QStandardItem(line.disasm_str()) comment = QtGui.QStandardItem(''.join( c for c in line.comment if line.comment is not None)) context = QtGui.QStandardItem(''.join( '%s:%s ' % (c, line.ctx[c]) for c in line.ctx if line.ctx is not None)) prev_ctx = line.ctx self.sim.appendRow([tid, addr, disasm, comment, context]) else: cluster_node = QtGui.QStandardItem( 'Cluster %x-%x' % (line[0].addr, line[-1].addr)) self.sim.appendRow(cluster_node) if vmr.bb: cluster = line bbs = [] bb = [] # subdivide the clusters by basic blocks for line in cluster: assert isinstance(line, Traceline) if is_basic_block_end(line.addr): bb.append(line) bbs.append(bb) bb = [] else: bb.append(line) for bb in bbs: bb_sum = self.bb_func(bb, self.ctx_reg_size, prev_ctx) bb_node = QtGui.QStandardItem( 'BB%s Summary %x-%x: %s\t%s\t%s' % (bbs.index(bb), bb[0].addr, bb[-1].addr, ''.join( '%s ; ' % (''.join('%s, ' % c for c in line)) for line in bb_sum.disasm), ''.join( '%s, ' % c for c in filter(None, bb_sum.comment) if bb_sum.comment is not None), ''.join( '%s:%s ' % (c, bb_sum.ctx[c]) for c in bb_sum.ctx if bb_sum.ctx is not None))) for line in bb: tid = QtGui.QStandardItem('%s' % line.thread_id) addr = QtGui.QStandardItem('%x' % line.addr) disasm = QtGui.QStandardItem(line.disasm_str()) comment = QtGui.QStandardItem(''.join( c for c in line.comment if line.comment is not None)) context = QtGui.QStandardItem(''.join( '%s:%s ' % (c, line.ctx[c]) for c in line.ctx if line.ctx is not None)) bb_node.appendRow( [tid, addr, disasm, comment, context]) cluster_node.appendRow(bb_node) self.treeView.setFirstColumnSpanned( bbs.index(bb), cluster_node.index(), True) prev_ctx = bb[-1].ctx else: for l in line: tid = QtGui.QStandardItem('%s' % l.thread_id) addr = QtGui.QStandardItem('%x' % l.addr) disasm = QtGui.QStandardItem(l.disasm_str()) comment = QtGui.QStandardItem(''.join( c for c in l.comment if l.comment is not None)) context = QtGui.QStandardItem(''.join( '%s:%s ' % (c, l.ctx[c]) for c in l.ctx if l.ctx is not None)) cluster_node.appendRow( [tid, addr, disasm, comment, context]) w.close() self.treeView.resizeColumnToContents(0) self.treeView.resizeColumnToContents(1) self.treeView.resizeColumnToContents(2) self.treeView.resizeColumnToContents(3) self.treeView.resizeColumnToContents(4)