class DebugView(QWidget, View): class DebugViewHistoryEntry(HistoryEntry): def __init__(self, memory_addr, address, is_raw): HistoryEntry.__init__(self) self.memory_addr = memory_addr self.address = address self.is_raw = is_raw def __repr__(self): if self.is_raw: return "<raw history: {}+{:0x} (memory: {:0x})>".format(self.address['module'], self.address['offset'], self.memory_addr) return "<code history: {:0x} (memory: {:0x})>".format(self.address, self.memory_addr) def __init__(self, parent, data): if not type(data) == BinaryView: raise Exception('expected widget data to be a BinaryView') self.bv = data self.debug_state = binjaplug.get_state(data) memory_view = self.debug_state.memory_view self.debug_state.ui.debug_view = self QWidget.__init__(self, parent) self.controls = ControlsWidget.DebugControlsWidget(self, "Controls", data, self.debug_state) View.__init__(self) self.setupView(self) self.current_offset = 0 self.splitter = QSplitter(Qt.Orientation.Horizontal, self) frame = ViewFrame.viewFrameForWidget(self) self.memory_editor = LinearView(memory_view, frame) self.binary_editor = DisassemblyContainer(frame, data, frame) self.binary_text = TokenizedTextView(self, memory_view) self.is_raw_disassembly = False self.raw_address = 0 self.is_navigating_history = False self.memory_history_addr = 0 # TODO: Handle these and change views accordingly # Currently they are just disabled as the DisassemblyContainer gets confused # about where to go and just shows a bad view self.binary_editor.getDisassembly().actionHandler().bindAction("View in Hex Editor", UIAction()) self.binary_editor.getDisassembly().actionHandler().bindAction("View in Linear Disassembly", UIAction()) self.binary_editor.getDisassembly().actionHandler().bindAction("View in Types View", UIAction()) self.memory_editor.actionHandler().bindAction("View in Hex Editor", UIAction()) self.memory_editor.actionHandler().bindAction("View in Disassembly Graph", UIAction()) self.memory_editor.actionHandler().bindAction("View in Types View", UIAction()) small_font = QApplication.font() small_font.setPointSize(11) bv_layout = QVBoxLayout() bv_layout.setSpacing(0) bv_layout.setContentsMargins(0, 0, 0, 0) bv_label = QLabel("Loaded File") bv_label.setFont(small_font) bv_layout.addWidget(bv_label) bv_layout.addWidget(self.binary_editor) self.bv_widget = QWidget() self.bv_widget.setLayout(bv_layout) disasm_layout = QVBoxLayout() disasm_layout.setSpacing(0) disasm_layout.setContentsMargins(0, 0, 0, 0) disasm_label = QLabel("Raw Disassembly at PC") disasm_label.setFont(small_font) disasm_layout.addWidget(disasm_label) disasm_layout.addWidget(self.binary_text) self.disasm_widget = QWidget() self.disasm_widget.setLayout(disasm_layout) memory_layout = QVBoxLayout() memory_layout.setSpacing(0) memory_layout.setContentsMargins(0, 0, 0, 0) memory_label = QLabel("Debugged Process") memory_label.setFont(small_font) memory_layout.addWidget(memory_label) memory_layout.addWidget(self.memory_editor) self.memory_widget = QWidget() self.memory_widget.setLayout(memory_layout) self.splitter.addWidget(self.bv_widget) self.splitter.addWidget(self.memory_widget) # Equally sized self.splitter.setSizes([0x7fffffff, 0x7fffffff]) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self.controls) layout.addWidget(self.splitter, 100) self.setLayout(layout) self.needs_update = True self.update_timer = QTimer(self) self.update_timer.setInterval(200) self.update_timer.setSingleShot(False) self.update_timer.timeout.connect(lambda: self.updateTimerEvent()) self.add_scripting_ref() def add_scripting_ref(self): # Hack: The interpreter is just a thread, so look through all threads # and assign our state to the interpreter's locals for thread in threading.enumerate(): if type(thread) == PythonScriptingInstance.InterpreterThread: thread.locals["dbg"] = self.debug_state def getData(self): return self.bv def getFont(self): return binaryninjaui.getMonospaceFont(self) def getCurrentOffset(self): if not self.is_raw_disassembly: return self.binary_editor.getDisassembly().getCurrentOffset() return self.raw_address def getSelectionOffsets(self): if not self.is_raw_disassembly: return self.binary_editor.getDisassembly().getSelectionOffsets() return (self.raw_address, self.raw_address) def getCurrentFunction(self): if not self.is_raw_disassembly: return self.binary_editor.getDisassembly().getCurrentFunction() return None def getCurrentBasicBlock(self): if not self.is_raw_disassembly: return self.binary_editor.getDisassembly().getCurrentBasicBlock() return None def getCurrentArchitecture(self): if not self.is_raw_disassembly: return self.binary_editor.getDisassembly().getCurrentArchitecture() return None def getCurrentLowLevelILFunction(self): if not self.is_raw_disassembly: return self.binary_editor.getDisassembly().getCurrentLowLevelILFunction() return None def getCurrentMediumLevelILFunction(self): if not self.is_raw_disassembly: return self.binary_editor.getDisassembly().getCurrentMediumLevelILFunction() return None def getHistoryEntry(self): if self.is_navigating_history: return None memory_addr = self.memory_editor.getCurrentOffset() if memory_addr != self.memory_history_addr: self.memory_history_addr = memory_addr if self.is_raw_disassembly and self.debug_state.connected: rel_addr = self.debug_state.modules.absolute_addr_to_relative(self.raw_address) return DebugView.DebugViewHistoryEntry(memory_addr, rel_addr, True) else: address = self.binary_editor.getDisassembly().getCurrentOffset() return DebugView.DebugViewHistoryEntry(memory_addr, address, False) def navigateToHistoryEntry(self, entry): self.is_navigating_history = True if hasattr(entry, 'is_raw'): self.memory_editor.navigate(entry.memory_addr) if entry.is_raw: if self.debug_state.connected: address = self.debug_state.modules.relative_addr_to_absolute(entry.address) self.navigate_raw(address) else: self.navigate_live(entry.address) View.navigateToHistoryEntry(self, entry) self.is_navigating_history = False def navigate(self, addr): # If we're not connected we cannot even check if the address is remote if not self.debug_state.connected: return self.navigate_live(addr) if self.debug_state.memory_view.is_local_addr(addr): local_addr = self.debug_state.memory_view.remote_addr_to_local(addr) if self.debug_state.bv.read(local_addr, 1) and len(self.debug_state.bv.get_functions_containing(local_addr)) > 0: return self.navigate_live(local_addr) # This runs into conflicts if some other address space is mapped over # where the local BV is currently loaded, but this is was less likely # than the user navigating to a function from the UI if self.debug_state.bv.read(addr, 1) and len(self.debug_state.bv.get_functions_containing(addr)) > 0: return self.navigate_live(addr) return self.navigate_raw(addr) def navigate_live(self, addr): self.show_raw_disassembly(False) return self.binary_editor.getDisassembly().navigate(addr) def navigate_raw(self, addr): if not self.debug_state.connected: # Can't navigate to remote addr when disconnected return False self.raw_address = addr self.show_raw_disassembly(True) self.load_raw_disassembly(addr) return True def notifyMemoryChanged(self): self.needs_update = True def updateTimerEvent(self): if self.needs_update: self.needs_update = False # Refresh the editor if not self.debug_state.connected: self.memory_editor.navigate(0) return # self.memory_editor.navigate(self.debug_state.stack_pointer) def showEvent(self, event): if not event.spontaneous(): self.update_timer.start() self.add_scripting_ref() def hideEvent(self, event): if not event.spontaneous(): self.update_timer.stop() def shouldBeVisible(self, view_frame): if view_frame is None: return False else: return True def load_raw_disassembly(self, start_ip): # Read a few instructions from rip and disassemble them inst_count = 50 arch_dis = self.debug_state.remote_arch rip = self.debug_state.ip # Assume the worst, just in case read_length = arch_dis.max_instr_length * inst_count data = self.debug_state.memory_view.read(start_ip, read_length) lines = [] # Append header line tokens = [InstructionTextToken(InstructionTextTokenType.TextToken, "(Code not backed by loaded file, showing only raw disassembly)")] contents = DisassemblyTextLine(tokens, start_ip) if (major == 2): line = LinearDisassemblyLine(LinearDisassemblyLineType.BasicLineType, None, None, contents) else: line = LinearDisassemblyLine(LinearDisassemblyLineType.BasicLineType, None, None, 0, contents) lines.append(line) total_read = 0 for i in range(inst_count): line_addr = start_ip + total_read (insn_tokens, length) = arch_dis.get_instruction_text(data[total_read:], line_addr) if insn_tokens is None: insn_tokens = [InstructionTextToken(InstructionTextTokenType.TextToken, "??")] length = arch_dis.instr_alignment if length == 0: length = 1 tokens = [] color = HighlightStandardColor.NoHighlightColor if line_addr == rip: if self.debug_state.breakpoints.contains_absolute(start_ip + total_read): # Breakpoint & pc tokens.append(InstructionTextToken(InstructionTextTokenType.TagToken, self.debug_state.ui.get_breakpoint_tag_type().icon + ">", width=5)) color = HighlightStandardColor.RedHighlightColor else: # PC tokens.append(InstructionTextToken(InstructionTextTokenType.TextToken, " ==> ")) color = HighlightStandardColor.BlueHighlightColor else: if self.debug_state.breakpoints.contains_absolute(start_ip + total_read): # Breakpoint tokens.append(InstructionTextToken(InstructionTextTokenType.TagToken, self.debug_state.ui.get_breakpoint_tag_type().icon, width=5)) color = HighlightStandardColor.RedHighlightColor else: # Regular line tokens.append(InstructionTextToken(InstructionTextTokenType.TextToken, " ")) # Address tokens.append(InstructionTextToken(InstructionTextTokenType.AddressDisplayToken, hex(line_addr)[2:], line_addr)) tokens.append(InstructionTextToken(InstructionTextTokenType.TextToken, " ")) tokens.extend(insn_tokens) # Convert to linear disassembly line contents = DisassemblyTextLine(tokens, line_addr, color=color) if (major == 2): line = LinearDisassemblyLine(LinearDisassemblyLineType.CodeDisassemblyLineType, None, None, contents) else: line = LinearDisassemblyLine(LinearDisassemblyLineType.CodeDisassemblyLineType, None, None, 0, contents) lines.append(line) total_read += length # terrible workaround for libshiboken conversion issue for line in lines: # line is LinearDisassemblyLine last_tok = line.contents.tokens[-1] #if last_tok.type != InstructionTextTokenType.PossibleAddressToken: continue #if last_tok.width != 18: continue # strlen("0xFFFFFFFFFFFFFFF0") if last_tok.size != 8: continue #print('fixing: %s' % line) last_tok.value &= 0x7FFFFFFFFFFFFFFF self.binary_text.setLines(lines) def show_raw_disassembly(self, raw): if raw != self.is_raw_disassembly: self.splitter.replaceWidget(0, self.disasm_widget if raw else self.bv_widget) self.is_raw_disassembly = raw def refresh_raw_disassembly(self): if not self.debug_state.connected: # Can't navigate to remote addr when disconnected return if self.is_raw_disassembly: self.load_raw_disassembly(self.getCurrentOffset())
class TimeSeriesPlot(DataView): def __init__(self, workbench: WorkbenchModel, parent: QWidget = None): super().__init__(workbench, parent) self.settingsPanel = _SettingsPanel(self) self.chartView = InteractiveChartView(parent=self, setInWindow=False) p: QSizePolicy = self.chartView.sizePolicy() p.setHorizontalStretch(20) self.chartView.setSizePolicy(p) self.searchableIndexTableModel: QSortFilterProxyModel = QSortFilterProxyModel( self) self.splitter = QSplitter(Qt.Horizontal, self) self.splitter.addWidget(self.chartView) self.splitter.addWidget(self.settingsPanel) # Adjust size policies policy = self.settingsPanel.sizePolicy() policy.setHorizontalStretch(2) self.settingsPanel.setSizePolicy(policy) # Add splitter to main layout layout = QHBoxLayout(self) layout.addWidget(self.splitter) self.settingsPanel.createButton.clicked.connect(self.createChart) self.settingsPanel.timeAxisFormatCB.currentTextChanged.connect( self.changeTimeFormat) @Slot(str) def changeTimeFormat(self, timeFormat: str) -> None: """ Changes the datetime format displayed on the X axis of the plot """ chart = self.chartView.chart() if chart: axis = chart.axisX() if axis and axis.type() == QtCharts.QAbstractAxis.AxisTypeDateTime: axis.setFormat(timeFormat) self.chartView.setBestTickCount(chart.size()) def __createTimeAxis(self, timeSeries: pd.Series, timeType: Type) -> QtCharts.QAbstractAxis: """ Creates a time axis showing 'timeSeries' values of specified type (Ordinal or Datetime) """ if timeType == Types.Datetime: # Time axis is Datetime xAxis = QtCharts.QDateTimeAxis() xAxis.setFormat(self.settingsPanel.timeAxisFormatCB.currentText()) else: # Time axis is Ordinal (time are str labels) xAxis = QtCharts.QCategoryAxis() for cat, code in zip(timeSeries, timeSeries.cat.codes): xAxis.append(cat, code) xAxis.setStartValue(0) xAxis.setLabelsPosition( QtCharts.QCategoryAxis.AxisLabelsPositionOnValue) xAxis.setTitleText('Time') return xAxis @staticmethod def __createSeriesForAttributes(dataframe: pd.DataFrame, timeIndex: int, timeIndexType: Type) \ -> Tuple[List[QtCharts.QLineSeries], float, float]: """ Creates a QLineSeries for every column in the dataframe. 'timeIndex' column is used for xAxis :return: tuple as (list of series, yMin, yMax) """ timeIndexName: str = dataframe.columns[timeIndex] # Convert time values to their numerical equivalent if timeIndexType == Types.Datetime: # Time axis is Datetime, so convert every date into the number of ms from 01/01/1970 # dataframe[timeIndexName]: pd.Series[pd.Timestamp] # This may not be super accurate dataframe.loc[:, timeIndexName] = pd.to_numeric( dataframe[timeIndexName], downcast='integer', errors='coerce').values / (10**6) # dataframe[timeIndexName] \ # .map(lambda timestamp: int(timestamp.to_pydatetime().timestamp() * 1000) if ) else: # Types.Ordinal # dataframe[timeIndexName]: pd.Series[pd.Categorical] dataframe.loc[:, timeIndexName] = dataframe[ timeIndexName].cat.codes.to_list() timeValues: pd.Series = dataframe[timeIndexName].astype(float) # Remove time column since we already used it to create the time points dataframe = dataframe.drop(timeIndexName, axis=1) # Create series for every column (excluding time) allSeries: List[QtCharts.QLineSeries] = list() # Also keep track of the range the y axis should have yMin: float = None yMax: float = None for colName, valueSeries in dataframe.items(): valueSeries = pd.Series(valueSeries) if pd.api.types.is_categorical(valueSeries): # makes sure this is a series of floats valueSeries = valueSeries.cat.codes.astype(float) # Compute minimum and maximum of series and update global range smin = valueSeries.min() smax = valueSeries.max() yMin = smin if (yMin is None or yMin > smin) else yMin yMax = smax if (yMax is None or yMax < smax) else yMax # Create series qSeries = QtCharts.QLineSeries() points: List[QPointF] = list( map(lambda t: QPointF(*t), zip(timeValues, valueSeries))) qSeries.append(points) qSeries.setName(colName) qSeries.setUseOpenGL(True) qSeries.setPointsVisible( True) # This is ignored with OpenGL enabled allSeries.append(qSeries) return allSeries, yMin, yMax def __createChartWithValues(self, dataframe: pd.DataFrame, attributes: Set[int], timeIndex: int, timeIndexType: Type) -> QtCharts.QChart: chart = QtCharts.QChart() # Sort by time timeIndexName: str = dataframe.columns[timeIndex] filteredDf = dataframe.iloc[:, [timeIndex, *attributes]].sort_values( by=timeIndexName, axis=0, ascending=True) # filteredDf has timeIndex at position 0, attributes following # Drop nan labels filteredDf.dropna(axis=0, inplace=True, subset=[timeIndexName]) # Create X axis timeSeries: pd.Series = filteredDf.iloc[:, 0] xAxis = self.__createTimeAxis(timeSeries, timeIndexType) chart.addAxis(xAxis, Qt.AlignBottom) # Create the Y axis yAxis = QtCharts.QValueAxis(chart) yAxis.setTitleText('Values') chart.addAxis(yAxis, Qt.AlignLeft) series: List[QtCharts.QLineSeries] series, yMin, yMax = self.__createSeriesForAttributes( filteredDf, timeIndex=0, timeIndexType=timeIndexType) # Set range to show every point in chart yAxis.setRange(yMin, yMax) for s in series: chart.addSeries(s) s.attachAxis(xAxis) s.attachAxis(yAxis) return chart def __createChartWithIndexes(self, dataframe: pd.DataFrame, attributes: Set[int], indexes: List[Any], timeIndex: int, timeIndexType: Type, indexMean: bool = False) -> QtCharts.QChart: """ Creates a chart with a series for every 'index' in 'dataframe' showing only column specified in 'attributes' :param attributes: the position of the columns to consider to create series :param indexes: the indexes of the dataframe to use :param timeIndex: the index of the column in 'dataframe' which should be considered the time axis :param timeIndexType: the type of column specified in 'timeIndex'. Can be Ordinal or Datetime :param indexMean: ignored for now """ columns = dataframe.columns timeIndexName: str = columns[timeIndex] # Get the subset of attribute columns and selected indexes filteredDf = dataframe.loc[indexes, columns[[timeIndex, *attributes]]] \ .dropna(axis=0, subset=[timeIndexName]) filteredDf = filteredDf.sort_values(by=timeIndexName, axis=0, ascending=True) # Group rows by their index attribute. Every index has a distinct list of values dfByIndex = filteredDf.groupby(filteredDf.index) timeAxisColumn: pd.Series = list(dfByIndex)[0][1][timeIndexName] chart = QtCharts.QChart() # There will be 1 time axis for all the series, so it is created based on the first series # This function is passed the original time label (either Ordinal or Datetime) xAxis = self.__createTimeAxis(timeAxisColumn, timeIndexType) chart.addAxis(xAxis, Qt.AlignBottom) # Create the Y axis yAxis = QtCharts.QValueAxis() yAxis.setTitleText('Values') # Add axis Y to chart chart.addAxis(yAxis, Qt.AlignLeft) # Add every index series groupedValues: pd.DataFrame # timeValue, timeAttribute, *seriesValue for group, groupedValues in dfByIndex: # Create a series for this 'index' groupName: str = str(group) allSeries: List[QtCharts.QLineSeries] allSeries, yMin, yMax = self.__createSeriesForAttributes( groupedValues, 0, timeIndexType=timeIndexType) yAxis.setRange(yMin, yMax) for series in allSeries: chart.addSeries(series) series.attachAxis(xAxis) series.attachAxis(yAxis) if len(allSeries) == 1: # Only 1 attribute was selected, so assume we have multiple indexes (groups) allSeries[0].setName(groupName) return chart @Slot() def createChart(self) -> None: # Get options timeAxis: str = self.settingsPanel.timeAxisAttributeCB.currentText() attributes: Set[int] = self.settingsPanel.valuesTable.model().checked indexes: List[Any] = self.settingsPanel.indexTable.model().sourceModel( ).checked # indexMean: bool = self.settingsPanel.meanCB.isChecked() # Possibilities: # 1) 0 indexes and 1+ attributes # 2) 1+ indexes and 1 attribute # 3) 1 index and 1+ attributes # Validation errors: str = '' if not timeAxis: errors += 'Error: no time axis is selected\n' if not attributes: errors += 'Error: at least one attribute to show must be selected\n' if len(attributes) > 1 and len(indexes) > 1: errors += 'Error: select either 0/1 index and 1 or more attributes or 1 attribute and 1 ' 'or more indexes\n' if errors: errors = errors.strip('\n') self.settingsPanel.errorLabel.setText(errors) self.settingsPanel.errorLabel.setStyleSheet('color: red') self.settingsPanel.errorLabel.show() return # stop else: self.settingsPanel.errorLabel.hide() # Get the integer index of the time attribute timeIndexModel: QAbstractItemModel = self.settingsPanel.timeAxisAttributeCB.model( ) i = self.settingsPanel.timeAxisAttributeCB.currentIndex() timeIndex: int = timeIndexModel.mapToSource( timeIndexModel.index(i, 0, QModelIndex())).row() # Get the type of time attribute timeType: Type = self.settingsPanel.valuesTable.model().frameModel( ).shape.colTypes[timeIndex] # Get the pandas dataframe dataframe: pd.DataFrame = self.settingsPanel.valuesTable.model( ).frameModel().frame.getRawFrame() if len(attributes) >= 1 and len(indexes) == 0: # Create line plot with different attributes as series chart = self.__createChartWithValues(dataframe, attributes, timeIndex, timeType) elif (len(attributes) == 1 and len(indexes) >= 1) or \ (len(attributes) >= 1 and len(indexes) == 1): # Create chart with 1 attribute and many indexes, or with many attributes and 1 index chart = self.__createChartWithIndexes(dataframe, attributes, indexes, timeIndex, timeType) else: raise NotImplementedError('Invalid chart parameters') chart.setDropShadowEnabled(False) chart.setAnimationOptions(QtCharts.QChart.NoAnimation) chart.legend().setVisible(True) # Set font size for axis font: QFont = chart.axisX().labelsFont() font.setPointSize(9) chart.axisX().setLabelsFont(font) chart.axisY().setLabelsFont(font) chart.setMargins(QMargins(5, 5, 5, 30)) chart.layout().setContentsMargins(2, 2, 2, 2) # Set new chart and delete previous one self.__setChart(chart) def __setChart(self, chart: QtCharts.QChart) -> None: """ Creates a new view and set the provided chart in it """ self.createChartView() self.chartView.setChart(chart) self.chartView.setBestTickCount(chart.size()) @Slot(str, str) def onFrameSelectionChanged(self, name: str, *_) -> None: # Reset the ChartView self.createChartView() if not name: return # Set attribute table frameModel = self._workbench.getDataframeModelByName(name) self.settingsPanel.valuesTable.setSourceFrameModel(frameModel) # Set up combo box for time axis timeAxisModel = AttributeTableModel(self, False, False, False) timeAxisModel.setFrameModel(frameModel) filteredModel = AttributeProxyModel( filterTypes=[Types.Datetime, Types.Ordinal], parent=self) filteredModel.setSourceModel(timeAxisModel) timeAxisModel.setParent(filteredModel) m = self.settingsPanel.timeAxisAttributeCB.model() self.settingsPanel.timeAxisAttributeCB.setModel(filteredModel) safeDelete(m) # Set up index table if self.searchableIndexTableModel.sourceModel(): indexTableModel: IndexTableModel = self.searchableIndexTableModel.sourceModel( ) indexTableModel.setFrameModel(frameModel) else: # Searchable model has not a source model indexTableModel: IndexTableModel = IndexTableModel( self.searchableIndexTableModel) indexTableModel.setFrameModel(frameModel) # Set up proxy model self.searchableIndexTableModel.setSourceModel(indexTableModel) self.searchableIndexTableModel.setFilterKeyColumn(1) # Connect view to proxy model self.settingsPanel.indexTable.setModel( self.searchableIndexTableModel) self.settingsPanel.indexTable.searchBar.textEdited.connect( self.searchableIndexTableModel.setFilterRegularExpression) self.settingsPanel.indexTable.tableView.horizontalHeader( ).sectionClicked.connect(indexTableModel.onHeaderClicked) # Must be connected because Qt slot is not virtual indexTableModel.headerDataChanged.connect( self.settingsPanel.indexTable.tableView.horizontalHeader( ).headerDataChanged) def createChartView(self) -> None: """ Creates a new chart view """ # Creating a new view, instead of deleting chart, avoids many problems self.chartView = InteractiveChartView(parent=self, setInWindow=False) oldView = self.splitter.replaceWidget(0, self.chartView) self.chartView.setSizePolicy(oldView.sizePolicy()) self.chartView.setRenderHint( QPainter.Antialiasing) # For better looking charts self.chartView.show() safeDelete(oldView)
class MainWidget(QWidget): def __init__(self, parent=None): super().__init__(parent) self.workbenchModel = WorkbenchModel(self) self.graph = flow.dag.OperationDag() self.operationMenu = OperationMenu() self.frameInfoPanel = FramePanel(parent=self, w=self.workbenchModel, opModel=self.operationMenu.model()) self.workbenchView = WorkbenchView() self.workbenchView.setModel(self.workbenchModel) self.workbenchModel.emptyRowInserted.connect( self.workbenchView.startEditNoSelection) tabs = QTabWidget(self) attributeTab = AttributePanel(self.workbenchModel, self) chartsTab = ViewPanel(self.workbenchModel, self) self.graphScene = GraphScene(self) self._flowView = GraphView(self.graphScene, self) self.controller = GraphController(self.graph, self.graphScene, self._flowView, self.workbenchModel, self) tabs.addTab(attributeTab, '&Attribute') tabs.addTab(chartsTab, '&Visualise') tabs.addTab(self._flowView, 'F&low') self.__curr_tab = tabs.currentIndex() self.__leftSide = QSplitter(Qt.Vertical) self.__leftSide.addWidget(self.frameInfoPanel) self.__leftSide.addWidget(self.workbenchView) # layout = QHBoxLayout() # layout.addWidget(leftSplit, 2) # layout.addWidget(tabs, 8) splitter = QSplitter(Qt.Horizontal, self) splitter.addWidget(self.__leftSide) splitter.addWidget(tabs) layout = QHBoxLayout(self) layout.addWidget(splitter) tabs.currentChanged.connect(self.changeTabsContext) self.workbenchView.selectedRowChanged[str, str].connect( attributeTab.onFrameSelectionChanged) self.workbenchView.selectedRowChanged[str, str].connect( chartsTab.onFrameSelectionChanged) self.workbenchView.selectedRowChanged[str, str].connect( self.frameInfoPanel.onFrameSelectionChanged) self.workbenchView.rightClick.connect(self.createWorkbenchPopupMenu) @Slot(int) def changeTabsContext(self, tab_index: int) -> None: if tab_index == 2: self.__leftSide.replaceWidget(0, self.operationMenu) self.frameInfoPanel.hide() self.operationMenu.show() self.__curr_tab = 2 elif self.__curr_tab == 2 and tab_index != 2: self.__leftSide.replaceWidget(0, self.frameInfoPanel) self.operationMenu.hide() self.frameInfoPanel.show() self.__curr_tab = tab_index @Slot(QModelIndex) def createWorkbenchPopupMenu(self, index: QModelIndex) -> None: # Create a popup menu when workbench is right-clicked over a valid frame name # Menu display delete and remove options frameName: str = index.data(Qt.DisplayRole) pMenu = QMenu(self) # Reuse MainWindow actions csvAction = self.parentWidget().aWriteCsv pickleAction = self.parentWidget().aWritePickle # Set correct args for the clicked row csvAction.setOperationArgs(w=self.workbenchModel, frameName=frameName) pickleAction.setOperationArgs(w=self.workbenchModel, frameName=frameName) deleteAction = QAction('Remove', pMenu) deleteAction.triggered.connect( lambda: self.workbenchModel.removeRow(index.row())) pMenu.addActions([csvAction, pickleAction, deleteAction]) pMenu.popup(QtGui.QCursor.pos()) def createNewFlow(self, graph: flow.dag.OperationDag) -> None: self.graph = graph oldScene = self._flowView.scene() self.graphScene = GraphScene(self) self._flowView.setScene(self.graphScene) oldScene.deleteLater() self.controller.deleteLater() self.controller = GraphController(self.graph, self.graphScene, self._flowView, self.workbenchModel, self)
class DebugView(QWidget, View): def __init__(self, parent, data): if not type(data) == binaryninja.binaryview.BinaryView: raise Exception('expected widget data to be a BinaryView') self.bv = data self.debug_state = binjaplug.get_state(data) memory_view = self.debug_state.memory_view self.debug_state.ui.debug_view = self QWidget.__init__(self, parent) self.controls = ControlsWidget.DebugControlsWidget( self, "Controls", data, self.debug_state) View.__init__(self) self.setupView(self) self.current_offset = 0 self.splitter = QSplitter(Qt.Orientation.Horizontal, self) frame = ViewFrame.viewFrameForWidget(self) self.memory_editor = LinearView(memory_view, frame) self.binary_editor = DisassemblyContainer(frame, data, frame) self.binary_text = TokenizedTextView(self, memory_view) self.is_raw_disassembly = False # TODO: Handle these and change views accordingly # Currently they are just disabled as the DisassemblyContainer gets confused # about where to go and just shows a bad view self.binary_editor.getDisassembly().actionHandler().bindAction( "View in Hex Editor", UIAction()) self.binary_editor.getDisassembly().actionHandler().bindAction( "View in Linear Disassembly", UIAction()) self.binary_editor.getDisassembly().actionHandler().bindAction( "View in Types View", UIAction()) self.memory_editor.actionHandler().bindAction("View in Hex Editor", UIAction()) self.memory_editor.actionHandler().bindAction( "View in Disassembly Graph", UIAction()) self.memory_editor.actionHandler().bindAction("View in Types View", UIAction()) small_font = QApplication.font() small_font.setPointSize(11) bv_layout = QVBoxLayout() bv_layout.setSpacing(0) bv_layout.setContentsMargins(0, 0, 0, 0) bv_label = QLabel("Loaded File") bv_label.setFont(small_font) bv_layout.addWidget(bv_label) bv_layout.addWidget(self.binary_editor) self.bv_widget = QWidget() self.bv_widget.setLayout(bv_layout) disasm_layout = QVBoxLayout() disasm_layout.setSpacing(0) disasm_layout.setContentsMargins(0, 0, 0, 0) disasm_label = QLabel("Raw Disassembly at PC") disasm_label.setFont(small_font) disasm_layout.addWidget(disasm_label) disasm_layout.addWidget(self.binary_text) self.disasm_widget = QWidget() self.disasm_widget.setLayout(disasm_layout) memory_layout = QVBoxLayout() memory_layout.setSpacing(0) memory_layout.setContentsMargins(0, 0, 0, 0) memory_label = QLabel("Debugged Process") memory_label.setFont(small_font) memory_layout.addWidget(memory_label) memory_layout.addWidget(self.memory_editor) self.memory_widget = QWidget() self.memory_widget.setLayout(memory_layout) self.splitter.addWidget(self.bv_widget) self.splitter.addWidget(self.memory_widget) # Equally sized self.splitter.setSizes([0x7fffffff, 0x7fffffff]) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self.controls) layout.addWidget(self.splitter, 100) self.setLayout(layout) self.needs_update = True self.update_timer = QTimer(self) self.update_timer.setInterval(200) self.update_timer.setSingleShot(False) self.update_timer.timeout.connect(lambda: self.updateTimerEvent()) self.add_scripting_ref() def add_scripting_ref(self): # Hack: The interpreter is just a thread, so look through all threads # and assign our state to the interpreter's locals for thread in threading.enumerate(): if type(thread) == PythonScriptingInstance.InterpreterThread: thread.locals["dbg"] = self.debug_state def getData(self): return self.bv def getCurrentOffset(self): return self.binary_editor.getDisassembly().getCurrentOffset() def getFont(self): return binaryninjaui.getMonospaceFont(self) def navigate(self, addr): return self.binary_editor.getDisassembly().navigate(addr) def notifyMemoryChanged(self): self.needs_update = True def updateTimerEvent(self): if self.needs_update: self.needs_update = False # Refresh the editor if not self.debug_state.connected: self.memory_editor.navigate(0) return self.memory_editor.navigate(self.debug_state.stack_pointer) def showEvent(self, event): if not event.spontaneous(): self.update_timer.start() self.add_scripting_ref() def hideEvent(self, event): if not event.spontaneous(): self.update_timer.stop() def shouldBeVisible(self, view_frame): if view_frame is None: return False else: return True def setRawDisassembly(self, raw=False, lines=[]): if raw != self.is_raw_disassembly: self.splitter.replaceWidget( 0, self.disasm_widget if raw else self.bv_widget) self.is_raw_disassembly = raw # terrible workaround for libshiboken conversion issue for line in lines: # line is LinearDisassemblyLine last_tok = line.contents.tokens[-1] #if last_tok.type != InstructionTextTokenType.PossibleAddressToken: continue #if last_tok.width != 18: continue # strlen("0xFFFFFFFFFFFFFFF0") if last_tok.size != 8: continue #print('fixing: %s' % line) last_tok.value &= 0x7FFFFFFFFFFFFFFF self.binary_text.setLines(lines)
class WTreeEdit(QWidget): """TreeEdit widget is to show and edit all of the pyleecan objects data.""" # Signals dataChanged = Signal() def __init__(self, obj, *args, **kwargs): QWidget.__init__(self, *args, **kwargs) self.class_dict = ClassInfo().get_dict() self.treeDict = None # helper to track changes self.obj = obj # the object self.is_save_needed = False self.model = TreeEditModel(obj) self.setupUi() # === Signals === self.selectionModel.selectionChanged.connect(self.onSelectionChanged) self.treeView.collapsed.connect(self.onItemCollapse) self.treeView.expanded.connect(self.onItemExpand) self.treeView.customContextMenuRequested.connect(self.openContextMenu) self.model.dataChanged.connect(self.onDataChanged) self.dataChanged.connect(self.setSaveNeeded) # === Finalize === # set 'root' the selected item and resize columns self.treeView.setCurrentIndex(self.treeView.model().index(0, 0)) self.treeView.resizeColumnToContents(0) def setupUi(self): """Setup the UI""" # === Widgets === # TreeView self.treeView = QTreeView() # self.treeView.rootNode = model.invisibleRootItem() self.treeView.setModel(self.model) self.treeView.setAlternatingRowColors(False) # self.treeView.setColumnWidth(0, 150) self.treeView.setMinimumWidth(100) self.treeView.setContextMenuPolicy(Qt.CustomContextMenu) self.selectionModel = self.treeView.selectionModel() self.statusBar = QStatusBar() self.statusBar.setSizeGripEnabled(False) self.statusBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) self.statusBar.setStyleSheet( "QStatusBar {border: 1px solid rgb(200, 200, 200)}") self.saveLabel = QLabel("unsaved") self.saveLabel.setVisible(False) self.statusBar.addPermanentWidget(self.saveLabel) # Splitters self.leftSplitter = QSplitter() self.leftSplitter.setStretchFactor(0, 0) self.leftSplitter.setStretchFactor(1, 1) # === Layout === # Horizontal Div. self.hLayout = QVBoxLayout() self.hLayout.setContentsMargins(0, 0, 0, 0) self.hLayout.setSpacing(0) # add widgets to layout self.hLayout.addWidget(self.leftSplitter) self.hLayout.addWidget(self.statusBar) # add widgets self.leftSplitter.addWidget(self.treeView) self.setLayout(self.hLayout) def update(self, obj): """Check if object has changed and update tree in case.""" if not obj is self.obj: self.obj = obj self.model = TreeEditModel(obj) self.treeView.setModel(self.model) self.model.dataChanged.connect(self.onDataChanged) self.selectionModel = self.treeView.selectionModel() self.selectionModel.selectionChanged.connect( self.onSelectionChanged) self.treeView.setCurrentIndex(self.treeView.model().index(0, 0)) self.setSaveNeeded(True) def setSaveNeeded(self, state=True): self.is_save_needed = state self.saveLabel.setVisible(state) def openContextMenu(self, point): """Generate and open context the menu at the given point position.""" index = self.treeView.indexAt(point) pos = QtGui.QCursor.pos() if not index.isValid(): return # get the data item = self.model.item(index) obj_info = self.model.get_obj_info(item) # init the menu menu = TreeEditContextMenu(obj_dict=obj_info, parent=self) menu.exec_(pos) self.onSelectionChanged(self.selectionModel.selection()) def onItemCollapse(self, index): """Slot for item collapsed""" # dynamic resize for ii in range(3): self.treeView.resizeColumnToContents(ii) def onItemExpand(self, index): """Slot for item expand""" # dynamic resize for ii in range(3): self.treeView.resizeColumnToContents(ii) def onDataChanged(self, first=None, last=None): """Slot for changed data""" self.dataChanged.emit() self.onSelectionChanged(self.selectionModel.selection()) def onSelectionChanged(self, itemSelection): """Slot for changed item selection""" # get the index if itemSelection.indexes(): index = itemSelection.indexes()[0] else: index = self.treeView.model().index(0, 0) self.treeView.setCurrentIndex(index) return # get the data item = self.model.item(index) obj = item.object() typ = type(obj).__name__ obj_info = self.model.get_obj_info(item) ref_typ = obj_info["ref_typ"] if obj_info else None # set statusbar information on class typ msg = f"{typ} (Ref: {ref_typ})" if ref_typ else f"{typ}" self.statusBar.showMessage(msg) # --- choose the respective widget by class type --- # numpy array -> table editor if typ == "ndarray": widget = WTableData(obj, editable=True) widget.dataChanged.connect(self.dataChanged.emit) elif typ == "MeshSolution": widget = WMeshSolution(obj) # only a view (not editable) # list (no pyleecan type, non empty) -> table editor # TODO add another widget for lists of non 'primitive' types (e.g. DataND) elif isinstance(obj, list) and not self.isListType(ref_typ) and obj: widget = WTableData(obj, editable=True) widget.dataChanged.connect(self.dataChanged.emit) # generic editor else: # widget = SimpleInputWidget().generate(obj) widget = WTableParameterEdit(obj) widget.dataChanged.connect(self.dataChanged.emit) # show the widget if self.leftSplitter.widget(1) is None: self.leftSplitter.addWidget(widget) else: self.leftSplitter.replaceWidget(1, widget) widget.setParent( self.leftSplitter) # workaround for PySide2 replace bug widget.show() pass def isListType(self, typ): if not typ: return False return typ[0] == "[" and typ[-1] == "]" and typ[1:-1] in self.class_dict def isDictType(self, typ): if not typ: return False return typ[0] == "{" and typ[-1] == "}" and typ[1:-1] in self.class_dict
class ScatterPlotMatrix(DataView): def __init__(self, workbench: WorkbenchModel, parent=None): super().__init__(workbench, parent) self.__frameModel: FrameModel = None # Create widget for the two tables sideLayout = QVBoxLayout() self.__matrixAttributes = SearchableAttributeTableWidget( self, True, False, False, [Types.Numeric, Types.Ordinal]) matrixLabel = QLabel( 'Select at least two numeric attributes and press \'Create chart\' to plot' ) matrixLabel.setWordWrap(True) self.__createButton = QPushButton('Create chart', self) self.__colorByBox = QComboBox(self) self.__autoDownsample = QCheckBox('Auto downsample', self) self.__useOpenGL = QCheckBox('Use OpenGL', self) self.__autoDownsample.setToolTip( 'If too many points are to be rendered, this will try\n' 'to plot only a subsample, improving performance with\n' 'zooming and panning, but increasing rendering time') self.__useOpenGL.setToolTip( 'Enforce usage of GPU acceleration to render charts.\n' 'It is still an experimental feature but should speed\n' 'up rendering with huge set of points') # Layout for checkboxes optionsLayout = QHBoxLayout() optionsLayout.addWidget(self.__autoDownsample, 0, Qt.AlignRight) optionsLayout.addWidget(self.__useOpenGL, 0, Qt.AlignRight) sideLayout.addWidget(matrixLabel) sideLayout.addWidget(self.__matrixAttributes) sideLayout.addLayout(optionsLayout) sideLayout.addWidget(self.__colorByBox, 0, Qt.AlignBottom) sideLayout.addWidget(self.__createButton, 0, Qt.AlignBottom) self.__matrixLayout: pg.GraphicsLayoutWidget = pg.GraphicsLayoutWidget( ) self.__layout = QHBoxLayout(self) self.__comboModel = AttributeProxyModel( [Types.String, Types.Ordinal, Types.Nominal], self) # Error label to signal errors self.errorLabel = QLabel(self) self.errorLabel.setWordWrap(True) sideLayout.addWidget(self.errorLabel) self.errorLabel.hide() self.__splitter = QSplitter(self) sideWidget = QWidget(self) sideWidget.setLayout(sideLayout) # chartWidget.setMinimumWidth(300) self.__splitter.addWidget(self.__matrixLayout) self.__splitter.addWidget(sideWidget) self.__splitter.setSizes([600, 300]) self.__layout.addWidget(self.__splitter) self.__splitter.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) # Connect self.__createButton.clicked.connect(self.showScatterPlots) self.spinner = QtWaitingSpinner(self.__matrixLayout) @Slot() def showScatterPlots(self) -> None: self.__createButton.setDisabled(True) # Create plot with selected attributes attributes: Set[int] = self.__matrixAttributes.model().checked if len(attributes) < 2: self.errorLabel.setText('Select at least 2 attributes') self.errorLabel.setStyleSheet('color: red') self.errorLabel.show() return # stop elif self.errorLabel.isVisible(): self.errorLabel.hide() # Get index of groupBy Attribute group: int = None selectedIndex = self.__colorByBox.currentIndex() if self.__comboModel.rowCount() > 0 and selectedIndex != -1: index: QModelIndex = self.__comboModel.mapToSource( self.__comboModel.index(selectedIndex, 0, QModelIndex())) group = index.row() if index.isValid() else None # Create a new matrix layout and delete the old one matrix = GraphicsPlotLayout(parent=self) self.spinner = QtWaitingSpinner(matrix) oldM = self.__splitter.replaceWidget(0, matrix) self.__matrixLayout = matrix safeDelete(oldM) matrix.useOpenGL(self.__useOpenGL.isChecked()) matrix.show() # Get attributes of interest toKeep: List[int] = list(attributes) if group is None else [ group, *attributes ] filterDf = self.__frameModel.frame.getRawFrame().iloc[:, toKeep] # Create a worker to create scatter-plots on different thread worker = Worker(ProcessDataframe(), (filterDf, group, attributes)) worker.signals.result.connect(self.__createPlots) # No need to deal with error/finished signals since there is nothing to do worker.setAutoDelete(True) self.spinner.start() QThreadPool.globalInstance().start(worker) def resetScatterPlotMatrix(self) -> None: # Create a new matrix layout matrix = pg.GraphicsLayoutWidget(parent=self) self.spinner = QtWaitingSpinner(matrix) oldM = self.__splitter.replaceWidget(0, matrix) self.__matrixLayout = matrix safeDelete(oldM) matrix.show() @Slot(object, object) def __createPlots( self, _, result: Tuple[pd.DataFrame, List[str], List[int], bool]) -> None: """ Create plots and render all graphic items """ # Unpack results df, names, attributes, grouped = result # Populate the matrix for r in range(len(attributes)): for c in range(len(attributes)): if r == c: name: str = names[r] self.__matrixLayout.addLabel(row=r, col=c, text=name) else: xColName: str = names[c] yColName: str = names[r] seriesList = self.__createScatterSeries( df=df, xCol=xColName, yCol=yColName, groupBy=grouped, ds=self.__autoDownsample.isChecked()) plot = self.__matrixLayout.addPlot(row=r, col=c) for series in seriesList: plot.addItem(series) # Coordinates and data for later use plot.row = r plot.col = c plot.xName = xColName plot.yName = yColName # When all plot are created stop spinner and re-enable button self.spinner.stop() self.__createButton.setEnabled(True) @staticmethod def __createScatterSeries(df: Union[pd.DataFrame, pd.core.groupby.DataFrameGroupBy], xCol: str, yCol: str, groupBy: bool, ds: bool) -> List[pg.PlotDataItem]: """ Creates a list of series of points to be plotted in the scatterplot :param df: the input dataframe :param xCol: name of the feature to use as x-axis :param yCol: name of the feature to use as y-axis :param groupBy: whether the feature dataframe is grouped by some attribute :param ds: whether to auto downsample the set of points during rendering :return: """ allSeries = list() if groupBy: df: pd.core.groupby.DataFrameGroupBy colours = randomColors(len(df)) i = 0 for groupName, groupedDf in df: # Remove any row with nan values gdf = groupedDf.dropna() qSeries1 = pg.PlotDataItem(x=gdf[xCol], y=gdf[yCol], autoDownsample=ds, name=str(groupName), symbolBrush=pg.mkBrush(colours[i]), symbol='o', pen=None) allSeries.append(qSeries1) i += 1 else: df: pd.DataFrame # Remove any row with nan values df = df.dropna() series = pg.PlotDataItem(x=df[xCol], y=df[yCol], autoDownsample=ds, symbol='o', pen=None) allSeries.append(series) return allSeries @Slot(str, str) def onFrameSelectionChanged(self, frameName: str, *_) -> None: if not frameName: return self.__frameModel = self._workbench.getDataframeModelByName(frameName) self.__matrixAttributes.setSourceFrameModel(self.__frameModel) # Combo box attributes = AttributeTableModel(self, False, False, False) attributes.setFrameModel(self.__frameModel) oldModel = self.__comboModel.sourceModel() self.__comboModel.setSourceModel(attributes) if oldModel: oldModel.deleteLater() self.__colorByBox.setModel(self.__comboModel) # Reset attribute panel self.resetScatterPlotMatrix()