diff --git a/api/debuggerapi.h b/api/debuggerapi.h index 0ff27f12..7605ecbd 100644 --- a/api/debuggerapi.h +++ b/api/debuggerapi.h @@ -981,6 +981,5 @@ namespace BinaryNinjaDebuggerAPI { bool IsWinDbgInstalled(const std::string& installPath = ""); std::string GetWinDbgInstallerPath(); std::string GetWinDbgInstalledVersion(const std::string& installPath = ""); - std::string GetWinDbgLatestVersion(); }; // namespace BinaryNinjaDebuggerAPI diff --git a/api/ffi.h b/api/ffi.h index 61bfc333..b6489598 100644 --- a/api/ffi.h +++ b/api/ffi.h @@ -877,7 +877,6 @@ extern "C" DEBUGGER_FFI_API bool BNDebuggerIsWinDbgInstalled(const char* installPath); DEBUGGER_FFI_API char* BNDebuggerGetWinDbgInstallerPath(void); DEBUGGER_FFI_API char* BNDebuggerGetWinDbgInstalledVersion(const char* installPath); - DEBUGGER_FFI_API char* BNDebuggerGetWinDbgLatestVersion(void); #ifdef __cplusplus } diff --git a/api/windbginstaller.cpp b/api/windbginstaller.cpp index 2a8b5a6a..8d3e46c7 100644 --- a/api/windbginstaller.cpp +++ b/api/windbginstaller.cpp @@ -57,12 +57,3 @@ std::string BinaryNinjaDebuggerAPI::GetWinDbgInstalledVersion(const std::string& BNDebuggerFreeString(version); return result; } - - -std::string BinaryNinjaDebuggerAPI::GetWinDbgLatestVersion() -{ - char* version = BNDebuggerGetWinDbgLatestVersion(); - std::string result = version ? version : ""; - BNDebuggerFreeString(version); - return result; -} diff --git a/core/ffi.cpp b/core/ffi.cpp index f6fd65f6..b6574fc5 100644 --- a/core/ffi.cpp +++ b/core/ffi.cpp @@ -2231,13 +2231,6 @@ char* BNDebuggerGetWinDbgInstalledVersion(const char* installPath) return BNAllocString(version.c_str()); } - -char* BNDebuggerGetWinDbgLatestVersion(void) -{ - std::string version = GetLatestVersion(); - return BNAllocString(version.c_str()); -} - #else // !WIN32 // Stub implementations for non-Windows platforms @@ -2280,10 +2273,4 @@ char* BNDebuggerGetWinDbgInstalledVersion(const char* installPath) return BNAllocString(""); } - -char* BNDebuggerGetWinDbgLatestVersion(void) -{ - return BNAllocString(""); -} - #endif // WIN32 diff --git a/core/windbginstaller.cpp b/core/windbginstaller.cpp index 36120685..3c7df7c9 100644 --- a/core/windbginstaller.cpp +++ b/core/windbginstaller.cpp @@ -160,92 +160,6 @@ InstallResult InstallWinDbg(const std::string& installPath, bool isUpdate) { } } -/* Helper function to run installer CLI and capture JSON output */ -static std::string RunInstallerCommand(const std::string& command, const std::string& extraArgs = "") { - std::string installerPath = GetInstallerPath(); - if (installerPath.empty()) { - return ""; - } - - std::string cmdLine = "\"" + installerPath + "\" " + command + " --json"; - if (!extraArgs.empty()) { - cmdLine += " " + extraArgs; - } - - /* Create pipes for stdout */ - SECURITY_ATTRIBUTES sa = {}; - sa.nLength = sizeof(sa); - sa.bInheritHandle = TRUE; - - HANDLE hReadPipe, hWritePipe; - if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) { - return ""; - } - - /* Ensure read handle is not inherited */ - SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0); - - STARTUPINFOA si = {}; - si.cb = sizeof(si); - si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; - si.hStdOutput = hWritePipe; - si.hStdError = hWritePipe; - si.wShowWindow = SW_HIDE; - - PROCESS_INFORMATION pi = {}; - - if (!CreateProcessA( - nullptr, - const_cast(cmdLine.c_str()), - nullptr, - nullptr, - TRUE, /* Inherit handles */ - CREATE_NO_WINDOW, - nullptr, - nullptr, - &si, - &pi)) { - CloseHandle(hReadPipe); - CloseHandle(hWritePipe); - return ""; - } - - /* Close write end in parent */ - CloseHandle(hWritePipe); - - /* Read output */ - std::string output; - char buffer[4096]; - DWORD bytesRead; - while (ReadFile(hReadPipe, buffer, sizeof(buffer) - 1, &bytesRead, nullptr) && bytesRead > 0) { - buffer[bytesRead] = '\0'; - output += buffer; - } - - CloseHandle(hReadPipe); - - WaitForSingleObject(pi.hProcess, INFINITE); - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - - return output; -} - -/* Simple JSON value extractor - finds "key":"value" pattern */ -static std::string ExtractJsonValue(const std::string& json, const std::string& key) { - std::string searchKey = "\"" + key + "\":\""; - size_t pos = json.find(searchKey); - if (pos == std::string::npos) { - return ""; - } - pos += searchKey.length(); - size_t endPos = json.find("\"", pos); - if (endPos == std::string::npos) { - return ""; - } - return json.substr(pos, endPos - pos); -} - std::string GetInstalledVersion(const std::string& installPath) { /* Read version directly from marker file (fast, no CLI call needed) */ std::string path = installPath; @@ -278,11 +192,6 @@ std::string GetInstalledVersion(const std::string& installPath) { return ""; } -std::string GetLatestVersion() { - std::string output = RunInstallerCommand("check-update"); - return ExtractJsonValue(output, "latest"); -} - } // namespace BinaryNinjaDebugger #endif // WIN32 diff --git a/core/windbginstaller.h b/core/windbginstaller.h index bfed6377..6dc9fe02 100644 --- a/core/windbginstaller.h +++ b/core/windbginstaller.h @@ -67,13 +67,6 @@ std::string GetInstallerPath(); */ std::string GetInstalledVersion(const std::string& installPath = ""); -/* - * Get the latest available WinDbg version from Microsoft - * - * @return Version string, or empty on error - */ -std::string GetLatestVersion(); - } // namespace BinaryNinjaDebugger #endif // WIN32 diff --git a/docs/guide/dbgeng-ttd.md b/docs/guide/dbgeng-ttd.md index ec832a1b..7d50dc7f 100644 --- a/docs/guide/dbgeng-ttd.md +++ b/docs/guide/dbgeng-ttd.md @@ -21,7 +21,7 @@ The WinDbg installation only needs to be done once. - Open Binary Ninja - Click `Debugger` -> `Install WinDbg/TTD` from the menu - A dialog will appear showing the installation progress: - - The installer automatically downloads the latest WinDbg from Microsoft + - The installer downloads a specific WinDbg version from Microsoft - It extracts the necessary files (DbgEng DLLs and TTD components) - WinDbg will be installed to `%APPDATA%\Binary Ninja\windbg` - Progress and status are displayed in real-time @@ -32,17 +32,21 @@ The WinDbg installation only needs to be done once. The automatic installer handles all the complexity of downloading and extracting the WinDbg MSIX bundle. -### Update WinDbg/TTD +Note that the installer deliberately installs a pinned WinDbg version that has been validated against the debugger, rather than the newest release. +New WinDbg releases occasionally ship regressions that break the DbgEng/TTD adapter, so the pinned version is bumped only after it has been tested. -If you already have WinDbg/TTD installed and want to check for updates: +### Reinstall WinDbg/TTD + +If you already have WinDbg/TTD installed: - Click `Debugger` -> `Install WinDbg/TTD` from the menu - A dialog will appear showing: - The currently installed version - - The latest available version from Microsoft -- If a newer version is available, click "Update" to download and install it -- If you are already on the latest version, the dialog will indicate that no update is needed -- Restart Binary Ninja after updating + - The version this debugger supports +- If the two match, you can click "Reinstall" to install it again, e.g. if the installation was damaged +- If they differ, click "Install" to replace the installed version with the supported one. + Note this can be a downgrade: if you installed a newer WinDbg yourself, this replaces it with the validated version +- Restart Binary Ninja afterwards diff --git a/docs/img/debugger/ttd_update_windbg.png b/docs/img/debugger/ttd_update_windbg.png index 5d93dfb0..77e909b4 100644 Binary files a/docs/img/debugger/ttd_update_windbg.png and b/docs/img/debugger/ttd_update_windbg.png differ diff --git a/installer/CMakeLists.txt b/installer/CMakeLists.txt index 8d8a03da..28ba273c 100644 --- a/installer/CMakeLists.txt +++ b/installer/CMakeLists.txt @@ -52,6 +52,7 @@ set(INSTALLER_LIB_HEADERS http_downloader.h zip_extractor.h windbg_installer.h + windbg_version.h signature_verifier.h ) diff --git a/installer/main.cpp b/installer/main.cpp index bc749da5..6fd69a23 100644 --- a/installer/main.cpp +++ b/installer/main.cpp @@ -6,7 +6,6 @@ * * Usage: * windbg-installer install [--path ] [--quiet] [--json] - * windbg-installer check-update [--path ] [--json] * windbg-installer version [--path ] [--json] * windbg-installer --help * @@ -17,6 +16,7 @@ #ifdef _WIN32 #include "windbg_installer.h" +#include "windbg_version.h" #include #include #include @@ -222,14 +222,6 @@ void PrintJsonResult(bool success, const std::string& message, const std::string std::cout << "}" << std::endl; } -/* Print JSON version info */ -void PrintJsonVersion(const std::string& installed, const std::string& latest, bool updateAvailable) { - std::cout << "{\"type\":\"version\",\"installed\":\"" << installed - << "\",\"latest\":\"" << latest - << "\",\"updateAvailable\":" << (updateAvailable ? "true" : "false") - << "}" << std::endl; -} - void PrintUsage(const char* programName) { std::cout << "WinDbg/TTD Installer for Binary Ninja Debugger\n" << "\n" @@ -237,9 +229,8 @@ void PrintUsage(const char* programName) { << " " << programName << " [options]\n" << "\n" << "Commands:\n" - << " install Install or update WinDbg/TTD\n" - << " version Show installed version (local only, no network)\n" - << " check-update Check for updates (compares local vs latest online)\n" + << " install Install or reinstall WinDbg/TTD\n" + << " version Show the installed and supported versions\n" << "\n" << "Options:\n" << " --path Specify installation directory\n" @@ -252,15 +243,13 @@ void PrintUsage(const char* programName) { << "\n" << "Examples:\n" << " " << programName << " version\n" - << " " << programName << " check-update\n" << " " << programName << " install\n" << " " << programName << " install --update\n" << " " << programName << " install --path C:\\Tools\\WinDbg\n" << "\n" << "Exit codes:\n" - << " 0 Success / up to date\n" + << " 0 Success\n" << " 1 Not installed / error\n" - << " 2 Update available (for check-update)\n" << "\n"; } @@ -410,6 +399,7 @@ int CmdVersion(const std::string& installPath, OutputMode mode) { if (mode == OutputMode::Json) { std::cout << "{\"type\":\"version\",\"isInstalled\":" << (installed.isInstalled ? "true" : "false") << ",\"installed\":\"" << installed.version + << "\",\"supported\":\"" << kPinnedVersion << "\",\"installPath\":\"" << path << "\"}" << std::endl; } else if (mode == OutputMode::Human) { std::cout << "\n"; @@ -425,78 +415,17 @@ int CmdVersion(const std::string& installPath, OutputMode mode) { std::cout << installed.version; } ResetConsoleColor(); - std::cout << "\n\n"; - } - - return installed.isInstalled ? 0 : 1; -} - -/* Command: check-update */ -int CmdCheckUpdate(const std::string& installPath, OutputMode mode) { - std::string path = installPath.empty() ? GetDefaultInstallPath() : installPath; - - /* Get installed version first (local, fast) */ - VersionInfo installed = GetInstalledVersion(path); - - if (mode == OutputMode::Human) { - std::cout << "Install path: " << path << "\n"; - std::cout << "Installed: "; - if (!installed.isInstalled) { - SetConsoleColor(COLOR_YELLOW); - std::cout << "(not installed)"; - ResetConsoleColor(); - std::cout << "\n"; - return 1; /* Exit early, no need to check latest */ - } else if (installed.version.empty()) { + std::cout << "\n"; + std::cout << " Supported: " << kPinnedVersion << "\n"; + if (installed.isInstalled && installed.version != kPinnedVersion) { SetConsoleColor(COLOR_YELLOW); - std::cout << "(installed, version unknown)"; + std::cout << "\n The installed version is not the supported one; run 'install' to replace it.\n"; ResetConsoleColor(); - std::cout << "\n"; - } else { - std::cout << installed.version << "\n"; } - - std::cout << "Latest: "; - std::cout << std::flush; /* Flush before network request */ - } - - /* Fetch latest version (network request, may take time) */ - VersionInfo latest = GetLatestVersion(nullptr); - bool updateAvailable = !IsVersionUpToDate(installed, latest); - - if (mode == OutputMode::Json) { - /* Include path and isInstalled in JSON output */ - std::cout << "{\"type\":\"version\",\"isInstalled\":" << (installed.isInstalled ? "true" : "false") - << ",\"installed\":\"" << installed.version - << "\",\"latest\":\"" << latest.version - << "\",\"updateAvailable\":" << (updateAvailable ? "true" : "false") - << ",\"installPath\":\"" << path << "\"}" << std::endl; - } else if (mode == OutputMode::Human) { - if (latest.version.empty()) { - SetConsoleColor(COLOR_YELLOW); - std::cout << "(unable to check)"; - } else { - std::cout << latest.version; - } - ResetConsoleColor(); std::cout << "\n"; - - if (installed.version.empty()) { - std::cout << "Recommend reinstalling with 'install --update' for version tracking.\n"; - } else if (updateAvailable) { - SetConsoleColor(COLOR_GREEN); - std::cout << "Update available!\n"; - ResetConsoleColor(); - } else { - std::cout << "No update available.\n"; - } } - /* Exit code 2 means update available, 1 means not installed */ - if (!installed.isInstalled) { - return 1; - } - return updateAvailable ? 2 : 0; + return installed.isInstalled ? 0 : 1; } } // anonymous namespace @@ -555,8 +484,6 @@ int main(int argc, char* argv[]) { return CmdInstall(installPath, mode, isUpdate); } else if (command == "version") { return CmdVersion(installPath, mode); - } else if (command == "check-update") { - return CmdCheckUpdate(installPath, mode); } else { std::cerr << "Error: Unknown command: " << command << "\n"; PrintUsage(argv[0]); diff --git a/installer/windbg_installer.cpp b/installer/windbg_installer.cpp index 95a801fb..46116b16 100644 --- a/installer/windbg_installer.cpp +++ b/installer/windbg_installer.cpp @@ -8,6 +8,7 @@ #ifdef _WIN32 #include "windbg_installer.h" +#include "windbg_version.h" #include "http_downloader.h" #include "zip_extractor.h" #include "signature_verifier.h" @@ -15,10 +16,10 @@ #include #include #include +#include #include #include #include -#include #pragma comment(lib, "version.lib") @@ -28,9 +29,19 @@ namespace WinDbgInstaller { namespace { -/* URL for WinDbg appinstaller file */ +/* URL for WinDbg appinstaller file (resolves to the latest release manifest) */ const char* kWinDbgDownloadUrl = "https://aka.ms/windbg/download"; +/* The pinned version we install lives in windbg_version.h, so that the UI can report the + * same value without asking the installer. + * + * If the pinned version cannot be downloaded for any reason (e.g. Microsoft removed it + * from the CDN), Install() automatically falls back to downloading the latest version via + * the appinstaller manifest, so installation still succeeds. */ + +/* Base host that serves the versioned MSIX bundles. */ +const char* kMsixBundleHost = "https://windbg.download.prss.microsoft.com/dbazure/prod"; + /* Files required for valid installation */ const std::vector kRequiredFiles = { "amd64\\dbgeng.dll", @@ -140,6 +151,137 @@ void CleanupTempFiles(const std::vector& files, LogCallback logCall } } +/* Build the direct MSIX bundle download URL for a specific WinDbg version. + * Microsoft hosts each release at a predictable path where the dotted version string is + * rewritten with dashes, e.g. "1.2603.20001.0" -> + * "https://windbg.download.prss.microsoft.com/dbazure/prod/1-2603-20001-0/windbg.msixbundle". */ +std::string BuildMsixBundleUrl(const std::string& version) { + std::string pathVersion = version; + std::replace(pathVersion.begin(), pathVersion.end(), '.', '-'); + return std::string(kMsixBundleHost) + "/" + pathVersion + "/windbg.msixbundle"; +} + +/* Read the version string from an appinstaller manifest (empty on failure). */ +std::string ParseAppInstallerVersion(const std::string& appInstallerPath) { + pugi::xml_document doc; + if (!doc.load_file(appInstallerPath.c_str())) { + return ""; + } + pugi::xml_node appInstaller = doc.child("AppInstaller"); + if (!appInstaller) { + return ""; + } + pugi::xml_attribute versionAttr = appInstaller.attribute("Version"); + return versionAttr ? std::string(versionAttr.value()) : ""; +} + +/* Download, verify, extract and install a WinDbg MSIX bundle from the given URL. + * + * This is the shared core used by both the pinned-version path and the latest-version + * fallback. On success it writes the version marker file using `version`. Any temporary + * artifacts created here are appended to `tempFiles` so the caller can clean them up. */ +InstallResult InstallFromMsixUrl(const std::string& msixUrl, const std::string& version, + const std::string& installTarget, const InstallConfig& config, + std::vector& tempFiles) { + LogCallback logCallback = config.onLog; + ProgressCallback progressCallback = config.onProgress; + + /* Download MSIX bundle (this is the main download that shows progress) */ + ReportProgress(progressCallback, "Downloading WinDbg/TTD package from:", 0); + ReportProgress(progressCallback, msixUrl, 0); + + /* Note: the extension must be a recognized MSIX/APPX extension (not .zip) so that + * WinVerifyTrust engages the AppX signature provider during signature verification. */ + std::string msixPath = GetTempFilePath(".msixbundle"); + tempFiles.push_back(msixPath); + + auto msixDownloadProgressCb = [&](const DownloadProgress& dp) { + /* Report download percentage (0-100%) directly - this is the only step that needs progress display */ + int percent = 0; + if (dp.totalBytes > 0) { + percent = (int)(100 * dp.bytesDownloaded / dp.totalBytes); + } + ReportProgress(progressCallback, "Downloading...", percent, + dp.bytesDownloaded, dp.totalBytes, dp.bytesPerSecond); + }; + + if (!DownloadFileWithProgress(msixUrl, msixPath, msixDownloadProgressCb, logCallback)) { + return InstallResult(false, "Failed to download MSIX bundle"); + } + + /* Verify the downloaded bundle is genuinely signed by Microsoft. + * This must happen before we extract or trust any of its contents so that a + * tampered or substituted package (supply-chain attack) is rejected. */ + ReportProgress(progressCallback, "Verifying package signature...", 0); + + SignatureResult sigResult = VerifyMicrosoftSignature(msixPath, logCallback); + if (!sigResult.valid) { + return InstallResult(false, sigResult.errorMessage.empty() + ? "MSIX bundle signature verification failed" + : sigResult.errorMessage); + } + + /* Extract inner MSIX file from bundle */ + ReportProgress(progressCallback, "Extracting package contents...", 0); + + std::string tempExtractDir = GetTempFilePath("_extract"); + tempFiles.push_back(tempExtractDir); + + std::string innerMsixPath = ExtractFileFromZipArchive(msixPath, kInnerMsixName, tempExtractDir, logCallback); + if (innerMsixPath.empty()) { + return InstallResult(false, "Failed to extract inner MSIX file"); + } + + /* Extract WinDbg contents to installation directory */ + ReportProgress(progressCallback, "Installing WinDbg/TTD files...", 0); + + if (!ExtractZipArchive(innerMsixPath, installTarget, nullptr, logCallback)) { + return InstallResult(false, "Failed to extract WinDbg contents"); + } + + /* Verify installation */ + ReportProgress(progressCallback, "Verifying installation...", 0); + + if (!CheckInstallation(installTarget)) { + return InstallResult(false, "Installation verification failed - required files missing"); + } + + Log(logCallback, LOG_INFO, "WinDbg/TTD installed to: " + installTarget); + + /* Write version marker file so we can report the installed version later */ + if (!version.empty()) { + std::string versionFilePath = installTarget + "\\installed_version.txt"; + std::ofstream versionFile(versionFilePath); + if (versionFile.is_open()) { + versionFile << version; + versionFile.close(); + Log(logCallback, LOG_INFO, "Wrote version marker: " + version); + } else { + Log(logCallback, LOG_WARN, "Could not write version marker file"); + } + } + + return InstallResult(true); +} + +/* Common post-install steps shared by both install paths. */ +void FinishInstall(const std::string& installTarget, const InstallConfig& config, + std::vector& tempFiles) { + LogCallback logCallback = config.onLog; + ProgressCallback progressCallback = config.onProgress; + + /* Print settings info (actual settings configuration is done by UI) */ + if (config.updateSettings) { + std::string x64dbgEngPath = installTarget + "\\amd64"; + PrintSettingsInfo(x64dbgEngPath, logCallback); + } + + CleanupTempFiles(tempFiles, logCallback); + + ReportProgress(progressCallback, "Installation completed successfully!", 0); + Log(logCallback, LOG_INFO, "Please restart Binary Ninja to use WinDbg/TTD."); +} + } // anonymous namespace std::string GetDefaultInstallPath() { @@ -186,7 +328,31 @@ InstallResult Install(const InstallConfig& config) { } Log(logCallback, LOG_INFO, "Installation target: " + installTarget); - /* Step 1: Download appinstaller file (small, no progress needed) */ + /* Attempt 1: install the pinned, known-good version directly by its versioned URL. + * We prefer a pinned version because the very latest WinDbg release occasionally + * ships regressions that break the debugger (issues #1129 and #1130). */ + { + std::string pinnedUrl = BuildMsixBundleUrl(kPinnedVersion); + Log(logCallback, LOG_INFO, "Installing pinned WinDbg version " + std::string(kPinnedVersion)); + + InstallResult pinnedResult = + InstallFromMsixUrl(pinnedUrl, kPinnedVersion, installTarget, config, tempFiles); + if (pinnedResult.success) { + FinishInstall(installTarget, config, tempFiles); + return pinnedResult; + } + + /* Pinned install failed (e.g. Microsoft removed this version from the CDN). + * Fall back to the latest version below so installation can still succeed. */ + Log(logCallback, LOG_WARN, "Failed to install pinned WinDbg version " + + std::string(kPinnedVersion) + " (" + pinnedResult.errorMessage + + "); falling back to the latest version"); + CleanupTempFiles(tempFiles, logCallback); + tempFiles.clear(); + } + + /* Attempt 2 (fallback): install the latest version the "old way" - download the + * appinstaller manifest, parse it for the current MSIX bundle URL and version. */ ReportProgress(progressCallback, "Downloading WinDbg package information from:", 0); ReportProgress(progressCallback, std::string(kWinDbgDownloadUrl), 0); @@ -200,7 +366,6 @@ InstallResult Install(const InstallConfig& config) { return InstallResult(false, error); } - /* Step 2: Parse XML to get MSIX bundle URL */ ReportProgress(progressCallback, "Parsing package information...", 0); std::string msixUrl = ParseAppInstallerXml(appInstallerPath, logCallback); @@ -211,124 +376,18 @@ InstallResult Install(const InstallConfig& config) { return InstallResult(false, error); } - /* Step 3: Download MSIX bundle (this is the main download that shows progress) */ - ReportProgress(progressCallback, "Downloading WinDbg/TTD package from:", 0); - ReportProgress(progressCallback, msixUrl, 0); - - /* Note: the extension must be a recognized MSIX/APPX extension (not .zip) so that - * WinVerifyTrust engages the AppX signature provider during Step 3.5 verification. */ - std::string msixPath = GetTempFilePath(".msixbundle"); - tempFiles.push_back(msixPath); - - auto msixDownloadProgressCb = [&](const DownloadProgress& dp) { - /* Report download percentage (0-100%) directly - this is the only step that needs progress display */ - int percent = 0; - if (dp.totalBytes > 0) { - percent = (int)(100 * dp.bytesDownloaded / dp.totalBytes); - } - ReportProgress(progressCallback, "Downloading...", percent, - dp.bytesDownloaded, dp.totalBytes, dp.bytesPerSecond); - }; - - if (!DownloadFileWithProgress(msixUrl, msixPath, msixDownloadProgressCb, logCallback)) { - std::string error = "Failed to download MSIX bundle"; - Log(logCallback, LOG_ERROR, error); - CleanupTempFiles(tempFiles, logCallback); - return InstallResult(false, error); - } - - /* Step 3.5: Verify the downloaded bundle is genuinely signed by Microsoft. - * This must happen before we extract or trust any of its contents so that a - * tampered or substituted package (supply-chain attack) is rejected. */ - ReportProgress(progressCallback, "Verifying package signature...", 0); - - SignatureResult sigResult = VerifyMicrosoftSignature(msixPath, logCallback); - if (!sigResult.valid) { - std::string error = sigResult.errorMessage.empty() - ? "MSIX bundle signature verification failed" - : sigResult.errorMessage; - Log(logCallback, LOG_ERROR, error); - CleanupTempFiles(tempFiles, logCallback); - return InstallResult(false, error); - } - - /* Step 4: Extract inner MSIX file from bundle */ - ReportProgress(progressCallback, "Extracting package contents...", 0); - - std::string tempExtractDir = GetTempFilePath("_extract"); - tempFiles.push_back(tempExtractDir); - - std::string innerMsixPath = ExtractFileFromZipArchive(msixPath, kInnerMsixName, tempExtractDir, logCallback); - if (innerMsixPath.empty()) { - std::string error = "Failed to extract inner MSIX file"; - Log(logCallback, LOG_ERROR, error); - CleanupTempFiles(tempFiles, logCallback); - return InstallResult(false, error); - } - - /* Step 5: Extract WinDbg contents to installation directory */ - ReportProgress(progressCallback, "Installing WinDbg/TTD files...", 0); + std::string latestVersion = ParseAppInstallerVersion(appInstallerPath); - if (!ExtractZipArchive(innerMsixPath, installTarget, nullptr, logCallback)) { - std::string error = "Failed to extract WinDbg contents"; - Log(logCallback, LOG_ERROR, error); + InstallResult latestResult = + InstallFromMsixUrl(msixUrl, latestVersion, installTarget, config, tempFiles); + if (!latestResult.success) { + Log(logCallback, LOG_ERROR, latestResult.errorMessage); CleanupTempFiles(tempFiles, logCallback); - return InstallResult(false, error); + return latestResult; } - /* Step 6: Verify installation */ - ReportProgress(progressCallback, "Verifying installation...", 0); - - if (!CheckInstallation(installTarget)) { - std::string error = "Installation verification failed - required files missing"; - Log(logCallback, LOG_ERROR, error); - CleanupTempFiles(tempFiles, logCallback); - return InstallResult(false, error); - } - - Log(logCallback, LOG_INFO, "WinDbg/TTD installed to: " + installTarget); - - /* Step 6b: Write version marker file */ - /* Re-parse appinstaller to get version (file is still on disk) */ - std::string installedVersion; - { - pugi::xml_document doc; - if (doc.load_file(appInstallerPath.c_str())) { - pugi::xml_node appInstaller = doc.child("AppInstaller"); - if (appInstaller) { - pugi::xml_attribute versionAttr = appInstaller.attribute("Version"); - if (versionAttr) { - installedVersion = versionAttr.value(); - } - } - } - } - - if (!installedVersion.empty()) { - std::string versionFilePath = installTarget + "\\installed_version.txt"; - std::ofstream versionFile(versionFilePath); - if (versionFile.is_open()) { - versionFile << installedVersion; - versionFile.close(); - Log(logCallback, LOG_INFO, "Wrote version marker: " + installedVersion); - } else { - Log(logCallback, LOG_WARN, "Could not write version marker file"); - } - } - - /* Step 7: Print settings info (actual settings configuration is done by UI) */ - if (config.updateSettings) { - std::string x64dbgEngPath = installTarget + "\\amd64"; - PrintSettingsInfo(x64dbgEngPath, logCallback); - } - - /* Cleanup */ - CleanupTempFiles(tempFiles, logCallback); - - ReportProgress(progressCallback, "Installation completed successfully!", 0); - Log(logCallback, LOG_INFO, "Please restart Binary Ninja to use WinDbg/TTD."); - - return InstallResult(true); + FinishInstall(installTarget, config, tempFiles); + return latestResult; } catch (const std::exception& e) { std::string error = "Exception during installation: " + std::string(e.what()); @@ -396,95 +455,6 @@ VersionInfo GetInstalledVersion(const std::string& installPath) { return info; } -VersionInfo GetLatestVersion(LogCallback logCallback) { - VersionInfo info; - - /* Download appinstaller file to temp location */ - std::string tempPath = GetTempFilePath(".appinstaller"); - - if (!DownloadFileWithProgress(kWinDbgDownloadUrl, tempPath, nullptr, logCallback)) { - Log(logCallback, LOG_ERROR, "Failed to download appinstaller for version check"); - return info; - } - - /* Parse XML to get version */ - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(tempPath.c_str()); - - if (!result) { - Log(logCallback, LOG_ERROR, "Failed to parse appinstaller XML: " + std::string(result.description())); - fs::remove(tempPath); - return info; - } - - /* Get version from AppInstaller element */ - pugi::xml_node appInstaller = doc.child("AppInstaller"); - if (appInstaller) { - pugi::xml_attribute versionAttr = appInstaller.attribute("Version"); - if (versionAttr) { - info.version = versionAttr.value(); - info.displayName = "WinDbg " + info.version; - } - - /* Get download URL from MainBundle */ - pugi::xml_node mainBundle = appInstaller.child("MainBundle"); - if (mainBundle) { - pugi::xml_attribute uriAttr = mainBundle.attribute("Uri"); - if (uriAttr) { - info.downloadUrl = uriAttr.value(); - } - } - } - - /* Cleanup */ - fs::remove(tempPath); - - return info; -} - -int CompareVersions(const std::string& v1, const std::string& v2) { - /* Parse version strings like "1.2404.24002.0" */ - auto parseVersion = [](const std::string& v) -> std::vector { - std::vector parts; - std::istringstream iss(v); - std::string part; - while (std::getline(iss, part, '.')) { - try { - parts.push_back(std::stoi(part)); - } catch (...) { - parts.push_back(0); - } - } - return parts; - }; - - std::vector parts1 = parseVersion(v1); - std::vector parts2 = parseVersion(v2); - - /* Pad with zeros to make them equal length */ - size_t maxLen = (std::max)(parts1.size(), parts2.size()); - parts1.resize(maxLen, 0); - parts2.resize(maxLen, 0); - - /* Compare part by part */ - for (size_t i = 0; i < maxLen; i++) { - if (parts1[i] < parts2[i]) return -1; - if (parts1[i] > parts2[i]) return 1; - } - - return 0; -} - -bool IsVersionUpToDate(const VersionInfo& installed, const VersionInfo& latest) { - /* If either version is invalid, assume up to date (can't determine) */ - if (!installed.IsValid() || !latest.IsValid()) { - return true; - } - - /* Installed >= Latest means up to date */ - return CompareVersions(installed.version, latest.version) >= 0; -} - } // namespace WinDbgInstaller #endif // _WIN32 diff --git a/installer/windbg_installer.h b/installer/windbg_installer.h index bb6a0bfe..58c9fb17 100644 --- a/installer/windbg_installer.h +++ b/installer/windbg_installer.h @@ -86,50 +86,24 @@ std::string GetDefaultInstallPath(); struct VersionInfo { std::string version; /* Version string (e.g., "1.2404.24002.0"), empty if unknown */ std::string displayName; /* Display name (e.g., "WinDbg 1.2404.24002.0") */ - std::string downloadUrl; /* Download URL for this version */ std::string installPath; /* Path where this version is installed */ bool isInstalled; /* True if WinDbg is installed (even if version unknown) */ VersionInfo() : isInstalled(false) {} - - /* Returns true if version string is known */ - bool IsValid() const { return !version.empty(); } }; /* * Get version of installed WinDbg * + * There is deliberately no "latest version" query: we install the pinned version from + * windbg_version.h, so the only question worth asking is whether what is on disk matches + * that constant. + * * @param installPath Path to WinDbg installation (empty = use default) * @return Version info, or empty VersionInfo if not installed */ VersionInfo GetInstalledVersion(const std::string& installPath = ""); -/* - * Get latest available version from Microsoft - * - * @param logCallback Optional callback for log messages - * @return Version info, or empty VersionInfo on error - */ -VersionInfo GetLatestVersion(LogCallback logCallback = nullptr); - -/* - * Check if installed version is up to date - * - * @param installed Installed version info - * @param latest Latest version info - * @return true if installed version >= latest version (or if comparison fails) - */ -bool IsVersionUpToDate(const VersionInfo& installed, const VersionInfo& latest); - -/* - * Compare two version strings - * - * @param v1 First version string - * @param v2 Second version string - * @return -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2 - */ -int CompareVersions(const std::string& v1, const std::string& v2); - /* ============================================================================ * Legacy API for backward compatibility with existing UI code * ============================================================================ */ diff --git a/installer/windbg_version.h b/installer/windbg_version.h new file mode 100644 index 00000000..5939b23e --- /dev/null +++ b/installer/windbg_version.h @@ -0,0 +1,23 @@ +/* + * Pinned WinDbg/TTD version + * + * Copyright 2020-2026 Vector 35 Inc. + * Licensed under the Apache License, Version 2.0 + */ + +#pragma once + +namespace WinDbgInstaller { + +/* The WinDbg version we install and support. + * + * We deliberately install a known-good, pinned version instead of always pulling the + * absolute latest, because a freshly released WinDbg occasionally ships regressions that + * break the debugger's DbgEng/TTD adapter (see issues #1129 and #1130). When Microsoft + * releases a new version we can validate it and bump this constant. + * + * This header is the single source of truth: the installer downloads this version, and the + * UI shows it as the version we support. */ +constexpr const char* kPinnedVersion = "1.2603.20001.0"; + +} // namespace WinDbgInstaller diff --git a/ui/ui.cpp b/ui/ui.cpp index f85cc332..9a1d56d6 100644 --- a/ui/ui.cpp +++ b/ui/ui.cpp @@ -1612,13 +1612,10 @@ void GlobalDebuggerUI::installTTD(const UIActionContext& ctxt) // Check if WinDbg is already installed if (std::filesystem::exists(installTarget) && IsWinDbgInstalled(installPath)) { - // Get installed version + // Get installed version (empty if the marker file is missing; the dialog reports that) std::string installedVersion = GetWinDbgInstalledVersion(installPath); - if (installedVersion.empty()) { - installedVersion = "(unknown)"; - } - // Show update dialog + // Show the version/reinstall dialog WinDbgUpdateDialog dialog(ctxt.context->mainWindow(), installPath, installedVersion); dialog.exec(); return; diff --git a/ui/windbgupdatedialog.cpp b/ui/windbgupdatedialog.cpp index af07b6d2..ceb2a1a6 100644 --- a/ui/windbgupdatedialog.cpp +++ b/ui/windbgupdatedialog.cpp @@ -17,6 +17,7 @@ limitations under the License. #ifdef WIN32 #include "windbgupdatedialog.h" +#include "../installer/windbg_version.h" #include "debuggerapi.h" #include "progresstask.h" #include @@ -29,9 +30,12 @@ using namespace BinaryNinjaDebuggerAPI; WinDbgUpdateDialog::WinDbgUpdateDialog(QWidget* parent, const std::string& installPath, const std::string& installedVersion) : QDialog(parent), m_installPath(installPath), m_installedVersion(installedVersion) { - setWindowTitle("WinDbg/TTD Update"); + setWindowTitle("WinDbg/TTD Version"); setMinimumWidth(450); + const std::string supportedVersion = WinDbgInstaller::kPinnedVersion; + const bool versionMatches = !m_installedVersion.empty() && (m_installedVersion == supportedVersion); + QVBoxLayout* mainLayout = new QVBoxLayout(this); /* Version information group */ @@ -40,38 +44,63 @@ WinDbgUpdateDialog::WinDbgUpdateDialog(QWidget* parent, const std::string& insta QHBoxLayout* installedLayout = new QHBoxLayout(); installedLayout->addWidget(new QLabel("Installed version:", this)); + QLabel* installedVersionLabel; if (m_installedVersion.empty()) { - m_installedVersionLabel = new QLabel("Unknown", this); - m_installedVersionLabel->setStyleSheet("font-weight: bold; color: gray;"); + installedVersionLabel = new QLabel("Unknown", this); + installedVersionLabel->setStyleSheet("font-weight: bold; color: gray;"); } else { - m_installedVersionLabel = new QLabel(QString::fromStdString(m_installedVersion), this); - m_installedVersionLabel->setStyleSheet("font-weight: bold;"); + installedVersionLabel = new QLabel(QString::fromStdString(m_installedVersion), this); + installedVersionLabel->setStyleSheet(versionMatches ? "font-weight: bold; color: green;" + : "font-weight: bold; color: orange;"); } - installedLayout->addWidget(m_installedVersionLabel); + installedLayout->addWidget(installedVersionLabel); installedLayout->addStretch(); versionLayout->addLayout(installedLayout); - QHBoxLayout* latestLayout = new QHBoxLayout(); - latestLayout->addWidget(new QLabel("Latest version:", this)); - m_latestVersionLabel = new QLabel("Checking...", this); - m_latestVersionLabel->setStyleSheet("font-weight: bold; color: gray;"); - latestLayout->addWidget(m_latestVersionLabel); - latestLayout->addStretch(); - versionLayout->addLayout(latestLayout); + QHBoxLayout* supportedLayout = new QHBoxLayout(); + supportedLayout->addWidget(new QLabel("Supported version:", this)); + QLabel* supportedVersionLabel = new QLabel(QString::fromStdString(supportedVersion), this); + supportedVersionLabel->setStyleSheet("font-weight: bold;"); + supportedLayout->addWidget(supportedVersionLabel); + supportedLayout->addStretch(); + versionLayout->addLayout(supportedLayout); mainLayout->addWidget(versionGroup); /* Status/explanation label */ - m_statusLabel = new QLabel(this); - m_statusLabel->setWordWrap(true); - m_statusLabel->setText( - "To update or reinstall WinDbg/TTD, Binary Ninja must be closed first.\n\n" - "Clicking 'Update' will:\n" - "1. Close Binary Ninja\n" - "2. Launch the installer to download and install the latest version\n" - "3. You can restart Binary Ninja after the installation completes" - ); - mainLayout->addWidget(m_statusLabel); + QLabel* statusLabel = new QLabel(this); + statusLabel->setWordWrap(true); + if (versionMatches) { + statusLabel->setText( + "You have the supported version of WinDbg/TTD installed.\n\n" + "To reinstall it anyway, click 'Reinstall'. Binary Ninja will be closed and the " + "installer will run." + ); + } else if (m_installedVersion.empty()) { + statusLabel->setText( + "Unable to determine which version of WinDbg/TTD is installed.\n\n" + "Click 'Install' to install the supported version. Binary Ninja will be closed and " + "the installer will run." + ); + } else { + statusLabel->setText( + "The installed version is not the version this debugger supports.\n\n" + "Clicking 'Install' will:\n" + "1. Close Binary Ninja\n" + "2. Launch the installer to download and install the supported version\n" + "3. You can restart Binary Ninja after the installation completes" + ); + } + mainLayout->addWidget(statusLabel); + + /* Explain why we do not simply track the latest WinDbg release */ + QLabel* noteLabel = new QLabel( + "Binary Ninja installs a specific WinDbg version that has been validated against the " + "debugger, rather than the newest release, because new WinDbg releases occasionally " + "break the DbgEng/TTD adapter.", this); + noteLabel->setWordWrap(true); + noteLabel->setStyleSheet("color: gray;"); + mainLayout->addWidget(noteLabel); mainLayout->addStretch(); @@ -83,80 +112,14 @@ WinDbgUpdateDialog::WinDbgUpdateDialog(QWidget* parent, const std::string& insta connect(m_cancelButton, &QPushButton::clicked, this, &WinDbgUpdateDialog::onCancelClicked); buttonLayout->addWidget(m_cancelButton); - m_updateButton = new QPushButton("Update", this); + /* Not "Update": when the installed version is newer than the supported one, this + * deliberately replaces it with an older build. */ + m_updateButton = new QPushButton(versionMatches ? "Reinstall" : "Install", this); m_updateButton->setDefault(true); connect(m_updateButton, &QPushButton::clicked, this, &WinDbgUpdateDialog::onUpdateClicked); buttonLayout->addWidget(m_updateButton); mainLayout->addLayout(buttonLayout); - - /* Connect signal for thread-safe UI update */ - connect(this, &WinDbgUpdateDialog::latestVersionReceived, - this, &WinDbgUpdateDialog::onLatestVersionReceived); - - /* Start fetching latest version in background */ - fetchLatestVersion(); -} - -void WinDbgUpdateDialog::fetchLatestVersion() -{ - /* Fetch in background thread */ - std::thread([this]() { - std::string version = GetWinDbgLatestVersion(); - emit latestVersionReceived(QString::fromStdString(version)); - }).detach(); -} - -void WinDbgUpdateDialog::onLatestVersionReceived(const QString& version) -{ - m_latestVersion = version.toStdString(); - updateUI(); -} - -void WinDbgUpdateDialog::updateUI() -{ - if (m_latestVersion.empty()) { - m_latestVersionLabel->setText("Unable to check"); - m_latestVersionLabel->setStyleSheet("font-weight: bold; color: red;"); - /* Can still reinstall even if we can't check latest version */ - m_statusLabel->setText( - "Unable to check for the latest version.\n\n" - "You can still reinstall the current version by clicking 'Reinstall'. " - "Binary Ninja will be closed and the installer will run." - ); - m_updateButton->setText("Reinstall"); - } else { - m_latestVersionLabel->setText(QString::fromStdString(m_latestVersion)); - - /* Handle case where installed version is unknown */ - if (m_installedVersion.empty()) { - m_latestVersionLabel->setStyleSheet("font-weight: bold; color: orange;"); - m_statusLabel->setText( - "Unable to determine installed version.\n\n" - "Click 'Reinstall' to install the latest version. " - "Binary Ninja will be closed and the installer will run." - ); - m_updateButton->setText("Reinstall"); - } else if (m_latestVersion == m_installedVersion) { - m_latestVersionLabel->setStyleSheet("font-weight: bold; color: green;"); - m_statusLabel->setText( - "You already have the latest version installed.\n\n" - "If you want to reinstall anyway, click 'Reinstall'. " - "Binary Ninja will be closed and the installer will run." - ); - m_updateButton->setText("Reinstall"); - } else { - m_latestVersionLabel->setStyleSheet("font-weight: bold; color: orange;"); - m_statusLabel->setText( - "A newer version is available!\n\n" - "Clicking 'Update' will:\n" - "1. Close Binary Ninja\n" - "2. Launch the installer to download and install the latest version\n" - "3. You can restart Binary Ninja after the installation completes" - ); - m_updateButton->setText("Update"); - } - } } void WinDbgUpdateDialog::onUpdateClicked() @@ -164,7 +127,7 @@ void WinDbgUpdateDialog::onUpdateClicked() /* Confirm with user */ QMessageBox::StandardButton reply = QMessageBox::question( this, - "Confirm Update", + "Confirm Installation", "Binary Ninja will now close and the WinDbg/TTD installer will start.\n\n" "Do you want to continue?", QMessageBox::Yes | QMessageBox::No, diff --git a/ui/windbgupdatedialog.h b/ui/windbgupdatedialog.h index 0ec49f36..5ccb6880 100644 --- a/ui/windbgupdatedialog.h +++ b/ui/windbgupdatedialog.h @@ -25,6 +25,11 @@ limitations under the License. #include #include +/* Dialog shown when WinDbg/TTD is already installed. + * + * The debugger installs a pinned WinDbg version rather than the latest release, so there is + * nothing to check online: we compare what is on disk against that constant and offer to + * (re)install it. */ class WinDbgUpdateDialog : public QDialog { Q_OBJECT @@ -32,27 +37,16 @@ class WinDbgUpdateDialog : public QDialog private: std::string m_installPath; std::string m_installedVersion; - std::string m_latestVersion; - QLabel* m_installedVersionLabel; - QLabel* m_latestVersionLabel; - QLabel* m_statusLabel; QPushButton* m_updateButton; QPushButton* m_cancelButton; - void fetchLatestVersion(); - void updateUI(); - public: WinDbgUpdateDialog(QWidget* parent, const std::string& installPath, const std::string& installedVersion); public Q_SLOTS: - void onLatestVersionReceived(const QString& version); void onUpdateClicked(); void onCancelClicked(); - -Q_SIGNALS: - void latestVersionReceived(const QString& version); }; #endif // WIN32