Commits

Daniel Plohmann committed bfcfaa5

release commit, dialog for showing the selected rule's source and minor improvements

Comments (0)

Files changed (8)

         """
         time_before = time.time()
         print ("[/] setting up shared modules...")
-        # FIXME: revert commenting
-        # self.documentation_helper = DocumentationHelper(self.config)
-        # self.semantic_identifier = SemanticIdentifier(self.config)
-        # self.winapi_provider = WinApiProvider(self.config)
-        # self.crypto_identifier = CryptoIdentifier()
+        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))

idascope/config.py

         "default_semantics": "win-ring3"
         },
     "yara": {
-        "yara_sigs": ["C:\\yara\\"]
+        "yara_sigs": ["C:\\yara"]
         }
 }

idascope/core/YaraScanner.py

 import re
 import time
 
-import yara
+try:
+    import yara
+except ImportError:
+    print("[-] ERROR: Could not import YARA (not installed?), scanner disabled.")
+    yara = None
 
 from IdaProxy import IdaProxy
 import idascope.core.helpers.Misc as Misc
     """
 
     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
         return self._results
 
     def load_rules(self):
+        if not self.yara:
+            return
         self.num_files_loaded = 0
         self._compiled_rules = []
         self._yara_rules = []
             self._load_recursive(yara_path)
 
     def _load_recursive(self, yara_path):
-        for dirpath, dirnames, filenames in os.walk(yara_path):
-            for filename in filenames:
-                filepath = dirpath + os.sep + filename
-                try:
-                    print "loading rules from file: %s" % filepath
-                    rules = yara.compile(filepath)
-                    self._compiled_rules.append(rules)
-                    self._yara_rules.extend(self.yrl.loadRulesFromFile(filepath))
-                    if rules:
-                        self.num_files_loaded += 1
-                except:
-                    print "[!] Could not load yara rules file: %s" % filepath
+        if os.path.isfile(yara_path):
+            self._load_file(yara_path)
+        elif os.path.isdir(yara_path):
+            for dirpath, dirnames, filenames in os.walk(yara_path):
+                for filename in filenames:
+                    filepath = dirpath + os.sep + filename
+                    self._load_file(filepath)
+
+    def _load_file(self, filepath):
+        try:
+            rules = yara.compile(filepath)
+            self._compiled_rules.append(rules)
+            rules_from_file = self.yrl.loadRulesFromFile(filepath)
+            self._yara_rules.extend(rules_from_file)
+            print "loading rules from file: %s (%d)" % (filepath, len(rules_from_file))
+            if rules:
+                self.num_files_loaded += 1
+        except:
+            print "[!] Could not load yara rules from file: %s" % filepath
 
     def scan(self):
+        if not self.yara:
+            print "[!] yara-python not available, please install it from (http://plusvic.github.io/yara/)"
+            return
         memory, offsets = self._get_memory()
         self.segment_offsets = offsets
         self._results = []
         matches = []
-        print "[!] Performing Yara scan..."
+        print "[!] Performing YARA scan..."
         for rule in self._compiled_rules:
             matches.append(rule.match(data=memory, callback=self._result_callback))
         if len(matches) == 0:
             adjusted_offsets.append((self._translateMemOffsetToVirtualAddress(string[0]), string[1], string[2]))
         data["strings"] = adjusted_offsets
         if data["matches"]:
-            print "  [+] Yara Match for signature: %s" % data["rule"]
+            print "  [+] YARA Match for signature: %s" % data["rule"]
         result_rule = None
         for rule in self._yara_rules:
             if rule.rule_name == data["rule"]:

idascope/core/helpers/YaraRule.py

 
     def __init__(self):
         self.statusController = StatusController()
+        self.filename = ""
         # set raw data of rule content, to be parsed by analyze* methods
         self.raw_header = ""
         self.raw_header_cleaned = ""
         result = ""
         result += "global " if self.is_global else ""
         result += "private " if self.is_private else ""
-        result += self.rule_name
+        result += "rule " + self.rule_name
         if self.rule_description:
             result += " : " + " ".join(self.rule_description)
         result += "\n{\n"
                 result += " " * 8 + "%s = %s%s%s %s\n" % (string_line[1],
                                                        start_delimiters[string_line[0]],
                                                        string_line[2],
-                                                       end_delimiters[string_line[0]], " ".join(string_line))
+                                                       end_delimiters[string_line[0]], " ".join(string_line[3]))
             result += "\n"
         result += "    condition:\n"
         result += " " * 8 + "%s\n" % self.condition

idascope/core/helpers/YaraRuleLoader.py

         #clean content
         content_cleaned = self._cleanContent(content)
         # split content in Yara Rules
-        return self._splitYaraRules(content, content_cleaned)
+        return self._splitYaraRules(content, content_cleaned, filename)
 
     def _cleanContent(self, content):
         """ clean content, replace comments by spaces, replace strings by underlines """
         # return content without comments and strings
         return result
 
-    def _splitYaraRules(self, content, content_cleaned):
+    def _splitYaraRules(self, content, content_cleaned, filename):
         """ get all Yara rules split in sections (header, meta, strings, condition) """
         # result, list of Yara rules
         yara_rules = []
                 # add fully parsed rule to list and create next rule
                 yara_rules.append(current_rule)
                 # analyze Yara rule
+                current_rule.filename = filename
                 current_rule.analyze()
                 current_rule = YaraRule()
 

idascope/widgets/FunctionInspectionWidget.py

         self.QtGui = QtGui
         self.QtCore = QtCore
         self.NumberQTableWidgetItem = NumberQTableWidgetItem
-        self. FunctionFilterDialog = FunctionFilterDialog
+        self.FunctionFilterDialog = FunctionFilterDialog
         self.central_widget = self.QtGui.QWidget()
         self.setCentralWidget(self.central_widget)
         self._createGui()

idascope/widgets/YaraRuleDialog.py

+#!/usr/bin/python
+########################################################################
+# Copyright (c) 2012
+# 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
+
+
+class YaraRuleDialog(QtGui.QDialog):
+    """ oriented on: https://stackoverflow.com/a/11764475 """
+
+    def __init__(self, rule, parent=None):
+        QtGui.QDialog.__init__(self, parent)
+        # create GUI elements
+        self.rule = rule
+        self._createOkButton()
+        # glue everything together
+        # create scroll for rule text edit
+        self.scroll = QtGui.QScrollArea()
+        self.scroll.setWidgetResizable(True)
+        self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
+        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred)
+        self.setSizePolicy(sizePolicy)
+        scrollContents = QtGui.QWidget()
+        self.scroll.setWidget(scrollContents)
+        # create growing textedit for rule display
+        self.textLayout = QtGui.QVBoxLayout()
+        self.rule_textedit = GrowingTextEdit()
+        self.setMinimumHeight(300)
+        self.setMinimumWidth(550)
+        self.rule_textedit.setReadOnly(True)
+        self.textLayout.addWidget(self.rule_textedit)
+
+        dialog_layout = QtGui.QVBoxLayout(scrollContents)
+
+        dialog_layout.addLayout(self.textLayout)
+        dialog_layout.addLayout(self.button_layout)
+        self.setLayout(dialog_layout)
+        if self.rule:
+            self.setWindowTitle(self.tr("YARA Rule: %s (%s)" % (self.rule.rule_name, self.rule.filename)))
+            self.rule_textedit.setText(str(rule))
+        else:
+            self.setWindowTitle(self.tr("No rule selected."))
+
+    def _createOkButton(self):
+        self.button_layout = QtGui.QHBoxLayout()
+        self.ok_button = QtGui.QPushButton(self.tr("OK"))
+        self.ok_button.clicked.connect(self.accept)
+        self.button_layout.addStretch(1)
+        self.button_layout.addWidget(self.ok_button)
+        self.button_layout.addStretch(1)
+
+    def accept(self):
+        self.done(1)
+
+
+class GrowingTextEdit(QtGui.QTextEdit):
+    """ source: https://stackoverflow.com/a/11764475 """
+
+    def __init__(self, *args, **kwargs):
+        super(GrowingTextEdit, self).__init__(*args, **kwargs)
+        self.document().contentsChanged.connect(self.sizeChange)
+
+        self.heightMin = 0
+        self.heightMax = 1400
+
+    def getHeight(self):
+        return self.document().size().height()
+
+    def sizeChange(self):
+        docHeight = self.getHeight()
+        if self.heightMin <= docHeight <= self.heightMax:
+            self.setMinimumHeight(docHeight)

idascope/widgets/YaraScannerWidget.py

 from PySide.QtGui import QIcon
 
 from NumberQTableWidgetItem import NumberQTableWidgetItem
+from YaraRuleDialog import YaraRuleDialog
 
 
 class YaraScannerWidget(QtGui.QMainWindow):
         print "[|] loading YaraScannerWidget"
         # enable access to shared IDAscope modules
         self.parent = parent
-        self.name = "Yara Scanner"
-        # FIXME: select a icon for this
+        self.name = "YARA Scanner"
         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.QtGui = QtGui
         self.QtCore = QtCore
         self.NumberQTableWidgetItem = NumberQTableWidgetItem
+        self.YaraRuleDialog = YaraRuleDialog
 
         self.central_widget = self.QtGui.QWidget()
         self.setCentralWidget(self.central_widget)
         Creates the toolbar, containing buttons to control the widget.
         """
         self._createLoadAndScanAction()
-        self.toolbar = self.addToolBar('Yara Scanner Toobar')
+        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)
+            "(Re)load YARA Signature files and scan", self)
         self.loadAndScanAction.triggered.connect(self._onLoadAndScanButtonClicked)
 
     def _onLoadAndScanButtonClicked(self):
         clicked_rule_name = self.rules_table.item(mi.row(), 0).text()
         for rule_result in self.ys.getResults():
             if rule_result.rule_name == clicked_rule_name:
-                print "rule found", clicked_rule_name
                 self.populateResultTable(rule_result)
                 self._selected_rule = rule_result
 
         num_hits = 0
         num_strings = 0
         self.result_label = QtGui.QLabel("%d out of %d strings matched" % (num_hits, num_strings))
-        # self.rule_builder_button = QIcon(self.parent.config.icon_file_path + "forward.png")
+        self.rule_display_icon = QIcon(self.parent.config.icon_file_path + "winapi.png")
 
         # rule visualization
         self.result_widget = QtGui.QWidget()
         result_layout = QtGui.QVBoxLayout()
         self._createResultTable()
+        self._createResultInfoButton()
+
+        self.result_info_widget = QtGui.QWidget()
+        result_info_layout = QtGui.QHBoxLayout()
+        result_info_layout.addWidget(self.result_info_button)
+        result_info_layout.addWidget(self.result_label)
+        result_info_layout.addStretch(1)
+        self.result_info_widget.setLayout(result_info_layout)
 
         # widget composition
-        result_layout.addWidget(self.result_label)
+        result_layout.addWidget(self.result_info_widget)
         result_layout.addWidget(self.result_table)
         self.result_widget.setLayout(result_layout)
 
+    def _createResultInfoButton(self):
+        """
+        Create a back button to allow easier browsing
+        """
+        self.result_info_button = QtGui.QPushButton(self.rule_display_icon, "", self)
+        self.result_info_button.setToolTip("Show full rule")
+        self.result_info_button.resize(self.result_info_button.sizeHint())
+        self.result_info_button.setEnabled(True)
+        self.result_info_button.clicked.connect(self._onResultInfoButtonClicked)
+
     def _createResultTable(self):
         """
         Create the result table for displaying results yara scanning
             if data_item[0]:
                 matched_str.update([data_item[2]])
             all_str.update([data_item[2]])
-        self.result_label.setText("%d out of %d strings matched" % (len(matched_str), len(all_str)))
+        rule_label = ""
+        if rule_result:
+            rule_label = "%d out of %d strings matched (%s)" % (len(matched_str), len(all_str), rule_result.rule_name)
+        else:
+            rule_label = "No rule selected."
+        self.result_label.setText(rule_label)
 
         self.result_table.setColumnCount(len(self.result_table_header_labels))
         self.result_table.setHorizontalHeaderLabels(self.result_table_header_labels)
             self.ip.Jump(int(clicked_address, 16))
         except ValueError:
             pass
+
+    def _onResultInfoButtonClicked(self):
+        dialog = self.YaraRuleDialog(self._selected_rule)
+        dialog.exec_()