chromium/chrome/browser/win/conflicts/module_info_util.cc

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/win/conflicts/module_info_util.h"

#include <windows.h>

#include <tlhelp32.h>
#include <wintrust.h>

#include <limits>
#include <memory>
#include <string>
#include <string_view>

#include "base/containers/heap_array.h"
#include "base/environment.h"
#include "base/files/file.h"
#include "base/i18n/case_conversion.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "base/scoped_generic.h"
#include "base/strings/strcat_win.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/pe_image_reader.h"
#include "base/win/scoped_handle.h"
#include "base/win/wincrypt_shim.h"
#include "crypto/scoped_capi_types.h"

// This must be after wincrypt and wintrust.
#include <mscat.h>

namespace {

// Returns the "Subject" field from the digital signature in the provided
// binary, if any is present. Returns an empty string on failure.
std::u16string GetSubjectNameInFile(const base::FilePath& filename) {
  // Find the crypto message for this filename.
  crypto::ScopedHCERTSTORE store;
  crypto::ScopedHCRYPTMSG message;
  if (!CryptQueryObject(
          CERT_QUERY_OBJECT_FILE, filename.value().c_str(),
          CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED,
          CERT_QUERY_FORMAT_FLAG_BINARY, 0, nullptr, nullptr, nullptr,
          crypto::ScopedHCERTSTORE::Receiver(store).get(),
          crypto::ScopedHCRYPTMSG::Receiver(message).get(), nullptr)) {
    return std::u16string();
  }

  // Determine the size of the signer info data.
  DWORD signer_info_size = 0;
  if (!CryptMsgGetParam(message.get(), CMSG_SIGNER_INFO_PARAM, 0, nullptr,
                        &signer_info_size)) {
    return std::u16string();
  }

  // Allocate enough space to hold the signer info.
  std::unique_ptr<BYTE[]> signer_info_buffer(new BYTE[signer_info_size]);
  CMSG_SIGNER_INFO* signer_info =
      reinterpret_cast<CMSG_SIGNER_INFO*>(signer_info_buffer.get());

  // Obtain the signer info.
  if (!CryptMsgGetParam(message.get(), CMSG_SIGNER_INFO_PARAM, 0, signer_info,
                        &signer_info_size)) {
    return std::u16string();
  }

  // Search for the signer certificate.
  CERT_INFO CertInfo = {0};
  CertInfo.Issuer = signer_info->Issuer;
  CertInfo.SerialNumber = signer_info->SerialNumber;

  crypto::ScopedPCCERT_CONTEXT cert_context(CertFindCertificateInStore(
      store.get(), X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 0,
      CERT_FIND_SUBJECT_CERT, &CertInfo, nullptr));
  if (!cert_context)
    return std::u16string();

  // Determine the size of the Subject name.
  DWORD subject_name_size =
      CertGetNameString(cert_context.get(), CERT_NAME_SIMPLE_DISPLAY_TYPE, 0,
                        nullptr, nullptr, 0);
  if (!subject_name_size)
    return std::u16string();

  std::wstring subject_name;
  subject_name.resize(subject_name_size);

  // Get subject name.
  if (!CertGetNameString(cert_context.get(), CERT_NAME_SIMPLE_DISPLAY_TYPE, 0,
                         nullptr, const_cast<LPWSTR>(subject_name.c_str()),
                         subject_name_size)) {
    return std::u16string();
  }

  // The subject name is normalized because it can contain trailing null
  // characters.
  internal::NormalizeCertificateSubject(&subject_name);

  return base::AsString16(subject_name);
}

// Helper for scoped tracking a catalog admin context.
struct CryptCATContextScopedTraits {
  static PVOID InvalidValue() { return nullptr; }
  static void Free(PVOID context) { CryptCATAdminReleaseContext(context, 0); }
};
using ScopedCryptCATContext =
    base::ScopedGeneric<PVOID, CryptCATContextScopedTraits>;

// Helper for scoped tracking of a catalog context. A catalog context is only
// valid with an associated admin context, so this is effectively a std::pair.
// A custom operator!= is required in order for a null |catalog_context| but
// non-null |context| to compare equal to the InvalidValue exposed by the
// traits class.
class CryptCATCatalogContext {
 public:
  CryptCATCatalogContext(PVOID context, PVOID catalog_context)
      : context_(context), catalog_context_(catalog_context) {}

  bool operator!=(const CryptCATCatalogContext& rhs) const {
    return catalog_context_ != rhs.catalog_context_;
  }

  PVOID context() const { return context_; }
  PVOID catalog_context() const { return catalog_context_; }

 private:
  PVOID context_;
  PVOID catalog_context_;
};

struct CryptCATCatalogContextScopedTraits {
  static CryptCATCatalogContext InvalidValue() {
    return CryptCATCatalogContext(nullptr, nullptr);
  }
  static void Free(const CryptCATCatalogContext& c) {
    CryptCATAdminReleaseCatalogContext(c.context(), c.catalog_context(), 0);
  }
};
using ScopedCryptCATCatalogContext =
    base::ScopedGeneric<CryptCATCatalogContext,
                        CryptCATCatalogContextScopedTraits>;

// Extracts the subject name and catalog path if the provided file is present in
// a catalog file.
void GetCatalogCertificateInfo(const base::FilePath& filename,
                               CertificateInfo* certificate_info) {
  // Get a crypt context for signature verification.
  ScopedCryptCATContext context;
  {
    PVOID raw_context = nullptr;
    if (!CryptCATAdminAcquireContext(&raw_context, nullptr, 0))
      return;
    context.reset(raw_context);
  }

  // Open the file of interest.
  base::win::ScopedHandle file_handle(
      CreateFileW(filename.value().c_str(), GENERIC_READ,
                  FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                  nullptr, OPEN_EXISTING, 0, nullptr));
  if (!file_handle.IsValid())
    return;

  // Get the size we need for our hash.
  DWORD hash_size = 0;
  CryptCATAdminCalcHashFromFileHandle(file_handle.Get(), &hash_size, nullptr,
                                      0);
  if (hash_size == 0)
    return;

  // Calculate the hash. If this fails then bail.
  std::vector<BYTE> buffer(hash_size);
  if (!CryptCATAdminCalcHashFromFileHandle(file_handle.Get(), &hash_size,
                                           buffer.data(), 0)) {
    return;
  }

  // Get catalog for our context.
  ScopedCryptCATCatalogContext catalog_context(CryptCATCatalogContext(
      context.get(), CryptCATAdminEnumCatalogFromHash(
                         context.get(), buffer.data(), hash_size, 0, nullptr)));
  if (!catalog_context.is_valid())
    return;

  // Get the catalog info. This includes the path to the catalog itself, which
  // contains the signature of interest.
  CATALOG_INFO catalog_info = {};
  catalog_info.cbStruct = sizeof(catalog_info);
  if (!CryptCATCatalogInfoFromContext(catalog_context.get().catalog_context(),
                                      &catalog_info, 0)) {
    return;
  }

  // Attempt to get the "Subject" field from the signature of the catalog file
  // itself.
  base::FilePath catalog_path(catalog_info.wszCatalogFile);
  std::u16string subject = GetSubjectNameInFile(catalog_path);

  if (subject.empty())
    return;

  certificate_info->type = CertificateInfo::Type::CERTIFICATE_IN_CATALOG;
  certificate_info->path = catalog_path;
  certificate_info->subject = subject;
}

}  // namespace

std::wstring GuidToClsid(std::wstring_view guid) {
  return base::StrCat({L"CLSID\\", guid, L"\\InProcServer32"});
}

// ModuleDatabase::CertificateInfo ---------------------------------------------

CertificateInfo::CertificateInfo() : type(Type::NO_CERTIFICATE) {}

// Extracts information about the certificate of the given file, if any is
// found.
void GetCertificateInfo(const base::FilePath& filename,
                        CertificateInfo* certificate_info) {
  DCHECK_EQ(CertificateInfo::Type::NO_CERTIFICATE, certificate_info->type);
  DCHECK(certificate_info->path.empty());
  DCHECK(certificate_info->subject.empty());

  GetCatalogCertificateInfo(filename, certificate_info);
  if (certificate_info->type == CertificateInfo::Type::CERTIFICATE_IN_CATALOG)
    return;

  std::u16string subject = GetSubjectNameInFile(filename);
  if (subject.empty())
    return;

  certificate_info->type = CertificateInfo::Type::CERTIFICATE_IN_FILE;
  certificate_info->path = filename;
  certificate_info->subject = subject;
}

bool IsMicrosoftModule(std::u16string_view subject) {
  static constexpr char16_t kMicrosoft[] = u"Microsoft ";
  return base::StartsWith(subject, kMicrosoft);
}

StringMapping GetEnvironmentVariablesMapping(
    const std::vector<std::wstring>& environment_variables) {
  std::unique_ptr<base::Environment> environment(base::Environment::Create());

  StringMapping string_mapping;
  for (const std::wstring& variable : environment_variables) {
    std::string value;
    if (environment->GetVar(base::WideToASCII(variable).c_str(), &value)) {
      value = std::string(base::TrimString(value, "\\", base::TRIM_TRAILING));
      string_mapping.push_back(std::make_pair(
          base::i18n::ToLower(base::UTF8ToUTF16(value)),
          u"%" + base::i18n::ToLower(base::AsString16(variable)) + u"%"));
    }
  }

  return string_mapping;
}

void CollapseMatchingPrefixInPath(const StringMapping& prefix_mapping,
                                  std::u16string* path) {
  const std::u16string path_copy = *path;
  DCHECK_EQ(base::i18n::ToLower(path_copy), path_copy);

  size_t min_length = std::numeric_limits<size_t>::max();
  for (const auto& mapping : prefix_mapping) {
    DCHECK_EQ(base::i18n::ToLower(mapping.first), mapping.first);
    if (base::StartsWith(path_copy, mapping.first)) {
      // Make sure the matching prefix is a full path component.
      if (path_copy[mapping.first.length()] != '\\' &&
          path_copy[mapping.first.length()] != '\0') {
        continue;
      }

      std::u16string collapsed_path = path_copy;
      base::ReplaceFirstSubstringAfterOffset(&collapsed_path, 0, mapping.first,
                                             mapping.second);
      size_t length = collapsed_path.length() - mapping.second.length();
      if (length < min_length) {
        *path = collapsed_path;
        min_length = length;
      }
    }
  }
}

bool GetModuleImageSizeAndTimeDateStamp(const base::FilePath& path,
                                        uint32_t* size_of_image,
                                        uint32_t* time_date_stamp) {
  base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
  if (!file.IsValid()) {
    return false;
  }

  // The values fetched here from the NT header live in the first 4k bytes of
  // the file in a well-formed dll.
  constexpr size_t kPageSize = 4096;

  auto buffer = base::HeapArray<uint8_t>::Uninit(kPageSize);
  std::optional<size_t> bytes_read = file.Read(0, buffer);
  if (!bytes_read.has_value()) {
    return false;
  }

  base::win::PeImageReader pe_image_reader;
  if (!pe_image_reader.Initialize(buffer.first(bytes_read.value()))) {
    return false;
  }

  *size_of_image = pe_image_reader.GetSizeOfImage();
  *time_date_stamp = pe_image_reader.GetCoffFileHeader()->TimeDateStamp;

  return true;
}

namespace internal {

void NormalizeCertificateSubject(std::wstring* subject) {
  size_t first_null = subject->find(L'\0');
  if (first_null != std::wstring::npos)
    subject->resize(first_null);
}

}  // namespace internal