summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJordan Wiens <github@psifertex.com>2026-01-01 12:21:22 -0500
committerJordan Wiens <github@psifertex.com>2026-01-01 12:21:22 -0500
commit37a0604d805991b85a0590279ad993a19ddee7bb (patch)
treefa8054497a4761f134ab5b537dfb0dbfc9a64f32
parent2448131059638409902821451115872036230328 (diff)
add whole-file entropy calculation to triage
-rw-r--r--examples/triage/entropy.cpp30
-rw-r--r--examples/triage/entropy.h11
-rw-r--r--examples/triage/fileinfo.cpp26
-rw-r--r--examples/triage/fileinfo.h10
-rw-r--r--examples/triage/view.cpp5
-rw-r--r--examples/triage/view.h2
-rw-r--r--python/examples/triage/entropy.py25
-rw-r--r--python/examples/triage/file_info.py154
-rw-r--r--python/examples/triage/view.py10
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":