diff --git a/core/adapters/dbgeng/install_windbg.py b/core/adapters/dbgeng/install_windbg.py new file mode 100644 index 0000000..fd16e3c --- /dev/null +++ b/core/adapters/dbgeng/install_windbg.py @@ -0,0 +1,85 @@ +import urllib.request +import xml.dom.minidom +import zipfile +import tempfile +import binaryninja +from binaryninja.settings import Settings +import os + + +def check_install_ok(path): + if not os.path.exists(os.path.join(path, 'amd64', 'dbgeng.dll')): + return False + + if not os.path.exists(os.path.join(path, 'amd64', 'dbghelp.dll')): + return False + + if not os.path.exists(os.path.join(path, 'amd64', 'dbgmodel.dll')): + return False + + if not os.path.exists(os.path.join(path, 'amd64', 'dbgcore.dll')): + return False + + if not os.path.exists(os.path.join(path, 'amd64', 'ttd', 'TTD.exe')): + return False + + if not os.path.exists(os.path.join(path, 'amd64', 'ttd', 'TTDRecord.dll')): + return False + + return True + + +def install_windbg(): + ttd_url = 'https://aka.ms/windbg/download' + print('Downloading appinstaller from: %s...' % ttd_url) + try: + local_file, _ = urllib.request.urlretrieve(ttd_url) + except Exception as e: + print('Failed to download appinstaller file from %s' % ttd_url) + print(e) + return + print('Successfully downloaded appinstaller') + + xml_doc = xml.dom.minidom.parse(local_file) + try: + msix_url = xml_doc.getElementsByTagName('MainBundle')[0].attributes['Uri'].value + except Exception as e: + print('Failed to parse XML') + print(e) + return + + print('Downloading MSIX bundle from: %s...' % msix_url) + try: + msix_file, _ = urllib.request.urlretrieve(msix_url) + except Exception as e: + print('Failed to download MSIX bundle from %s' % msix_url) + print(e) + return + print('Successfully downloaded MSIX bundle') + + zip_file = zipfile.ZipFile(msix_file) + temp_dir = tempfile.mkdtemp() + inner_msix = zip_file.extract('windbg_win7-x64.msix', temp_dir) + print('Extracted windbg_win7-x64 to %s' % inner_msix) + + install_target = os.path.join(binaryninja.user_directory(), 'windbg') + print('Installing to: %s' % install_target) + + inner_zip = zipfile.ZipFile(inner_msix) + inner_zip.extractall(install_target) + + if check_install_ok(install_target): + print('WinDbg/TTD installed to %s!' % install_target) + else: + print('The WinDbg/TTD installation appears to be successful, but important files are missing from %s, ' + 'and the TTD recording may not work properly.' % install_target) + return + + x64dbgEngPath = os.path.join(install_target, 'amd64') + if Settings().set_string("debugger.x64dbgEngPath", x64dbgEngPath): + print('Please restart Binary Ninja to make the changes take effect!') + else: + print('Failed to set debugger.x64dbgEngPath to %s, the WinDbg/TTD installation is not being used' % (x64dbgEngPath)) + + +install_windbg() diff --git a/core/adapters/dbgengadapter.cpp b/core/adapters/dbgengadapter.cpp index f474287..b303c68 100644 --- a/core/adapters/dbgengadapter.cpp +++ b/core/adapters/dbgengadapter.cpp @@ -83,8 +83,14 @@ std::string DbgEngAdapter::GetDbgEngPath(const std::string& arch) return ""; } + std::string pluginRoot; + if (getenv("BN_STANDALONE_DEBUGGER") != nullptr) + pluginRoot = GetUserPluginDirectory(); + else + pluginRoot = GetBundledPluginDirectory(); + // If the user does not specify a path (the default case), find the one from the plugins/dbgeng/arch - auto debuggerRoot = filesystem::path(GetBundledPluginDirectory()) / "dbgeng" / arch; + auto debuggerRoot = filesystem::path(pluginRoot) / "dbgeng" / arch; if (IsValidDbgEngPaths(debuggerRoot.string())) return debuggerRoot.string(); diff --git a/core/adapters/dbgengttdadapter.cpp b/core/adapters/dbgengttdadapter.cpp index 0862f1b..7996a3e 100644 --- a/core/adapters/dbgengttdadapter.cpp +++ b/core/adapters/dbgengttdadapter.cpp @@ -62,6 +62,8 @@ bool DbgEngTTDAdapter::ExecuteWithArgsInternal(const std::string& path, const st // Apply the breakpoints added before the m_debugClient is created ApplyBreakpoints(); + DbgEngAdapter::InvokeBackendCommand("!index"); + auto settings = Settings::Instance(); if (settings->Get("debugger.stopAtEntryPoint") && m_hasEntryFunction) { AddBreakpoint(ModuleNameAndOffset(configs.inputFile, m_entryPoint - m_start)); diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index dc15000..5f28abc 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -9,6 +9,10 @@ find_package(Qt6 COMPONENTS Core Gui Widgets REQUIRED) file(GLOB SOURCES *.cpp *.h ../debuggerui.qrc) list(FILTER SOURCES EXCLUDE REGEX moc_.*) list(FILTER SOURCES EXCLUDE REGEX qrc_.*) +if (NOT WIN32) + list(REMOVE_ITEM SOURCES ${PROJECT_SOURCE_DIR}/ttdrecord.h) + list(REMOVE_ITEM SOURCES ${PROJECT_SOURCE_DIR}/ttdrecord.cpp) +endif () if(DEMO) add_library(debuggerui STATIC ${SOURCES}) diff --git a/ui/ttdrecord.cpp b/ui/ttdrecord.cpp new file mode 100644 index 0000000..7d8aa07 --- /dev/null +++ b/ui/ttdrecord.cpp @@ -0,0 +1,217 @@ +/* +Copyright 2020-2024 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "ttdrecord.h" +#include "uicontext.h" +#include "qfiledialog.h" +#include "fmt/format.h" + +using namespace BinaryNinjaDebuggerAPI; +using namespace BinaryNinja; +using namespace std; + +TTDRecordDialog::TTDRecordDialog(QWidget* parent, BinaryView* data) : + QDialog() +{ + if (data) + m_controller = DebuggerController::GetController(data); + + setWindowTitle("TTD Record"); + setAttribute(Qt::WA_DeleteOnClose); + + setModal(true); + QVBoxLayout* layout = new QVBoxLayout; + layout->setSpacing(0); + + m_pathEntry = new QLineEdit(this); + m_pathEntry->setMinimumWidth(800); + m_argumentsEntry = new QLineEdit(this); + m_workingDirectoryEntry = new QLineEdit(this); + m_outputDirectory = new QLineEdit(this); + m_launchWithoutTracing = new QCheckBox(this); + + auto* pathSelector = new QPushButton("...", this); + pathSelector->setMaximumWidth(30); + connect(pathSelector, &QPushButton::clicked, [&]() { + auto fileName = QFileDialog::getOpenFileName(this, "Select Executable Path", m_pathEntry->text()); + if (!fileName.isEmpty()) + m_pathEntry->setText(fileName); + }); + + auto* workingDirSelector = new QPushButton("...", this); + workingDirSelector->setMaximumWidth(30); + connect(workingDirSelector, &QPushButton::clicked, [&]() { + auto pathName = QFileDialog::getExistingDirectory(this, "Specify Working Directory", + m_workingDirectoryEntry->text(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + if (!pathName.isEmpty()) + m_workingDirectoryEntry->setText(pathName); + }); + + auto* outputDirSelector = new QPushButton("...", this); + outputDirSelector->setMaximumWidth(30); + connect(outputDirSelector, &QPushButton::clicked, [&]() { + auto pathName = QFileDialog::getExistingDirectory(this, "Specify Trace Output Directory", + m_workingDirectoryEntry->text(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + if (!pathName.isEmpty()) + m_workingDirectoryEntry->setText(pathName); + }); + + auto pathEntryLayout = new QHBoxLayout; + pathEntryLayout->addWidget(m_pathEntry); + pathEntryLayout->addWidget(pathSelector); + + auto workingDirLayout = new QHBoxLayout; + workingDirLayout->addWidget(m_workingDirectoryEntry); + workingDirLayout->addWidget(workingDirSelector); + + auto outputLayout = new QHBoxLayout; + outputLayout->addWidget(m_outputDirectory); + outputLayout->addWidget(outputDirSelector); + + QVBoxLayout* contentLayout = new QVBoxLayout; + contentLayout->setSpacing(10); + contentLayout->addWidget(new QLabel("Executable Path")); + contentLayout->addLayout(pathEntryLayout); + contentLayout->addWidget(new QLabel("Working Directory")); + contentLayout->addLayout(workingDirLayout); + contentLayout->addWidget(new QLabel("Command Line Arguments")); + contentLayout->addWidget(m_argumentsEntry); + contentLayout->addWidget(new QLabel("Trace Output Directory")); + contentLayout->addLayout(outputLayout); + contentLayout->addWidget(new QLabel("Start application With Recording Off")); + contentLayout->addWidget(m_launchWithoutTracing); + + QHBoxLayout* buttonLayout = new QHBoxLayout; + buttonLayout->setContentsMargins(0, 0, 0, 0); + + QPushButton* cancelButton = new QPushButton("Cancel"); + connect(cancelButton, &QPushButton::clicked, [&]() { reject(); }); + QPushButton* acceptButton = new QPushButton("Record"); + connect(acceptButton, &QPushButton::clicked, [&]() { apply(); }); + acceptButton->setDefault(true); + + buttonLayout->addStretch(1); + buttonLayout->addWidget(cancelButton); + buttonLayout->addWidget(acceptButton); + + layout->addLayout(contentLayout); + layout->addStretch(1); + layout->addSpacing(10); + layout->addLayout(buttonLayout); + setLayout(layout); + + if (m_controller) + { + m_pathEntry->setText(QString::fromStdString(m_controller->GetExecutablePath())); + m_argumentsEntry->setText(QString::fromStdString(m_controller->GetCommandLineArguments())); + m_workingDirectoryEntry->setText(QString::fromStdString(m_controller->GetWorkingDirectory())); + m_outputDirectory->setText(QString::fromStdString(m_controller->GetWorkingDirectory())); + } + m_launchWithoutTracing->setChecked(false); + + setFixedSize(QDialog::sizeHint()); + + CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); +} + + +void TTDRecordDialog::apply() +{ + DoTTDTrace(); + + accept(); +} + + +static bool IsValidDbgEngTTDPaths(const std::string& path) +{ + if (path.empty()) + return false; + + auto enginePath = filesystem::path(path); + if (!filesystem::exists(enginePath)) + return false; + + if (!filesystem::exists(enginePath / "TTD.exe")) + return false; + + if (!filesystem::exists(enginePath / "TTDRecord.dll")) + return false; + + return true; +} + + +std::string TTDRecordDialog::GetTTDRecorderPath() +{ + std::string path = Settings::Instance()->Get("debugger.x64dbgEngPath"); + if (!path.empty()) + { + // If the user has specified the path in the setting, then check it for validity. If it is valid, then use it; + // if it is invalid, fail the operation -- do not fallback to the default one + auto userTTDPath = filesystem::path(path) / "TTD"; + if (IsValidDbgEngTTDPaths(userTTDPath.string())) + return userTTDPath.string(); + else + return ""; + } + + std::string pluginRoot; + if (getenv("BN_STANDALONE_DEBUGGER") != nullptr) + pluginRoot = GetUserPluginDirectory(); + else + pluginRoot = GetBundledPluginDirectory(); + + // If the user does not specify a path (the default case), find the one from the plugins/dbgeng/arch + auto TTDRecorderRoot = filesystem::path(pluginRoot) / "dbgeng" / "amd64" / "TTD"; + if (IsValidDbgEngTTDPaths(TTDRecorderRoot.string())) + return TTDRecorderRoot.string(); + + return ""; +} + + +void TTDRecordDialog::DoTTDTrace() +{ + auto ttdPath = GetTTDRecorderPath(); + if (ttdPath.empty()) + { + LogWarn("The debugger cannot find the path for the TTD recorder. " + "If you have set debugger.x64dbgEngPath, check if it valid"); + return; + } + LogDebug("TTD Recorder in path %s", ttdPath.c_str()); + + auto ttdRecorder = fmt::format("\"{}\\TTD.exe\"", ttdPath); + auto ttdCommandLine = fmt::format("-accepteula -out \"{}\" {} -launch \"{}\" {}", + m_outputDirectory->text().toStdString(), + m_launchWithoutTracing->isChecked() ? "-tracingOff -recordMode Manual" : "", + m_pathEntry->text().toStdString(), + m_argumentsEntry->text().toStdString()); + LogWarn("TTD tracer cmd: %s %s", ttdRecorder.c_str(), ttdCommandLine.c_str()); + + HINSTANCE ret = ShellExecuteA( + NULL, + "runas", + ttdRecorder.c_str(), + ttdCommandLine.c_str(), + m_workingDirectoryEntry->text().toStdString().c_str(), + SW_NORMAL + ); + + if ((INT_PTR)ret < 32) + LogWarn("TTD recording failed: %d", ret); +} diff --git a/ui/ttdrecord.h b/ui/ttdrecord.h new file mode 100644 index 0000000..8d21f73 --- /dev/null +++ b/ui/ttdrecord.h @@ -0,0 +1,52 @@ +/* +Copyright 2020-2024 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "inttypes.h" +#include "binaryninjaapi.h" +#include "viewframe.h" +#include "fontsettings.h" +#include "debuggerapi.h" + +using namespace BinaryNinjaDebuggerAPI; + +class TTDRecordDialog : public QDialog +{ + Q_OBJECT + +private: + DbgRef m_controller = nullptr; + QLineEdit* m_pathEntry; + QLineEdit* m_workingDirectoryEntry; + QLineEdit* m_argumentsEntry; + QLineEdit* m_outputDirectory; + QCheckBox* m_launchWithoutTracing; + +public: + TTDRecordDialog(QWidget* parent, BinaryView* data); + void DoTTDTrace(); + std::string GetTTDRecorderPath(); + +private Q_SLOTS: + void apply(); +}; diff --git a/ui/ui.cpp b/ui/ui.cpp index 77bf588..a86dbab 100644 --- a/ui/ui.cpp +++ b/ui/ui.cpp @@ -41,6 +41,12 @@ limitations under the License. #include "attachprocess.h" #include "progresstask.h" +#ifdef WIN32 + #include "ttdrecord.h" + #include "scriptingconsole.h" +#endif + + using namespace BinaryNinja; using namespace BinaryNinjaDebuggerAPI; using namespace std; @@ -840,9 +846,56 @@ void GlobalDebuggerUI::SetupMenu(UIContext* context) }, connectedAndStopped)); debuggerMenu->addAction("Force Update Memory Cache", "Misc"); + +#ifdef WIN32 + UIAction::registerAction("Record TTD Trace"); + context->globalActions()->bindAction("Record TTD Trace", + UIAction( + [=](const UIActionContext& ctxt) { + auto* dialog = new TTDRecordDialog(context->mainWindow(), ctxt.binaryView); + dialog->show(); + })); + debuggerMenu->addAction("Record TTD Trace", "TTD"); + + UIAction::registerAction("Install WinDbg/TTD"); + context->globalActions()->bindAction("Install WinDbg/TTD", + UIAction( + [=](const UIActionContext& ctxt) { installTTD(ctxt); })); + debuggerMenu->addAction("Install WinDbg/TTD", "TTD"); +#endif } +#ifdef WIN32 +void GlobalDebuggerUI::installTTD(const UIActionContext& ctxt) +{ + std::string pluginRoot; + if (getenv("BN_STANDALONE_DEBUGGER") != nullptr) + pluginRoot = GetUserPluginDirectory(); + else + pluginRoot = GetBundledPluginDirectory(); + + auto ttdInstallerScript = filesystem::path(pluginRoot) / "dbgeng" / "install_windbg.py"; + LogDebug("WinDbg/TTD installer script expected at: %s", ttdInstallerScript.string().c_str()); + if (!std::filesystem::exists(ttdInstallerScript)) + { + LogWarn("WinDbg/TTD installer script does not exist at: %s", ttdInstallerScript.string().c_str()); + return; + } + + auto sidebar = ctxt.context->sidebar(); + if (!sidebar) + return; + + auto *widget = qobject_cast(sidebar->widget("Console")); + if (!widget) + return; + + widget->runScriptFromFile(ttdInstallerScript.string()); +} +#endif + + DebuggerUI::DebuggerUI(UIContext* context, DebuggerControllerRef controller) : m_context(context), m_controller(controller) { diff --git a/ui/ui.h b/ui/ui.h index 18e56d0..77900a5 100644 --- a/ui/ui.h +++ b/ui/ui.h @@ -42,6 +42,8 @@ class GlobalDebuggerUI : public QObject static void CreateGlobalAreaWidgets(UIContext* context); static void CloseGlobalAreaWidgets(UIContext* context); + void installTTD(const UIActionContext& ctxt); + public: GlobalDebuggerUI(UIContext* context); ~GlobalDebuggerUI();