From 13102552b782190f27a403c5baede1c5afaad0cd Mon Sep 17 00:00:00 2001 From: Tixx <83774803+WiserTixx@users.noreply.github.com> Date: Tue, 31 Mar 2026 23:47:05 +0200 Subject: [PATCH 1/2] Check launcher update signature --- src/Startup.cpp | 172 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 163 insertions(+), 9 deletions(-) diff --git a/src/Startup.cpp b/src/Startup.cpp index 8f1875d..cbbeb22 100644 --- a/src/Startup.cpp +++ b/src/Startup.cpp @@ -4,7 +4,6 @@ SPDX-License-Identifier: AGPL-3.0-or-later */ - #include "zip_file.h" #include #include @@ -14,9 +13,9 @@ #if defined(_WIN32) #elif defined(__linux__) #include -#elif defined (__APPLE__) -#include +#elif defined(__APPLE__) #include +#include #endif // __APPLE__ #include "Http.h" #include "Logger.h" @@ -114,7 +113,7 @@ fs::path GetBP(const beammp_fs_char* P) { // should instead be placed in Application Support. proc_pidpath(pid, path, sizeof(path)); fspath = std::string(path); - #else +#else fspath = beammp_fs_string(P); #endif fspath = fs::weakly_canonical(fspath.string() + "/.."); @@ -194,6 +193,143 @@ void CheckName() { } } +#if defined(_WIN32) +#include +#include +#include +#include + +#pragma comment(lib, "wintrust.lib") +#pragma comment(lib, "crypt32.lib") + +bool CheckThumbprint(std::filesystem::path filepath) +{ + HCERTSTORE hStore = NULL; + HCRYPTMSG hMsg = NULL; + + if (!CryptQueryObject( + CERT_QUERY_OBJECT_FILE, + filepath.wstring().c_str(), + CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, + CERT_QUERY_FORMAT_FLAG_BINARY, + 0, + NULL, NULL, NULL, + &hStore, + &hMsg, + NULL)) + { + return false; + } + + DWORD dwSignerInfo = 0; + if (!CryptMsgGetParam(hMsg, CMSG_SIGNER_INFO_PARAM, 0, NULL, &dwSignerInfo) || dwSignerInfo == 0) + return false; + + PCMSG_SIGNER_INFO pSignerInfo = (PCMSG_SIGNER_INFO)LocalAlloc(LPTR, dwSignerInfo); + if (!CryptMsgGetParam(hMsg, CMSG_SIGNER_INFO_PARAM, 0, pSignerInfo, &dwSignerInfo)) + { + LocalFree(pSignerInfo); + return false; + } + + CERT_INFO certInfo = {}; + certInfo.Issuer = pSignerInfo->Issuer; + certInfo.SerialNumber = pSignerInfo->SerialNumber; + + PCCERT_CONTEXT pCertContext = CertFindCertificateInStore( + hStore, + X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + 0, + CERT_FIND_SUBJECT_CERT, + &certInfo, + NULL); + + if (!pCertContext) + { + LocalFree(pSignerInfo); + return false; + } + + BYTE hash[64]; + DWORD hashSize = sizeof(hash); + + bool match = false; + if (CertGetCertificateContextProperty(pCertContext, CERT_SHA256_HASH_PROP_ID, hash, &hashSize)) + { + std::string pubKeyData( + reinterpret_cast(pCertContext->pCertInfo->SubjectPublicKeyInfo.PublicKey.pbData), + pCertContext->pCertInfo->SubjectPublicKeyInfo.PublicKey.cbData + ); + + std::string pubKeyHash = Utils::GetSha256HashReallyFast(pubKeyData, L"PubKey"); + debug("pub key hash: " + pubKeyHash); + + std::string fileThumbprint; + for (DWORD i = 0; i < hashSize; i++) + { + char buf[3]; + sprintf_s(buf, "%02x", hash[i]); + fileThumbprint += buf; + } + + debug("File thumbprint: " +fileThumbprint); + debug(filepath); + + if (fileThumbprint == "937f055b713de69416926ed4651d65219a0a0e77d7a78c1932c007e14326da33" && pubKeyHash == "2afad4a5773b0ac449f48350ce0d09c372be0d5bcbaa6d01332ce000baffde99"){ + match = true; + } + } + + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + LocalFree(pSignerInfo); + + return match; +} +#include +#include +#include +#include +#include + +#pragma comment(lib, "wintrust") + +bool VerifySignature(const std::filesystem::path& filePath) +{ + std::wstring path = filePath.wstring(); + + WINTRUST_FILE_INFO fileInfo = {}; + fileInfo.cbStruct = sizeof(WINTRUST_FILE_INFO); + fileInfo.pcwszFilePath = path.c_str(); + fileInfo.hFile = NULL; + fileInfo.pgKnownSubject = NULL; + + WINTRUST_DATA winTrustData = {}; + winTrustData.cbStruct = sizeof(WINTRUST_DATA); + winTrustData.dwUIChoice = WTD_UI_NONE; + winTrustData.dwUnionChoice = WTD_CHOICE_FILE; + winTrustData.pFile = &fileInfo; + + winTrustData.dwStateAction = WTD_STATEACTION_VERIFY; + + GUID policyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2; + + LONG status = WinVerifyTrust( + NULL, + &policyGUID, + &winTrustData + ); + + debug(filePath); + debug("Signature check code: " + std::to_string(status)); + + winTrustData.dwStateAction = WTD_STATEACTION_CLOSE; + WinVerifyTrust(NULL, &policyGUID, &winTrustData); + + return (status == CERT_E_UNTRUSTEDROOT); +} +#endif + void CheckForUpdates(const std::string& CV) { std::string LatestHash = HTTP::Get("https://backend.beammp.com/sha/launcher?branch=" + Branch + "&pk=" + PublicKey); std::string LatestVersion = HTTP::Get( @@ -211,11 +347,23 @@ void CheckForUpdates(const std::string& CV) { error("Auto update is NOT implemented for the Linux version. Please update manually ASAP as updates contain security patches."); #else info("Downloading Launcher update " + LatestHash); + std::wstring DownloadLocation = GetBP() / (beammp_wide("new_") + GetEN()); if (HTTP::Download( - "https://backend.beammp.com/builds/launcher?download=true" - "&pk=" - + PublicKey + "&branch=" + Branch, - GetBP() / (beammp_wide("new_") + GetEN()), LatestHash)) { + "https://backend.beammp.com/builds/launcher?download=true" + "&pk=" + + PublicKey + "&branch=" + Branch, + DownloadLocation, LatestHash)) { + if (!VerifySignature(DownloadLocation) || !CheckThumbprint(DownloadLocation)) { + std::error_code ec; + fs::remove(DownloadLocation, ec); + if (ec) { + error("Failed to remove broken launcher update"); + } + throw std::runtime_error("The authenticity of the updated launcher could not be verified, it was corrupted or tampered with."); + } + + info("Update signature is valid"); + std::error_code ec; fs::remove(Back, ec); if (ec == std::errc::permission_denied) { @@ -227,6 +375,13 @@ void CheckForUpdates(const std::string& CV) { fs::rename(GetBP() / (beammp_wide("new_") + GetEN()), BP); URelaunch(); } else { + if (fs::exists(DownloadLocation)) { + std::error_code error_code; + fs::remove(DownloadLocation, error_code); + if (error_code) { + error("Failed to remove broken launcher update"); + } + } throw std::runtime_error("Failed to download the launcher update! Please try manually updating it, https://docs.beammp.com/FAQ/Update-launcher/"); } #endif @@ -238,7 +393,6 @@ void CheckForUpdates(const std::string& CV) { TraceBack++; } - #ifdef _WIN32 void LinuxPatch() { HKEY hKey = nullptr; From 7ae273f5500d2f977e12b2585d37e411051aac95 Mon Sep 17 00:00:00 2001 From: Tixx <83774803+WiserTixx@users.noreply.github.com> Date: Tue, 31 Mar 2026 23:47:46 +0200 Subject: [PATCH 2/2] Check hash with regex --- src/Startup.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Startup.cpp b/src/Startup.cpp index cbbeb22..ca90b29 100644 --- a/src/Startup.cpp +++ b/src/Startup.cpp @@ -335,6 +335,15 @@ void CheckForUpdates(const std::string& CV) { std::string LatestVersion = HTTP::Get( "https://backend.beammp.com/version/launcher?branch=" + Branch + "&pk=" + PublicKey); + std::regex sha256_pattern(R"(^[a-fA-F0-9]{64}$)"); + std::smatch match; + + if (LatestHash.length() != 64 || !std::regex_match(LatestHash, match, sha256_pattern)) { + error("Invalid hash from backend, skipping update check."); + debug("Launcher hash in question: " + LatestHash); + return; + } + transform(LatestHash.begin(), LatestHash.end(), LatestHash.begin(), ::tolower); beammp_fs_string BP(GetBP() / GetEN()), Back(GetBP() / beammp_wide("BeamMP-Launcher.back")); @@ -511,6 +520,15 @@ void PreGame(const beammp_fs_string& GamePath) { [](auto const& c) -> bool { return !std::isalnum(c); }), LatestHash.end()); + std::regex sha256_pattern(R"(^[a-fA-F0-9]{64}$)"); + std::smatch match; + + if (LatestHash.length() != 64 || !std::regex_match(LatestHash, match, sha256_pattern)) { + error("Invalid hash from backend, skipping mod update check."); + debug("Mod hash in question: " + LatestHash); + return; + } + try { if (!fs::exists(GetGamePath() / beammp_wide("mods/multiplayer"))) { fs::create_directories(GetGamePath() / beammp_wide("mods/multiplayer"));