├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── codecov.yml ├── cutterdrcov_plugin ├── __init__.py ├── autogen │ ├── __init__.py │ ├── dockui.py │ └── icon_rc.py ├── covtable.py ├── cutterplugin.py ├── drcov.py ├── extras.py ├── plugin_interface.py └── sortable_table_item.py ├── resources ├── ICON-LICENSE ├── icon.qrc └── icon │ ├── brush.svg │ ├── data-transfer-download.svg │ ├── reload.svg │ └── x.svg ├── screanshots ├── overview.jpg ├── path.jpg └── usage.gif ├── scripts ├── lint.sh ├── local.sh └── travis.sh ├── test_files ├── drcov.test2.log ├── drcov.test3.log ├── drcov2.2.log ├── drcov2.3.log ├── drcov2.4.log ├── drcov_orphan.log ├── test1.asm ├── test1.bin ├── test1.o ├── test2.asm ├── test2.bin ├── test2.o ├── test3.asm ├── test3.bin └── test3.o ├── ui └── dock.ui └── unittest ├── __main__.py ├── test_covtable.py ├── test_drcov.py ├── test_extras.py └── test_sortable_table_item.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.swp 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | install: 5 | pip install Pyside2 codecov pylint 6 | script: 7 | ./scripts/travis.sh 8 | after_success: 9 | - codecov 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ahmed Abd El Mawgood 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | pyside2-uic ui/dock.ui --from-imports > cutterDRcovPlugin/autogen/dockui.py 3 | pyside2-rcc resources/icon.qrc > cutterDRcovPlugin/autogen/icon_rc.py 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CutterDRcov - DynamoRIO Coverage Visualizer for Cutter 2 | 3 | ![Screenshot](screanshots/overview.jpg?raw=true) 4 | 5 | [![Build Status](https://travis-ci.org/oddcoder/CutterDRcov.svg?branch=master)](https://travis-ci.org/oddcoder/CutterDRcov) 6 | [![codecov](https://codecov.io/gh/oddcoder/CutterDRcov/branch/master/graph/badge.svg)](https://codecov.io/gh/oddcoder/CutterDRcov) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) 8 | [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/oddcoder/CutterDRcov.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/oddcoder/CutterDRcov/context:python) 9 | 10 | ## Introduction 11 | 12 | CutterDrcov is code coverage plugin that visualize DynamoRIO 13 | [drcov](http://dynamorio.org/docs/page_drcov.html) into [Cutter](https://cutter.re) 14 | static analysis. 15 | 16 | IDA or Binary ninja user? checkout 17 | [lighthouse](https://github.com/gaasedelen/lighthouse) 18 | 19 | ## Installation 20 | First, locate the folder used by cutter for loading plugins, you can find it 21 | from inside cutter by going to *Edit menu* → *preferences* and finally 22 | selecting *Plugins*. 23 | ![pathlocation](screanshots/path.jpg?raw=true) 24 | 25 | Inside that folder you will want to go *python* folder, 26 | [download](https://github.com/oddcoder/CutterDRcov/archive/master.zip) this 27 | repository, and move *cutterdrcov_plugin* folder there. 28 | 29 | ## Usage 30 | 31 | First you will need to get some drcov trace, you can get DynamoRIO from their 32 | official repository [here](https://github.com/DynamoRIO/dynamorio/releases). 33 | 34 | For example, on 64Bit linux to get coverage trace by issuing this shell command: 35 | 36 | ```sh 37 | $ dynamoRIO/bin64/drrun -t drcov -- [program name] [arguments] 38 | ``` 39 | Finally you will need to load that trace into CutterDRcov! 40 | 41 | ![usage](screanshots/usage.gif?raw=true) 42 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "unittest" 3 | - "cutterdrcov_plugin/autogen" 4 | - "cutterdrcov_plugin/plugin_interface.py" 5 | - "cutterdrcov_plugin/cutterplugin.py" 6 | -------------------------------------------------------------------------------- /cutterdrcov_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from .plugin_interface import create_cutter_plugin 3 | except Exception: 4 | pass 5 | -------------------------------------------------------------------------------- /cutterdrcov_plugin/autogen/__init__.py: -------------------------------------------------------------------------------- 1 | from .dockui import Ui_DockWidget 2 | -------------------------------------------------------------------------------- /cutterdrcov_plugin/autogen/dockui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'ui/dock.ui', 4 | # licensing of 'ui/dock.ui' applies. 5 | # 6 | # Created: Tue Mar 26 18:55:32 2019 7 | # by: pyside2-uic running on PySide2 5.12.1 8 | # 9 | # WARNING! All changes made in this file will be lost! 10 | 11 | from PySide2 import QtCore, QtGui, QtWidgets 12 | 13 | class Ui_DockWidget(object): 14 | def setupUi(self, DockWidget): 15 | DockWidget.setObjectName("DockWidget") 16 | DockWidget.resize(687, 455) 17 | self.contents = QtWidgets.QWidget() 18 | self.contents.setObjectName("contents") 19 | self.gridLayout_5 = QtWidgets.QGridLayout(self.contents) 20 | self.gridLayout_5.setObjectName("gridLayout_5") 21 | self.stackedWidget = QtWidgets.QStackedWidget(self.contents) 22 | self.stackedWidget.setAcceptDrops(False) 23 | self.stackedWidget.setObjectName("stackedWidget") 24 | self.LoaderPage = QtWidgets.QWidget() 25 | self.LoaderPage.setObjectName("LoaderPage") 26 | self.gridLayout_2 = QtWidgets.QGridLayout(self.LoaderPage) 27 | self.gridLayout_2.setObjectName("gridLayout_2") 28 | self.loader = QtWidgets.QLabel(self.LoaderPage) 29 | self.loader.setCursor(QtCore.Qt.PointingHandCursor) 30 | self.loader.setAcceptDrops(True) 31 | self.loader.setStyleSheet("") 32 | self.loader.setObjectName("loader") 33 | self.gridLayout_2.addWidget(self.loader, 0, 0, 1, 1) 34 | self.stackedWidget.addWidget(self.LoaderPage) 35 | self.CovTablePage = QtWidgets.QWidget() 36 | self.CovTablePage.setObjectName("CovTablePage") 37 | self.gridLayout = QtWidgets.QGridLayout(self.CovTablePage) 38 | self.gridLayout.setObjectName("gridLayout") 39 | self.horizontalLayout = QtWidgets.QHBoxLayout() 40 | self.horizontalLayout.setObjectName("horizontalLayout") 41 | self.close = QtWidgets.QToolButton(self.CovTablePage) 42 | self.close.setText("") 43 | icon = QtGui.QIcon() 44 | icon.addPixmap(QtGui.QPixmap(":/icons/icon/x.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off) 45 | self.close.setIcon(icon) 46 | self.close.setObjectName("close") 47 | self.horizontalLayout.addWidget(self.close) 48 | self.reload = QtWidgets.QToolButton(self.CovTablePage) 49 | self.reload.setText("") 50 | icon1 = QtGui.QIcon() 51 | icon1.addPixmap(QtGui.QPixmap(":/icons/icon/reload.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off) 52 | self.reload.setIcon(icon1) 53 | self.reload.setObjectName("reload") 54 | self.horizontalLayout.addWidget(self.reload) 55 | self.selectColor = QtWidgets.QToolButton(self.CovTablePage) 56 | self.selectColor.setText("") 57 | icon2 = QtGui.QIcon() 58 | icon2.addPixmap(QtGui.QPixmap(":/icons/icon/brush.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off) 59 | self.selectColor.setIcon(icon2) 60 | self.selectColor.setObjectName("selectColor") 61 | self.horizontalLayout.addWidget(self.selectColor) 62 | self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1) 63 | spacerItem = QtWidgets.QSpacerItem(540, 38, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) 64 | self.gridLayout.addItem(spacerItem, 0, 1, 1, 1) 65 | self.colorize = QtWidgets.QCheckBox(self.CovTablePage) 66 | self.colorize.setChecked(True) 67 | self.colorize.setObjectName("colorize") 68 | self.gridLayout.addWidget(self.colorize, 0, 2, 1, 1) 69 | self.covtable = QtWidgets.QTableWidget(self.CovTablePage) 70 | self.covtable.setFrameShape(QtWidgets.QFrame.NoFrame) 71 | self.covtable.setFrameShadow(QtWidgets.QFrame.Plain) 72 | self.covtable.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) 73 | self.covtable.setAlternatingRowColors(True) 74 | self.covtable.setSelectionMode(QtWidgets.QAbstractItemView.ContiguousSelection) 75 | self.covtable.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) 76 | self.covtable.setShowGrid(False) 77 | self.covtable.setObjectName("covtable") 78 | self.covtable.setColumnCount(5) 79 | self.covtable.setRowCount(0) 80 | item = QtWidgets.QTableWidgetItem() 81 | self.covtable.setHorizontalHeaderItem(0, item) 82 | item = QtWidgets.QTableWidgetItem() 83 | self.covtable.setHorizontalHeaderItem(1, item) 84 | item = QtWidgets.QTableWidgetItem() 85 | self.covtable.setHorizontalHeaderItem(2, item) 86 | item = QtWidgets.QTableWidgetItem() 87 | self.covtable.setHorizontalHeaderItem(3, item) 88 | item = QtWidgets.QTableWidgetItem() 89 | self.covtable.setHorizontalHeaderItem(4, item) 90 | self.covtable.horizontalHeader().setDefaultSectionSize(118) 91 | self.covtable.horizontalHeader().setStretchLastSection(True) 92 | self.covtable.verticalHeader().setVisible(False) 93 | self.gridLayout.addWidget(self.covtable, 1, 0, 1, 3) 94 | self.stackedWidget.addWidget(self.CovTablePage) 95 | self.gridLayout_5.addWidget(self.stackedWidget, 0, 0, 1, 1) 96 | DockWidget.setWidget(self.contents) 97 | 98 | self.retranslateUi(DockWidget) 99 | self.stackedWidget.setCurrentIndex(1) 100 | QtCore.QMetaObject.connectSlotsByName(DockWidget) 101 | 102 | def retranslateUi(self, DockWidget): 103 | DockWidget.setWindowTitle(QtWidgets.QApplication.translate("DockWidget", "Cutter DynamoRIO Coverage", None, -1)) 104 | self.loader.setText(QtWidgets.QApplication.translate("DockWidget", "

Click to Open drcov file or drag it here.

", None, -1)) 105 | self.colorize.setText(QtWidgets.QApplication.translate("DockWidget", "Colorize (On / OFF)", None, -1)) 106 | self.covtable.setSortingEnabled(True) 107 | self.covtable.horizontalHeaderItem(0).setText(QtWidgets.QApplication.translate("DockWidget", "Coverage", None, -1)) 108 | self.covtable.horizontalHeaderItem(1).setText(QtWidgets.QApplication.translate("DockWidget", "Function Name", None, -1)) 109 | self.covtable.horizontalHeaderItem(2).setText(QtWidgets.QApplication.translate("DockWidget", "Address", None, -1)) 110 | self.covtable.horizontalHeaderItem(3).setText(QtWidgets.QApplication.translate("DockWidget", "Instructions Hits", None, -1)) 111 | self.covtable.horizontalHeaderItem(4).setText(QtWidgets.QApplication.translate("DockWidget", "Basic Block Hits", None, -1)) 112 | 113 | from . import icon_rc 114 | -------------------------------------------------------------------------------- /cutterdrcov_plugin/autogen/icon_rc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Resource object code 4 | # 5 | # Created: Tue Mar 26 18:55:33 2019 6 | # by: The Resource Compiler for PySide2 (Qt v5.12.1) 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from PySide2 import QtCore 11 | 12 | qt_resource_data = b"\ 13 | \x00\x00\x011\ 14 | <\ 15 | svg fill=\x22#92b0b\ 16 | 3\x22 xmlns=\x22http:/\ 17 | /www.w3.org/2000\ 18 | /svg\x22 width=\x2232\x22\ 19 | height=\x2232\x22 vie\ 20 | wBox=\x220 0 8 8\x22>\x0a\ 21 | \x0a\x0a\ 34 | \ 35 | \x00\x00\x01=\ 36 | <\ 37 | svg fill=\x22#92b0b\ 38 | 3\x22 xmlns=\x22http:/\ 39 | /www.w3.org/2000\ 40 | /svg\x22 width=\x2232\x22\ 41 | height=\x2232\x22 vie\ 42 | wBox=\x220 0 8 8\x22>\x0a\ 43 | \x0a\x0a\ 57 | \x00\x00\x01\xbc\ 58 | <\ 59 | svg fill=\x22#92b0b\ 60 | 3\x22 xmlns=\x22http:/\ 61 | /www.w3.org/2000\ 62 | /svg\x22 width=\x2232\x22\ 63 | height=\x2232\x22 vie\ 64 | wBox=\x220 0 8 8\x22>\x0a\ 65 | \x0a\x0a\ 87 | \x00\x00\x00\xa3\ 88 | <\ 89 | svg fill=\x22#92b0b\ 90 | 3\x22 xmlns=\x22http:/\ 91 | /www.w3.org/2000\ 92 | /svg\x22 width=\x2264\x22\ 93 | height=\x2264\x22 vie\ 94 | wBox=\x220 0 8 8\x22>\x0a\ 95 | \x0a\x0a\ 100 | " 101 | 102 | qt_resource_name = b"\ 103 | \x00\x05\ 104 | \x00o\xa6S\ 105 | \x00i\ 106 | \x00c\x00o\x00n\x00s\ 107 | \x00\x04\ 108 | \x00\x06\xfa^\ 109 | \x00i\ 110 | \x00c\x00o\x00n\ 111 | \x00\x05\ 112 | \x00{Z\xc7\ 113 | \x00x\ 114 | \x00.\x00s\x00v\x00g\ 115 | \x00\x0a\ 116 | \x05xB\xa7\ 117 | \x00r\ 118 | \x00e\x00l\x00o\x00a\x00d\x00.\x00s\x00v\x00g\ 119 | \x00\x09\ 120 | \x0c\x9b\x89\xe7\ 121 | \x00b\ 122 | \x00r\x00u\x00s\x00h\x00.\x00s\x00v\x00g\ 123 | \x00\x1a\ 124 | \x06_\x97'\ 125 | \x00d\ 126 | \x00a\x00t\x00a\x00-\x00t\x00r\x00a\x00n\x00s\x00f\x00e\x00r\x00-\x00d\x00o\x00w\ 127 | \x00n\x00l\x00o\x00a\x00d\x00.\x00s\x00v\x00g\ 128 | " 129 | 130 | qt_resource_struct = b"\ 131 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 132 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ 133 | \x00\x00\x00\x10\x00\x02\x00\x00\x00\x04\x00\x00\x00\x03\ 134 | \x00\x00\x00\x1e\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ 135 | \x00\x00\x00.\x00\x00\x00\x00\x00\x01\x00\x00\x015\ 136 | \x00\x00\x00`\x00\x00\x00\x00\x00\x01\x00\x00\x046\ 137 | \x00\x00\x00H\x00\x00\x00\x00\x00\x01\x00\x00\x02v\ 138 | " 139 | 140 | def qInitResources(): 141 | QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) 142 | 143 | def qCleanupResources(): 144 | QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) 145 | 146 | qInitResources() 147 | -------------------------------------------------------------------------------- /cutterdrcov_plugin/covtable.py: -------------------------------------------------------------------------------- 1 | import cutter 2 | from .extras import hex_pad, file_name 3 | 4 | def get_module_idx(modules, module): 5 | for i in range(len(modules)): 6 | if modules[i]['name'] == module: 7 | return i 8 | return -1 9 | 10 | 11 | 12 | def analyse_function(function, base, covbbs): 13 | entry = ["", function['name'], hex_pad(function['offset'], 8), "", ""] 14 | bbs = cutter.cmdj("afbj @ " + function['name']) 15 | inst_count = 0 16 | inst_hits = 0 17 | bbs_count = 0 18 | bbs_hits = 0 19 | hit_set = set() 20 | to_be_added = {} 21 | for bblock in bbs: 22 | # radare2's basic block can't have jump inside it while that is 23 | # possible in DynamoRIO, for this reason we first need to check 24 | # if the size of the 2 basic block matches, if radare2's basic block 25 | # size is smaller thant we would add the next block untill the sizes 26 | # match. If dynamoRIO size is smaller then something is wrong with 27 | # r2 analysis or dynamoRIO coverage. 28 | bbs_count += 1 29 | inst_count += bblock['ninstr'] 30 | dynamorio_size = 0 31 | covbbaddr = bblock['addr'] - base 32 | if covbbaddr in covbbs: 33 | dynamorio_size = covbbs[covbbaddr] 34 | if bblock['addr'] in to_be_added: 35 | dynamorio_size = to_be_added[bblock['addr']] 36 | if dynamorio_size == 0: 37 | continue 38 | bbs_hits += 1 39 | inst_hits += bblock['ninstr'] 40 | hit_set.add(bblock['addr']) 41 | r2_size = bblock['size'] 42 | if dynamorio_size > r2_size: 43 | to_be_added[bblock['addr'] + r2_size] = dynamorio_size - r2_size 44 | if bbs_hits == 0: 45 | return (None, hit_set) 46 | entry[3] = str(inst_hits) + "/" + str(inst_count) 47 | entry[4] = str(bbs_hits) + "/" + str(bbs_count) 48 | entry[0] = str(round(inst_hits * 100 / inst_count, 3)) + "%" 49 | return (entry, hit_set) 50 | 51 | def analyse(config): 52 | config['bb_hits'] = set() 53 | config['table'] = [] 54 | functions = cutter.cmdj("aflj") 55 | info = cutter.cmdj("ij") 56 | module = file_name(info['core']['file']) 57 | base = info["bin"]["baddr"] 58 | idx = get_module_idx(config['modules'], module) 59 | if idx == -1: 60 | return 61 | # [coverage, name, address, instruction hits, basic block hits] 62 | for function in functions: 63 | entry, hits = analyse_function(function, base, config['bbs'][idx]) 64 | if entry is None: 65 | continue 66 | config['table'].append(entry) 67 | config['bb_hits'].update(hits) 68 | -------------------------------------------------------------------------------- /cutterdrcov_plugin/cutterplugin.py: -------------------------------------------------------------------------------- 1 | from PySide2 import QtCore 2 | from PySide2.QtGui import QColor 3 | from PySide2.QtWidgets import QFileDialog, QColorDialog 4 | import cutter 5 | from .autogen import Ui_DockWidget 6 | from . import drcov 7 | from .covtable import analyse 8 | from . import sortable_table_item as sit 9 | 10 | class MyDockWidget(cutter.CutterDockWidget, Ui_DockWidget): 11 | def __init__(self, parent, action): 12 | super(MyDockWidget, self).__init__(parent, action) 13 | self.setupUi(self) 14 | self.new_config() 15 | self.stackedWidget.setCurrentIndex(0) 16 | self.loader.dragEnterEvent = self.loader_drag_event 17 | self.loader.dropEvent = self.drop_file 18 | self.loader.mousePressEvent = self.open_file 19 | self.colorize.clicked.connect(self.toggle_colorize) 20 | self.selectColor.clicked.connect(self.set_color) 21 | self.close.clicked.connect(self.close_callback) 22 | self.reload.clicked.connect(self.reload_callback) 23 | self.covtable.doubleClicked.connect(self.seek) 24 | 25 | def seek(self, idx): 26 | row = idx.row() 27 | addr = int(self.covtable.item(row, 2).text(), 16) 28 | cutter.core().seek(addr) 29 | 30 | def toggle_colorize(self): 31 | self.config['colorize'] = not self.config['colorize'] 32 | self.paint() 33 | 34 | def close_callback(self): 35 | self.clear_highlight() 36 | self.new_config() 37 | self.stackedWidget.setCurrentIndex(0) 38 | 39 | def reload_callback(self): 40 | self.clear_highlight() 41 | analyse(self.config) 42 | self.paint() 43 | 44 | def new_config(self): 45 | self.config = { 46 | 'color': 0x800000, 47 | 'colorize' : self.colorize.isChecked() 48 | } 49 | 50 | def loader_drag_event(self, event): 51 | if event.mimeData().hasUrls(): 52 | self.loader.setCursor(QtCore.Qt.DragMoveCursor) 53 | event.acceptProposedAction() 54 | 55 | def drop_file(self, event): 56 | self.loader.setCursor(QtCore.Qt.PointingHandCursor) 57 | files = event.mimeData().urls() 58 | file_name = files[0].toLocalFile() 59 | self.loadcov(file_name) 60 | 61 | def open_file(self, _): 62 | file_name = QFileDialog.getOpenFileName(self) 63 | if not file_name[0]: 64 | return 65 | self.loadcov(file_name[0]) 66 | 67 | def loadcov(self, file_name): 68 | try: 69 | modules, bbs = drcov.load(file_name) 70 | except Exception: 71 | self.parent().messageBoxWarning("", "Invalid Coverage File") 72 | return 73 | self.config['modules'] = modules 74 | self.config['bbs'] = bbs 75 | self.stackedWidget.setCurrentIndex(1) 76 | analyse(self.config) 77 | self.display() 78 | self.paint() 79 | 80 | def display(self): 81 | """ 82 | This function displays analysed coverage entires (1 entry per function) 83 | """ 84 | covtable = self.covtable 85 | covtable.clearContents() 86 | covtable.setRowCount(0) 87 | for entry in self.config['table']: 88 | row_position = covtable.rowCount() 89 | covtable.insertRow(row_position) 90 | covtable.setItem(row_position, 0, sit.PercentWidgetItem(entry[0])) 91 | covtable.setItem(row_position, 1, sit.QTableWidgetItem(entry[1])) 92 | covtable.setItem(row_position, 2, sit.HexWidgetItem(entry[2])) 93 | covtable.setItem(row_position, 3, sit.RatioWidgetItem(entry[3])) 94 | covtable.setItem(row_position, 4, sit.centered_text(entry[4])) 95 | 96 | def set_color(self): 97 | """ 98 | This function changes color of drcoved bbs and reflects 99 | the change to gui. 100 | """ 101 | new_color = QColorDialog.getColor(QColor(self.config['color'])) 102 | if not new_color.isValid(): 103 | return 104 | self.config['color'] = new_color.rgb() 105 | self.paint() 106 | 107 | def paint(self): 108 | """ 109 | This function checks if we are allowed to highlight basic block, 110 | if yes it will highlight all drcov-ed basic blocks, otherwise it 111 | will clear remove highlights from all drcov-ed bbs. 112 | """ 113 | if self.config['colorize']: 114 | self.highlight() 115 | else: 116 | self.clear_highlight() 117 | 118 | def highlight(self): 119 | """ 120 | This function highlights all drcov-ed basic blocks 121 | """ 122 | core = cutter.core() 123 | highlighter = core.getBBHighlighter() 124 | for bblock in self.config['bb_hits']: 125 | highlighter.highlight(bblock, self.config['color']) 126 | 127 | def clear_highlight(self): 128 | """ 129 | This function removes highlights from all drcov-ed basic blocks 130 | """ 131 | core = cutter.core() 132 | highlighter = core.getBBHighlighter() 133 | for bblock in self.config['bb_hits']: 134 | highlighter.clear(bblock) 135 | -------------------------------------------------------------------------------- /cutterdrcov_plugin/drcov.py: -------------------------------------------------------------------------------- 1 | import re 2 | import struct 3 | from .extras import file_name 4 | 5 | MIN_DRCOV_FILE_SIZE = 20 6 | DRCOV_VERSION = 3 7 | 8 | DRCOV_HEADER_RE = r"DRCOV VERSION: (?P\d+)\n" 9 | MODULE_HEADER_V2_RE = r"Module Table: version (?P\d+), count (?P\d+)\n" 10 | BB_HEADER_RE = r"BB Table: (?P\d+) bbs\n" 11 | 12 | class DRCovVersionMisMatch(Exception): 13 | pass 14 | 15 | def check_module_header(drcov_file): 16 | header = drcov_file.readline().decode('utf-8') 17 | pattern = re.match(DRCOV_HEADER_RE, header) 18 | version = int(pattern.group('version')) 19 | if version != DRCOV_VERSION: 20 | raise DRCovVersionMisMatch 21 | # "DRCOV FLAVOR" doesn't really matter 22 | drcov_file.readline() 23 | 24 | def get_module_header_info(drcov_file): 25 | header = drcov_file.readline().decode('utf-8') 26 | pattern = re.match(MODULE_HEADER_V2_RE, header) 27 | # skip "Columns: id, containing_id, start, end, entry, offset, path" 28 | drcov_file.readline() 29 | return (int(pattern.group("mod_num")), int(pattern.group("version"))) 30 | 31 | def parse_module_entry(drcov_file, version): 32 | entry = drcov_file.readline().decode('utf-8')[:-1] 33 | #XXX now put commas and spaces in the file path and this gets fucked up 34 | entry = re.split(r",\s+", entry) 35 | if version == 2: 36 | return {"start": int(entry[1], 16), "name": file_name(entry[-1])} 37 | return {"start": int(entry[2], 16), "name": file_name(entry[-1])} 38 | 39 | def read_module_list(drcov_file): 40 | modules = [] 41 | check_module_header(drcov_file) 42 | mod_num, mod_version = get_module_header_info(drcov_file) 43 | for _ in range(mod_num): 44 | modules.append(parse_module_entry(drcov_file, mod_version)) 45 | return modules 46 | 47 | def parse_bb_header(drcov_file): 48 | header = drcov_file.readline().decode('utf-8') 49 | pattern = re.match(BB_HEADER_RE, header) 50 | return int(pattern.group("bbcount")) 51 | 52 | def read_bb_list(drcov_file, module_count): 53 | bblist = [{} for i in range(module_count)] 54 | bb_count = parse_bb_header(drcov_file) 55 | struct_fmt = ' module_count: 63 | # we have a case where dynamocov failed to capture which modules 64 | # does this basic block belongs to 65 | # print("Warning: we have unknown module number:", mod_num) 66 | continue 67 | bblist[mod_num][offset] = size 68 | return bblist 69 | 70 | def dead_module_elimination(modules, bbs): 71 | delete = [] 72 | for i in range(len(bbs)): 73 | if not bbs[i]: 74 | delete.insert(0, i) 75 | for i in delete: 76 | del bbs[i] 77 | del modules[i] 78 | 79 | def load(path): 80 | drcov_file = open(path, "rb") 81 | modules = read_module_list(drcov_file) 82 | bbs = read_bb_list(drcov_file, len(modules)) 83 | drcov_file.close() 84 | dead_module_elimination(modules, bbs) 85 | return [modules, bbs] 86 | -------------------------------------------------------------------------------- /cutterdrcov_plugin/extras.py: -------------------------------------------------------------------------------- 1 | import ntpath 2 | 3 | def hex_pad(num, pad): 4 | return "{0:#0{1}x}".format(num, pad + 2) 5 | 6 | # https://stackoverflow.com/questions/8384737/extract-file-name-from-path-no-matter-what-the-os-path-format 7 | # cuz windows sucks :( .. hard 8 | def file_name(path): 9 | # There's one caveat: Linux filenames may contain backslashes. So on linux, 10 | # r'a/b\c' always refers to the file b\c in the a folder, while on Windows, 11 | # it always refers to the c file in the b subfolder of the a folder. So when 12 | # both forward and backward slashes are used in a path, you need to know the 13 | # associated platform to be able to interpret it correctly. In practice it's 14 | # usually safe to assume it's a windows path since backslashes are seldom 15 | # used in Linux filenames, but keep this in mind when you code so you don't 16 | # create accidental security holes. 17 | 18 | head, tail = ntpath.split(path) 19 | return tail or ntpath.basename(head) 20 | -------------------------------------------------------------------------------- /cutterdrcov_plugin/plugin_interface.py: -------------------------------------------------------------------------------- 1 | from PySide2.QtWidgets import QAction 2 | import cutter 3 | 4 | from .cutterplugin import MyDockWidget 5 | 6 | class CutterCovPlugin(cutter.CutterPlugin): 7 | name = "CutterCov" 8 | description = "Visualize DynamoRIOCov data into Cutter" 9 | version = "0.0.1" 10 | author = "oddcoder" 11 | 12 | def setupPlugin(self): 13 | pass 14 | 15 | def setupInterface(self, main): 16 | action = QAction("CutterCov", main) 17 | action.setCheckable(True) 18 | widget = MyDockWidget(main, action) 19 | main.addPluginDockWidget(widget, action) 20 | 21 | def create_cutter_plugin(): 22 | """ 23 | This function is the only api accessed by cutter, it 24 | would return object of cutter.CutterPlugin for cutter to work with 25 | """ 26 | return CutterCovPlugin() 27 | 28 | -------------------------------------------------------------------------------- /cutterdrcov_plugin/sortable_table_item.py: -------------------------------------------------------------------------------- 1 | from PySide2.QtWidgets import QTableWidgetItem 2 | from PySide2.QtCore import Qt 3 | 4 | class PercentWidgetItem(QTableWidgetItem): 5 | def __lt__(self, other): 6 | return float(self.text()[:-1]) < float(other.text()[:-1]) 7 | 8 | class HexWidgetItem(QTableWidgetItem): 9 | def __lt__(self, other): 10 | return int(self.text(), 16) < int(other.text(), 16) 11 | 12 | class RatioWidgetItem(QTableWidgetItem): 13 | def get_ratio(self): 14 | splt = self.text().split("/") 15 | return int(splt[0]) / int(splt[1]) 16 | def __lt__(self, other): 17 | return self.get_ratio() < other.get_ratio() 18 | 19 | def centered_text(txt): 20 | item = RatioWidgetItem(txt) 21 | item.setTextAlignment(Qt.AlignCenter) 22 | return item 23 | -------------------------------------------------------------------------------- /resources/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /resources/icon.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon/brush.svg 4 | icon/reload.svg 5 | icon/x.svg 6 | icon/data-transfer-download.svg 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/icon/brush.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /resources/icon/data-transfer-download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icon/reload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icon/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /screanshots/overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/screanshots/overview.jpg -------------------------------------------------------------------------------- /screanshots/path.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/screanshots/path.jpg -------------------------------------------------------------------------------- /screanshots/usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/screanshots/usage.gif -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | # W0703: Catching too general exception 4 | # C0111: missing-docstring 5 | # E0401: import-error (we hook import cutter) 6 | # E0611: No name X in module Y 7 | # C0200: Consider using enumerate instead of iterating with range and len 8 | # R0903: too-few-public-methods 9 | # W0511: fixme 10 | pylint --disable=W0511,R0903,C0200,E0611,W0703,C0111,I1101,E0401 \ 11 | --ignore=autogen,plugin_interface.py cutterdrcov_plugin 12 | lint1=$? 13 | # C0413: wrong-import-position 14 | # R0201: no-self-use (use method as function) 15 | pylint --disable=C0413,C0111,R0201,R0903,E0401 unittest/*.py 16 | lint2=$? 17 | if [ $lint1 -ne 0 ]; then 18 | exit $lint1 19 | fi 20 | if [ $lint2 -ne 0 ]; then 21 | exit $lint2 22 | fi 23 | -------------------------------------------------------------------------------- /scripts/local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | 4 | ./scripts/lint.sh 5 | python unittest 6 | -------------------------------------------------------------------------------- /scripts/travis.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | 4 | ./scripts/lint.sh 5 | lint_result=$? 6 | coverage run -m unittest discover unittest 7 | if [ $lint_result -ne 0 ]; then 8 | exit $lint_result 9 | fi 10 | 11 | -------------------------------------------------------------------------------- /test_files/drcov.test2.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/test_files/drcov.test2.log -------------------------------------------------------------------------------- /test_files/drcov.test3.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/test_files/drcov.test3.log -------------------------------------------------------------------------------- /test_files/drcov2.2.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/test_files/drcov2.2.log -------------------------------------------------------------------------------- /test_files/drcov2.3.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/test_files/drcov2.3.log -------------------------------------------------------------------------------- /test_files/drcov2.4.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/test_files/drcov2.4.log -------------------------------------------------------------------------------- /test_files/drcov_orphan.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/test_files/drcov_orphan.log -------------------------------------------------------------------------------- /test_files/test1.asm: -------------------------------------------------------------------------------- 1 | SECTION .text 2 | GLOBAL _start 3 | _start: 4 | mov eax, 5 5 | mov ebx, 6 6 | mov ecx, 7 7 | cmp ecx, 6 8 | je BB2 9 | mov eax, 7 10 | jmp BB3 11 | 12 | BB2: mov eax, 5 13 | mov ebx, 6 14 | mov ecx, 7 15 | 16 | BB3: mov eax,1 ; 'exit' system call 17 | mov ebx,0 ; exit with error code 0 18 | int 80h ; call the kernel 19 | 20 | -------------------------------------------------------------------------------- /test_files/test1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/test_files/test1.bin -------------------------------------------------------------------------------- /test_files/test1.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/test_files/test1.o -------------------------------------------------------------------------------- /test_files/test2.asm: -------------------------------------------------------------------------------- 1 | SECTION .text 2 | GLOBAL _start 3 | GLOBAL uncalled 4 | 5 | uncalled: 6 | push ebp 7 | mov ebp, esp 8 | mov eax, 1 9 | pop ebp 10 | ret 11 | 12 | _start: 13 | mov eax, 5 14 | mov ebx, 6 15 | mov ecx, 7 16 | cmp ecx, 6 17 | je BB2 18 | mov eax, 7 19 | jmp BB3 20 | 21 | BB2: mov eax, 5 22 | mov ebx, 6 23 | mov ecx, 7 24 | 25 | BB3: mov eax,1 ; 'exit' system call 26 | mov ebx,0 ; exit with error code 0 27 | int 80h ; call the kernel 28 | call uncalled 29 | -------------------------------------------------------------------------------- /test_files/test2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/test_files/test2.bin -------------------------------------------------------------------------------- /test_files/test2.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/test_files/test2.o -------------------------------------------------------------------------------- /test_files/test3.asm: -------------------------------------------------------------------------------- 1 | SECTION .text 2 | GLOBAL _start 3 | _start: 4 | mov eax, 5 5 | mov ebx, 6 6 | mov ecx, 7 7 | cmp ecx, 1 8 | jne BB2 9 | cmp eax, 2 10 | je BB3 11 | cmp eax, 3 12 | je BB4 13 | cmp eax, 4 14 | je BB5 15 | 16 | BB2: mov eax, 5 17 | mov ebx, 6 18 | mov ecx, 7 19 | BB3: mov eax, 8 20 | mov ebx, 9 21 | mov ecx, 10 22 | BB4: mov eax, 11 23 | mov ebx, 12 24 | mov ecx, 13 25 | BB5: mov eax,1 ; 'exit' system call 26 | mov ebx,0 ; exit with error code 0 27 | int 80h ; call the kernel 28 | -------------------------------------------------------------------------------- /test_files/test3.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/test_files/test3.bin -------------------------------------------------------------------------------- /test_files/test3.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizinorg/CutterDRcov/7735cafd44ffa2cdf4dca950d6d13d74a3d2fb25/test_files/test3.o -------------------------------------------------------------------------------- /ui/dock.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | DockWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 687 10 | 455 11 | 12 | 13 | 14 | Cutter DynamoRIO Coverage 15 | 16 | 17 | 18 | 19 | 20 | 21 | false 22 | 23 | 24 | 1 25 | 26 | 27 | 28 | 29 | 30 | 31 | PointingHandCursor 32 | 33 | 34 | true 35 | 36 | 37 | 38 | 39 | 40 | <html><head/><body><p align="center"><img src=":/icons/icon/data-transfer-download.svg"/></p><p align="center">Click to <span style=" font-weight:600;">Open drcov file</span> or drag it here.</p></body></html> 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | :/icons/icon/x.svg:/icons/icon/x.svg 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | :/icons/icon/reload.svg:/icons/icon/reload.svg 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | :/icons/icon/brush.svg:/icons/icon/brush.svg 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Qt::Horizontal 89 | 90 | 91 | 92 | 540 93 | 38 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | Colorize (On / OFF) 102 | 103 | 104 | true 105 | 106 | 107 | 108 | 109 | 110 | 111 | QFrame::NoFrame 112 | 113 | 114 | QFrame::Plain 115 | 116 | 117 | QAbstractItemView::NoEditTriggers 118 | 119 | 120 | true 121 | 122 | 123 | QAbstractItemView::ContiguousSelection 124 | 125 | 126 | QAbstractItemView::SelectRows 127 | 128 | 129 | false 130 | 131 | 132 | true 133 | 134 | 135 | 118 136 | 137 | 138 | true 139 | 140 | 141 | false 142 | 143 | 144 | 145 | Coverage 146 | 147 | 148 | 149 | 150 | Function Name 151 | 152 | 153 | 154 | 155 | Address 156 | 157 | 158 | 159 | 160 | Instructions Hits 161 | 162 | 163 | 164 | 165 | Basic Block Hits 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /unittest/__main__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | def main(): 4 | loader = unittest.TestLoader() 5 | current_dir = os.path.dirname(os.path.realpath(__file__)) 6 | test_suite = loader.discover(current_dir) 7 | runner = unittest.TextTestRunner(verbosity=2) 8 | runner.run(test_suite) 9 | 10 | if __name__ == "__main__": 11 | main() 12 | -------------------------------------------------------------------------------- /unittest/test_covtable.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | from mock import patch 4 | sys.path.append(".") 5 | 6 | class FakeCutter(): 7 | def cmdj(self, _): 8 | return "" 9 | 10 | sys.modules['cutter'] = FakeCutter() 11 | 12 | import cutter 13 | from cutterdrcov_plugin import covtable, drcov 14 | 15 | def test_1_cmdj(cmd): 16 | if cmd == "ij": 17 | return { 18 | 'core': {'file': 'test1.bin'}, 19 | 'bin': {'baddr': 0x8048000} 20 | } 21 | if cmd == "aflj": 22 | return [{'offset': 0x8048060, 'name': 'entry0'}] 23 | if cmd == "afbj @entry0": 24 | return [ 25 | {'addr': 0x8048060, 'size': 20, 'ninstr': 5}, 26 | {'addr': 0x8048074, 'size': 7, 'ninstr': 2}, 27 | {'addr': 0x804807b, 'size': 15, 'ninstr': 3}, 28 | {'addr': 0x804808a, 'size': 12, 'ninstr': 3} 29 | ] 30 | return None 31 | 32 | def test_2_cmdj(cmd): 33 | if cmd == "ij": 34 | return { 35 | 'core': {'file': 'test2.bin'}, 36 | 'bin': {'baddr': 0x8048000} 37 | } 38 | if cmd == "aflj": 39 | return [ 40 | {'offset': 0x8048060, 'name': 'loc.uncalled'}, 41 | {'offset': 0x804806a, 'name': 'entry0'} 42 | ] 43 | if cmd == "afbj @entry0": 44 | return [ 45 | {'addr': 0x804806a, 'size': 20, 'ninstr': 5}, 46 | {'addr': 0x804807e, 'size': 7, 'ninstr': 2}, 47 | {'addr': 0x8048085, 'size': 15, 'ninstr': 3}, 48 | {'addr': 0x8048094, 'size': 17, 'ninstr': 4} 49 | ] 50 | if cmd == "afbj @loc.uncalled": 51 | return [ 52 | {"addr": 0x8048060, "size": 10, "ninstr": 5} 53 | ] 54 | return None 55 | 56 | def test_3_cmdj(cmd): 57 | if cmd == "ij": 58 | return { 59 | "core": {"file": "/some/crazy/path/test3.bin",}, 60 | "bin":{"baddr": 0x8048000} 61 | } 62 | if cmd == "aflj": 63 | return [{"offset": 0x8048060, "name": "entry0"}] 64 | if cmd == "afbj @entry0": 65 | return [ 66 | {"addr":134512736, "size":20, "ninstr":5}, 67 | {"addr":134512756, "size":5, "ninstr":2}, 68 | {"addr":134512761, "size":5, "ninstr":2}, 69 | {"addr":134512766, "size":5, "ninstr":2}, 70 | {"addr":134512771, "size":15, "ninstr":3}, 71 | {"addr":134512786, "size":15, "ninstr":3}, 72 | {"addr":134512801, "size":15, "ninstr":3}, 73 | {"addr":134512816, "size":12, "ninstr":3} 74 | ] 75 | return None 76 | 77 | 78 | 79 | class TestcovTable(unittest.TestCase): 80 | 81 | @patch("cutter.cmdj", side_effect=test_1_cmdj) 82 | def test_analyse_function(self, _): 83 | # Here, checking if cutter works as expected is out of scope but rather 84 | # what I am checking is given that cutter works as expected does my code 85 | # work as expected as well or not 86 | _, bbs = drcov.load("test_files/drcov2.4.log") 87 | function = cutter.cmdj("aflj")[0] 88 | base = cutter.cmdj("ij")['bin']['baddr'] 89 | entry, hits = covtable.analyse_function(function, base, bbs[0]) 90 | hardcoded_entry = ['76.923%', 'entry0', '0x08048060', '10/13', '3/4'] 91 | self.assertEqual(entry, hardcoded_entry) 92 | self.assertEqual(hits, {0x8048060, 0x804808a, 0x8048074}) 93 | 94 | def do_analyse(self, modules, bbs, table, hits): 95 | config = {} 96 | config['modules'] = modules 97 | config['bbs'] = bbs 98 | covtable.analyse(config) 99 | self.assertEqual(config['table'], table) 100 | self.assertEqual(config['bb_hits'], hits) 101 | 102 | @patch("cutter.cmdj", side_effect=test_1_cmdj) 103 | def test_analyse1(self, _): 104 | modules, bbs = drcov.load("test_files/drcov2.4.log") 105 | table = [['76.923%', 'entry0', '0x08048060', '10/13', '3/4']] 106 | hits = {0x8048060, 0x804808a, 0x8048074} 107 | self.do_analyse(modules, bbs, table, hits) 108 | 109 | @patch("cutter.cmdj", side_effect=test_2_cmdj) 110 | def test_analyse2(self, _): 111 | """ 112 | This test case where not all functions gets covered, 113 | so they needed be reported 114 | """ 115 | modules, bbs = drcov.load("test_files/drcov.test2.log") 116 | # technically speaking only 10 instructions can get executed 117 | # but what can I do ... any way it wouldn't matter 118 | table = [["78.571%", "entry0", "0x0804806a", "11/14", "3/4"]] 119 | hits = {0x0804806a, 0x0804807e, 0x08048094} 120 | self.do_analyse(modules, bbs, table, hits) 121 | 122 | @patch("cutter.cmdj", side_effect=test_3_cmdj) 123 | def test_analyse3(self, _): 124 | """ 125 | This tests the case when dynamoRIO block is equivalent to more 126 | than one radare2 block 127 | """ 128 | modules, bbs = drcov.load("test_files/drcov.test3.log") 129 | table = [["73.913%", "entry0", "0x08048060", "17/23", "5/8"]] 130 | hits = {0x08048060, 0x08048083, 0x08048092, 0x080480a1, 0x080480b0} 131 | self.do_analyse(modules, bbs, table, hits) 132 | 133 | @patch("cutter.cmdj", side_effect=test_1_cmdj) 134 | def test_bad_analysis(self, _): 135 | """ 136 | I found this by mistake, it triggered exception while it shouldn't 137 | """ 138 | modules, bbs = drcov.load("test_files/drcov.test2.log") 139 | self.do_analyse(modules, bbs, [], set()) 140 | 141 | if __name__ == '__main__': 142 | unittest.main() 143 | -------------------------------------------------------------------------------- /unittest/test_drcov.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | sys.path.append(".") 4 | from cutterdrcov_plugin import drcov 5 | 6 | class TestDRcov(unittest.TestCase): 7 | def verify_test1_asm(self, covfile): 8 | """ 9 | covfile: file name 10 | This function tests that given coverage file represents 11 | running test_files/test1.asm 12 | """ 13 | modules, bbs = drcov.load(covfile) 14 | self.assertEqual(len(modules), 1) 15 | self.assertEqual(modules[0]['start'], 0x8048000) 16 | self.assertEqual(modules[0]['name'], 'test1.bin') 17 | self.assertEqual(len(bbs), 1) 18 | self.assertEqual(len(bbs[0]), 3) 19 | bbs = bbs[0] 20 | self.assertEqual(bbs[0x60], 20) 21 | self.assertEqual(bbs[0x74], 7) 22 | self.assertEqual(bbs[0x8a], 12) 23 | 24 | def test_drcov_2_4_linux(self): 25 | """ 26 | DRCOV VERSION: 2 27 | Module Table: version 4 28 | dynamoRIO 7.1.0-1 linux 32 29 | test1.asm 30 | """ 31 | self.verify_test1_asm("test_files/drcov2.4.log") 32 | 33 | def test_drcov_2_3_linux(self): 34 | """ 35 | DRCOV VERSION: 2 36 | Module Table: version 3 37 | dynamoRIO 7.0.17595-0 linux 32 38 | test1.asm 39 | """ 40 | self.verify_test1_asm("test_files/drcov2.3.log") 41 | 42 | def test_drcov_2_2_linux(self): 43 | """ 44 | DRCOV VERSION: 2 45 | Module Table: version 2 46 | dynamoRIO 7.0.0-RC1 linux 32 47 | test1.asm 48 | """ 49 | self.verify_test1_asm("test_files/drcov2.2.log") 50 | 51 | def test_drcov_orphan(self): 52 | """ 53 | Some times a block structure will reference module with 54 | numbers that wasn't previously declared typically 0xffff 55 | I don't know what would cause this to happen, but I have 56 | seen it inside some complex application. I couldn't write 57 | code that would trigger this behavior so instead I wrote a 58 | test drcov file that would have this trait. 59 | """ 60 | self.verify_test1_asm("test_files/drcov_orphan.log") 61 | if __name__ == '__main__': 62 | unittest.main() 63 | -------------------------------------------------------------------------------- /unittest/test_extras.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | sys.path.append(".") 4 | from cutterdrcov_plugin import extras 5 | 6 | class TestExtras(unittest.TestCase): 7 | 8 | def test_hex_pad(self): 9 | self.assertEqual(extras.hex_pad(0x123, 0), "0x123") 10 | self.assertEqual(extras.hex_pad(0x123, 1), "0x123") 11 | self.assertEqual(extras.hex_pad(0x123, 2), "0x123") 12 | self.assertEqual(extras.hex_pad(0x123, 3), "0x123") 13 | self.assertEqual(extras.hex_pad(0x123, 4), "0x0123") 14 | self.assertEqual(extras.hex_pad(0x123, 5), "0x00123") 15 | self.assertEqual(extras.hex_pad(0x123, 6), "0x000123") 16 | self.assertEqual(extras.hex_pad(0x123, 7), "0x0000123") 17 | self.assertEqual(extras.hex_pad(0x123, 8), "0x00000123") 18 | 19 | def test_file_name(self): 20 | self.assertEqual(extras.file_name(r"C:\Windows\notepad.exe"), "notepad.exe") 21 | self.assertEqual(extras.file_name(r"C:/Windows/notepad.exe"), "notepad.exe") 22 | self.assertEqual(extras.file_name(r"/bin/ls"), "ls") 23 | self.assertEqual(extras.file_name(r"./test"), "test") 24 | if __name__ == '__main__': 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /unittest/test_sortable_table_item.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | sys.path.append(".") 4 | import cutterdrcov_plugin.sortable_table_item as sti 5 | 6 | class TestSortableTableItem(unittest.TestCase): 7 | 8 | def check_lt(self, objs): 9 | self.assertEqual(objs[0] < objs[1], True) 10 | self.assertEqual(objs[1] < objs[2], False) 11 | self.assertEqual(objs[2] < objs[0], False) 12 | 13 | def test_percent_widget_item(self): 14 | objs = [ 15 | sti.PercentWidgetItem("50%"), 16 | sti.PercentWidgetItem("100%"), 17 | sti.PercentWidgetItem("50%") 18 | ] 19 | self.check_lt(objs) 20 | 21 | def test_hex_widget_item(self): 22 | objs = [ 23 | sti.HexWidgetItem("0x5"), 24 | sti.HexWidgetItem("0X07"), 25 | sti.HexWidgetItem("0x5") 26 | ] 27 | self.check_lt(objs) 28 | 29 | def test_ratio_widget_item(self): 30 | objs = [ 31 | sti.RatioWidgetItem("1/2"), 32 | sti.RatioWidgetItem("3/4"), 33 | sti.RatioWidgetItem("1/2") 34 | ] 35 | self.check_lt(objs) 36 | 37 | if __name__ == '__main__': 38 | unittest.main() 39 | --------------------------------------------------------------------------------