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 | |
| parent | 2448131059638409902821451115872036230328 (diff) | |
add whole-file entropy calculation to triage
| -rw-r--r-- | examples/triage/entropy.cpp | 30 | ||||
| -rw-r--r-- | examples/triage/entropy.h | 11 | ||||
| -rw-r--r-- | examples/triage/fileinfo.cpp | 26 | ||||
| -rw-r--r-- | examples/triage/fileinfo.h | 10 | ||||
| -rw-r--r-- | examples/triage/view.cpp | 5 | ||||
| -rw-r--r-- | examples/triage/view.h | 2 | ||||
| -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 |
9 files changed, 265 insertions, 8 deletions
diff --git a/examples/triage/entropy.cpp b/examples/triage/entropy.cpp index da089fd6..dd643af7 100644 --- a/examples/triage/entropy.cpp +++ b/examples/triage/entropy.cpp @@ -12,6 +12,8 @@ EntropyThread::EntropyThread(BinaryViewRef data, size_t blockSize, QImage* image m_blockSize = blockSize; m_updated = false; m_running = true; + m_averageEntropy = 0.0; + m_sampleCount = 0; m_thread = std::thread([=, this]() { Run(); }); } @@ -33,10 +35,22 @@ void EntropyThread::Run() std::vector<float> entropy = m_data->GetEntropy(m_data->GetStart() + ((uint64_t)i * m_blockSize), m_blockSize, m_blockSize); int v; + float entropyValue = 0.0f; if (entropy.size() == 0) v = 0; else - v = (int)(entropy[0] * 255); + { + entropyValue = entropy[0]; + v = (int)(entropyValue * 255); + } + + // Update running average + { + std::lock_guard<std::mutex> lock(m_mutex); + m_sampleCount++; + m_averageEntropy += (entropyValue - m_averageEntropy) / m_sampleCount; + } + if (v >= 240) { QColor color = getThemeColor(YellowStandardHighlightColor); @@ -54,6 +68,13 @@ void EntropyThread::Run() } +double EntropyThread::GetAverageEntropy() const +{ + std::lock_guard<std::mutex> lock(m_mutex); + return m_averageEntropy; +} + + EntropyWidget::EntropyWidget(QWidget* parent, TriageView* view, BinaryViewRef data) : QWidget(parent) { m_view = view; @@ -106,6 +127,7 @@ void EntropyWidget::timerExpired() { m_thread->ResetUpdated(); update(); + emit entropyUpdated(m_thread->GetAverageEntropy()); } } @@ -131,3 +153,9 @@ void EntropyWidget::mouseMoveEvent(QMouseEvent* event) else setToolTip(QString("File offset: 0x%1").arg(offset, 0, 16)); } + + +double EntropyWidget::getAverageEntropy() const +{ + return m_thread->GetAverageEntropy(); +} diff --git a/examples/triage/entropy.h b/examples/triage/entropy.h index 821dfb7b..bd50081d 100644 --- a/examples/triage/entropy.h +++ b/examples/triage/entropy.h @@ -3,6 +3,7 @@ #include <QtWidgets/QWidget> #include <QtGui/QImage> #include <thread> +#include <mutex> #include "uitypes.h" @@ -13,6 +14,9 @@ class EntropyThread size_t m_blockSize; bool m_updated, m_running; std::thread m_thread; + double m_averageEntropy; + int m_sampleCount; + mutable std::mutex m_mutex; public: EntropyThread(BinaryViewRef data, size_t blockSize, QImage* image); @@ -21,6 +25,7 @@ class EntropyThread void Run(); bool IsUpdated() { return m_updated; } void ResetUpdated() { m_updated = false; } + double GetAverageEntropy() const; }; @@ -28,6 +33,8 @@ class TriageView; class EntropyWidget : public QWidget { + Q_OBJECT + TriageView* m_view; BinaryViewRef m_data, m_rawData; size_t m_blockSize; @@ -40,6 +47,10 @@ class EntropyWidget : public QWidget virtual ~EntropyWidget(); virtual QSize sizeHint() const override; + double getAverageEntropy() const; + + Q_SIGNALS: + void entropyUpdated(double avgEntropy); protected: virtual void paintEvent(QPaintEvent* event) override; diff --git a/examples/triage/fileinfo.cpp b/examples/triage/fileinfo.cpp index e18c94d8..22ce2423 100644 --- a/examples/triage/fileinfo.cpp +++ b/examples/triage/fileinfo.cpp @@ -1,4 +1,5 @@ #include "fileinfo.h" +#include "entropy.h" #include "fontsettings.h" #include "theme.h" #include "copyablelabel.h" @@ -129,7 +130,7 @@ void FileInfoWidget::addHashFields(BinaryViewRef view) }); } -FileInfoWidget::FileInfoWidget(QWidget* parent, BinaryViewRef bv) +FileInfoWidget::FileInfoWidget(QWidget* parent, BinaryViewRef bv, EntropyWidget* entropyWidget) { this->m_layout = new QGridLayout(); this->m_layout->setContentsMargins(0, 0, 0, 0); @@ -157,10 +158,33 @@ FileInfoWidget::FileInfoWidget(QWidget* parent, BinaryViewRef bv) const auto fileSize = QString::number(view->GetLength(), 16).prepend("0x"); this->addCopyableField("Size: ", fileSize); + // Display entropy value from the entropy widget + if (entropyWidget) + { + auto& [row, column] = this->m_fieldPosition; + m_entropyLabel = new CopyableLabel("Calculating...", getThemeColor(AlphanumericHighlightColor)); + m_entropyLabel->setFont(getMonospaceFont(this)); + this->m_layout->addWidget(new QLabel("Entropy: "), row, column); + this->m_layout->addWidget(m_entropyLabel, row++, column + 1); + + // Connect to entropy updates + connect(entropyWidget, &EntropyWidget::entropyUpdated, this, &FileInfoWidget::updateEntropy); + } + this->addHashFields(view); const auto scaledWidth = UIContext::getScaledWindowSize(20, 20).width(); this->m_layout->setColumnMinimumWidth(FileInfoWidget::m_maxColumns * 3 - 1, scaledWidth); this->m_layout->setColumnStretch(FileInfoWidget::m_maxColumns * 3 - 1, 1); setLayout(this->m_layout); +} + + +void FileInfoWidget::updateEntropy(double avgEntropy) +{ + if (m_entropyLabel) + { + const auto entropyStr = QString::number(avgEntropy, 'f', 6); + m_entropyLabel->setText(entropyStr); + } }
\ No newline at end of file diff --git a/examples/triage/fileinfo.h b/examples/triage/fileinfo.h index 78ba578b..8eac3fbe 100644 --- a/examples/triage/fileinfo.h +++ b/examples/triage/fileinfo.h @@ -5,11 +5,16 @@ #include "uitypes.h" #include "viewframe.h" +class EntropyWidget; + class FileInfoWidget : public QWidget { + Q_OBJECT + static constexpr std::int32_t m_maxColumns {2}; std::pair<std::int32_t, std::int32_t> m_fieldPosition {}; // row, column QGridLayout* m_layout {}; + QLabel* m_entropyLabel {}; void addField(const QString& name, const QVariant& value); void addCopyableField(const QString& name, const QVariant& value); @@ -17,5 +22,8 @@ class FileInfoWidget : public QWidget void addHashFields(BinaryViewRef view); public: - FileInfoWidget(QWidget* parent, BinaryViewRef bv); + FileInfoWidget(QWidget* parent, BinaryViewRef bv, EntropyWidget* entropyWidget); + + private Q_SLOTS: + void updateEntropy(double avgEntropy); }; diff --git a/examples/triage/view.cpp b/examples/triage/view.cpp index 2c563470..0e995ae2 100644 --- a/examples/triage/view.cpp +++ b/examples/triage/view.cpp @@ -27,13 +27,14 @@ TriageView::TriageView(QWidget* parent, BinaryViewRef data) : QScrollArea(parent QGroupBox* entropyGroup = new QGroupBox("Entropy", container); QVBoxLayout* entropyLayout = new QVBoxLayout(); - entropyLayout->addWidget(new EntropyWidget(entropyGroup, this, m_data)); + m_entropyWidget = new EntropyWidget(entropyGroup, this, m_data); + entropyLayout->addWidget(m_entropyWidget); entropyGroup->setLayout(entropyLayout); layout->addWidget(entropyGroup); QGroupBox* fileInfoGroup = new QGroupBox("File Info", container); QVBoxLayout* fileInfoLayout = new QVBoxLayout(); - fileInfoLayout->addWidget(new FileInfoWidget(fileInfoGroup, m_data)); + fileInfoLayout->addWidget(new FileInfoWidget(fileInfoGroup, m_data, m_entropyWidget)); fileInfoGroup->setLayout(fileInfoLayout); layout->addWidget(fileInfoGroup); diff --git a/examples/triage/view.h b/examples/triage/view.h index ab4f1024..fc9c7ff3 100644 --- a/examples/triage/view.h +++ b/examples/triage/view.h @@ -7,6 +7,7 @@ class HeaderWidget; +class EntropyWidget; class TriageView : public QScrollArea, public View { @@ -16,6 +17,7 @@ class TriageView : public QScrollArea, public View QPushButton* m_fullAnalysisButton = nullptr; QSplitter* m_importExportSplitter = nullptr; HeaderWidget* m_headerWidget = nullptr; + EntropyWidget* m_entropyWidget = nullptr; public: TriageView(QWidget* parent, BinaryViewRef data); 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": |
