WindowsUserChoice.cpp (17361B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 /* 7 * Generate and check the UserChoice Hash, which protects file and protocol 8 * associations on Windows 10. 9 * 10 * NOTE: This is also used in the WDBA, so it avoids XUL and XPCOM. 11 * 12 * References: 13 * - PS-SFTA by Danysys <https://github.com/DanysysTeam/PS-SFTA> 14 * - based on a PureBasic version by LMongrain 15 * <https://github.com/DanysysTeam/SFTA> 16 * - AssocHashGen by "halfmeasuresdisabled", see bug 1225660 and 17 * <https://www.reddit.com/r/ReverseEngineering/comments/3t7q9m/assochashgen_a_reverse_engineered_version_of/> 18 * - SetUserFTA changelog 19 * <https://kolbi.cz/blog/2017/10/25/setuserfta-userchoice-hash-defeated-set-file-type-associations-per-user/> 20 */ 21 22 #include <windows.h> 23 #include <appmodel.h> // for GetPackageFamilyName 24 #include <sddl.h> // for ConvertSidToStringSidW 25 #include <wincrypt.h> // for CryptoAPI base64 26 #include <bcrypt.h> // for CNG MD5 27 #include <winternl.h> // for NT_SUCCESS() 28 29 #include "nsDebug.h" 30 #include "mozilla/UniquePtr.h" 31 #include "nsWindowsHelpers.h" 32 33 #include "WindowsUserChoice.h" 34 35 using namespace mozilla; 36 37 UniquePtr<wchar_t[]> GetCurrentUserStringSid() { 38 HANDLE rawProcessToken; 39 if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, 40 &rawProcessToken)) { 41 return nullptr; 42 } 43 nsAutoHandle processToken(rawProcessToken); 44 45 DWORD userSize = 0; 46 if (!(!::GetTokenInformation(processToken.get(), TokenUser, nullptr, 0, 47 &userSize) && 48 GetLastError() == ERROR_INSUFFICIENT_BUFFER)) { 49 return nullptr; 50 } 51 52 auto userBytes = MakeUnique<unsigned char[]>(userSize); 53 if (!::GetTokenInformation(processToken.get(), TokenUser, userBytes.get(), 54 userSize, &userSize)) { 55 return nullptr; 56 } 57 58 wchar_t* rawSid = nullptr; 59 if (!::ConvertSidToStringSidW( 60 reinterpret_cast<PTOKEN_USER>(userBytes.get())->User.Sid, &rawSid)) { 61 return nullptr; 62 } 63 UniquePtr<wchar_t, LocalFreeDeleter> sid(rawSid); 64 65 // Copy instead of passing UniquePtr<wchar_t, LocalFreeDeleter> back to 66 // the caller. 67 int sidLen = ::lstrlenW(sid.get()) + 1; 68 auto outSid = MakeUnique<wchar_t[]>(sidLen); 69 memcpy(outSid.get(), sid.get(), sidLen * sizeof(wchar_t)); 70 71 return outSid; 72 } 73 74 /* 75 * Create the string which becomes the input to the UserChoice hash. 76 * 77 * @see GenerateUserChoiceHash() for parameters. 78 * 79 * @return The formatted string, nullptr on failure. 80 * 81 * NOTE: This uses the format as of Windows 10 20H2 (latest as of this writing), 82 * used at least since 1803. 83 * There was at least one older version, not currently supported: On Win10 RTM 84 * (build 10240, aka 1507) the hash function is the same, but the timestamp and 85 * User Experience string aren't included; instead (for protocols) the string 86 * ends with the exe path. The changelog of SetUserFTA suggests the algorithm 87 * changed in 1703, so there may be two versions: before 1703, and 1703 to now. 88 */ 89 static UniquePtr<wchar_t[]> FormatUserChoiceString(const wchar_t* aExt, 90 const wchar_t* aUserSid, 91 const wchar_t* aProgId, 92 SYSTEMTIME aTimestamp) { 93 aTimestamp.wSecond = 0; 94 aTimestamp.wMilliseconds = 0; 95 96 FILETIME fileTime = {0}; 97 if (!::SystemTimeToFileTime(&aTimestamp, &fileTime)) { 98 return nullptr; 99 } 100 101 // This string is built into Windows as part of the UserChoice hash algorithm. 102 // It might vary across Windows SKUs (e.g. Windows 10 vs. Windows Server), or 103 // across builds of the same SKU, but this is the only currently known 104 // version. There isn't any known way of deriving it, so we assume this 105 // constant value. If we are wrong, we will not be able to generate correct 106 // UserChoice hashes. 107 const wchar_t* userExperience = 108 L"User Choice set via Windows User Experience " 109 L"{D18B6DD5-6124-4341-9318-804003BAFA0B}"; 110 111 const wchar_t* userChoiceFmt = 112 L"%s%s%s" 113 L"%08lx" 114 L"%08lx" 115 L"%s"; 116 int userChoiceLen = _scwprintf(userChoiceFmt, aExt, aUserSid, aProgId, 117 fileTime.dwHighDateTime, 118 fileTime.dwLowDateTime, userExperience); 119 userChoiceLen += 1; // _scwprintf does not include the terminator 120 121 auto userChoice = MakeUnique<wchar_t[]>(userChoiceLen); 122 _snwprintf_s(userChoice.get(), userChoiceLen, _TRUNCATE, userChoiceFmt, aExt, 123 aUserSid, aProgId, fileTime.dwHighDateTime, 124 fileTime.dwLowDateTime, userExperience); 125 126 ::CharLowerW(userChoice.get()); 127 128 return userChoice; 129 } 130 131 // @return The MD5 hash of the input, nullptr on failure. 132 static UniquePtr<DWORD[]> CNG_MD5(const unsigned char* bytes, ULONG bytesLen) { 133 constexpr ULONG MD5_BYTES = 16; 134 constexpr ULONG MD5_DWORDS = MD5_BYTES / sizeof(DWORD); 135 UniquePtr<DWORD[]> hash; 136 137 BCRYPT_ALG_HANDLE hAlg = nullptr; 138 if (NT_SUCCESS(::BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_MD5_ALGORITHM, 139 nullptr, 0))) { 140 BCRYPT_HASH_HANDLE hHash = nullptr; 141 // As of Windows 7 the hash handle will manage its own object buffer when 142 // pbHashObject is nullptr and cbHashObject is 0. 143 if (NT_SUCCESS( 144 ::BCryptCreateHash(hAlg, &hHash, nullptr, 0, nullptr, 0, 0))) { 145 // BCryptHashData promises not to modify pbInput. 146 if (NT_SUCCESS(::BCryptHashData(hHash, const_cast<unsigned char*>(bytes), 147 bytesLen, 0))) { 148 hash = MakeUnique<DWORD[]>(MD5_DWORDS); 149 if (!NT_SUCCESS(::BCryptFinishHash( 150 hHash, reinterpret_cast<unsigned char*>(hash.get()), 151 MD5_DWORDS * sizeof(DWORD), 0))) { 152 hash.reset(); 153 } 154 } 155 ::BCryptDestroyHash(hHash); 156 } 157 ::BCryptCloseAlgorithmProvider(hAlg, 0); 158 } 159 160 return hash; 161 } 162 163 // @return The input bytes encoded as base64, nullptr on failure. 164 static UniquePtr<wchar_t[]> CryptoAPI_Base64Encode(const unsigned char* bytes, 165 DWORD bytesLen) { 166 DWORD base64Len = 0; 167 if (!::CryptBinaryToStringW(bytes, bytesLen, 168 CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, 169 nullptr, &base64Len)) { 170 return nullptr; 171 } 172 auto base64 = MakeUnique<wchar_t[]>(base64Len); 173 if (!::CryptBinaryToStringW(bytes, bytesLen, 174 CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, 175 base64.get(), &base64Len)) { 176 return nullptr; 177 } 178 179 return base64; 180 } 181 182 static inline DWORD WordSwap(DWORD v) { return (v >> 16) | (v << 16); } 183 184 /* 185 * Generate the UserChoice Hash. 186 * 187 * This implementation is based on the references listed above. 188 * It is organized to show the logic as clearly as possible, but at some 189 * point the reasoning is just "this is how it works". 190 * 191 * @param inputString A null-terminated string to hash. 192 * 193 * @return The base64-encoded hash, or nullptr on failure. 194 */ 195 static UniquePtr<wchar_t[]> HashString(const wchar_t* inputString) { 196 auto inputBytes = reinterpret_cast<const unsigned char*>(inputString); 197 int inputByteCount = (::lstrlenW(inputString) + 1) * sizeof(wchar_t); 198 199 constexpr size_t DWORDS_PER_BLOCK = 2; 200 constexpr size_t BLOCK_SIZE = sizeof(DWORD) * DWORDS_PER_BLOCK; 201 // Incomplete blocks are ignored. 202 int blockCount = inputByteCount / BLOCK_SIZE; 203 204 if (blockCount == 0) { 205 return nullptr; 206 } 207 208 // Compute an MD5 hash. md5[0] and md5[1] will be used as constant multipliers 209 // in the scramble below. 210 auto md5 = CNG_MD5(inputBytes, inputByteCount); 211 if (!md5) { 212 return nullptr; 213 } 214 215 // The following loop effectively computes two checksums, scrambled like a 216 // hash after every DWORD is added. 217 218 // Constant multipliers for the scramble, one set for each DWORD in a block. 219 const DWORD C0s[DWORDS_PER_BLOCK][5] = { 220 {md5[0] | 1, 0xCF98B111uL, 0x87085B9FuL, 0x12CEB96DuL, 0x257E1D83uL}, 221 {md5[1] | 1, 0xA27416F5uL, 0xD38396FFuL, 0x7C932B89uL, 0xBFA49F69uL}}; 222 const DWORD C1s[DWORDS_PER_BLOCK][5] = { 223 {md5[0] | 1, 0xEF0569FBuL, 0x689B6B9FuL, 0x79F8A395uL, 0xC3EFEA97uL}, 224 {md5[1] | 1, 0xC31713DBuL, 0xDDCD1F0FuL, 0x59C3AF2DuL, 0x35BD1EC9uL}}; 225 226 // The checksums. 227 DWORD h0 = 0; 228 DWORD h1 = 0; 229 // Accumulated total of the checksum after each DWORD. 230 DWORD h0Acc = 0; 231 DWORD h1Acc = 0; 232 233 for (int i = 0; i < blockCount; ++i) { 234 for (size_t j = 0; j < DWORDS_PER_BLOCK; ++j) { 235 const DWORD* C0 = C0s[j]; 236 const DWORD* C1 = C1s[j]; 237 238 DWORD input; 239 memcpy(&input, &inputBytes[(i * DWORDS_PER_BLOCK + j) * sizeof(DWORD)], 240 sizeof(DWORD)); 241 242 h0 += input; 243 // Scramble 0 244 h0 *= C0[0]; 245 h0 = WordSwap(h0) * C0[1]; 246 h0 = WordSwap(h0) * C0[2]; 247 h0 = WordSwap(h0) * C0[3]; 248 h0 = WordSwap(h0) * C0[4]; 249 h0Acc += h0; 250 251 h1 += input; 252 // Scramble 1 253 h1 = WordSwap(h1) * C1[1] + h1 * C1[0]; 254 h1 = (h1 >> 16) * C1[2] + h1 * C1[3]; 255 h1 = WordSwap(h1) * C1[4] + h1; 256 h1Acc += h1; 257 } 258 } 259 260 DWORD hash[2] = {h0 ^ h1, h0Acc ^ h1Acc}; 261 262 return CryptoAPI_Base64Encode(reinterpret_cast<const unsigned char*>(hash), 263 sizeof(hash)); 264 } 265 266 UniquePtr<wchar_t[]> GetAssociationKeyPath(const wchar_t* aExt) { 267 const wchar_t* keyPathFmt; 268 if (aExt[0] == L'.') { 269 keyPathFmt = 270 L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\%s"; 271 } else { 272 keyPathFmt = 273 L"SOFTWARE\\Microsoft\\Windows\\Shell\\Associations\\" 274 L"UrlAssociations\\%s"; 275 } 276 277 int keyPathLen = _scwprintf(keyPathFmt, aExt); 278 keyPathLen += 1; // _scwprintf does not include the terminator 279 280 auto keyPath = MakeUnique<wchar_t[]>(keyPathLen); 281 _snwprintf_s(keyPath.get(), keyPathLen, _TRUNCATE, keyPathFmt, aExt); 282 283 return keyPath; 284 } 285 286 void AppendAssociationKeyPath(const wchar_t* aExt, nsAString& aOutput) { 287 if (aExt[0] == L'.') { 288 aOutput.AppendLiteral( 289 u"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\"); 290 } else { 291 aOutput.AppendLiteral( 292 u"SOFTWARE\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations" 293 u"\\"); 294 } 295 296 aOutput.Append(aExt); 297 } 298 299 UniquePtr<wchar_t[]> GenerateUserChoiceHash(const wchar_t* aExt, 300 const wchar_t* aUserSid, 301 const wchar_t* aProgId, 302 SYSTEMTIME aTimestamp) { 303 auto userChoice = FormatUserChoiceString(aExt, aUserSid, aProgId, aTimestamp); 304 if (!userChoice) { 305 return nullptr; 306 } 307 return HashString(userChoice.get()); 308 } 309 310 /* 311 * NOTE: The passed-in current user SID is used here, instead of getting the SID 312 * for the owner of the key. We are assuming that this key in HKCU is owned by 313 * the current user, since we want to replace that key ourselves. If the key is 314 * owned by someone else, then this check will fail; this is ok because we would 315 * likely not want to replace that other user's key anyway. 316 */ 317 CheckUserChoiceHashResult CheckUserChoiceHash(const wchar_t* aExt, 318 const wchar_t* aUserSid) { 319 auto keyPath = GetAssociationKeyPath(aExt); 320 if (!keyPath) { 321 return CheckUserChoiceHashResult::ERR_OTHER; 322 } 323 324 HKEY rawAssocKey; 325 if (::RegOpenKeyExW(HKEY_CURRENT_USER, keyPath.get(), 0, KEY_READ, 326 &rawAssocKey) != ERROR_SUCCESS) { 327 return CheckUserChoiceHashResult::ERR_OTHER; 328 } 329 nsAutoRegKey assocKey(rawAssocKey); 330 331 FILETIME lastWriteFileTime; 332 { 333 HKEY rawUserChoiceKey; 334 if (::RegOpenKeyExW(assocKey.get(), L"UserChoice", 0, KEY_READ, 335 &rawUserChoiceKey) != ERROR_SUCCESS) { 336 return CheckUserChoiceHashResult::ERR_OTHER; 337 } 338 nsAutoRegKey userChoiceKey(rawUserChoiceKey); 339 340 if (::RegQueryInfoKeyW(userChoiceKey.get(), nullptr, nullptr, nullptr, 341 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, 342 nullptr, &lastWriteFileTime) != ERROR_SUCCESS) { 343 return CheckUserChoiceHashResult::ERR_OTHER; 344 } 345 } 346 347 SYSTEMTIME lastWriteSystemTime; 348 if (!::FileTimeToSystemTime(&lastWriteFileTime, &lastWriteSystemTime)) { 349 return CheckUserChoiceHashResult::ERR_OTHER; 350 } 351 352 // Read ProgId 353 DWORD dataSizeBytes = 0; 354 if (::RegGetValueW(assocKey.get(), L"UserChoice", L"ProgId", RRF_RT_REG_SZ, 355 nullptr, nullptr, &dataSizeBytes) != ERROR_SUCCESS) { 356 return CheckUserChoiceHashResult::ERR_OTHER; 357 } 358 // +1 in case dataSizeBytes was odd, +1 to ensure termination 359 DWORD dataSizeChars = (dataSizeBytes / sizeof(wchar_t)) + 2; 360 UniquePtr<wchar_t[]> progId(new wchar_t[dataSizeChars]()); 361 if (::RegGetValueW(assocKey.get(), L"UserChoice", L"ProgId", RRF_RT_REG_SZ, 362 nullptr, progId.get(), &dataSizeBytes) != ERROR_SUCCESS) { 363 return CheckUserChoiceHashResult::ERR_OTHER; 364 } 365 366 // Read Hash 367 dataSizeBytes = 0; 368 if (::RegGetValueW(assocKey.get(), L"UserChoice", L"Hash", RRF_RT_REG_SZ, 369 nullptr, nullptr, &dataSizeBytes) != ERROR_SUCCESS) { 370 return CheckUserChoiceHashResult::ERR_OTHER; 371 } 372 dataSizeChars = (dataSizeBytes / sizeof(wchar_t)) + 2; 373 UniquePtr<wchar_t[]> storedHash(new wchar_t[dataSizeChars]()); 374 if (::RegGetValueW(assocKey.get(), L"UserChoice", L"Hash", RRF_RT_REG_SZ, 375 nullptr, storedHash.get(), 376 &dataSizeBytes) != ERROR_SUCCESS) { 377 return CheckUserChoiceHashResult::ERR_OTHER; 378 } 379 380 auto computedHash = 381 GenerateUserChoiceHash(aExt, aUserSid, progId.get(), lastWriteSystemTime); 382 if (!computedHash) { 383 return CheckUserChoiceHashResult::ERR_OTHER; 384 } 385 386 if (::CompareStringOrdinal(computedHash.get(), -1, storedHash.get(), -1, 387 FALSE) != CSTR_EQUAL) { 388 return CheckUserChoiceHashResult::ERR_MISMATCH; 389 } 390 391 return CheckUserChoiceHashResult::OK_V1; 392 } 393 394 bool CheckBrowserUserChoiceHashes() { 395 auto userSid = GetCurrentUserStringSid(); 396 if (!userSid) { 397 return false; 398 } 399 400 const wchar_t* exts[] = {L"https", L"http", L".html", L".htm"}; 401 402 for (size_t i = 0; i < std::size(exts); ++i) { 403 switch (CheckUserChoiceHash(exts[i], userSid.get())) { 404 case CheckUserChoiceHashResult::OK_V1: 405 break; 406 case CheckUserChoiceHashResult::ERR_MISMATCH: 407 case CheckUserChoiceHashResult::ERR_OTHER: 408 return false; 409 } 410 } 411 412 return true; 413 } 414 415 UniquePtr<wchar_t[]> FormatProgID(const wchar_t* aProgIDBase, 416 const wchar_t* aAumi) { 417 const wchar_t* progIDFmt = L"%s-%s"; 418 int progIDLen = _scwprintf(progIDFmt, aProgIDBase, aAumi); 419 progIDLen += 1; // _scwprintf does not include the terminator 420 421 auto progID = MakeUnique<wchar_t[]>(progIDLen); 422 _snwprintf_s(progID.get(), progIDLen, _TRUNCATE, progIDFmt, aProgIDBase, 423 aAumi); 424 425 return progID; 426 } 427 428 bool CheckProgIDExists(const wchar_t* aProgID) { 429 HKEY key; 430 if (::RegOpenKeyExW(HKEY_CLASSES_ROOT, aProgID, 0, KEY_READ, &key) != 431 ERROR_SUCCESS) { 432 return false; 433 } 434 ::RegCloseKey(key); 435 return true; 436 } 437 438 nsresult GetMsixProgId(const wchar_t* assoc, UniquePtr<wchar_t[]>& aProgId) { 439 // Retrieve the registry path to the package from registry path: 440 // clang-format off 441 // HKEY_CLASSES_ROOT\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages\[Package Full Name]\App\Capabilities\[FileAssociations | URLAssociations]\[File | URL] 442 // clang-format on 443 444 UINT32 pfnLen = 0; 445 LONG rv = GetCurrentPackageFullName(&pfnLen, nullptr); 446 NS_ENSURE_TRUE(rv != APPMODEL_ERROR_NO_PACKAGE, NS_ERROR_FAILURE); 447 448 auto pfn = mozilla::MakeUnique<wchar_t[]>(pfnLen); 449 rv = GetCurrentPackageFullName(&pfnLen, pfn.get()); 450 NS_ENSURE_TRUE(rv == ERROR_SUCCESS, NS_ERROR_FAILURE); 451 452 const wchar_t* assocSuffix; 453 if (assoc[0] == L'.') { 454 // File association. 455 assocSuffix = LR"(App\Capabilities\FileAssociations)"; 456 } else { 457 // URL association. 458 assocSuffix = LR"(App\Capabilities\URLAssociations)"; 459 } 460 461 const wchar_t* assocPathFmt = 462 LR"(Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages\%s\%s)"; 463 int assocPathLen = _scwprintf(assocPathFmt, pfn.get(), assocSuffix); 464 assocPathLen += 1; // _scwprintf does not include the terminator 465 466 auto assocPath = MakeUnique<wchar_t[]>(assocPathLen); 467 _snwprintf_s(assocPath.get(), assocPathLen, _TRUNCATE, assocPathFmt, 468 pfn.get(), assocSuffix); 469 470 LSTATUS ls; 471 472 // Retrieve the package association's ProgID, always in the form `AppX[32 hash 473 // characters]`. 474 const size_t appxProgIdLen = 37; 475 auto progId = MakeUnique<wchar_t[]>(appxProgIdLen); 476 DWORD progIdLen = appxProgIdLen * sizeof(wchar_t); 477 ls = ::RegGetValueW(HKEY_CLASSES_ROOT, assocPath.get(), assoc, RRF_RT_REG_SZ, 478 nullptr, (LPBYTE)progId.get(), &progIdLen); 479 if (ls != ERROR_SUCCESS) { 480 return NS_ERROR_WDBA_NO_PROGID; 481 } 482 483 aProgId.swap(progId); 484 485 return NS_OK; 486 }