summaryrefslogtreecommitdiff
path: root/view/kernelcache/ui
diff options
context:
space:
mode:
authorkat <kat@vector35.com>2025-03-19 09:12:52 -0400
committerkat <kat@vector35.com>2025-03-19 15:04:23 -0400
commit7f3f394c8bb50c987a5237bc74aa503486571058 (patch)
tree9f5ada34f6226562b79f0dce70bfebab124a1bdb /view/kernelcache/ui
parente6d4d38e2a5dc66d3c8004cc917bc08e39337482 (diff)
Add iOS/macOS MH_FILESET KernelCache View and loader.
This loader is inspired by/based on our dyld_shared_cache loader, following the same design language. It targets primarily the latest kernels, but should support any with the MH_FILESET format. It allows you to decide which images you would like to map in (the kernel itself included), resulting in targeted analysis when you may only need to load a singular image. It also supports dropping in compressed KernelCaches, directly from the ipsw. This is an early solution and we have many more changes and improvements planned. We look forward to your feedback
Diffstat (limited to 'view/kernelcache/ui')
-rw-r--r--view/kernelcache/ui/CMakeLists.txt95
-rw-r--r--view/kernelcache/ui/KernelCacheUINotifications.cpp92
-rw-r--r--view/kernelcache/ui/KernelCacheUINotifications.h23
-rw-r--r--view/kernelcache/ui/Plugin.cpp22
-rw-r--r--view/kernelcache/ui/kctriage.cpp556
-rw-r--r--view/kernelcache/ui/kctriage.h243
6 files changed, 1031 insertions, 0 deletions
diff --git a/view/kernelcache/ui/CMakeLists.txt b/view/kernelcache/ui/CMakeLists.txt
new file mode 100644
index 00000000..369dc603
--- /dev/null
+++ b/view/kernelcache/ui/CMakeLists.txt
@@ -0,0 +1,95 @@
+cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
+
+project(kernelcacheui)
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+find_package(Qt6 COMPONENTS Core Gui Widgets REQUIRED)
+
+file(GLOB SOURCES *.cpp *.h)
+list(FILTER SOURCES EXCLUDE REGEX moc_.*)
+list(FILTER SOURCES EXCLUDE REGEX qrc_.*)
+
+add_library(kernelcacheui SHARED ${SOURCES})
+
+set(COMPILE_DEFS "")
+
+if (HARD_FAIL_MODE)
+ set(COMPILE_DEFS "${COMPILE_DEFS} ABORT_FAILURES;")
+endif()
+
+if (BN_REF_COUNT_DEBUG)
+ set(COMPILE_DEFS "${COMPILE_DEFS} BN_REF_COUNT_DEBUG;")
+endif()
+
+if (SLIDEINFO_DEBUG_TAGS)
+ set(COMPILE_DEFS "${COMPILE_DEFS} SLIDEINFO_DEBUG_TAGS;")
+endif()
+
+if (METADATA_VERSION)
+ set(COMPILE_DEFS "${COMPILE_DEFS} METADATA_VERSION=${METADATA_VERSION};")
+else()
+ message(FATAL_ERROR "No metadata version provided. Fatal.")
+endif()
+
+if (KC_VIEW_NAME)
+ set(COMPILE_DEFS "${COMPILE_DEFS} KC_VIEW_NAME=\"${KC_VIEW_NAME}\";")
+else()
+ message(FATAL_ERROR "No view name provided. Fatal.")
+endif()
+
+target_compile_definitions(kernelcacheui PRIVATE ${COMPILE_DEFS})
+
+
+if(BN_INTERNAL_BUILD)
+ set_target_properties(kernelcacheui PROPERTIES
+ LIBRARY_OUTPUT_DIRECTORY ${BN_CORE_PLUGIN_DIR}
+ RUNTIME_OUTPUT_DIRECTORY ${BN_CORE_PLUGIN_DIR})
+else()
+ set_target_properties(kernelcacheui PROPERTIES
+ LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/out/plugins
+ RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/out/plugins
+ )
+endif()
+
+set_target_properties(kernelcacheui PROPERTIES
+ CXX_STANDARD 17
+ CXX_STANDARD_REQUIRED ON
+ CXX_VISIBILITY_PRESET hidden
+ VISIBILITY_INLINES_HIDDEN ON
+ POSITION_INDEPENDENT_CODE ON
+ )
+
+function(get_recursive_include_dirs target result)
+ # Initialize an empty list to store include directories
+ set(include_dirs "")
+
+ # Get the include directories of the current target
+ get_target_property(current_target_includes ${target} INTERFACE_INCLUDE_DIRECTORIES)
+ if(current_target_includes)
+ list(APPEND include_dirs ${current_target_includes})
+ endif()
+
+ # Get the libraries that this target links to
+ get_target_property(linked_libraries ${target} INTERFACE_LINK_LIBRARIES)
+ if(linked_libraries)
+ foreach(linked_library IN LISTS linked_libraries)
+ # Skip plain library names (non-target libraries)
+ if(TARGET ${linked_library})
+ # Recursively get include directories from linked libraries
+ get_recursive_include_dirs(${linked_library} linked_library_includes)
+ list(APPEND include_dirs ${linked_library_includes})
+ endif()
+ endforeach()
+ endif()
+
+ # Set the result to the collected include directories
+ set(${result} ${include_dirs} PARENT_SCOPE)
+endfunction()
+
+get_recursive_include_dirs(kernelcacheapi INCLUDES)
+
+target_include_directories(kernelcacheui PRIVATE ${INCLUDES})
+
+target_link_libraries(kernelcacheui kernelcacheapi kernelcache binaryninjaui Qt6::Core Qt6::Gui Qt6::Widgets)
+
diff --git a/view/kernelcache/ui/KernelCacheUINotifications.cpp b/view/kernelcache/ui/KernelCacheUINotifications.cpp
new file mode 100644
index 00000000..6bd8fdf0
--- /dev/null
+++ b/view/kernelcache/ui/KernelCacheUINotifications.cpp
@@ -0,0 +1,92 @@
+//
+// Created by kat on 5/8/23.
+//
+
+#include "KernelCacheUINotifications.h"
+#include <QLayout>
+#include <kernelcacheapi.h>
+#include "ui/sidebar.h"
+#include "ui/linearview.h"
+#include "ui/viewframe.h"
+#include "progresstask.h"
+
+UINotifications* UINotifications::m_instance = nullptr;
+
+void UINotifications::init()
+{
+ m_instance = new UINotifications;
+ UIContext::registerNotification(m_instance);
+}
+
+
+void UINotifications::OnViewChange(UIContext* context, ViewFrame* frame, const QString& type)
+{
+ if (!frame)
+ return;
+
+ // FIXME there is a bv func for this
+ static std::function<bool(Ref<BinaryView>, uint64_t)> isAddrMapped = [](Ref<BinaryView> view, uint64_t addr) {
+ if (view && view->GetTypeName() == KC_VIEW_NAME)
+ {
+ for (const auto& seg : view->GetSegments())
+ {
+ if (seg->GetStart() <= addr && seg->GetEnd() > addr)
+ return true;
+ }
+ }
+ return false;
+ };
+
+ auto view = frame->getCurrentBinaryView();
+ if (view && view->GetTypeName() == KC_VIEW_NAME)
+ {
+ if (auto viewInt = frame->getCurrentViewInterface())
+ {
+ auto ah = viewInt->actionHandler();
+ if (!ah->isBoundAction("KC Load IMGHERE"))
+ {
+ ah->bindAction("KC Load IMGHERE",
+ UIAction(
+ [](const UIActionContext& ctx) {
+ Ref<BinaryView> view = ctx.binaryView;
+ Ref<KernelCacheAPI::KernelCache> cache = new KernelCacheAPI::KernelCache(view);
+ uint64_t addr = ctx.token.token.value;
+ if (addr)
+ {
+ BackgroundThread::create(ctx.context->mainWindow())->thenBackground(
+ [cache=cache, addr=addr]() {
+ cache->LoadImageContainingAddress(addr);
+ })->start();
+ }
+ },
+ [](const UIActionContext& ctx) {
+ Ref<KernelCacheAPI::KernelCache> cache = new KernelCacheAPI::KernelCache(ctx.binaryView);
+ uint64_t addr = ctx.token.token.value;
+ if (isAddrMapped(ctx.binaryView, addr))
+ return false;
+ return addr && cache->GetImageNameForAddress(addr) != ""; // bool
+ }));
+ ah->setActionDisplayName("KC Load IMGHERE", [](const UIActionContext& ctx) {
+ Ref<KernelCacheAPI::KernelCache> cache = new KernelCacheAPI::KernelCache(ctx.binaryView);
+ uint64_t addr = ctx.token.token.value;
+ if (addr)
+ return QString("Load ") + cache->GetImageNameForAddress(addr).c_str();
+ return QString("Error");
+ });
+ if (auto linearView = qobject_cast<LinearView*>(viewInt->widget()))
+ {
+ linearView->contextMenu().addAction("KC Load IMGHERE", KC_VIEW_NAME);
+ linearView->contextMenu().setGroupOrdering(KC_VIEW_NAME, 0);
+ }
+ }
+ }
+ }
+}
+void UINotifications::OnAfterOpenFile(UIContext* context, FileContext* file, ViewFrame* frame)
+{
+ if (frame->getCurrentBinaryView())
+ {
+ // Register BD notifications. We dont use them right now.
+ }
+ UIContextNotification::OnAfterOpenFile(context, file, frame);
+}
diff --git a/view/kernelcache/ui/KernelCacheUINotifications.h b/view/kernelcache/ui/KernelCacheUINotifications.h
new file mode 100644
index 00000000..9ea4b50d
--- /dev/null
+++ b/view/kernelcache/ui/KernelCacheUINotifications.h
@@ -0,0 +1,23 @@
+//
+// Created by kat on 5/8/23.
+//
+#include "ui/uicontext.h"
+
+#ifndef SHAREDCACHE_NOTIFICATIONS_H
+#define SHAREDCACHE_NOTIFICATIONS_H
+
+class UINotifications : public UIContextNotification {
+ static UINotifications* m_instance;
+
+ std::vector<size_t> m_sessionsAlreadyDisplayedPickerFor;
+
+public:
+ virtual void OnViewChange(UIContext *context, ViewFrame *frame, const QString &type) override;
+ // bool OnAfterOpenDatabase(UIContext* context, FileMetadataRef metadata, BinaryViewRef data) override;
+ void OnAfterOpenFile(UIContext* context, FileContext* file, ViewFrame* frame) override;
+
+ static void init();
+};
+
+
+#endif //SHAREDCACHE_NOTIFICATIONS_H
diff --git a/view/kernelcache/ui/Plugin.cpp b/view/kernelcache/ui/Plugin.cpp
new file mode 100644
index 00000000..cfa92fe4
--- /dev/null
+++ b/view/kernelcache/ui/Plugin.cpp
@@ -0,0 +1,22 @@
+//
+// Created by kat on 8/6/24.
+//
+#include <binaryninjaapi.h>
+#include "KernelCacheUINotifications.h"
+#include "kctriage.h"
+
+extern "C"
+{
+ BN_DECLARE_CORE_ABI_VERSION
+ BN_DECLARE_UI_ABI_VERSION
+
+ BINARYNINJAPLUGIN bool UIPluginInit()
+ {
+ UINotifications::init();
+ UIAction::registerAction("KC Load IMGHERE");
+
+ KCTriageViewType::Register();
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/view/kernelcache/ui/kctriage.cpp b/view/kernelcache/ui/kctriage.cpp
new file mode 100644
index 00000000..5f0fa9ce
--- /dev/null
+++ b/view/kernelcache/ui/kctriage.cpp
@@ -0,0 +1,556 @@
+//
+// Created by kat on 8/15/24.
+//
+
+#include "kctriage.h"
+#include "ui/fontsettings.h"
+#include <QPainter>
+#include <QTextBrowser>
+#include "tabwidget.h"
+#include "globalarea.h"
+#include "progresstask.h"
+
+#include <cmath>
+#include <QMessageBox>
+
+
+#define QSETTINGS_KEY_SELECTED_TAB "KCTriage-SelectedTab"
+#define QSETTINGS_KEY_TAB_LAYOUT "KCTriage-TabLayout"
+#define QSETTINGS_KEY_IMAGELOAD_TAB_LAYOUT "KCTriage-ImageLoadTabLayout"
+#define QSETTINGS_KEY_ALPHA_POPUP_SEEN "KCTriage-AlphaPopupSeenV2"
+
+
+SymbolTableModel::SymbolTableModel(SymbolTableView* parent)
+ : QAbstractTableModel(parent), m_parent(parent) {
+}
+
+int SymbolTableModel::rowCount(const QModelIndex& parent) const {
+ Q_UNUSED(parent);
+ return static_cast<int>(m_symbols.size());
+}
+
+int SymbolTableModel::columnCount(const QModelIndex& parent) const {
+ Q_UNUSED(parent);
+ // We have 3 columns: Address, Name, and Image
+ return 3;
+}
+
+QVariant SymbolTableModel::data(const QModelIndex& index, int role) const {
+ if (!index.isValid() || role != Qt::DisplayRole) {
+ return QVariant();
+ }
+
+ const KernelCacheAPI::KCSymbol& symbol = m_symbols.at(index.row());
+
+ switch (index.column()) {
+ case 0: // Address column
+ return QString("0x%1").arg(symbol.address, 0, 16); // Display address as hexadecimal
+ case 1: // Name column
+ return QString::fromStdString(symbol.name);
+ case 2: // Image column
+ return QString::fromStdString(symbol.image);
+ default:
+ return QVariant();
+ }
+}
+
+QVariant SymbolTableModel::headerData(int section, Qt::Orientation orientation, int role) const {
+ if (role != Qt::DisplayRole || orientation != Qt::Horizontal) {
+ return QVariant();
+ }
+
+ switch (section) {
+ case 0:
+ return QString("Address");
+ case 1:
+ return QString("Name");
+ case 2:
+ return QString("Image");
+ default:
+ return QVariant();
+ }
+}
+
+void SymbolTableModel::updateSymbols() {
+ m_symbols = m_parent->m_symbols;
+ setFilter(m_filter);
+}
+
+const KernelCacheAPI::KCSymbol& SymbolTableModel::symbolAt(int row) const {
+ return m_symbols.at(row);
+}
+
+
+void SymbolTableModel::setFilter(std::string text)
+{
+ beginResetModel();
+
+ m_filter = text;
+ m_symbols.clear();
+
+ if (m_filter.empty())
+ {
+ m_symbols = m_parent->m_symbols;
+ }
+ else
+ {
+ m_symbols.reserve(m_parent->m_symbols.size());
+ for (const auto& symbol : m_parent->m_symbols)
+ {
+ if (symbol.name.find(m_filter) != std::string::npos)
+ {
+ m_symbols.push_back(symbol);
+ }
+ }
+ m_symbols.shrink_to_fit();
+ }
+
+ endResetModel();
+}
+
+
+SymbolTableView::SymbolTableView(QWidget* parent, Ref<KernelCacheAPI::KernelCache> cache)
+ : m_model(new SymbolTableModel(this)) {
+
+ // Set up the filter model
+ setModel(m_model);
+
+ // Configure view settings
+ horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
+ setSelectionBehavior(QAbstractItemView::SelectRows);
+ setSelectionMode(QAbstractItemView::SingleSelection);
+
+ BackgroundThread::create(this)->thenBackground([this, cache=cache](){
+ // LogInfo("Symbol Search: Loading symbols...");
+ m_symbols = cache->LoadAllSymbolsAndWait();
+ // LogInfo("Symbol Search: Loaded 0x%zx symbols", m_symbols.size());
+ })->thenMainThread([this](){
+ m_model->updateSymbols();
+ })->start();
+}
+
+SymbolTableView::~SymbolTableView() {
+ delete m_model;
+}
+
+void SymbolTableView::setFilter(const std::string& filter) {
+ m_model->setFilter(filter);
+}
+
+
+KCTriageView::KCTriageView(QWidget* parent, BinaryViewRef data) : QWidget(parent), View(), m_data(data), m_cache(new KernelCacheAPI::KernelCache(data))
+{
+ setBinaryDataNavigable(true);
+ setupView(this);
+
+ m_triageCollection = new DockableTabCollection();
+ m_triageTabs = new SplitTabWidget(m_triageCollection);
+
+ auto triageTabStyle = new GlobalAreaTabStyle();
+ m_triageTabs->setTabStyle(triageTabStyle);
+
+ QSplitter* containerWidget = new QSplitter;
+ containerWidget->setOrientation(Qt::Vertical);
+
+ QWidget* defaultWidget = nullptr;
+
+ m_bottomRegionCollection = new DockableTabCollection();
+ m_bottomRegionTabs = new SplitTabWidget(m_bottomRegionCollection);
+ m_bottomRegionTabs->setTabStyle(new GlobalAreaTabStyle());
+
+ auto loadImageTable = new FilterableTableView;
+ {
+ auto loadImageModel = new QStandardItemModel(0, 2, loadImageTable);
+ {
+ loadImageModel->setHorizontalHeaderLabels({"Name", "VM Address"});
+ BackgroundThread::create(loadImageTable)->thenBackground([this, loadImageModel](QVariant var)
+ {
+ QVariantList rows;
+
+ auto images = m_cache->GetImages();
+
+ auto newHeaders = std::make_shared<std::vector<KernelCacheAPI::KernelCacheMachOHeader>>();
+ newHeaders->reserve(images.size());
+
+ for (const auto& img : images)
+ {
+ if (auto header = m_cache->GetMachOHeaderForImage(img.name); header)
+ {
+ newHeaders->push_back(*header);
+ rows.push_back(QList<QVariant>{
+ QString::fromStdString(img.name),
+ QString("0x%1").arg(header->textBase, 0, 16)
+ });
+ }
+ }
+
+ {
+ std::unique_lock<std::mutex> lock(m_headersMutex);
+ m_headers.swap(newHeaders);
+ }
+
+ return QVariant(rows);
+ })->thenMainThread([this, loadImageModel, loadImageTable](QVariant var){
+ QVariantList rows = var.toList();
+
+ if (loadImageModel->rowCount() > 0)
+ {
+ loadImageModel->removeRows(0, loadImageModel->rowCount());
+ }
+
+ for (const QVariant &rowVariant : rows) {
+ QVariantList row = rowVariant.toList();
+
+ QList<QStandardItem*> items;
+ for (const QVariant &cellValue : row) {
+ items.append(new QStandardItem(cellValue.toString()));
+ }
+
+ loadImageModel->appendRow(items);
+ loadImageTable->resizeColumnsToContents();
+ }
+ })->start();
+ } // loadImageModel
+
+ auto loadImageButton = new CustomStyleFlatPushButton();
+ {
+ connect(loadImageButton, &QPushButton::clicked,
+ [this, loadImageTable](bool) {
+ auto selected = loadImageTable->selectionModel()->selectedRows();
+ if (selected.size() == 0)
+ {
+ return;
+ }
+
+ for (const auto& selection : selected)
+ {
+ auto name = selection.data().toString().toStdString();
+ WorkerPriorityEnqueue([this, name]() { m_cache->LoadImageWithInstallName(name); });
+ }
+ });
+ loadImageButton->setText("Load");
+
+ loadImageButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+ loadImageButton->setMinimumWidth(100);
+ loadImageButton->setMinimumHeight(30);
+
+ } // loadImageButton
+ loadImageTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
+
+ auto loadImageFilterEdit = new FilterEdit(loadImageTable);
+ {
+ connect(loadImageFilterEdit, &FilterEdit::textChanged, [loadImageTable](const QString& filter) {
+ loadImageTable->setFilter(filter.toStdString());
+ });
+ } // loadImageFilterEdit
+
+ connect(loadImageTable, &FilterableTableView::activated, this, [=](const QModelIndex& index)
+ {
+ auto selected = loadImageTable->selectionModel()->selectedRows();
+ if (selected.size() == 0)
+ {
+ return;
+ }
+
+ for (const auto& selection : selected)
+ {
+ auto name = selection.data().toString().toStdString();
+ WorkerPriorityEnqueue([this, name]() { m_cache->LoadImageWithInstallName(name); });
+ }
+ });
+ connect(loadImageTable, &FilterableTableView::doubleClicked, this, [=](const QModelIndex& index)
+ {
+ auto selected = loadImageTable->selectionModel()->selectedRows();
+ if (selected.size() == 0)
+ {
+ return;
+ }
+
+ for (const auto& selection : selected)
+ {
+ auto name = selection.data().toString().toStdString();
+ WorkerPriorityEnqueue([this, name]() { m_cache->LoadImageWithInstallName(name); });
+ }
+ });
+
+ auto loadImageLayout = new QVBoxLayout;
+ loadImageLayout->addWidget(loadImageFilterEdit);
+ loadImageLayout->addWidget(loadImageTable);
+ loadImageLayout->addWidget(loadImageButton);
+
+ auto loadImageWidget = new QWidget;
+ loadImageWidget->setLayout(loadImageLayout);
+
+ m_bottomRegionTabs->addTab(loadImageWidget, "Load an Image");
+
+ loadImageTable->setModel(loadImageModel);
+
+ loadImageTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
+ loadImageTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
+
+ loadImageTable->setSelectionBehavior(QAbstractItemView::SelectRows);
+
+ m_triageTabs->addTab(loadImageWidget, "Images");
+ defaultWidget = loadImageWidget;
+ m_triageTabs->setCanCloseTab(loadImageWidget, false);
+ } // loadImageTable
+
+ auto symbolSearch = new SymbolTableView(this, m_cache);
+ {
+ auto symbolFilterEdit = new FilterEdit(symbolSearch);
+ {
+ connect(symbolFilterEdit, &FilterEdit::textChanged, [symbolSearch](const QString& filter) {
+ symbolSearch->setFilter(filter.toStdString());
+ });
+ }
+
+ auto symbolLayout = new QVBoxLayout;
+ symbolLayout->addWidget(symbolFilterEdit);
+ symbolLayout->addWidget(symbolSearch);
+
+ auto symbolWidget = new QWidget;
+ symbolWidget->setLayout(symbolLayout);
+
+ symbolSearch->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); // Address
+ symbolSearch->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); // Name
+ symbolSearch->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); // Image
+
+ symbolSearch->setSelectionBehavior(QAbstractItemView::SelectRows);
+ symbolSearch->setSelectionMode(QAbstractItemView::SingleSelection);
+
+ connect(symbolSearch, &SymbolTableView::activated, this, [=](const QModelIndex& index)
+ {
+ auto symbol = symbolSearch->getSymbolAtRow(index.row());
+ auto dialog = new QMessageBox(this);
+ dialog->setText("Load " + QString::fromStdString(symbol.image) + "?");
+ dialog->setStandardButtons(QMessageBox::Yes | QMessageBox::No);
+
+ connect(dialog, &QMessageBox::buttonClicked, this, [=](QAbstractButton* button)
+ {
+ if (button == dialog->button(QMessageBox::Yes))
+ {
+ WorkerPriorityEnqueue([this, symbol]()
+ {
+ m_cache->LoadImageWithInstallName(symbol.image);
+ });
+ }
+ });
+ dialog->exec();
+ });
+
+ m_triageTabs->addTab(symbolWidget, "Symbol Search");
+ m_triageTabs->setCanCloseTab(symbolWidget, false);
+ } // symbolSearch
+
+ auto loadedRegions = new QTreeView;
+ {
+ auto loadedRegionsModel = new QStandardItemModel(0, 3, loadedRegions);
+ loadedRegionsModel->setHorizontalHeaderLabels({"VM Address", "Size", "Pretty Name"});
+
+ auto loadedRegionsLayout = new QVBoxLayout;
+ loadedRegionsLayout->addWidget(loadedRegions);
+
+ auto loadedRegionsWidget = new QWidget;
+ loadedRegionsWidget->setLayout(loadedRegionsLayout);
+
+ loadedRegions->setModel(loadedRegionsModel);
+
+ loadedRegions->header()->setSectionResizeMode(QHeaderView::Stretch);
+
+ loadedRegions->setSelectionBehavior(QAbstractItemView::SelectRows);
+ loadedRegions->setSelectionMode(QAbstractItemView::SingleSelection);
+
+ connect(loadedRegions, &QTreeView::doubleClicked, this, [=](const QModelIndex& index)
+ {
+ auto addr = loadedRegionsModel->item(index.row(), 0)->text().toULongLong(nullptr, 16);
+ });
+
+ connect(loadedRegions, &QTreeView::activated, this, [=](const QModelIndex& index)
+ {
+ auto addr = loadedRegionsModel->item(index.row(), 0)->text().toULongLong(nullptr, 16);
+ });
+
+ // m_triageTabs->addTab(loadedRegionsWidget, "Loaded Regions");
+ } // loadedRegions
+
+
+ { // Doc tabs
+
+ QTextBrowser *mainDocBrowser = new QTextBrowser(this);
+ {
+ mainDocBrowser->setOpenExternalLinks(true);
+ auto alphaHtml =
+ R"(
+<h1>KernelCache Parser</h1>
+
+<p> This parser has support for MH_FILESET (>= macOS 11, >= iOS 14) kernels. </p>
+
+<h2> Getting the latest version of the plugin </h2>
+<p> We frequently release "dev" builds which will contain the latest version of the KernelCache plugin (and many other things). </p>
+<p> You can find instructions on how to install these builds <a href="https://docs.binary.ninja/guide/index.html#development-branch">here</a>. </p>
+
+<h3> Reading / building the source </h3>
+<p> Like most of our platforms, architectures, debug information parsing, and the entire API and documentation, this plugin is open source. </p>
+<p> You can read the source and find instructions for building it <a href="https://github.com/Vector35/binaryninja-api/tree/dev/view/kernelcache">here</a>. </p>
+<p> Contributions are always welcome! </p>
+)";
+ mainDocBrowser->setHtml(alphaHtml);
+
+ m_triageTabs->addTab(mainDocBrowser, "KernelCache Parser");
+ m_triageTabs->setCanCloseTab(mainDocBrowser, false);
+
+ }
+ }
+
+ containerWidget->addWidget(m_bottomRegionTabs);
+
+ m_layout = new QVBoxLayout(this);
+ m_layout->addWidget(m_triageTabs);
+ setLayout(m_layout);
+
+ m_triageTabs->selectWidget(defaultWidget);
+}
+
+
+QFont KCTriageView::getFont()
+{
+ return getMonospaceFont(this);
+}
+
+
+BinaryViewRef KCTriageView::getData()
+{
+ return m_data;
+}
+
+
+bool KCTriageView::navigate(uint64_t offset)
+{
+ return false;
+}
+
+
+uint64_t KCTriageView::getCurrentOffset()
+{
+ return 0;
+}
+
+
+CollapsibleSection::CollapsibleSection(QWidget* parent)
+ : QWidget(parent)
+{
+ auto layout = new QVBoxLayout(this);
+ {
+ layout->setContentsMargins(0, 0, 0, 0);
+
+ auto hLayout = new QHBoxLayout;
+ {
+ hLayout->setContentsMargins(0, 0, 0, 0);
+
+ m_titleLabel = new QLabel;
+ m_titleLabel->setStyleSheet("font-weight: bold; font-size: 16px;");
+ hLayout->addWidget(m_titleLabel, 1);
+
+ m_subtitleRightLabel = new QLabel;
+ m_subtitleRightLabel->setStyleSheet("font-size: 12px;");
+ hLayout->addWidget(m_subtitleRightLabel);
+
+ m_collapseButton = new CustomStyleFlatPushButton;
+ m_collapseButton->setFlat(true);
+ m_collapseButton->setCheckable(true);
+ }
+
+ layout->addLayout(hLayout);
+ }
+
+ m_contentWidgetContainer = new QWidget;
+ {
+ layout->addWidget(m_contentWidgetContainer);
+ new QVBoxLayout(m_contentWidgetContainer);
+ }
+
+}
+
+
+void CollapsibleSection::setTitle(const QString& title)
+{
+ m_titleLabel->setText(title);
+}
+
+
+void CollapsibleSection::setSubtitleRight(const QString& subtitle)
+{
+ m_subtitleRightLabel->setVisible(subtitle != "");
+ m_subtitleRightLabel->setText(subtitle);
+}
+
+
+void CollapsibleSection::setContentWidget(QWidget* contentWidget)
+{
+ m_contentWidget = contentWidget;
+ m_contentWidgetContainer->layout()->addWidget(contentWidget);
+}
+
+
+QSize CollapsibleSection::sizeHint() const
+{
+ return QWidget::sizeHint();
+}
+
+
+void CollapsibleSection::setCollapsed(bool collapsed, bool animated)
+{
+ if (collapsed == m_collapsed)
+ {
+ return;
+ }
+
+ m_collapsed = collapsed;
+
+ if (m_collapsed)
+ {
+ m_contentWidget->hide();
+ }
+ else
+ {
+ m_contentWidget->show();
+ }
+
+ if (animated)
+ {
+ m_onContentAddedAnimation->start();
+ }
+}
+
+
+KCTriageViewType::KCTriageViewType()
+ : ViewType("KCTriage", "Kernel Cache Triage")
+{
+
+}
+
+
+int KCTriageViewType::getPriority(BinaryViewRef data, const QString& filename)
+{
+ if (data->GetTypeName() == KC_VIEW_NAME)
+ {
+ return 100;
+ }
+ return 0;
+}
+
+
+QWidget* KCTriageViewType::create(BinaryViewRef data, ViewFrame* viewFrame)
+{
+ if (data->GetTypeName() != KC_VIEW_NAME)
+ {
+ return nullptr;
+ }
+ return new KCTriageView(viewFrame, data);
+}
+
+
+void KCTriageViewType::Register()
+{
+ ViewType::registerViewType(new KCTriageViewType());
+}
diff --git a/view/kernelcache/ui/kctriage.h b/view/kernelcache/ui/kctriage.h
new file mode 100644
index 00000000..a6b7f7ac
--- /dev/null
+++ b/view/kernelcache/ui/kctriage.h
@@ -0,0 +1,243 @@
+//
+// Created by kat on 8/15/24.
+//
+
+#include <kernelcacheapi.h>
+#include <binaryninjaapi.h>
+#include "uitypes.h"
+#include "viewframe.h"
+#include "animation.h"
+#include "uicontext.h"
+
+#include <QTableView>
+#include <QStandardItemModel>
+#include <QSortFilterProxyModel>
+#include <QHeaderView>
+#include "filter.h"
+
+#ifndef BINARYNINJA_KCTRIAGE_H
+#define BINARYNINJA_KCTRIAGE_H
+
+class CollapsibleSection : public QWidget
+{
+ Q_OBJECT
+
+ QLabel* m_titleLabel;
+ QLabel* m_subtitleRightLabel;
+ QPushButton* m_collapseButton;
+
+ bool m_collapsed = true;
+
+ Animation* m_onContentAddedAnimation;
+
+ QWidget* m_contentWidgetContainer;
+ QWidget* m_contentWidget;
+
+protected:
+ QSize sizeHint() const override;
+
+public:
+ CollapsibleSection(QWidget* parent);
+ void setTitle(const QString& title);
+ void setSubtitleRight(const QString& subtitle);
+
+ void setContentWidget(QWidget* contentWidget);
+
+ void setCollapsed(bool collapsed, bool animated = true);
+ bool isCollapsed() const { return m_collapsed; }
+};
+
+
+class FilterableTableView : public QTableView, public FilterTarget {
+ Q_OBJECT
+
+ bool m_filterByHiding;
+
+public:
+ FilterableTableView(QWidget* parent = nullptr, bool filterByHiding = true)
+ : QTableView(parent), m_filterByHiding(filterByHiding) {
+ viewport()->installEventFilter(this);
+ }
+
+ ~FilterableTableView() override {}
+
+ void setFilter(const std::string& filter) override {
+ if (!m_filterByHiding)
+ {
+ emit filterTextChanged(QString::fromStdString(filter));
+ return;
+ }
+ QString qFilter = QString::fromStdString(filter);
+ for (int row = 0; row < model()->rowCount(); ++row) {
+ bool match = false;
+ for (int col = 0; col < model()->columnCount(); ++col) {
+ QModelIndex index = model()->index(row, col);
+ QString data = model()->data(index).toString();
+ if (data.contains(qFilter, Qt::CaseInsensitive)) {
+ match = true;
+ break;
+ }
+ }
+ setRowHidden(row, !match);
+ }
+ }
+
+ void scrollToFirstItem() override {
+ if (model()->rowCount() > 0) {
+ scrollTo(model()->index(0, 0));
+ }
+ }
+
+ void scrollToCurrentItem() override {
+ QModelIndex currentIndex = selectionModel()->currentIndex();
+ if (currentIndex.isValid()) {
+ scrollTo(currentIndex);
+ }
+ }
+
+ void selectFirstItem() override {
+ if (model()->rowCount() > 0) {
+ QModelIndex firstIndex = model()->index(0, 0);
+ selectionModel()->select(firstIndex, QItemSelectionModel::ClearAndSelect);
+ }
+ }
+
+ void activateFirstItem() override {
+ if (model()->rowCount() > 0) {
+ QModelIndex firstIndex = model()->index(0, 0);
+ setCurrentIndex(firstIndex);
+ emit activated(firstIndex);
+ }
+ }
+
+ bool eventFilter(QObject* obj, QEvent* event) override {
+ if (event->type() == QEvent::KeyPress) {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->key() == Qt::Key_Escape) {
+ clearSelection();
+ return true;
+ }
+ if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) {
+ emit activated(currentIndex());
+ return true;
+ }
+ }
+ return QTableView::eventFilter(obj, event);
+ }
+
+signals:
+ void filterTextChanged(const QString& text);
+};
+
+class SymbolTableView;
+
+class SymbolTableModel : public QAbstractTableModel {
+ Q_OBJECT
+
+ SymbolTableView* m_parent;
+ std::string m_filter;
+ std::vector<KernelCacheAPI::KCSymbol> m_symbols;
+
+public:
+ explicit SymbolTableModel(SymbolTableView* parent);
+
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+
+ void updateSymbols();
+
+ void setFilter(std::string text);
+
+ const KernelCacheAPI::KCSymbol& symbolAt(int row) const;
+};
+
+
+class SymbolTableView : public QTableView, public FilterTarget
+{
+ Q_OBJECT
+ friend class SymbolTableModel;
+
+ std::vector<KernelCacheAPI::KCSymbol> m_symbols;
+
+ SymbolTableModel* m_model;
+
+public:
+ SymbolTableView(QWidget* parent, Ref<KernelCacheAPI::KernelCache> cache);
+ virtual ~SymbolTableView() override;
+
+ void scrollToFirstItem() override {
+ if (model()->rowCount() > 0) {
+ scrollTo(model()->index(0, 0));
+ }
+ }
+
+ void scrollToCurrentItem() override {
+ QModelIndex currentIndex = selectionModel()->currentIndex();
+ if (currentIndex.isValid()) {
+ scrollTo(currentIndex);
+ }
+ }
+
+ void selectFirstItem() override {
+ if (model()->rowCount() > 0) {
+ QModelIndex firstIndex = model()->index(0, 0);
+ selectionModel()->select(firstIndex, QItemSelectionModel::ClearAndSelect);
+ }
+ }
+
+ void activateFirstItem() override {
+ if (model()->rowCount() > 0) {
+ QModelIndex firstIndex = model()->index(0, 0);
+ setCurrentIndex(firstIndex);
+ emit activated(firstIndex);
+ }
+ }
+
+ KernelCacheAPI::KCSymbol getSymbolAtRow(int row) const
+ {
+ return m_model->symbolAt(row);
+ }
+
+ void setFilter(const std::string& filter) override;
+};
+
+
+class KCTriageView : public QWidget, public View
+{
+ BinaryViewRef m_data;
+ QVBoxLayout* m_layout;
+ Ref<KernelCacheAPI::KernelCache> m_cache;
+
+ std::mutex m_headersMutex;
+ std::shared_ptr<std::vector<KernelCacheAPI::KernelCacheMachOHeader>> m_headers;
+
+ SplitTabWidget* m_triageTabs;
+ DockableTabCollection* m_triageCollection;
+
+ SplitTabWidget* m_bottomRegionTabs;
+ DockableTabCollection* m_bottomRegionCollection;
+
+
+public:
+ KCTriageView(QWidget* parent, BinaryViewRef data);
+ BinaryViewRef getData() override;
+ void setSelectionOffsets(BNAddressRange range) override {};
+ QFont getFont() override;
+ bool navigate(uint64_t offset) override;
+ uint64_t getCurrentOffset() override;
+};
+
+
+class KCTriageViewType : public ViewType
+{
+public:
+ KCTriageViewType();
+ int getPriority(BinaryViewRef data, const QString& filename) override;
+ QWidget* create(BinaryViewRef data, ViewFrame* viewFrame) override;
+ static void Register();
+};
+
+
+#endif // BINARYNINJA_KCTRIAGE_H