Skip to content

Commit

Permalink
Support installing WinDbg and making a TTD recording directly from wi…
Browse files Browse the repository at this point in the history
…thin the debugger. Fix Vector35#552
  • Loading branch information
xusheng6 committed Jun 21, 2024
1 parent f1e6407 commit 97f8068
Show file tree
Hide file tree
Showing 8 changed files with 422 additions and 1 deletion.
85 changes: 85 additions & 0 deletions core/adapters/dbgeng/install_windbg.py
Original file line number Diff line number Diff line change
@@ -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()
8 changes: 7 additions & 1 deletion core/adapters/dbgengadapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
2 changes: 2 additions & 0 deletions core/adapters/dbgengttdadapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>("debugger.stopAtEntryPoint") && m_hasEntryFunction) {
AddBreakpoint(ModuleNameAndOffset(configs.inputFile, m_entryPoint - m_start));
Expand Down
4 changes: 4 additions & 0 deletions ui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
217 changes: 217 additions & 0 deletions ui/ttdrecord.cpp
Original file line number Diff line number Diff line change
@@ -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<string>("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);
}
52 changes: 52 additions & 0 deletions ui/ttdrecord.h
Original file line number Diff line number Diff line change
@@ -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 <QDialog>
#include <QPushButton>
#include <QLineEdit>
#include <QComboBox>
#include <QFormLayout>
#include <QCheckBox>
#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<DebuggerController> 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();
};
Loading

0 comments on commit 97f8068

Please sign in to comment.