tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }