Commits

Daniel Plohmann  committed aa03bbd

working progress, added YaraScannerWidget and made it functional.

  • Participants
  • Parent commits 2a3e8c5

Comments (0)

Files changed (4)

 from idascope.widgets.FunctionInspectionWidget import FunctionInspectionWidget
 from idascope.widgets.WinApiWidget import WinApiWidget
 from idascope.widgets.CryptoIdentificationWidget import CryptoIdentificationWidget
+from idascope.widgets.YaraScannerWidget import YaraScannerWidget
 
 ################################################################################
 # Core of the IDAscope GUI.
 
 HOTKEYS = None
 IDASCOPE = None
-NAME = "simpliFiRE.IDAscope v1.0"
+NAME = "simpliFiRE.IDAscope v1.1"
 
 
 class IDAscopeForm(PluginForm):
         time_before = time.time()
         print ("[/] setting up shared modules...")
         # FIXME: revert commenting
-        # self.semantic_identifier = SemanticIdentifier(self.config)
-        # self.crypto_identifier = CryptoIdentifier()
-        # self.documentation_helper = DocumentationHelper(self.config)
-        # self.winapi_provider = WinApiProvider(self.config)
+        self.documentation_helper = DocumentationHelper(self.config)
+        self.semantic_identifier = SemanticIdentifier(self.config)
+        self.winapi_provider = WinApiProvider(self.config)
+        self.crypto_identifier = CryptoIdentifier()
         self.yara_scanner = YaraScanner(self.config)
         self.ida_proxy = IdaProxy()
         print ("[\\] this took %3.2f seconds.\n" % (time.time() - time_before))
         time_before = time.time()
         print ("[/] setting up widgets...")
         # FIXME: revert commenting
-        # self.idascope_widgets.append(FunctionInspectionWidget(self))
-        # self.idascope_widgets.append(WinApiWidget(self))
-        # self.idascope_widgets.append(CryptoIdentificationWidget(self))
+        self.idascope_widgets.append(FunctionInspectionWidget(self))
+        self.idascope_widgets.append(WinApiWidget(self))
+        self.idascope_widgets.append(CryptoIdentificationWidget(self))
+        self.idascope_widgets.append(YaraScannerWidget(self))
         self.setupIDAscopeForm()
         print ("[\\] this took %3.2f seconds.\n" % (time.time() - time_before))
 

File idascope/core/YaraScanner.py

     """
 
     def __init__(self, idascope_config):
+        # FIXME: APT1 sample source: http://contagiodump.blogspot.de/2013/03/mandiant-apt1-samples-categorized-by.html
         print ("[|] loading YaraScanner")
         self.os = os
         self.re = re
         self.yara = yara
         # fields
         self.idascope_config = idascope_config
+        self.num_files_loaded = 0
         self._yara_rules = []
         self._results = []
-        # FIXME: test more, then create GUI
-        self.test()
+        self.segment_offsets = []
 
     def test(self):
         self.load_rules()
-        print self._yara_rules
         self.scan()
-        print self._results
+
+    def getResults(self):
+        return self._results
 
     def load_rules(self):
+        self.num_files_loaded = 0
         self._yara_rules = []
         for yara_path in self.idascope_config.yara_sig_folders:
             for dirpath, dirnames, filenames in os.walk(yara_path):
                     try:
                         rules = yara.compile(filepath)
                         self._yara_rules.append(rules)
+                        if rules:
+                            self.num_files_loaded += 1
                     except:
                         print "[!] Could not load yara rules file: %s" % filepath
 
     def _get_memory(self):
         result = ""
-        start = [ea for ea in self.ida_proxy.Segments()][0]
-        end = self.ida_proxy.SegEnd(start)
-        for ea in Misc.lrange(start, end):
-            result += chr(self.ida_proxy.Byte(ea))
-        return result
+        segment_starts = [ea for ea in self.ida_proxy.Segments()]
+        offsets = []
+        start_len = 0
+        for start in segment_starts:
+            end = self.ida_proxy.SegEnd(start)
+            for ea in Misc.lrange(start, end):
+                result += chr(self.ida_proxy.Byte(ea))
+            offsets.append((start, start_len, len(result)))
+            start_len = len(result)
+        return result, offsets
 
     def _result_callback(self, data):
+        adjusted_offsets = []
+        for string in data["strings"]:
+            adjusted_offsets.append((self._translateMemOffsetToVirtualAddress(string[0]), string[1], string[2]))
+        data["strings"] = adjusted_offsets
         self._results.append(data)
+        if data["matches"]:
+            print "  [+] Yara Match for signature: %s" % data["rule"]
         yara.CALLBACK_CONTINUE
 
+    def _translateMemOffsetToVirtualAddress(self, offset):
+        va_offset = 0
+        for seg in self.segment_offsets:
+            if seg[1] < offset < seg[2]:
+                va_offset = seg[0] + (offset - seg[1])
+        return va_offset
+
     def scan(self):
-        memory = self._get_memory()
+        memory, offsets = self._get_memory()
+        self.segment_offsets = offsets
         self._results = []
         matches = []
+        print "[!] Performing Yara scan..."
         for rule in self._yara_rules:
             matches.append(rule.match(data=memory, callback=self._result_callback))
+        if len(matches) == 0:
+            print "  [-] no matches. :("

File idascope/icons/yarascan.png

Added
New image

File idascope/widgets/YaraScannerWidget.py

+#!/usr/bin/python
+########################################################################
+# Copyright (c) 2014
+# Daniel Plohmann <daniel.plohmann<at>gmail<dot>com>
+# Alexander Hanel <alexander.hanel<at>gmail<dot>com>
+# All rights reserved.
+########################################################################
+#
+#  This file is part of IDAscope
+#
+#  IDAscope is free software: you can redistribute it and/or modify it
+#  under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful, but
+#  WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see
+#  <http://www.gnu.org/licenses/>.
+#
+########################################################################
+
+from PySide import QtGui, QtCore
+from PySide.QtGui import QIcon
+
+from NumberQTableWidgetItem import NumberQTableWidgetItem
+
+
+class YaraScannerWidget(QtGui.QMainWindow):
+
+    def __init__(self, parent):
+        QtGui.QMainWindow.__init__(self)
+        print "[|] loading YaraScannerWidget"
+        # enable access to shared IDAscope modules
+        self.parent = parent
+        self.name = "Yara Scanner"
+        # FIXME: select a icon for this
+        self.icon = QIcon(self.parent.config.icon_file_path + "yarascan.png")
+        # This widget relies on yara scanner for resuls and scanning as well as IdaProxy for navigation
+        self.ys = self.parent.yara_scanner
+        self.ip = self.parent.ida_proxy
+        # references to Qt-specific modules
+        self.QtGui = QtGui
+        self.QtCore = QtCore
+        self.NumberQTableWidgetItem = NumberQTableWidgetItem
+
+        self.central_widget = self.QtGui.QWidget()
+        self.setCentralWidget(self.central_widget)
+        self._createGui()
+
+    def _createGui(self):
+        """
+        Setup function for the full GUI of this widget.
+        """
+        # toolbar
+        self._createToolbar()
+        # Overview of rules and matches
+        self.result_table = None
+        self._createRulesWidget()
+        # Details for a selected rule
+        self._createResultWidget()
+        # layout and fill the widget
+        yara_layout = QtGui.QVBoxLayout()
+        splitter = self.QtGui.QSplitter(self.QtCore.Qt.Vertical)
+        q_clean_style = QtGui.QStyleFactory.create('Plastique')
+        splitter.setStyle(q_clean_style)
+        splitter.addWidget(self.rules_widget)
+        splitter.addWidget(self.result_widget)
+        yara_layout.addWidget(splitter)
+
+        self.central_widget.setLayout(yara_layout)
+
+    def _createToolbar(self):
+        """
+        Creates the toolbar, containing buttons to control the widget.
+        """
+        self._createLoadAndScanAction()
+        self.toolbar = self.addToolBar('Yara Scanner Toobar')
+        self.toolbar.addAction(self.loadAndScanAction)
+
+    def _createLoadAndScanAction(self):
+        """
+        Create an action for the scan button of the toolbar and connect it.
+        """
+        self.loadAndScanAction = QtGui.QAction(QIcon(self.parent.config.icon_file_path + "search.png"), \
+            "(Re)load Yara Signature files and scan", self)
+        self.loadAndScanAction.triggered.connect(self._onLoadAndScanButtonClicked)
+
+    def _onLoadAndScanButtonClicked(self):
+        """
+        reload yara rules and scan all segments, then present results
+        """
+        self.ys.load_rules()
+        self.ys.scan()
+        self.setRulesLabel(len(self.ys.getResults()), self.ys.num_files_loaded)
+        self.populateRulesTable()
+
+################################################################################
+# Rules GUI
+################################################################################
+
+    def _createRulesWidget(self):
+        """
+        Create the widget for the arithmetic/logic heuristic.
+        """
+        self.rules_widget = QtGui.QWidget()
+        rules_layout = QtGui.QVBoxLayout()
+        self.rules_label = self.QtGui.QLabel()
+        self.setRulesLabel(0, 0)
+
+        # rule visualization
+        self.rules_widget = QtGui.QWidget()
+        rules_layout = QtGui.QVBoxLayout()
+        self._createRuleTable()
+
+        # widget composition
+        rules_layout.addWidget(self.rules_label)
+        rules_layout.addWidget(self.rules_table)
+        self.rules_widget.setLayout(rules_layout)
+
+    def setRulesLabel(self, num_rules, num_files):
+        self.rules_label.setText("Results for %d rules loaded from %d files" % (num_rules, num_files))
+
+    def _createRuleTable(self):
+        """
+        Create the result table for displaying results yara scanning
+        """
+        self.rules_table = QtGui.QTableWidget()
+        self.populateRulesTable()
+        self.rules_table.clicked.connect(self._onRuleClicked)
+
+    def populateRulesTable(self):
+        """
+        Populate the result table for yara scanning.
+        Called everytime rules are loaded / scanned.
+        """
+        self.rules_table.clear()
+        self.rules_table.setSortingEnabled(False)
+
+        rule_results = self.ys.getResults()
+
+        self._setRuleTableHeaderLabels()
+        table_data = self._calculateRuleTableData(rule_results)
+        row_count = len(table_data)
+
+        self.rules_table.setColumnCount(len(self.rules_table_header_labels))
+        self.rules_table.setHorizontalHeaderLabels(self.rules_table_header_labels)
+        self.rules_table.setRowCount(row_count)
+        self.rules_table.resizeRowToContents(0)
+
+        for row, data_item in enumerate(table_data):
+            for column, column_name in enumerate(self.rules_table_header_labels):
+                tmp_item = self._getRuleTableItem(data_item, column)
+                tmp_item.setFlags(tmp_item.flags() & ~self.QtCore.Qt.ItemIsEditable)
+                tmp_item.setTextAlignment(self.QtCore.Qt.AlignRight)
+                self.rules_table.setItem(row, column, tmp_item)
+            self.rules_table.resizeRowToContents(row)
+
+        self.rules_table.setSelectionMode(self.QtGui.QAbstractItemView.SingleSelection)
+        self.rules_table.resizeColumnsToContents()
+        self.rules_table.setSortingEnabled(True)
+
+        if len(rule_results) > 0:
+            self.populateResultTable(rule_results[0])
+
+    def _calculateRuleTableData(self, rule_results):
+        """
+        Prepare data for display in the result table for yara scan.
+        @param rule_results: results of matching as performed by Yarascanner
+        @type: rule_results: a list of dict, as generated by Yara
+        @return: (a list of elements) where elements are temporary dictionaries prepared for display
+        """
+        result = []
+        for rule in rule_results:
+            string_ids = [item[1] for item in rule["strings"]]
+            num_unique_strings = len(set(string_ids))
+            result.append({"name": rule["rule"], "strings": num_unique_strings, "match": "%s" % rule["matches"]})
+        return result
+
+    def _setRuleTableHeaderLabels(self):
+        """
+        Set the header labels for the yara scan result table.
+        """
+        self.rules_table_header_labels = ["Rule Name", "Strings Matched", "Match Outcome"]
+
+    def _getRuleTableItem(self, data_item, column_index):
+        """
+        Transform a data item for display in the result table
+        @param data_item: the item to prepare for display
+        @type data_item: a dictionary containing rule results
+        @param column_index: the column to prepare the item for
+        @type column_index: int
+        @return: the prepared item
+        """
+        tmp_item = self.QtGui.QTableWidgetItem()
+        if column_index == 0:
+            tmp_item = self.QtGui.QTableWidgetItem(data_item["name"])
+        elif column_index == 1:
+            tmp_item = self.NumberQTableWidgetItem("%d" % data_item["strings"])
+        elif column_index == 2:
+            tmp_item = self.QtGui.QTableWidgetItem(data_item["match"])
+        if data_item["match"] == "True":
+            tmp_item.setBackground(self.QtGui.QBrush(self.QtGui.QColor(0xCC0000)))
+        elif data_item["match"] == "False" and data_item["strings"] > 0:
+            tmp_item.setBackground(self.QtGui.QBrush(self.QtGui.QColor(0xFFBB00)))
+        else:
+            tmp_item.setBackground(self.QtGui.QBrush(self.QtGui.QColor(0x22CC00)))
+        return tmp_item
+
+    def _onRuleClicked(self, mi):
+        """
+        The action to perform when an entry in the arithmetic/logic table is double clicked.
+        Changes IDA View either to the function or basic block, depending on the column clicked.
+        """
+        clicked_rule_name = self.rules_table.item(mi.row(), 0).text()
+        for rule_result in self.ys.getResults():
+            if rule_result["rule"] == clicked_rule_name:
+                print "rule found", rule_result
+                self.populateResultTable(rule_result)
+
+################################################################################
+# Detailed Result GUI
+################################################################################
+
+    def _createResultWidget(self):
+        """
+        Create the widget for the arithmetic/logic heuristic.
+        """
+        self.result_widget = QtGui.QWidget()
+        result_layout = QtGui.QVBoxLayout()
+        num_hits = 0
+        num_strings = 0
+        self.result_label = QtGui.QLabel("%d out of %d strings matched" % (num_hits, num_strings))
+
+        # rule visualization
+        self.result_widget = QtGui.QWidget()
+        result_layout = QtGui.QVBoxLayout()
+        self._createResultTable()
+
+        # widget composition
+        result_layout.addWidget(self.result_label)
+        result_layout.addWidget(self.result_table)
+        self.result_widget.setLayout(result_layout)
+
+    def _createResultTable(self):
+        """
+        Create the result table for displaying results yara scanning
+        """
+        self.result_table = QtGui.QTableWidget()
+        self.populateResultTable({})
+        self.result_table.doubleClicked.connect(self._onResultDoubleClicked)
+
+    def populateResultTable(self, rule_result):
+        """
+        Populate the result table for yara scanning.
+        Called everytime rules are loaded / scanned.
+        """
+        self.result_table.clear()
+        self.result_table.setSortingEnabled(False)
+
+        self._setResultTableHeaderLabels()
+        table_data = self._calculateResultTableData(rule_result)
+        row_count = len(table_data)
+        self.result_label.setText("%d out of ? strings matched" % (row_count))
+
+        self.result_table.setColumnCount(len(self.result_table_header_labels))
+        self.result_table.setHorizontalHeaderLabels(self.result_table_header_labels)
+        self.result_table.setRowCount(row_count)
+        self.result_table.resizeRowToContents(0)
+
+        for row, data_item in enumerate(table_data):
+            for column, column_name in enumerate(self.result_table_header_labels):
+                tmp_item = self._getResultTableItem(data_item, column)
+                tmp_item.setFlags(tmp_item.flags() & ~self.QtCore.Qt.ItemIsEditable)
+                tmp_item.setTextAlignment(self.QtCore.Qt.AlignRight)
+                self.result_table.setItem(row, column, tmp_item)
+            self.result_table.resizeRowToContents(row)
+
+        self.result_table.setSelectionMode(self.QtGui.QAbstractItemView.SingleSelection)
+        self.result_table.resizeColumnsToContents()
+        self.result_table.setSortingEnabled(True)
+
+    def _setResultTableHeaderLabels(self):
+        """
+        Set the header labels for the yara scan result table.
+        """
+        self.result_table_header_labels = ["Address", "String ID", "Value"]
+
+    def _calculateResultTableData(self, rule_result):
+        """
+        Prepare data for display in the result table for yara scan.
+        @param rule_results: results of matching as performed by Yarascanner
+        @type: rule_results: a list of dict, as generated by Yara
+        @return: (a list of elements) where elements are temporary dictionaries prepared for display
+        """
+        if "strings" in rule_result:
+            return rule_result["strings"]
+        else:
+            return []
+
+    def _getResultTableItem(self, data_item, column_index):
+        """
+        Transform a data item for display in the result table
+        @param data_item: the item to prepare for display
+        @type data_item: a dictionary containing rule results
+        @param column_index: the column to prepare the item for
+        @type column_index: int
+        @return: the prepared item
+        """
+        tmp_item = self.QtGui.QTableWidgetItem()
+        if column_index == 0:
+            tmp_item = self.QtGui.QTableWidgetItem("0x%x" % data_item[0])
+        elif column_index == 1:
+            tmp_item = self.QtGui.QTableWidgetItem("%s" % data_item[1])
+        elif column_index == 2:
+            tmp_item = self.QtGui.QTableWidgetItem("%s" % data_item[2])
+        return tmp_item
+
+    def _onResultDoubleClicked(self, mi):
+        """
+        The action to perform when an entry in the arithmetic/logic table is double clicked.
+        Changes IDA View either to the function or basic block, depending on the column clicked.
+        """
+        clicked_address = 0
+        if mi.column() == 0 or mi.column() == 1:
+            clicked_address = self.result_table.item(mi.row(), 0).text()
+        elif mi.column() >= 2:
+            clicked_address = self.result_table.item(mi.row(), 2).text()
+        self.ip.Jump(int(clicked_address, 16))