diff options
| author | Jordan Wiens <github@psifertex.com> | 2026-01-01 12:21:22 -0500 |
|---|---|---|
| committer | Jordan Wiens <github@psifertex.com> | 2026-01-01 12:21:22 -0500 |
| commit | 37a0604d805991b85a0590279ad993a19ddee7bb (patch) | |
| tree | fa8054497a4761f134ab5b537dfb0dbfc9a64f32 /python/examples | |
| parent | 2448131059638409902821451115872036230328 (diff) | |
add whole-file entropy calculation to triage
Diffstat (limited to 'python/examples')
| -rw-r--r-- | python/examples/triage/entropy.py | 25 | ||||
| -rw-r--r-- | python/examples/triage/file_info.py | 154 | ||||
| -rw-r--r-- | python/examples/triage/view.py | 10 |
3 files changed, 186 insertions, 3 deletions
diff --git a/python/examples/triage/entropy.py b/python/examples/triage/entropy.py index a2ab41a6..8dbd3900 100644 --- a/python/examples/triage/entropy.py +++ b/python/examples/triage/entropy.py @@ -2,7 +2,7 @@ import math import threading from PySide6.QtWidgets import QWidget from PySide6.QtGui import QImage, QColor, QPainter -from PySide6.QtCore import Qt, QSize, QTimer +from PySide6.QtCore import Qt, QSize, QTimer, Signal import binaryninjaui from binaryninjaui import ViewFrame, UIContext from binaryninja.enums import ThemeColor @@ -14,11 +14,22 @@ class EntropyThread(threading.Thread): self.image = image self.block_size = block_size self.updated = False + self.average_entropy = 0.0 + self.sample_count = 0 + self.lock = threading.Lock() def run(self): width = self.image.width() for i in range(0, width): - v = int(self.data.get_entropy(self.data.start + i * self.block_size, self.block_size)[0] * 255) + entropy_result = self.data.get_entropy(self.data.start + i * self.block_size, self.block_size) + entropy_value = entropy_result[0] if entropy_result else 0.0 + v = int(entropy_value * 255) + + # Update running average + with self.lock: + self.sample_count += 1 + self.average_entropy += (entropy_value - self.average_entropy) / self.sample_count + if v >= 240: color = binaryninjaui.getThemeColor(ThemeColor.YellowStandardHighlightColor) self.image.setPixelColor(i, 0, color) @@ -29,8 +40,14 @@ class EntropyThread(threading.Thread): self.image.setPixelColor(i, 0, color) self.updated = True + def get_average_entropy(self): + with self.lock: + return self.average_entropy + class EntropyWidget(QWidget): + entropyUpdated = Signal(float) + def __init__(self, parent, view, data): super(EntropyWidget, self).__init__(parent) self.view = view @@ -72,6 +89,7 @@ class EntropyWidget(QWidget): if self.thread.updated: self.thread.updated = False self.update() + self.entropyUpdated.emit(self.thread.get_average_entropy()) def mousePressEvent(self, event): if event.button() != Qt.LeftButton: @@ -88,3 +106,6 @@ class EntropyWidget(QWidget): self.setToolTip(f"0x{addr:x}") else: self.setToolTip(f"File offset: 0x{offset:x}") + + def get_average_entropy(self): + return self.thread.get_average_entropy() diff --git a/python/examples/triage/file_info.py b/python/examples/triage/file_info.py new file mode 100644 index 00000000..ec98aeff --- /dev/null +++ b/python/examples/triage/file_info.py @@ -0,0 +1,154 @@ +import os +import hashlib +import threading +from PySide6.QtWidgets import QWidget, QLabel, QGridLayout +from PySide6.QtCore import Qt +from binaryninjaui import UIContext +from binaryninja.enums import ThemeColor +import binaryninjaui + + +class CopyableLabel(QLabel): + def __init__(self, text, color): + super(CopyableLabel, self).__init__(text) + from PySide6.QtGui import QPalette + style = QPalette(self.palette()) + style.setColor(QPalette.WindowText, color) + self.setPalette(style) + self.setFont(binaryninjaui.getMonospaceFont(self)) + self.setCursor(Qt.PointingHandCursor) + self.setMouseTracking(True) + self.setToolTip("Click to Copy") + self.copy_text = "" # Empty - will fall back to current text + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + from PySide6.QtWidgets import QApplication + # Fallback to current text if copy_text not set + if self.copy_text: + QApplication.clipboard().setText(self.copy_text) + else: + QApplication.clipboard().setText(self.text()) + + def setCopyText(self, text): + self.copy_text = text + + +class FileInfoWidget(QWidget): + def __init__(self, parent, data, entropy_widget): + super(FileInfoWidget, self).__init__(parent) + self.data = data + self.entropy_widget = entropy_widget + + layout = QGridLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setVerticalSpacing(1) + + row = 0 + + # Get the file path + file_path = data.file.original_filename + view = data.parent_view if data.parent_view else data + + # Add path on disk + self._add_field(layout, row, "Path on disk:", file_path) + row += 1 + + # Add path in project if available + if data.file.project_file: + project_path = data.file.project_file.path_in_project + self._add_field(layout, row, "Path in project:", project_path) + row += 1 + + # Add file size + file_size = f"0x{view.length:x}" + self._add_copyable_field(layout, row, "Size:", file_size) + row += 1 + + # Add entropy (will be updated when calculation completes) + self.entropy_label = CopyableLabel("Calculating...", + binaryninjaui.getThemeColor(ThemeColor.AlphanumericHighlightColor)) + layout.addWidget(QLabel("Entropy:"), row, 0) + layout.addWidget(self.entropy_label, row, 1) + row += 1 + + # Add hash fields (calculated in background) + self.md5_label = CopyableLabel("Calculating...", + binaryninjaui.getThemeColor(ThemeColor.AlphanumericHighlightColor)) + layout.addWidget(QLabel("MD5:"), row, 0) + layout.addWidget(self.md5_label, row, 1) + row += 1 + + self.sha1_label = CopyableLabel("Calculating...", + binaryninjaui.getThemeColor(ThemeColor.AlphanumericHighlightColor)) + layout.addWidget(QLabel("SHA-1:"), row, 0) + layout.addWidget(self.sha1_label, row, 1) + row += 1 + + self.sha256_label = CopyableLabel("Calculating...", + binaryninjaui.getThemeColor(ThemeColor.AlphanumericHighlightColor)) + layout.addWidget(QLabel("SHA-256:"), row, 0) + layout.addWidget(self.sha256_label, row, 1) + row += 1 + + # Set column stretch so value column expands + layout.setColumnStretch(1, 1) + + self.setLayout(layout) + + # Connect to entropy widget signal + if entropy_widget: + entropy_widget.entropyUpdated.connect(self._update_entropy) + + # Start hash calculation in background thread + self.hash_thread = threading.Thread(target=self._calculate_hashes) + self.hash_thread.daemon = True + self.hash_thread.start() + + def _add_field(self, layout, row, name, value): + value_label = QLabel(value) + value_label.setFont(binaryninjaui.getMonospaceFont(self)) + layout.addWidget(QLabel(name), row, 0) + layout.addWidget(value_label, row, 1) + + def _add_copyable_field(self, layout, row, name, value): + value_label = CopyableLabel(value, + binaryninjaui.getThemeColor(ThemeColor.AlphanumericHighlightColor)) + layout.addWidget(QLabel(name), row, 0) + layout.addWidget(value_label, row, 1) + + def _calculate_hashes(self): + view = self.data.parent_view if self.data.parent_view else self.data + total_size = view.length + + md5 = hashlib.md5() + sha1 = hashlib.sha1() + sha256 = hashlib.sha256() + + offset = 0 + chunk_size = 128 * 1024 * 1024 # 128MB chunks + + while offset < total_size: + remaining = total_size - offset + current_chunk = min(chunk_size, remaining) + data = view.read(offset, current_chunk) + + if data is None or len(data) != current_chunk: + self.md5_label.setText("Error") + self.sha1_label.setText("Error") + self.sha256_label.setText("Error") + return + + md5.update(data) + sha1.update(data) + sha256.update(data) + offset += current_chunk + + # Update labels on main thread + from PySide6.QtCore import QMetaObject, Q_ARG + QMetaObject.invokeMethod(self.md5_label, "setText", Qt.QueuedConnection, Q_ARG(str, md5.hexdigest())) + QMetaObject.invokeMethod(self.sha1_label, "setText", Qt.QueuedConnection, Q_ARG(str, sha1.hexdigest())) + QMetaObject.invokeMethod(self.sha256_label, "setText", Qt.QueuedConnection, Q_ARG(str, sha256.hexdigest())) + + def _update_entropy(self, avg_entropy): + self.entropy_label.setText(f"{avg_entropy:.6f}") diff --git a/python/examples/triage/view.py b/python/examples/triage/view.py index 5a39197f..28a99a75 100644 --- a/python/examples/triage/view.py +++ b/python/examples/triage/view.py @@ -7,6 +7,7 @@ from PySide6.QtWidgets import QScrollArea, QWidget, QVBoxLayout, QHBoxLayout, QP from PySide6.QtCore import Qt from . import headers from . import entropy +from . import file_info from . import imports from . import exports from . import sections @@ -30,10 +31,17 @@ class TriageView(QScrollArea, View): entropyGroup = QGroupBox("Entropy", container) entropyLayout = QVBoxLayout() - entropyLayout.addWidget(entropy.EntropyWidget(entropyGroup, self, self.data)) + self.entropyWidget = entropy.EntropyWidget(entropyGroup, self, self.data) + entropyLayout.addWidget(self.entropyWidget) entropyGroup.setLayout(entropyLayout) layout.addWidget(entropyGroup) + fileInfoGroup = QGroupBox("File Info", container) + fileInfoLayout = QVBoxLayout() + fileInfoLayout.addWidget(file_info.FileInfoWidget(fileInfoGroup, self.data, self.entropyWidget)) + fileInfoGroup.setLayout(fileInfoLayout) + layout.addWidget(fileInfoGroup) + hdr = None try: if self.data.view_type == "PE": |
