| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/conflicts/module_blacklist_cache_updater_win.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/hash/sha1.h" |
| #include "base/i18n/case_conversion.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/path_service.h" |
| #include "base/sequenced_task_runner.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/post_task.h" |
| #include "base/task_runner_util.h" |
| #include "base/time/time.h" |
| #include "base/win/registry.h" |
| #include "base/win/windows_version.h" |
| #include "chrome/browser/conflicts/module_blacklist_cache_util_win.h" |
| #include "chrome/browser/conflicts/module_database_win.h" |
| #include "chrome/browser/conflicts/module_info_util_win.h" |
| #include "chrome/browser/conflicts/module_list_filter_win.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/install_static/install_util.h" |
| |
| #if !defined(OFFICIAL_BUILD) |
| #include "base/base_paths.h" |
| #endif |
| |
| namespace { |
| |
| using ModuleBlockingDecision = |
| ModuleBlacklistCacheUpdater::ModuleBlockingDecision; |
| |
| // The maximum number of modules allowed in the cache. This keeps the cache |
| // from growing indefinitely. |
| // Note: This value is tied to the "ModuleBlacklistCache.ModuleCount" histogram. |
| // Rename the histogram if this value is ever changed. |
| static constexpr size_t kMaxModuleCount = 5000u; |
| |
| // The maximum amount of time a stale entry is kept in the cache before it is |
| // deleted. |
| static constexpr base::TimeDelta kMaxEntryAge = base::TimeDelta::FromDays(180); |
| |
| // This enum is used for UMA. Therefore, the values should never change. |
| enum class BlacklistStatus { |
| // A module was marked as blacklisted during the current browser execution. |
| kNewlyBlacklisted = 0, |
| // A module was blocked when it tried to load into the process. |
| kBlocked = 1, |
| kMaxValue = kBlocked, |
| }; |
| |
| // Updates the module blacklist cache asynchronously on a background sequence |
| // and return a CacheUpdateResult value. |
| ModuleBlacklistCacheUpdater::CacheUpdateResult UpdateModuleBlacklistCache( |
| const base::FilePath& module_blacklist_cache_path, |
| scoped_refptr<ModuleListFilter> module_list_filter, |
| const std::vector<third_party_dlls::PackedListModule>& |
| newly_blacklisted_modules, |
| const std::vector<third_party_dlls::PackedListModule>& blocked_modules, |
| size_t max_module_count, |
| uint32_t min_time_date_stamp) { |
| DCHECK(module_list_filter); |
| |
| // Emit some UMA metrics about the update. |
| for (size_t i = 0; i < newly_blacklisted_modules.size(); ++i) { |
| UMA_HISTOGRAM_ENUMERATION("ModuleBlacklistCache.BlacklistStatus", |
| BlacklistStatus::kNewlyBlacklisted); |
| } |
| for (size_t i = 0; i < blocked_modules.size(); ++i) { |
| UMA_HISTOGRAM_ENUMERATION("ModuleBlacklistCache.BlacklistStatus", |
| BlacklistStatus::kBlocked); |
| } |
| |
| ModuleBlacklistCacheUpdater::CacheUpdateResult result; |
| |
| // Read the existing cache. |
| third_party_dlls::PackedListMetadata metadata; |
| std::vector<third_party_dlls::PackedListModule> blacklisted_modules; |
| ReadResult read_result = |
| ReadModuleBlacklistCache(module_blacklist_cache_path, &metadata, |
| &blacklisted_modules, &result.old_md5_digest); |
| UMA_HISTOGRAM_ENUMERATION("ModuleBlacklistCache.ReadResult", read_result); |
| |
| // Update the existing data with |newly_blacklisted_modules| and |
| // |blocked_modules|. |
| UpdateModuleBlacklistCacheData( |
| *module_list_filter, newly_blacklisted_modules, blocked_modules, |
| max_module_count, min_time_date_stamp, &metadata, &blacklisted_modules); |
| // Note: This histogram is tied to the current value of kMaxModuleCount. |
| // Rename the histogram if that value is ever changed. |
| UMA_HISTOGRAM_CUSTOM_COUNTS("ModuleBlacklistCache.ModuleCount", |
| blacklisted_modules.size(), 1, kMaxModuleCount, |
| 50); |
| |
| // Then write the updated cache to disk. |
| bool write_result = |
| WriteModuleBlacklistCache(module_blacklist_cache_path, metadata, |
| blacklisted_modules, &result.new_md5_digest); |
| UMA_HISTOGRAM_BOOLEAN("ModuleBlacklistCache.WriteResult", write_result); |
| |
| if (write_result) { |
| // Write the path of the cache into the registry so that chrome_elf can find |
| // it on its own. |
| base::string16 cache_path_registry_key = |
| install_static::GetRegistryPath().append( |
| third_party_dlls::kThirdPartyRegKeyName); |
| base::win::RegKey registry_key( |
| HKEY_CURRENT_USER, cache_path_registry_key.c_str(), KEY_SET_VALUE); |
| |
| bool cache_path_updated = SUCCEEDED( |
| registry_key.WriteValue(third_party_dlls::kBlFilePathRegValue, |
| module_blacklist_cache_path.value().c_str())); |
| UMA_HISTOGRAM_BOOLEAN("ModuleBlacklistCache.BlacklistPathUpdated", |
| cache_path_updated); |
| } |
| |
| return result; |
| } |
| |
| // Populates a third_party_dlls::PackedListModule entry from a ModuleInfoKey. |
| void PopulatePackedListModule( |
| const ModuleInfoKey& module_key, |
| third_party_dlls::PackedListModule* packed_list_module) { |
| // Hash the basename. |
| const std::string module_basename = base::UTF16ToUTF8( |
| base::i18n::ToLower(module_key.module_path.BaseName().value())); |
| base::SHA1HashBytes(reinterpret_cast<const uint8_t*>(module_basename.data()), |
| module_basename.length(), |
| &packed_list_module->basename_hash[0]); |
| |
| // Hash the code id. |
| const std::string module_code_id = GenerateCodeId(module_key); |
| base::SHA1HashBytes(reinterpret_cast<const uint8_t*>(module_code_id.data()), |
| module_code_id.length(), |
| &packed_list_module->code_id_hash[0]); |
| |
| packed_list_module->time_date_stamp = |
| CalculateTimeDateStamp(base::Time::Now()); |
| } |
| |
| // Returns true if a ModuleBlockingDecision means that the module should be |
| // added to the blacklist cache. |
| bool ShouldInsertInBlacklistCache(ModuleBlockingDecision blocking_decision) { |
| switch (blocking_decision) { |
| case ModuleBlockingDecision::kUnknown: |
| break; |
| |
| // All of these are reasons that allow the module to be loaded. |
| case ModuleBlockingDecision::kNotLoaded: |
| case ModuleBlockingDecision::kAllowedInProcessType: |
| case ModuleBlockingDecision::kAllowedIME: |
| case ModuleBlockingDecision::kAllowedSameCertificate: |
| case ModuleBlockingDecision::kAllowedSameDirectory: |
| case ModuleBlockingDecision::kAllowedMicrosoft: |
| case ModuleBlockingDecision::kAllowedWhitelisted: |
| case ModuleBlockingDecision::kTolerated: |
| case ModuleBlockingDecision::kNotAnalyzed: |
| return false; |
| |
| // The following are reasons for the module to be blocked. |
| case ModuleBlockingDecision::kDisallowedExplicit: |
| case ModuleBlockingDecision::kDisallowedImplicit: |
| return true; |
| } |
| |
| NOTREACHED() << static_cast<int>(blocking_decision); |
| return false; |
| } |
| |
| } // namespace |
| |
| ModuleBlacklistCacheUpdater::ModuleBlacklistCacheUpdater( |
| ModuleDatabaseEventSource* module_database_event_source, |
| const CertificateInfo& exe_certificate_info, |
| scoped_refptr<ModuleListFilter> module_list_filter, |
| const std::vector<third_party_dlls::PackedListModule>& |
| initial_blacklisted_modules, |
| OnCacheUpdatedCallback on_cache_updated_callback, |
| bool module_analysis_disabled) |
| : module_database_event_source_(module_database_event_source), |
| exe_certificate_info_(exe_certificate_info), |
| module_list_filter_(std::move(module_list_filter)), |
| initial_blacklisted_modules_(initial_blacklisted_modules), |
| on_cache_updated_callback_(std::move(on_cache_updated_callback)), |
| background_sequence_(base::CreateSequencedTaskRunnerWithTraits( |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN})), |
| module_analysis_disabled_(module_analysis_disabled), |
| weak_ptr_factory_(this) { |
| DCHECK(module_list_filter_); |
| module_database_event_source_->AddObserver(this); |
| } |
| |
| ModuleBlacklistCacheUpdater::~ModuleBlacklistCacheUpdater() { |
| module_database_event_source_->RemoveObserver(this); |
| } |
| |
| // static |
| bool ModuleBlacklistCacheUpdater::IsBlockingEnabled() { |
| return base::win::GetVersion() >= base::win::VERSION_WIN8 && |
| base::FeatureList::IsEnabled(features::kThirdPartyModulesBlocking); |
| } |
| |
| // static |
| base::FilePath ModuleBlacklistCacheUpdater::GetModuleBlacklistCachePath() { |
| base::FilePath user_data_dir; |
| if (!base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir)) |
| return base::FilePath(); |
| |
| return user_data_dir.Append(kModuleListComponentRelativePath) |
| .Append(L"bldata"); |
| } |
| |
| // static |
| void ModuleBlacklistCacheUpdater::DeleteModuleBlacklistCache() { |
| bool delete_result = |
| base::DeleteFile(GetModuleBlacklistCachePath(), false /* recursive */); |
| UMA_HISTOGRAM_BOOLEAN("ModuleBlacklistCache.DeleteResult", delete_result); |
| } |
| |
| void ModuleBlacklistCacheUpdater::OnNewModuleFound( |
| const ModuleInfoKey& module_key, |
| const ModuleInfoData& module_data) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Create a "packed list module" entry for this module. |
| third_party_dlls::PackedListModule packed_list_module; |
| PopulatePackedListModule(module_key, &packed_list_module); |
| |
| // This is meant to create the element in the map if it doesn't exist yet. |
| ModuleBlockingState& blocking_state = module_blocking_states_[module_key]; |
| |
| // Determine if the module was in the initial blacklist cache. |
| blocking_state.was_in_blacklist_cache = |
| std::binary_search(std::begin(initial_blacklisted_modules_), |
| std::end(initial_blacklisted_modules_), |
| packed_list_module, internal::ModuleLess()); |
| |
| // Make note of the fact that the module was blocked. It could be that the |
| // module subsequently ends up being loaded, but an earlier load attempt was |
| // blocked (ie, the injector actively worked around the blocking logic). This |
| // is a one way switch so that it doesn't get reset if the module is analyzed |
| // a second time. |
| if (module_data.module_properties & ModuleInfoData::kPropertyBlocked) |
| blocking_state.was_blocked = true; |
| |
| // Make note of the fact that the module was loaded. This is a one-way toggle |
| // for the same reasons as above. |
| if (module_data.module_properties & ModuleInfoData::kPropertyLoadedModule) |
| blocking_state.was_loaded = true; |
| |
| // Determine the current blocking decision. This can change at runtime as the |
| // module list component changes so re-run this analysis every time through. |
| blocking_state.blocking_decision = |
| DetermineModuleBlockingDecision(module_key, module_data); |
| |
| if (blocking_state.was_blocked) |
| blocked_modules_.push_back(packed_list_module); |
| |
| if (ShouldInsertInBlacklistCache(blocking_state.blocking_decision)) { |
| newly_blacklisted_modules_.push_back(packed_list_module); |
| |
| // Signal the module database that this module will be added to the cache. |
| // Note that observers that care about this information should register to |
| // the Module Database's observer interface after the ModuleBlacklistCache |
| // instance. |
| // The Module Database can be null during tests. |
| auto* module_database = ModuleDatabase::GetInstance(); |
| if (module_database) { |
| module_database->OnModuleAddedToBlacklist( |
| module_key.module_path, module_key.module_size, |
| module_key.module_time_date_stamp); |
| } |
| } |
| } |
| |
| void ModuleBlacklistCacheUpdater::OnKnownModuleLoaded( |
| const ModuleInfoKey& module_key, |
| const ModuleInfoData& module_data) { |
| // Analyze the module again. |
| OnNewModuleFound(module_key, module_data); |
| } |
| |
| void ModuleBlacklistCacheUpdater::OnModuleDatabaseIdle() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| StartModuleBlacklistCacheUpdate(); |
| } |
| |
| const ModuleBlacklistCacheUpdater::ModuleBlockingState& |
| ModuleBlacklistCacheUpdater::GetModuleBlockingState( |
| const ModuleInfoKey& module_key) const { |
| auto it = module_blocking_states_.find(module_key); |
| DCHECK(it != module_blocking_states_.end()); |
| return it->second; |
| } |
| |
| void ModuleBlacklistCacheUpdater::DisableModuleAnalysis() { |
| module_analysis_disabled_ = true; |
| } |
| |
| ModuleBlacklistCacheUpdater::ModuleListState |
| ModuleBlacklistCacheUpdater::DetermineModuleListState( |
| const ModuleInfoKey& module_key, |
| const ModuleInfoData& module_data) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (module_list_filter_->IsWhitelisted(module_key, module_data)) |
| return ModuleListState::kWhitelisted; |
| std::unique_ptr<chrome::conflicts::BlacklistAction> blacklist_action = |
| module_list_filter_->IsBlacklisted(module_key, module_data); |
| if (!blacklist_action) |
| return ModuleListState::kUnlisted; |
| return blacklist_action->allow_load() ? ModuleListState::kTolerated |
| : ModuleListState::kBlacklisted; |
| } |
| |
| ModuleBlacklistCacheUpdater::ModuleBlockingDecision |
| ModuleBlacklistCacheUpdater::DetermineModuleBlockingDecision( |
| const ModuleInfoKey& module_key, |
| const ModuleInfoData& module_data) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Don't analyze unloaded modules. |
| if ((module_data.module_properties & ModuleInfoData::kPropertyLoadedModule) == |
| 0) { |
| return ModuleBlockingDecision::kNotLoaded; |
| } |
| |
| // Don't add modules to the blacklist if they were never loaded in a process |
| // where blocking is enabled. |
| if (!IsBlockingEnabledInProcessTypes(module_data.process_types)) |
| return ModuleBlockingDecision::kAllowedInProcessType; |
| |
| // New modules should not be added to the cache when the module analysis is |
| // disabled. |
| if (module_analysis_disabled_) |
| return ModuleBlockingDecision::kNotAnalyzed; |
| |
| // First check if this module is a part of Chrome's installation. This can |
| // override explicit directions in the module list. This prevents us from |
| // shooting ourselves in the foot by accidentally issuing a blacklisting |
| // rule that blocks one of our own modules. |
| |
| // Explicitly whitelist modules whose signing cert's Subject field matches the |
| // one in the current executable. No attempt is made to check the validity of |
| // module signatures or of signing certs. |
| if (exe_certificate_info_.type != CertificateInfo::Type::NO_CERTIFICATE && |
| exe_certificate_info_.subject == |
| module_data.inspection_result->certificate_info.subject) { |
| return ModuleBlockingDecision::kAllowedSameCertificate; |
| } |
| |
| #if !defined(OFFICIAL_BUILD) |
| // For developer builds only, whitelist modules in the same directory as the |
| // executable. |
| base::FilePath exe_path; |
| if (base::PathService::Get(base::DIR_EXE, &exe_path) && |
| exe_path.DirName().IsParent(module_key.module_path)) { |
| return ModuleBlockingDecision::kAllowedSameDirectory; |
| } |
| #endif |
| |
| // Get the state of the module with respect to the module list component. If |
| // there are explicit directions in the list then respect those. |
| switch (DetermineModuleListState(module_key, module_data)) { |
| case ModuleListState::kUnlisted: |
| break; |
| case ModuleListState::kWhitelisted: |
| return ModuleBlockingDecision::kAllowedWhitelisted; |
| case ModuleListState::kTolerated: |
| return ModuleBlockingDecision::kTolerated; |
| case ModuleListState::kBlacklisted: |
| return ModuleBlockingDecision::kDisallowedExplicit; |
| } |
| |
| // If the module isn't explicitly listed in the module list then it is either |
| // implicitly whitelisted or implicitly blacklisted by other policy. |
| |
| // Check if the module is seemingly signed by Microsoft. Again, no attempt is |
| // made to check the validity of the certificate. |
| if (IsMicrosoftModule( |
| module_data.inspection_result->certificate_info.subject)) { |
| return ModuleBlockingDecision::kAllowedMicrosoft; |
| } |
| |
| // It is preferable to mark a whitelisted IME as allowed because it is |
| // whitelisted, not because it's a shell extension. Thus, check for the module |
| // type after. Note that shell extensions are blocked. |
| if (module_data.module_properties & ModuleInfoData::kPropertyIme) |
| return ModuleBlockingDecision::kAllowedIME; |
| |
| // Getting here means that the module is implicitly blacklisted. |
| return ModuleBlockingDecision::kDisallowedImplicit; |
| } |
| |
| void ModuleBlacklistCacheUpdater::StartModuleBlacklistCacheUpdate() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| base::FilePath cache_file_path = GetModuleBlacklistCachePath(); |
| if (cache_file_path.empty()) |
| return; |
| |
| // Calculate the minimum time date stamp. |
| uint32_t min_time_date_stamp = |
| CalculateTimeDateStamp(base::Time::Now() - kMaxEntryAge); |
| |
| // Update the module blacklist cache on a background sequence. |
| base::PostTaskAndReplyWithResult( |
| background_sequence_.get(), FROM_HERE, |
| base::BindOnce(&UpdateModuleBlacklistCache, cache_file_path, |
| module_list_filter_, std::move(newly_blacklisted_modules_), |
| std::move(blocked_modules_), kMaxModuleCount, |
| min_time_date_stamp), |
| base::BindOnce( |
| &ModuleBlacklistCacheUpdater::OnModuleBlacklistCacheUpdated, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ModuleBlacklistCacheUpdater::OnModuleBlacklistCacheUpdated( |
| const CacheUpdateResult& result) { |
| on_cache_updated_callback_.Run(result); |
| } |