diff options
| author | Xusheng <xusheng@vector35.com> | 2026-03-04 17:04:53 +0800 |
|---|---|---|
| committer | Xusheng <xusheng@vector35.com> | 2026-03-05 12:59:40 +0800 |
| commit | 432e144beeccf1d9a2d9ed9e7d016384eed0bb32 (patch) | |
| tree | f963730f2b0b18cbc0620a755e6aad415228c2dc | |
| parent | 3520262824637979418554656607433424896809 (diff) | |
Parse and display PE resource information in triage view. Fix https://github.com/Vector35/binaryninja-api/issues/4052, fix https://github.com/Vector35/binaryninja-api/issues/5607
| -rw-r--r-- | examples/triage/headers.cpp | 13 | ||||
| -rw-r--r-- | examples/triage/resources.cpp | 515 | ||||
| -rw-r--r-- | examples/triage/resources.h | 102 | ||||
| -rw-r--r-- | examples/triage/view.cpp | 14 | ||||
| -rw-r--r-- | view/pe/peview.cpp | 604 |
5 files changed, 1170 insertions, 78 deletions
diff --git a/examples/triage/headers.cpp b/examples/triage/headers.cpp index f2165de0..8aff9f59 100644 --- a/examples/triage/headers.cpp +++ b/examples/triage/headers.cpp @@ -306,8 +306,19 @@ PEHeaders::PEHeaders(BinaryViewRef data) AddField("Compiler(s) Used", compilersUsed); } + auto versionInfo = data->QueryMetadata("PEVersionInfo"); + if (versionInfo && versionInfo->IsKeyValueStore()) + { + for (const auto& [key, value] : versionInfo->GetKeyValueStore()) + { + if (value->IsString() && !value->GetString().empty()) + AddField(QString::fromStdString(key), QString::fromStdString(value->GetString())); + } + } + SetColumns(3); - SetRowsPerColumn(9); + size_t numFields = GetFields().size(); + SetRowsPerColumn((numFields + 2) / 3); } diff --git a/examples/triage/resources.cpp b/examples/triage/resources.cpp new file mode 100644 index 00000000..71080e2c --- /dev/null +++ b/examples/triage/resources.cpp @@ -0,0 +1,515 @@ +#include <cstring> +#include <algorithm> +#include <QtGui/QClipboard> +#include <QtGui/QGuiApplication> +#include <QtCore/QStringList> +#include <QtWidgets/QMenu> +#include <QtWidgets/QFileDialog> +#include <QtWidgets/QVBoxLayout> +#include <QtCore/QFile> +#include "resources.h" +#include "view.h" +#include "fontsettings.h" + + +static std::string GetResourceFileExtension(const std::string& type) +{ + if (type == "RT_ICON" || type == "RT_GROUP_ICON") + return ".ico"; + if (type == "RT_CURSOR" || type == "RT_GROUP_CURSOR") + return ".cur"; + if (type == "RT_BITMAP") + return ".bmp"; + if (type == "RT_HTML") + return ".html"; + if (type == "RT_MANIFEST") + return ".xml"; + if (type == "RT_FONT") + return ".fnt"; + if (type == "RT_VERSION" || type == "RT_STRING" || type == "RT_MESSAGETABLE" + || type == "RT_RCDATA" || type == "RT_DLGINCLUDE") + return ".bin"; + return ".bin"; +} + + +ResourcesModel::ResourcesModel(QWidget* parent, BinaryViewRef data) : QAbstractItemModel(parent) +{ + m_data = data; + m_sortCol = TypeCol; + m_sortOrder = Qt::AscendingOrder; + + auto md = m_data->QueryMetadata("PEResources"); + if (md && md->IsArray()) + { + for (auto& item : md->GetArray()) + { + if (!item->IsKeyValueStore()) + continue; + auto kv = item->GetKeyValueStore(); + + ResourceEntry entry; + auto getStr = [&](const std::string& key) -> std::string { + auto it = kv.find(key); + if (it != kv.end() && it->second->IsString()) + return it->second->GetString(); + return ""; + }; + auto getUint = [&](const std::string& key) -> uint64_t { + auto it = kv.find(key); + if (it != kv.end() && it->second->IsUnsignedInteger()) + return it->second->GetUnsignedInteger(); + return 0; + }; + + entry.type = getStr("type"); + entry.typeId = getUint("typeId"); + entry.name = getStr("name"); + entry.nameId = getUint("nameId"); + entry.language = getStr("language"); + entry.languageId = getUint("languageId"); + entry.dataRva = getUint("dataRva"); + entry.dataSize = getUint("dataSize"); + entry.dataAddress = getUint("dataAddress"); + entry.codepage = getUint("codepage"); + entry.preview = getStr("preview"); + + m_allEntries.push_back(entry); + } + } + m_entries = m_allEntries; +} + + +int ResourcesModel::columnCount(const QModelIndex&) const +{ + return ColumnCount; +} + + +int ResourcesModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + return (int)m_entries.size(); +} + + +QVariant ResourcesModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || index.row() >= (int)m_entries.size()) + return QVariant(); + + const auto& entry = m_entries[index.row()]; + + switch (role) + { + case Qt::DisplayRole: + switch (index.column()) + { + case TypeCol: + return QString::fromStdString(entry.type); + case NameCol: + return QString::fromStdString(entry.name); + case LanguageCol: + return QString::fromStdString(entry.language); + case SizeCol: + return QString("0x") + QString::number(entry.dataSize, 16); + case AddressCol: + return QString("0x") + QString::number(entry.dataAddress, 16); + case PreviewCol: + return QString::fromStdString(entry.preview); + } + break; + case Qt::ForegroundRole: + if (index.column() == AddressCol) + return getThemeColor(AddressColor); + break; + default: + break; + } + + return QVariant(); +} + + +QVariant ResourcesModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical || role != Qt::DisplayRole) + return QVariant(); + + switch (section) + { + case TypeCol: return QString("Type"); + case NameCol: return QString("Name"); + case LanguageCol: return QString("Language"); + case SizeCol: return QString("Size"); + case AddressCol: return QString("Address"); + case PreviewCol: return QString("Preview"); + } + return QVariant(); +} + + +QModelIndex ResourcesModel::index(int row, int col, const QModelIndex& parent) const +{ + if (parent.isValid()) + return QModelIndex(); + if (row >= (int)m_entries.size() || col >= ColumnCount) + return QModelIndex(); + return createIndex(row, col); +} + + +QModelIndex ResourcesModel::parent(const QModelIndex&) const +{ + return QModelIndex(); +} + + +const ResourceEntry* ResourcesModel::getEntry(const QModelIndex& index) const +{ + if (!index.isValid() || index.row() >= (int)m_entries.size()) + return nullptr; + return &m_entries[index.row()]; +} + + +void ResourcesModel::performSort(int col, Qt::SortOrder order) +{ + std::sort(m_entries.begin(), m_entries.end(), [&](const ResourceEntry& a, const ResourceEntry& b) { + bool less = false; + bool greater = false; + switch (col) + { + case TypeCol: + less = a.type < b.type; + greater = a.type > b.type; + break; + case NameCol: + // Sort numerically by ID when both names are numeric (e.g. "#303") + less = a.nameId < b.nameId; + greater = a.nameId > b.nameId; + if (a.nameId == b.nameId) + { + less = a.name < b.name; + greater = a.name > b.name; + } + break; + case LanguageCol: + less = a.language < b.language; + greater = a.language > b.language; + break; + case SizeCol: + less = a.dataSize < b.dataSize; + greater = a.dataSize > b.dataSize; + break; + case AddressCol: + less = a.dataAddress < b.dataAddress; + greater = a.dataAddress > b.dataAddress; + break; + case PreviewCol: + less = a.preview < b.preview; + greater = a.preview > b.preview; + break; + } + if (order == Qt::AscendingOrder) + return less; + return greater; + }); +} + + +void ResourcesModel::sort(int col, Qt::SortOrder order) +{ + beginResetModel(); + m_sortCol = col; + m_sortOrder = order; + performSort(col, order); + endResetModel(); +} + + +void ResourcesModel::setFilter(const std::string& filterText, FilterOptions options) +{ + beginResetModel(); + m_entries.clear(); + bool caseSensitive = options.testFlag(FilterOption::CaseSensitiveOption); + for (auto& entry : m_allEntries) + { + if (FilteredView::match(entry.type, filterText, caseSensitive)) + m_entries.push_back(entry); + else if (FilteredView::match(entry.name, filterText, caseSensitive)) + m_entries.push_back(entry); + else if (FilteredView::match(entry.language, filterText, caseSensitive)) + m_entries.push_back(entry); + else if (FilteredView::match(entry.preview, filterText, caseSensitive)) + m_entries.push_back(entry); + } + performSort(m_sortCol, m_sortOrder); + endResetModel(); +} + + +ResourcesTreeView::ResourcesTreeView(ResourcesWidget* parent, TriageView* view, BinaryViewRef data) : QTreeView(parent) +{ + setFont(getMonospaceFont(this)); + + m_data = data; + m_parent = parent; + m_view = view; + + // Allow view-specific shortcuts when resources are focused + m_actionHandler.setupActionHandler(this); + m_actionHandler.setActionContext([=, this]() { return m_view->actionContext(); }); + + m_model = new ResourcesModel(this, m_data); + setModel(m_model); + setRootIsDecorated(false); + setUniformRowHeights(true); + setSortingEnabled(true); + setSelectionMode(QAbstractItemView::ExtendedSelection); + setSelectionBehavior(QAbstractItemView::SelectRows); + setAllColumnsShowFocus(true); + sortByColumn(ResourcesModel::TypeCol, Qt::AscendingOrder); + for (int i = 0; i < ResourcesModel::ColumnCount; i++) + resizeColumnToContents(i); + + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &QTreeView::customContextMenuRequested, this, [this](const QPoint& pos) { + const ResourceEntry* entry = m_model->getEntry(indexAt(pos)); + if (!entry) + return; + QMenu menu(this); + menu.addAction("Save Resource to File...", this, &ResourcesTreeView::saveSelectedResource); + menu.addAction("Save All Resources to Folder...", this, &ResourcesTreeView::saveAllResources); + menu.addSeparator(); + menu.addAction("Copy", this, &ResourcesTreeView::copySelection); + menu.exec(viewport()->mapToGlobal(pos)); + }); + + connect(selectionModel(), &QItemSelectionModel::currentChanged, this, &ResourcesTreeView::resourceSelected); + connect(this, &QTreeView::doubleClicked, this, &ResourcesTreeView::resourceDoubleClicked); + + m_actionHandler.bindAction("Copy", UIAction([this]() { copySelection(); }, [this]() { return canCopySelection(); })); +} + + +void ResourcesTreeView::copySelection() +{ + if (!model() || !selectionModel()) + return; + + QModelIndexList rows = selectionModel()->selectedRows(); + if (rows.isEmpty()) + return; + + std::sort(rows.begin(), rows.end(), [](const QModelIndex& a, const QModelIndex& b) { return a.row() < b.row(); }); + + QStringList lines; + for (const QModelIndex& rowIndex : rows) + { + QStringList cells; + for (int column = 0; column < m_model->columnCount(QModelIndex()); column++) + { + if (isColumnHidden(column)) + continue; + + QModelIndex idx = m_model->index(rowIndex.row(), column, QModelIndex()); + cells << m_model->data(idx, Qt::DisplayRole).toString(); + } + lines << cells.join("\t"); + } + + if (QClipboard* clipboard = QGuiApplication::clipboard()) + clipboard->setText(lines.join("\n")); +} + + +bool ResourcesTreeView::canCopySelection() const +{ + return !selectionModel()->selectedRows().isEmpty(); +} + + +static std::string SanitizeFilename(std::string name) +{ + for (char& c : name) + { + if (c == '#' || c == '/' || c == '\\' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|') + c = '_'; + } + return name; +} + + +void ResourcesTreeView::saveSelectedResource() +{ + QModelIndexList rows = selectionModel()->selectedRows(); + if (rows.isEmpty()) + return; + + const ResourceEntry* entry = m_model->getEntry(rows.first()); + if (!entry || entry->dataSize == 0) + return; + + // Suggest a filename + std::string ext = GetResourceFileExtension(entry->type); + std::string suggestedName = SanitizeFilename(entry->type); + if (!entry->name.empty()) + suggestedName += "_" + SanitizeFilename(entry->name); + if (!entry->language.empty()) + suggestedName += "_" + SanitizeFilename(entry->language); + suggestedName += ext; + + QString path = QFileDialog::getSaveFileName(this, "Save Resource", QString::fromStdString(suggestedName)); + if (path.isEmpty()) + return; + + BinaryNinja::DataBuffer buf = m_data->ReadBuffer(entry->dataAddress, entry->dataSize); + QFile file(path); + if (file.open(QIODevice::WriteOnly)) + { + file.write(reinterpret_cast<const char*>(buf.GetData()), buf.GetLength()); + file.close(); + } +} + + +void ResourcesTreeView::saveAllResources() +{ + QString dir = QFileDialog::getExistingDirectory(this, "Save All Resources to Folder"); + if (dir.isEmpty()) + return; + + const auto& entries = m_model->getAllEntries(); + for (const auto& entry : entries) + { + if (entry.dataSize == 0) + continue; + + // Build filename: type/name_language.ext, organized into type subdirectories + std::string subdir = SanitizeFilename(entry.type.empty() ? "unknown" : entry.type); + QDir typeDir(dir + "/" + QString::fromStdString(subdir)); + if (!typeDir.exists()) + typeDir.mkpath("."); + + std::string basename = entry.name.empty() ? "unnamed" : SanitizeFilename(entry.name); + if (!entry.language.empty()) + basename += "_" + SanitizeFilename(entry.language); + basename += GetResourceFileExtension(entry.type); + + QString path = typeDir.filePath(QString::fromStdString(basename)); + + BinaryNinja::DataBuffer buf = m_data->ReadBuffer(entry.dataAddress, entry.dataSize); + QFile file(path); + if (file.open(QIODevice::WriteOnly)) + { + file.write(reinterpret_cast<const char*>(buf.GetData()), buf.GetLength()); + file.close(); + } + } +} + + +void ResourcesTreeView::resourceSelected(const QModelIndex& cur, const QModelIndex&) +{ + const ResourceEntry* entry = m_model->getEntry(cur); + if (entry) + m_view->setCurrentOffset(entry->dataAddress); +} + + +void ResourcesTreeView::resourceDoubleClicked(const QModelIndex& cur) +{ + const ResourceEntry* entry = m_model->getEntry(cur); + if (entry) + { + ViewFrame* viewFrame = ViewFrame::viewFrameForWidget(this); + if (viewFrame) + { + viewFrame->navigate("Linear:" + viewFrame->getCurrentDataType(), entry->dataAddress); + } + } +} + + +void ResourcesTreeView::setFilter(const std::string& filterText, FilterOptions options) +{ + m_model->setFilter(filterText, options); +} + + +void ResourcesTreeView::scrollToFirstItem() +{ + scrollToTop(); +} + + +void ResourcesTreeView::scrollToCurrentItem() +{ + scrollTo(currentIndex()); +} + + +void ResourcesTreeView::ensureSelection() +{ + if (auto current = currentIndex(); !current.isValid()) + setCurrentIndex(m_model->index(0, 0, QModelIndex())); +} + + +void ResourcesTreeView::activateSelection() +{ + ensureSelection(); + if (auto current = currentIndex(); current.isValid()) + resourceDoubleClicked(current); +} + + +void ResourcesTreeView::closeFilter() +{ + setFocus(Qt::OtherFocusReason); +} + + +void ResourcesTreeView::keyPressEvent(QKeyEvent* event) +{ + if ((event->text().size() == 1) && (event->text()[0] > ' ') && (event->text()[0] <= '~')) + { + m_parent->showFilter(event->text()); + event->accept(); + } + else if ((event->key() == Qt::Key_Return) || (event->key() == Qt::Key_Enter)) + { + QList<QModelIndex> sel = selectionModel()->selectedIndexes(); + if (sel.size() != 0) + resourceDoubleClicked(sel[0]); + } + else if (event->matches(QKeySequence::Copy)) + { + copySelection(); + event->accept(); + return; + } + QTreeView::keyPressEvent(event); +} + + +ResourcesWidget::ResourcesWidget(QWidget* parent, TriageView* view, BinaryViewRef data) : QWidget(parent) +{ + QVBoxLayout* layout = new QVBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + ResourcesTreeView* resources = new ResourcesTreeView(this, view, data); + m_filter = new FilteredView(this, resources, resources); + m_filter->setFilterPlaceholderText("Search resources"); + layout->addWidget(m_filter, 1); + setLayout(layout); + setMinimumSize(UIContext::getScaledWindowSize(100, 196)); +} + + +void ResourcesWidget::showFilter(const QString& filter) +{ + m_filter->showFilter(filter); +} diff --git a/examples/triage/resources.h b/examples/triage/resources.h new file mode 100644 index 00000000..79869aea --- /dev/null +++ b/examples/triage/resources.h @@ -0,0 +1,102 @@ +#pragma once + +#include <QtCore/QAbstractItemModel> +#include <QtWidgets/QTreeView> +#include "filter.h" + + +struct ResourceEntry +{ + std::string type; + uint64_t typeId; + std::string name; + uint64_t nameId; + std::string language; + uint64_t languageId; + uint64_t dataRva; + uint64_t dataSize; + uint64_t dataAddress; + uint64_t codepage; + std::string preview; +}; + + +class ResourcesModel : public QAbstractItemModel +{ + BinaryViewRef m_data; + std::vector<ResourceEntry> m_allEntries, m_entries; + int m_sortCol; + Qt::SortOrder m_sortOrder; + + void performSort(int col, Qt::SortOrder order); + + public: + enum Column + { + TypeCol = 0, + NameCol = 1, + LanguageCol = 2, + SizeCol = 3, + AddressCol = 4, + PreviewCol = 5, + ColumnCount = 6 + }; + + ResourcesModel(QWidget* parent, BinaryViewRef data); + + virtual int columnCount(const QModelIndex& parent) const override; + virtual int rowCount(const QModelIndex& parent) const override; + virtual QVariant data(const QModelIndex& index, int role) const override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + virtual QModelIndex index(int row, int col, const QModelIndex& parent) const override; + virtual QModelIndex parent(const QModelIndex& index) const override; + virtual void sort(int col, Qt::SortOrder order) override; + void setFilter(const std::string& filterText, FilterOptions options); + + const ResourceEntry* getEntry(const QModelIndex& index) const; + const std::vector<ResourceEntry>& getAllEntries() const { return m_allEntries; } +}; + + +class TriageView; +class ResourcesWidget; + +class ResourcesTreeView : public QTreeView, public FilterTarget +{ + BinaryViewRef m_data; + ResourcesWidget* m_parent; + TriageView* m_view; + UIActionHandler m_actionHandler; + ResourcesModel* m_model; + + public: + ResourcesTreeView(ResourcesWidget* parent, TriageView* view, BinaryViewRef data); + void copySelection(); + bool canCopySelection() const; + void saveSelectedResource(); + void saveAllResources(); + + virtual void setFilter(const std::string& filterText, FilterOptions options) override; + virtual void scrollToFirstItem() override; + virtual void scrollToCurrentItem() override; + virtual void ensureSelection() override; + virtual void activateSelection() override; + virtual void closeFilter() override; + + protected: + virtual void keyPressEvent(QKeyEvent* event) override; + + private Q_SLOTS: + void resourceSelected(const QModelIndex& cur, const QModelIndex& prev); + void resourceDoubleClicked(const QModelIndex& cur); +}; + + +class ResourcesWidget : public QWidget +{ + FilteredView* m_filter; + + public: + ResourcesWidget(QWidget* parent, TriageView* view, BinaryViewRef data); + void showFilter(const QString& filter); +}; diff --git a/examples/triage/view.cpp b/examples/triage/view.cpp index 0e995ae2..67eb27aa 100644 --- a/examples/triage/view.cpp +++ b/examples/triage/view.cpp @@ -6,6 +6,7 @@ #include "entry.h" #include "imports.h" #include "exports.h" +#include "resources.h" #include "sections.h" #include "fileinfo.h" #include "librariesinfo.h" @@ -119,6 +120,19 @@ TriageView::TriageView(QWidget* parent, BinaryViewRef data) : QScrollArea(parent if (sectionsWidget->GetSections().size() == 0) sectionsGroup->hide(); + if (m_data->GetTypeName() == "PE") + { + auto resourcesMd = m_data->QueryMetadata("PEResources"); + if (resourcesMd && resourcesMd->IsArray() && !resourcesMd->GetArray().empty()) + { + QGroupBox* resourcesGroup = new QGroupBox("Resources", container); + QVBoxLayout* resourcesLayout = new QVBoxLayout(); + resourcesLayout->addWidget(new ResourcesWidget(resourcesGroup, this, m_data)); + resourcesGroup->setLayout(resourcesLayout); + layout->addWidget(resourcesGroup); + } + } + QGroupBox* analysisInfoGroup = new QGroupBox("Analysis Info", container); QVBoxLayout* analysisInfoLayout = new QVBoxLayout(); analysisInfoLayout->addWidget(new AnalysisInfoWidget(analysisInfoGroup, m_data)); diff --git a/view/pe/peview.cpp b/view/pe/peview.cpp index ffe9c91a..d7e49dd3 100644 --- a/view/pe/peview.cpp +++ b/view/pe/peview.cpp @@ -22,6 +22,47 @@ using namespace std; static PEViewType* g_peViewType = nullptr; static const char* imageDirName[] = { "exportTable", "importTable", "resourceTable", "exceptionTable", "certificateTable", "baseRelocationTable", "debug", "architecture", "globalPtr", "tlsTable", "loadConfigTable", "boundImport", "iat", "delayImportDescriptor", "clrRuntimeHeader", "reserved"}; + +static const char* GetResourceTypeName(uint32_t id) +{ + switch (id) + { + case 1: return "RT_CURSOR"; + case 2: return "RT_BITMAP"; + case 3: return "RT_ICON"; + case 4: return "RT_MENU"; + case 5: return "RT_DIALOG"; + case 6: return "RT_STRING"; + case 7: return "RT_FONTDIR"; + case 8: return "RT_FONT"; + case 9: return "RT_ACCELERATOR"; + case 10: return "RT_RCDATA"; + case 11: return "RT_MESSAGETABLE"; + case 12: return "RT_GROUP_CURSOR"; + case 14: return "RT_GROUP_ICON"; + case 16: return "RT_VERSION"; + case 17: return "RT_DLGINCLUDE"; + case 19: return "RT_PLUGPLAY"; + case 20: return "RT_VXD"; + case 21: return "RT_ANICURSOR"; + case 22: return "RT_ANIICON"; + case 23: return "RT_HTML"; + case 24: return "RT_MANIFEST"; + default: return nullptr; + } +} + + +struct ResourceParseItem +{ + uint64_t tableAddr; + uint32_t depth; // 0=root, 1=type children, 2=name children + std::string typeName; // resolved at depth 0 + std::string resourceName;// resolved at depth 1 + uint32_t typeId; // raw type ID + uint32_t nameId; // raw name/ID +}; + void BinaryNinja::InitPEViewType() { static PEViewType type; @@ -2657,8 +2698,6 @@ bool PEView::Init() try { - //TODO: properly name tables, entries, data entries - PEDataDirectory dir; // Read resource directory if (m_dataDirs.size() > IMAGE_DIRECTORY_ENTRY_RESOURCE) @@ -2707,7 +2746,51 @@ bool PEView::Init() string resourceDataEntryTypeId = Type::GenerateAutoTypeId("pe", resourceDataEntryName); QualifiedName resourceDataEntryTypeName = DefineType(resourceDataEntryTypeId, resourceDataEntryName, resourceDataEntryType); - std::list<uint64_t> tableAddrsToParse = {dir.virtualAddress}; + // Helper: convert UTF-16LE code units to UTF-8 + auto utf16ToUtf8 = [](const std::vector<uint16_t>& codeUnits) -> std::string { + std::string result; + result.reserve(codeUnits.size()); + for (uint16_t ch : codeUnits) + { + if (ch < 0x80) + result.push_back(static_cast<char>(ch)); + else if (ch < 0x800) + { + result.push_back(static_cast<char>(0xC0 | (ch >> 6))); + result.push_back(static_cast<char>(0x80 | (ch & 0x3F))); + } + else + { + result.push_back(static_cast<char>(0xE0 | (ch >> 12))); + result.push_back(static_cast<char>(0x80 | ((ch >> 6) & 0x3F))); + result.push_back(static_cast<char>(0x80 | (ch & 0x3F))); + } + } + return result; + }; + + // Helper lambda to read a UTF-16LE resource name string and convert to UTF-8 + auto readResourceName = [&](uint64_t nameRva) -> std::string { + if (nameRva < dir.virtualAddress || nameRva >= dir.virtualAddress + dir.size) + return ""; + BinaryReader nameReader(GetParentView(), LittleEndian); + nameReader.Seek(RVAToFileOffset(nameRva)); + uint16_t nameLen = nameReader.Read16(); + if (nameLen == 0 || nameRva + 2 + (nameLen * 2) > dir.virtualAddress + dir.size) + return ""; + + // Define the wide char array data variable + DefineDataVariable(m_imageBase + nameRva + 2, Type::ArrayType(Type::WideCharType(2), nameLen)); + + std::vector<uint16_t> codeUnits(nameLen); + for (uint16_t i = 0; i < nameLen; i++) + codeUnits[i] = nameReader.Read16(); + return utf16ToUtf8(codeUnits); + }; + + // Path-tracking BFS traversal of the resource directory tree + std::list<ResourceParseItem> itemsToParse; + itemsToParse.push_back({dir.virtualAddress, 0, "", "", 0, 0}); std::unordered_set<uint64_t> visitedTables; uint64_t maxTableCount = 10000; @@ -2715,7 +2798,24 @@ bool PEView::Init() maxTableCount = settings->Get<uint64_t>("loader.pe.maxResourceDirectoryTableCount", this); uint32_t resourceDirectoryTableNum = 0; - while (!tableAddrsToParse.empty()) + + // Collect resource leaf entries for metadata + std::vector<std::map<std::string, Ref<Metadata>>> resourceEntries; + // Track used symbol names to handle duplicates + std::unordered_map<std::string, int> usedSymbolNames; + + auto makeUniqueSymbolName = [&](const std::string& baseName) -> std::string { + auto it = usedSymbolNames.find(baseName); + if (it == usedSymbolNames.end()) + { + usedSymbolNames[baseName] = 1; + return baseName; + } + int count = it->second++; + return fmt::format("{}_{}", baseName, count); + }; + + while (!itemsToParse.empty()) { // Check for safety limit to prevent infinite parsing if (resourceDirectoryTableNum >= maxTableCount) @@ -2724,97 +2824,135 @@ bool PEView::Init() break; } - uint64_t tableAddr = tableAddrsToParse.front(); - tableAddrsToParse.pop_front(); + ResourceParseItem item = itemsToParse.front(); + itemsToParse.pop_front(); // Cycle detection - skip if we've already processed this table - if (visitedTables.count(tableAddr)) + if (visitedTables.count(item.tableAddr)) { - m_logger->LogWarn("Resource directory cycle detected at RVA 0x%" PRIx64 ", skipping", tableAddr); + m_logger->LogWarn("Resource directory cycle detected at RVA 0x%" PRIx64 ", skipping", item.tableAddr); continue; } - visitedTables.insert(tableAddr); + visitedTables.insert(item.tableAddr); // Bounds check - ensure table address is within resource section - if (tableAddr < dir.virtualAddress || tableAddr >= dir.virtualAddress + dir.size) + if (item.tableAddr < dir.virtualAddress || item.tableAddr >= dir.virtualAddress + dir.size) { - m_logger->LogWarn("Resource directory table at RVA 0x%" PRIx64 " is outside resource section bounds", tableAddr); + m_logger->LogWarn("Resource directory table at RVA 0x%" PRIx64 " is outside resource section bounds", item.tableAddr); continue; } - // Read in next directory entry - reader.Seek(RVAToFileOffset(tableAddr)); - PEResourceDirectoryTable importDirTable; - importDirTable.characteristics = reader.Read32(); - importDirTable.timeDateStamp = reader.Read32(); - importDirTable.majorVersion = reader.Read16(); - importDirTable.minorVersion = reader.Read16(); - importDirTable.numNameEntries = reader.Read16(); - importDirTable.numIdEntries = reader.Read16(); + // Read in next directory table + reader.Seek(RVAToFileOffset(item.tableAddr)); + PEResourceDirectoryTable dirTable; + dirTable.characteristics = reader.Read32(); + dirTable.timeDateStamp = reader.Read32(); + dirTable.majorVersion = reader.Read16(); + dirTable.minorVersion = reader.Read16(); + dirTable.numNameEntries = reader.Read16(); + dirTable.numIdEntries = reader.Read16(); - DefineDataVariable(m_imageBase + tableAddr, Type::NamedType(this, resourceDirTableTypeName)); - DefineAutoSymbol(new Symbol(DataSymbol, fmt::format("__resource_directory_table_{}", resourceDirectoryTableNum), m_imageBase + tableAddr, NoBinding)); + // Build a descriptive symbol name for this directory table + std::string tableSymName; + if (item.depth == 0) + tableSymName = "__pe_rsrc_root"; + else if (item.depth == 1) + tableSymName = makeUniqueSymbolName(fmt::format("__pe_rsrc_{}", item.typeName)); + else + tableSymName = makeUniqueSymbolName(fmt::format("__pe_rsrc_{}_{}", item.typeName, item.resourceName)); + + DefineDataVariable(m_imageBase + item.tableAddr, Type::NamedType(this, resourceDirTableTypeName)); + DefineAutoSymbol(new Symbol(DataSymbol, tableSymName, m_imageBase + item.tableAddr, NoBinding)); // All the Name entries precede all the ID entries for the table but we treat them the same - // All entries for the table are sorted in ascending order: the Name entries by case-sensitive string and the ID entries by numeric value. // Offsets are relative to the address in the IMAGE_DIRECTORY_ENTRY_RESOURCE DataDirectory. - // Offset value: - // High bit 0. Address of a Resource Data entry (a leaf). - // High bit 1. The lower 31 bits are the address of another resource directory table (the next level down). + struct PendingDataEntry { + size_t offset; + std::string typeName; + std::string resourceName; + std::string langName; + uint32_t typeId; + uint32_t nameId; + uint32_t langId; + }; + std::vector<PendingDataEntry> pendingDataEntries; - std::vector<size_t> dataEntryOffsets; - - size_t numTableEntries = importDirTable.numNameEntries + importDirTable.numIdEntries; + size_t numTableEntries = dirTable.numNameEntries + dirTable.numIdEntries; if (numTableEntries > 0) { for (size_t entryNum = 0; entryNum < numTableEntries; entryNum++) { - PEResourceDirectoryEntry importDirEntry; - importDirEntry.id = reader.Read32(); - importDirEntry.offset = reader.Read32(); - - if (importDirEntry.id & 0x80000000) - { - // Name entry - // First 2 bytes of name are length + PEResourceDirectoryEntry dirEntry; + dirEntry.id = reader.Read32(); + dirEntry.offset = reader.Read32(); - size_t nameAddr = dir.virtualAddress + (importDirEntry.id ^ 0x80000000); + // Resolve the name/ID for this entry based on depth + std::string entryName; + uint32_t entryRawId = dirEntry.id & 0x7FFFFFFF; - // Bounds check - ensure name address is within resource section - if (nameAddr < dir.virtualAddress || nameAddr >= dir.virtualAddress + dir.size) + if (dirEntry.id & 0x80000000) + { + // Name entry — UTF-16LE string at the offset + uint64_t nameAddr = dir.virtualAddress + entryRawId; + std::string uniName = readResourceName(nameAddr); + if (!uniName.empty()) + entryName = uniName; + else + entryName = fmt::format("name_{:x}", entryRawId); + } + else + { + // ID entry — resolve based on depth + if (item.depth == 0) { - m_logger->LogWarn("Resource name at RVA 0x%zx is outside resource section bounds, skipping", nameAddr); - continue; + // Type ID + const char* typeName = GetResourceTypeName(entryRawId); + if (typeName) + entryName = typeName; + else + entryName = fmt::format("type_{}", entryRawId); } - - BinaryReader nameReader(GetParentView(), LittleEndian); - nameReader.Seek(RVAToFileOffset(nameAddr)); - - uint16_t nameLen = nameReader.Read16(); - - // Check that the full name fits within the resource section - // nameLen is in UTF-16 code-units, each 2 bytes, plus 2 bytes for the length prefix - if (nameAddr + 2 + (nameLen * 2) > dir.virtualAddress + dir.size) + else if (item.depth == 1) { - m_logger->LogWarn("Resource name extends beyond resource section bounds, skipping"); - continue; + // Resource name/ID + entryName = fmt::format("#{}", entryRawId); + } + else + { + // Language ID + entryName = fmt::format("lang{}", entryRawId); } + } + + // Build child item context + std::string childTypeName = item.typeName; + std::string childResourceName = item.resourceName; + uint32_t childTypeId = item.typeId; + uint32_t childNameId = item.nameId; - // Plus 2 because it's length-prefixed - DefineDataVariable(m_imageBase + nameAddr + 2, Type::ArrayType(Type::WideCharType(2), nameLen)); + if (item.depth == 0) + { + childTypeName = entryName; + childTypeId = entryRawId; + } + else if (item.depth == 1) + { + childResourceName = entryName; + childNameId = entryRawId; } - if (importDirEntry.offset & 0x80000000) + if (dirEntry.offset & 0x80000000) { // Lower 31 bits are address of another table - uint64_t nextTableAddr = dir.virtualAddress + (importDirEntry.offset ^ 0x80000000); + uint64_t nextTableAddr = dir.virtualAddress + (dirEntry.offset ^ 0x80000000); // Bounds check before adding to queue to prevent invalid references if (nextTableAddr >= dir.virtualAddress && nextTableAddr < dir.virtualAddress + dir.size) { - tableAddrsToParse.push_back(nextTableAddr); + itemsToParse.push_back({nextTableAddr, item.depth + 1, + childTypeName, childResourceName, childTypeId, childNameId}); } else { @@ -2823,32 +2961,54 @@ bool PEView::Init() } else { - // Address of data entry - // Validate offset is within resource section bounds - uint64_t dataEntryAddr = dir.virtualAddress + importDirEntry.offset; + // Address of data entry (leaf) + uint64_t dataEntryAddr = dir.virtualAddress + dirEntry.offset; if (dataEntryAddr >= dir.virtualAddress && dataEntryAddr + sizeof(PEResourceDataEntry) <= dir.virtualAddress + dir.size) { - dataEntryOffsets.push_back(importDirEntry.offset); + std::string langName; + uint32_t langId = 0; + if (item.depth >= 2) + { + // This shouldn't happen in a well-formed PE (depth 2 entries point to data), + // but handle gracefully + langName = entryName; + langId = entryRawId; + } + else if (item.depth == 1) + { + // We're at the name level, entry is language + langName = entryName; + langId = entryRawId; + } + else + { + langName = entryName; + langId = entryRawId; + } + + pendingDataEntries.push_back({(size_t)dirEntry.offset, + childTypeName, childResourceName, langName, + childTypeId, childNameId, langId}); } else { - m_logger->LogWarn("Resource data entry at offset 0x%" PRIx32 " is outside resource section bounds, skipping", importDirEntry.offset); + m_logger->LogWarn("Resource data entry at offset 0x%" PRIx32 " is outside resource section bounds, skipping", dirEntry.offset); } } } - size_t tableEntriesStart = m_imageBase + tableAddr + sizeof(PEResourceDirectoryTable); + size_t tableEntriesStart = m_imageBase + item.tableAddr + sizeof(PEResourceDirectoryTable); DefineDataVariable(tableEntriesStart, Type::ArrayType(Type::NamedType(this, resourceDirEntryTypeName), numTableEntries)); - DefineAutoSymbol(new Symbol(DataSymbol, fmt::format("__resource_directory_table_{}_entries", resourceDirectoryTableNum), tableEntriesStart, NoBinding)); + std::string entriesSymName = tableSymName + "_entries"; + DefineAutoSymbol(new Symbol(DataSymbol, entriesSymName, tableEntriesStart, NoBinding)); } - // Create BinaryReader once outside the loop for better performance + // Process data entries (leaves) BinaryReader entryReader(GetParentView(), LittleEndian); - for(size_t dataEntryNum = 0; dataEntryNum < dataEntryOffsets.size(); dataEntryNum++) + for (auto& pending : pendingDataEntries) { - size_t entryOffset = dataEntryOffsets[dataEntryNum]; - entryReader.Seek(RVAToFileOffset(dir.virtualAddress + entryOffset)); + entryReader.Seek(RVAToFileOffset(dir.virtualAddress + pending.offset)); PEResourceDataEntry dataEntry; dataEntry.dataRva = entryReader.Read32(); dataEntry.dataSize = entryReader.Read32(); @@ -2856,12 +3016,8 @@ bool PEView::Init() dataEntry.reserved = entryReader.Read32(); if (dataEntry.reserved != 0) - { - // Invalid entry, this needs to be 0 continue; - } - // Limit excessively large data sizes const uint32_t MAX_REASONABLE_RESOURCE_SIZE = 100 * 1024 * 1024; // 100 MB if (dataEntry.dataSize > MAX_REASONABLE_RESOURCE_SIZE) { @@ -2869,7 +3025,6 @@ bool PEView::Init() continue; } - // Check for overflow when adding dataRva + dataSize if (dataEntry.dataSize > 0) { uint64_t dataEnd = (uint64_t)dataEntry.dataRva + dataEntry.dataSize; @@ -2880,20 +3035,315 @@ bool PEView::Init() } } - size_t entryAddr = m_imageBase + dir.virtualAddress + entryOffset; + // Build descriptive path-based symbol names + std::string pathPrefix; + if (!pending.typeName.empty() && !pending.resourceName.empty() && !pending.langName.empty()) + pathPrefix = fmt::format("__pe_rsrc_{}_{}_{}", pending.typeName, pending.resourceName, pending.langName); + else if (!pending.typeName.empty() && !pending.resourceName.empty()) + pathPrefix = fmt::format("__pe_rsrc_{}_{}", pending.typeName, pending.resourceName); + else if (!pending.typeName.empty()) + pathPrefix = fmt::format("__pe_rsrc_{}", pending.typeName); + else + pathPrefix = fmt::format("__pe_rsrc_entry_{}", resourceDirectoryTableNum); + size_t entryAddr = m_imageBase + dir.virtualAddress + pending.offset; DefineDataVariable(entryAddr, Type::NamedType(this, resourceDataEntryTypeName)); - DefineAutoSymbol(new Symbol(DataSymbol, fmt::format("__resource_directory_table_{}_data_entry_{}", resourceDirectoryTableNum, dataEntryNum), entryAddr, NoBinding)); + DefineAutoSymbol(new Symbol(DataSymbol, makeUniqueSymbolName(pathPrefix + "_data_entry"), entryAddr, NoBinding)); - //TODO: properly name based on path taken to get here if (dataEntry.dataSize > 0) { DefineDataVariable(m_imageBase + dataEntry.dataRva, Type::ArrayType(Type::IntegerType(1, true), dataEntry.dataSize)); + DefineAutoSymbol(new Symbol(DataSymbol, makeUniqueSymbolName(pathPrefix + "_data"), m_imageBase + dataEntry.dataRva, NoBinding)); } + + // Generate preview for parseable resource types + std::string preview; + if (pending.typeId == 6 && dataEntry.dataSize > 0) // RT_STRING + { + try + { + BinaryReader strReader(GetParentView(), LittleEndian); + strReader.Seek(RVAToFileOffset(dataEntry.dataRva)); + size_t bytesRead = 0; + std::vector<std::string> strings; + for (int strIdx = 0; strIdx < 16 && bytesRead < dataEntry.dataSize; strIdx++) + { + uint16_t strLen = strReader.Read16(); + bytesRead += 2; + if (strLen == 0) + continue; + if (bytesRead + strLen * 2 > dataEntry.dataSize) + break; + std::vector<uint16_t> codeUnits(strLen); + for (uint16_t i = 0; i < strLen; i++) + codeUnits[i] = strReader.Read16(); + bytesRead += strLen * 2; + std::string s = utf16ToUtf8(codeUnits); + if (!s.empty()) + strings.push_back(s); + } + for (size_t i = 0; i < strings.size(); i++) + { + if (i > 0) + preview += ", "; + if (preview.size() + strings[i].size() > 200) + { + preview += "..."; + break; + } + preview += strings[i]; + } + } + catch (...) {} + } + + // Collect metadata for this resource leaf + std::map<std::string, Ref<Metadata>> entry; + entry["type"] = new Metadata(pending.typeName); + entry["typeId"] = new Metadata((uint64_t)pending.typeId); + entry["name"] = new Metadata(pending.resourceName); + entry["nameId"] = new Metadata((uint64_t)pending.nameId); + entry["language"] = new Metadata(pending.langName); + entry["languageId"] = new Metadata((uint64_t)pending.langId); + entry["dataRva"] = new Metadata((uint64_t)dataEntry.dataRva); + entry["dataSize"] = new Metadata((uint64_t)dataEntry.dataSize); + entry["dataAddress"] = new Metadata((uint64_t)(m_imageBase + dataEntry.dataRva)); + entry["codepage"] = new Metadata((uint64_t)dataEntry.dataCodePage); + entry["preview"] = new Metadata(preview); + resourceEntries.push_back(entry); } resourceDirectoryTableNum++; } + + // Parse RT_VERSION resource and store version info metadata + for (auto& resEntry : resourceEntries) + { + uint64_t typeId = resEntry["typeId"]->GetUnsignedInteger(); + if (typeId != 16) // RT_VERSION + continue; + uint64_t dataAddr = resEntry["dataAddress"]->GetUnsignedInteger(); + uint64_t dataSize = resEntry["dataSize"]->GetUnsignedInteger(); + if (dataSize < 6 || dataSize > 65536) + break; + + try + { + BinaryReader vr(GetParentView(), LittleEndian); + vr.Seek(RVAToFileOffset(resEntry["dataRva"]->GetUnsignedInteger())); + size_t baseOffset = vr.GetOffset(); + + uint16_t viLength = vr.Read16(); + uint16_t viValueLength = vr.Read16(); + uint16_t viType = vr.Read16(); + (void)viType; + + // Read and verify "VS_VERSION_INFO" key + std::vector<uint16_t> keyChars; + for (int i = 0; i < 20; i++) + { + uint16_t ch = vr.Read16(); + if (ch == 0) break; + keyChars.push_back(ch); + } + std::string keyStr = utf16ToUtf8(keyChars); + if (keyStr != "VS_VERSION_INFO") + break; + + // Align to DWORD boundary + size_t pos = vr.GetOffset() - baseOffset; + if (pos % 4 != 0) + vr.SeekRelative(4 - (pos % 4)); + + // Parse VS_FIXEDFILEINFO + std::map<std::string, Ref<Metadata>> versionInfo; + if (viValueLength >= 52) + { + uint32_t sig = vr.Read32(); + if (sig == 0xFEEF04BD) + { + vr.Read32(); // dwStrucVersion + uint32_t fileVerMS = vr.Read32(); + uint32_t fileVerLS = vr.Read32(); + uint32_t prodVerMS = vr.Read32(); + uint32_t prodVerLS = vr.Read32(); + + std::string fileVer = fmt::format("{}.{}.{}.{}", + (fileVerMS >> 16) & 0xFFFF, fileVerMS & 0xFFFF, + (fileVerLS >> 16) & 0xFFFF, fileVerLS & 0xFFFF); + std::string prodVer = fmt::format("{}.{}.{}.{}", + (prodVerMS >> 16) & 0xFFFF, prodVerMS & 0xFFFF, + (prodVerLS >> 16) & 0xFFFF, prodVerLS & 0xFFFF); + + versionInfo["FileVersion"] = new Metadata(fileVer); + versionInfo["ProductVersion"] = new Metadata(prodVer); + + // Skip rest of VS_FIXEDFILEINFO (7 more DWORDs): + // fileFlagsMask, fileFlags, fileOS, fileType, fileSubtype, fileDateMS, fileDateLS + for (int i = 0; i < 7; i++) + vr.Read32(); + } + } + + // Align after VS_FIXEDFILEINFO + pos = vr.GetOffset() - baseOffset; + if (pos % 4 != 0) + vr.SeekRelative(4 - (pos % 4)); + + // Parse StringFileInfo / VarFileInfo children + size_t endOffset = baseOffset + std::min((size_t)viLength, (size_t)dataSize); + while ((size_t)vr.GetOffset() + 6 < endOffset) + { + size_t childBase = vr.GetOffset(); + uint16_t childLength = vr.Read16(); + uint16_t childValueLength = vr.Read16(); + uint16_t childType = vr.Read16(); + (void)childValueLength; + (void)childType; + + if (childLength < 6 || childBase + childLength > baseOffset + dataSize) + break; + + // Read child key + std::vector<uint16_t> childKeyChars; + for (int i = 0; i < 30; i++) + { + if ((size_t)vr.GetOffset() >= childBase + childLength) + break; + uint16_t ch = vr.Read16(); + if (ch == 0) break; + childKeyChars.push_back(ch); + } + std::string childKey = utf16ToUtf8(childKeyChars); + + if (childKey == "StringFileInfo") + { + // Align + pos = vr.GetOffset() - baseOffset; + if (pos % 4 != 0) + vr.SeekRelative(4 - (pos % 4)); + + size_t sfiEnd = childBase + childLength; + // Parse StringTable(s) + while ((size_t)vr.GetOffset() + 6 < sfiEnd) + { + size_t stBase = vr.GetOffset(); + uint16_t stLength = vr.Read16(); + vr.Read16(); // stValueLength + vr.Read16(); // stType + + if (stLength < 6 || stBase + stLength > sfiEnd) + break; + + // Skip StringTable key (language+codepage like "040904b0") + for (int i = 0; i < 12; i++) + { + if ((size_t)vr.GetOffset() >= stBase + stLength) + break; + uint16_t ch = vr.Read16(); + if (ch == 0) break; + } + + // Align + pos = vr.GetOffset() - baseOffset; + if (pos % 4 != 0) + vr.SeekRelative(4 - (pos % 4)); + + size_t stEnd = stBase + stLength; + // Parse individual String entries + while ((size_t)vr.GetOffset() + 6 < stEnd) + { + size_t strBase = vr.GetOffset(); + uint16_t strLength = vr.Read16(); + uint16_t strValueLength = vr.Read16(); + uint16_t strType = vr.Read16(); + (void)strType; + + if (strLength < 6 || strBase + strLength > stEnd) + break; + + // Read key + std::vector<uint16_t> strKeyChars; + for (int i = 0; i < 80; i++) + { + if ((size_t)vr.GetOffset() >= strBase + strLength) + break; + uint16_t ch = vr.Read16(); + if (ch == 0) break; + strKeyChars.push_back(ch); + } + std::string strKey = utf16ToUtf8(strKeyChars); + + // Align + pos = vr.GetOffset() - baseOffset; + if (pos % 4 != 0) + vr.SeekRelative(4 - (pos % 4)); + + // Read value + std::string strValue; + if (strValueLength > 0 && (size_t)vr.GetOffset() < strBase + strLength) + { + std::vector<uint16_t> valChars; + uint16_t charsToRead = strValueLength; + for (uint16_t i = 0; i < charsToRead; i++) + { + if ((size_t)vr.GetOffset() >= strBase + strLength) + break; + uint16_t ch = vr.Read16(); + if (ch == 0) break; + valChars.push_back(ch); + } + strValue = utf16ToUtf8(valChars); + } + + if (!strKey.empty() && !strValue.empty()) + versionInfo[strKey] = new Metadata(strValue); + + // Advance to next String entry + vr.Seek(strBase + ((strLength + 3) & ~3)); + } + + // Advance to next StringTable + vr.Seek(stBase + ((stLength + 3) & ~3)); + } + } + + // Advance to next child + vr.Seek(childBase + ((childLength + 3) & ~3)); + } + + if (!versionInfo.empty()) + { + StoreMetadata("PEVersionInfo", new Metadata(versionInfo), true); + + // Set preview on this resource entry + std::string verPreview; + auto fv = versionInfo.find("FileVersion"); + if (fv != versionInfo.end()) + verPreview = fv->second->GetString(); + auto cn = versionInfo.find("CompanyName"); + if (cn != versionInfo.end()) + { + if (!verPreview.empty()) + verPreview += " - "; + verPreview += cn->second->GetString(); + } + resEntry["preview"] = new Metadata(verPreview); + } + } + catch (...) {} + break; // Only parse first RT_VERSION + } + + // Store resource tree as metadata for programmatic access + if (!resourceEntries.empty()) + { + std::vector<Ref<Metadata>> metadataArray; + metadataArray.reserve(resourceEntries.size()); + for (auto& entry : resourceEntries) + metadataArray.push_back(new Metadata(entry)); + StoreMetadata("PEResources", new Metadata(metadataArray), true); + } } } catch (std::exception& e) |
