| // Copyright 2016 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/metrics/antivirus_metrics_provider_win.h" |
| |
| #include <windows.h> |
| #include <iwscapi.h> |
| #include <objbase.h> |
| #include <stddef.h> |
| #include <wbemidl.h> |
| #include <wscapi.h> |
| #include <wrl/client.h> |
| |
| #include <algorithm> |
| #include <string> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/feature_list.h" |
| #include "base/file_version_info_win.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/path_service.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/task/post_task.h" |
| #include "base/task_runner_util.h" |
| #include "base/threading/scoped_blocking_call.h" |
| #include "base/version.h" |
| #include "base/win/com_init_util.h" |
| #include "base/win/scoped_bstr.h" |
| #include "base/win/scoped_com_initializer.h" |
| #include "base/win/scoped_variant.h" |
| #include "base/win/windows_version.h" |
| #include "chrome/common/channel_info.h" |
| #include "components/variations/hashing.h" |
| #include "components/version_info/version_info.h" |
| #include "third_party/metrics_proto/system_profile.pb.h" |
| |
| namespace { |
| |
| // This is an undocumented structure returned from querying the "productState" |
| // uint32 from the AntiVirusProduct in WMI. |
| // http://neophob.com/2010/03/wmi-query-windows-securitycenter2/ gives a good |
| // summary and testing was also done with a variety of AV products to determine |
| // these values as accurately as possible. |
| #pragma pack(push) |
| #pragma pack(1) |
| struct PRODUCT_STATE { |
| uint8_t unknown_1 : 4; |
| uint8_t definition_state : 4; // 1 = Out of date, 0 = Up to date. |
| uint8_t unknown_2 : 4; |
| uint8_t security_state : 4; // 0 = Inactive, 1 = Active, 2 = Snoozed. |
| uint8_t security_provider; // matches WSC_SECURITY_PROVIDER in wscapi.h. |
| uint8_t unknown_3; |
| }; |
| #pragma pack(pop) |
| |
| static_assert(sizeof(PRODUCT_STATE) == 4, "Wrong packing!"); |
| |
| // Filter any part of a product string that looks like it might be a version |
| // number. Returns true if the part should be removed from the product name. |
| bool ShouldFilterPart(const std::string& str) { |
| // Special case for "360" (used by Norton), "365" (used by Kaspersky) and |
| // "NOD32" (used by ESET). |
| if (str == "365" || str == "360" || str == "NOD32") |
| return false; |
| for (const auto ch : str) { |
| if (isdigit(ch)) |
| return true; |
| } |
| return false; |
| } |
| |
| bool ShouldReportFullNames() { |
| // The expectation is that this will be disabled for the majority of users, |
| // but this allows a small group to be enabled on other channels if there are |
| // a large percentage of hashes collected on these channels that are not |
| // resolved to names previously collected on Canary channel. |
| bool enabled = base::FeatureList::IsEnabled( |
| AntiVirusMetricsProvider::kReportNamesFeature); |
| |
| if (chrome::GetChannel() == version_info::Channel::CANARY) |
| return true; |
| |
| return enabled; |
| } |
| |
| // Helper function for expanding all environment variables in |path|. |
| std::wstring ExpandEnvironmentVariables(const std::wstring& path) { |
| static const DWORD kMaxBuffer = 32 * 1024; // Max according to MSDN. |
| std::wstring path_expanded; |
| DWORD path_len = MAX_PATH; |
| do { |
| DWORD result = ExpandEnvironmentStrings( |
| path.c_str(), base::WriteInto(&path_expanded, path_len), path_len); |
| if (!result) { |
| // Failed to expand variables. Return the original string. |
| DPLOG(ERROR) << path; |
| break; |
| } |
| if (result <= path_len) |
| return path_expanded.substr(0, result - 1); |
| path_len = result; |
| } while (path_len < kMaxBuffer); |
| |
| return path; |
| } |
| |
| // Helper function to take a |path| to a file, that might contain environment |
| // strings, and read the file version information in |product_version|. Returns |
| // true if it was possible to extract the file information correctly. |
| bool GetProductVersion(std::wstring* path, std::string* product_version) { |
| base::FilePath full_path(ExpandEnvironmentVariables(*path)); |
| |
| #if !defined(_WIN64) |
| if (!base::PathExists(full_path)) { |
| // On 32-bit builds, path might contain C:\Program Files (x86) instead of |
| // C:\Program Files. |
| base::ReplaceFirstSubstringAfterOffset(path, 0, L"%ProgramFiles%", |
| L"%ProgramW6432%"); |
| full_path = base::FilePath(ExpandEnvironmentVariables(*path)); |
| } |
| #endif // !defined(_WIN64) |
| std::unique_ptr<FileVersionInfo> version_info( |
| FileVersionInfo::CreateFileVersionInfo(full_path)); |
| |
| // It is not an error if the product version cannot be read, so continue in |
| // this case. |
| if (version_info.get()) { |
| FileVersionInfoWin* version_info_win = |
| static_cast<FileVersionInfoWin*>(version_info.get()); |
| std::string version_str = |
| base::SysWideToUTF8(version_info_win->product_version()); |
| |
| *product_version = std::move(version_str); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| } // namespace |
| |
| constexpr base::Feature AntiVirusMetricsProvider::kReportNamesFeature; |
| |
| AntiVirusMetricsProvider::AntiVirusMetricsProvider() |
| : weak_ptr_factory_(this) {} |
| |
| AntiVirusMetricsProvider::~AntiVirusMetricsProvider() = default; |
| |
| void AntiVirusMetricsProvider::ProvideSystemProfileMetrics( |
| metrics::SystemProfileProto* system_profile_proto) { |
| for (const auto& av_product : av_products_) { |
| metrics::SystemProfileProto_AntiVirusProduct* product = |
| system_profile_proto->add_antivirus_product(); |
| *product = av_product; |
| } |
| } |
| |
| void AntiVirusMetricsProvider::AsyncInit(const base::Closure& done_callback) { |
| // __uuidof(WSCProductList) expects to be run in an STA and CLSID_WbemLocator |
| // is fine with an STA or MTA. The COM STA task runner accomodates both of |
| // these requirements. |
| base::PostTaskAndReplyWithResult( |
| base::CreateCOMSTATaskRunnerWithTraits( |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}) |
| .get(), |
| FROM_HERE, |
| base::BindOnce( |
| &AntiVirusMetricsProvider::GetAntiVirusProductsOnCOMSTAThread), |
| base::BindOnce(&AntiVirusMetricsProvider::GotAntiVirusProducts, |
| weak_ptr_factory_.GetWeakPtr(), done_callback)); |
| } |
| |
| // static |
| std::vector<AntiVirusMetricsProvider::AvProduct> |
| AntiVirusMetricsProvider::GetAntiVirusProductsOnCOMSTAThread() { |
| base::win::AssertComApartmentType(base::win::ComApartmentType::STA); |
| |
| std::vector<AvProduct> av_products; |
| |
| ResultCode result = RESULT_GENERIC_FAILURE; |
| |
| base::win::OSInfo* os_info = base::win::OSInfo::GetInstance(); |
| |
| // Windows Security Center APIs are not available on Server products. |
| // See https://msdn.microsoft.com/en-us/library/bb432506.aspx. |
| if (os_info->version_type() == base::win::SUITE_SERVER) { |
| result = RESULT_WSC_NOT_AVAILABLE; |
| } else { |
| // The WSC interface is preferred here as it's fully documented, but only |
| // available on Windows 8 and above, so instead use the undocumented WMI |
| // interface on Windows 7 and below. |
| if (os_info->version() >= base::win::VERSION_WIN8) |
| result = FillAntiVirusProductsFromWSC(&av_products); |
| else |
| result = FillAntiVirusProductsFromWMI(&av_products); |
| } |
| |
| MaybeAddUnregisteredAntiVirusProducts(&av_products); |
| |
| UMA_HISTOGRAM_ENUMERATION("UMA.AntiVirusMetricsProvider.Result", |
| result, |
| RESULT_COUNT); |
| |
| return av_products; |
| } |
| |
| std::string AntiVirusMetricsProvider::TrimVersionOfAvProductName( |
| const std::string& av_product) { |
| auto av_product_parts = base::SplitString( |
| av_product, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| |
| if (av_product_parts.size() >= 2) { |
| // Skipping first element, remove any that look like version numbers. |
| av_product_parts.erase( |
| std::remove_if(av_product_parts.begin() + 1, av_product_parts.end(), |
| ShouldFilterPart), |
| av_product_parts.end()); |
| } |
| |
| return base::JoinString(av_product_parts, " "); |
| } |
| |
| void AntiVirusMetricsProvider::GotAntiVirusProducts( |
| const base::Closure& done_callback, |
| const std::vector<AvProduct>& av_products) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| av_products_ = av_products; |
| done_callback.Run(); |
| } |
| |
| // static |
| AntiVirusMetricsProvider::ResultCode |
| AntiVirusMetricsProvider::FillAntiVirusProductsFromWSC( |
| std::vector<AvProduct>* products) { |
| std::vector<AvProduct> result_list; |
| base::ScopedBlockingCall scoped_blocking_call(base::BlockingType::MAY_BLOCK); |
| |
| Microsoft::WRL::ComPtr<IWSCProductList> product_list; |
| HRESULT result = |
| CoCreateInstance(__uuidof(WSCProductList), nullptr, CLSCTX_INPROC_SERVER, |
| IID_PPV_ARGS(&product_list)); |
| if (FAILED(result)) |
| return RESULT_FAILED_TO_CREATE_INSTANCE; |
| |
| result = product_list->Initialize(WSC_SECURITY_PROVIDER_ANTIVIRUS); |
| if (FAILED(result)) |
| return RESULT_FAILED_TO_INITIALIZE_PRODUCT_LIST; |
| |
| LONG product_count; |
| result = product_list->get_Count(&product_count); |
| if (FAILED(result)) |
| return RESULT_FAILED_TO_GET_PRODUCT_COUNT; |
| |
| for (LONG i = 0; i < product_count; i++) { |
| IWscProduct* product = nullptr; |
| result = product_list->get_Item(i, &product); |
| if (FAILED(result)) |
| return RESULT_FAILED_TO_GET_ITEM; |
| |
| static_assert(metrics::SystemProfileProto::AntiVirusState:: |
| SystemProfileProto_AntiVirusState_STATE_ON == |
| static_cast<metrics::SystemProfileProto::AntiVirusState>( |
| WSC_SECURITY_PRODUCT_STATE_ON), |
| "proto and API values must be the same"); |
| static_assert(metrics::SystemProfileProto::AntiVirusState:: |
| SystemProfileProto_AntiVirusState_STATE_OFF == |
| static_cast<metrics::SystemProfileProto::AntiVirusState>( |
| WSC_SECURITY_PRODUCT_STATE_OFF), |
| "proto and API values must be the same"); |
| static_assert(metrics::SystemProfileProto::AntiVirusState:: |
| SystemProfileProto_AntiVirusState_STATE_SNOOZED == |
| static_cast<metrics::SystemProfileProto::AntiVirusState>( |
| WSC_SECURITY_PRODUCT_STATE_SNOOZED), |
| "proto and API values must be the same"); |
| static_assert(metrics::SystemProfileProto::AntiVirusState:: |
| SystemProfileProto_AntiVirusState_STATE_EXPIRED == |
| static_cast<metrics::SystemProfileProto::AntiVirusState>( |
| WSC_SECURITY_PRODUCT_STATE_EXPIRED), |
| "proto and API values must be the same"); |
| |
| AvProduct av_product; |
| WSC_SECURITY_PRODUCT_STATE product_state; |
| result = product->get_ProductState(&product_state); |
| if (FAILED(result)) |
| return RESULT_FAILED_TO_GET_PRODUCT_STATE; |
| |
| if (!metrics::SystemProfileProto_AntiVirusState_IsValid(product_state)) |
| return RESULT_PRODUCT_STATE_INVALID; |
| |
| av_product.set_product_state( |
| static_cast<metrics::SystemProfileProto::AntiVirusState>( |
| product_state)); |
| |
| base::win::ScopedBstr product_name; |
| result = product->get_ProductName(product_name.Receive()); |
| if (FAILED(result)) |
| return RESULT_FAILED_TO_GET_PRODUCT_NAME; |
| std::string name = TrimVersionOfAvProductName( |
| base::SysWideToUTF8(std::wstring(product_name, product_name.Length()))); |
| product_name.Release(); |
| if (ShouldReportFullNames()) |
| av_product.set_product_name(name); |
| av_product.set_product_name_hash(variations::HashName(name)); |
| |
| base::win::ScopedBstr remediation_path; |
| result = product->get_RemediationPath(remediation_path.Receive()); |
| if (FAILED(result)) |
| return RESULT_FAILED_TO_GET_REMEDIATION_PATH; |
| std::wstring path_str(remediation_path, remediation_path.Length()); |
| remediation_path.Release(); |
| |
| std::string product_version; |
| // Not a failure if the product version cannot be read from the file on |
| // disk. |
| if (GetProductVersion(&path_str, &product_version)) { |
| if (ShouldReportFullNames()) |
| av_product.set_product_version(product_version); |
| av_product.set_product_version_hash( |
| variations::HashName(product_version)); |
| } |
| |
| result_list.push_back(av_product); |
| } |
| |
| *products = std::move(result_list); |
| |
| return RESULT_SUCCESS; |
| } |
| |
| AntiVirusMetricsProvider::ResultCode |
| AntiVirusMetricsProvider::FillAntiVirusProductsFromWMI( |
| std::vector<AvProduct>* products) { |
| std::vector<AvProduct> result_list; |
| base::ScopedBlockingCall scoped_blocking_call(base::BlockingType::MAY_BLOCK); |
| |
| Microsoft::WRL::ComPtr<IWbemLocator> wmi_locator; |
| HRESULT hr = |
| ::CoCreateInstance(CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER, |
| IID_PPV_ARGS(&wmi_locator)); |
| if (FAILED(hr)) |
| return RESULT_FAILED_TO_CREATE_INSTANCE; |
| |
| Microsoft::WRL::ComPtr<IWbemServices> wmi_services; |
| hr = wmi_locator->ConnectServer( |
| base::win::ScopedBstr(L"ROOT\\SecurityCenter2"), nullptr, nullptr, |
| nullptr, 0, nullptr, nullptr, wmi_services.GetAddressOf()); |
| if (FAILED(hr)) |
| return RESULT_FAILED_TO_CONNECT_TO_WMI; |
| |
| hr = ::CoSetProxyBlanket(wmi_services.Get(), RPC_C_AUTHN_WINNT, |
| RPC_C_AUTHZ_NONE, nullptr, RPC_C_AUTHN_LEVEL_CALL, |
| RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE); |
| if (FAILED(hr)) |
| return RESULT_FAILED_TO_SET_SECURITY_BLANKET; |
| |
| // This interface is available on Windows Vista and above, and is officially |
| // undocumented. |
| base::win::ScopedBstr query_language(L"WQL"); |
| base::win::ScopedBstr query(L"SELECT * FROM AntiVirusProduct"); |
| Microsoft::WRL::ComPtr<IEnumWbemClassObject> enumerator; |
| |
| hr = wmi_services->ExecQuery( |
| query_language, query, |
| WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, nullptr, |
| enumerator.GetAddressOf()); |
| if (FAILED(hr)) |
| return RESULT_FAILED_TO_EXEC_WMI_QUERY; |
| |
| // Iterate over the results of the WMI query. Each result will be an |
| // AntiVirusProduct instance. |
| while (true) { |
| Microsoft::WRL::ComPtr<IWbemClassObject> class_object; |
| ULONG items_returned = 0; |
| hr = enumerator->Next(WBEM_INFINITE, 1, class_object.GetAddressOf(), |
| &items_returned); |
| if (FAILED(hr)) |
| return RESULT_FAILED_TO_ITERATE_RESULTS; |
| |
| if (hr == WBEM_S_FALSE || items_returned == 0) |
| break; |
| |
| AvProduct av_product; |
| av_product.set_product_state( |
| metrics::SystemProfileProto::AntiVirusState:: |
| SystemProfileProto_AntiVirusState_STATE_ON); |
| |
| // See definition of PRODUCT_STATE structure above for how this is being |
| // used. |
| base::win::ScopedVariant product_state; |
| hr = class_object->Get(L"productState", 0, product_state.Receive(), 0, 0); |
| |
| if (FAILED(hr) || product_state.type() != VT_I4) |
| return RESULT_FAILED_TO_GET_PRODUCT_STATE; |
| |
| LONG state_val = V_I4(product_state.ptr()); |
| PRODUCT_STATE product_state_struct; |
| std::copy(reinterpret_cast<const char*>(&state_val), |
| reinterpret_cast<const char*>(&state_val) + sizeof state_val, |
| reinterpret_cast<char*>(&product_state_struct)); |
| // Map the values from product_state_struct to the proto values. |
| switch (product_state_struct.security_state) { |
| case 0: |
| av_product.set_product_state( |
| metrics::SystemProfileProto::AntiVirusState:: |
| SystemProfileProto_AntiVirusState_STATE_OFF); |
| break; |
| case 1: |
| av_product.set_product_state( |
| metrics::SystemProfileProto::AntiVirusState:: |
| SystemProfileProto_AntiVirusState_STATE_ON); |
| break; |
| case 2: |
| av_product.set_product_state( |
| metrics::SystemProfileProto::AntiVirusState:: |
| SystemProfileProto_AntiVirusState_STATE_SNOOZED); |
| break; |
| default: |
| // unknown state. |
| return RESULT_PRODUCT_STATE_INVALID; |
| break; |
| } |
| |
| base::win::ScopedVariant display_name; |
| hr = class_object->Get(L"displayName", 0, display_name.Receive(), 0, 0); |
| |
| if (FAILED(hr) || display_name.type() != VT_BSTR) |
| return RESULT_FAILED_TO_GET_PRODUCT_NAME; |
| |
| // Owned by ScopedVariant. |
| BSTR temp_bstr = V_BSTR(display_name.ptr()); |
| std::string name = TrimVersionOfAvProductName(base::SysWideToUTF8( |
| std::wstring(temp_bstr, ::SysStringLen(temp_bstr)))); |
| |
| if (ShouldReportFullNames()) |
| av_product.set_product_name(name); |
| av_product.set_product_name_hash(variations::HashName(name)); |
| |
| base::win::ScopedVariant exe_path; |
| hr = class_object->Get(L"pathToSignedProductExe", 0, exe_path.Receive(), 0, |
| 0); |
| |
| if (FAILED(hr) || exe_path.type() != VT_BSTR) |
| return RESULT_FAILED_TO_GET_REMEDIATION_PATH; |
| |
| temp_bstr = V_BSTR(exe_path.ptr()); |
| std::wstring path_str(temp_bstr, ::SysStringLen(temp_bstr)); |
| |
| std::string product_version; |
| // Not a failure if the product version cannot be read from the file on |
| // disk. |
| if (GetProductVersion(&path_str, &product_version)) { |
| if (ShouldReportFullNames()) |
| av_product.set_product_version(product_version); |
| av_product.set_product_version_hash( |
| variations::HashName(product_version)); |
| } |
| |
| result_list.push_back(av_product); |
| } |
| |
| *products = std::move(result_list); |
| |
| return RESULT_SUCCESS; |
| } |
| |
| void AntiVirusMetricsProvider::MaybeAddUnregisteredAntiVirusProducts( |
| std::vector<AvProduct>* products) { |
| base::ScopedBlockingCall scoped_blocking_call(base::BlockingType::MAY_BLOCK); |
| |
| // Trusteer Rapport does not register with WMI or Security Center so do some |
| // "best efforts" detection here. |
| |
| // Rapport always installs into 32-bit Program Files in directory |
| // %DIR_PROGRAM_FILESX86%\Trusteer\Rapport |
| base::FilePath binary_path; |
| if (!base::PathService::Get(base::DIR_PROGRAM_FILESX86, &binary_path)) |
| return; |
| |
| binary_path = binary_path.AppendASCII("Trusteer") |
| .AppendASCII("Rapport") |
| .AppendASCII("bin") |
| .AppendASCII("RapportService.exe"); |
| |
| if (!base::PathExists(binary_path)) |
| return; |
| |
| std::wstring mutable_path_str(binary_path.value()); |
| std::string product_version; |
| |
| if (!GetProductVersion(&mutable_path_str, &product_version)) |
| return; |
| |
| AvProduct av_product; |
| |
| // Assume enabled, no easy way of knowing for sure. |
| av_product.set_product_state(metrics::SystemProfileProto::AntiVirusState:: |
| SystemProfileProto_AntiVirusState_STATE_ON); |
| |
| // Taken from Add/Remove programs as the product name. |
| std::string product_name("Trusteer Endpoint Protection"); |
| if (ShouldReportFullNames()) { |
| av_product.set_product_name(product_name); |
| av_product.set_product_version(product_version); |
| } |
| av_product.set_product_name_hash(variations::HashName(product_name)); |
| av_product.set_product_version_hash(variations::HashName(product_version)); |
| |
| products->push_back(av_product); |
| } |