blob: ecb1b96e9abf444b1acee837f05f22317e69fc9e [file]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "remoting/host/setup/daemon_controller_delegate_win.h"
#include <stddef.h>
#include <windows.h>
#include <aclapi.h>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/process/process_info.h"
#include "base/strings/string_util_win.h"
#include "base/unguessable_token.h"
#include "base/values.h"
#include "base/win/scoped_handle.h"
#include "remoting/base/branding.h"
#include "remoting/base/is_google_email.h"
#include "remoting/base/scoped_sc_handle_win.h"
#include "remoting/host/config_file_watcher.h"
#include "remoting/host/host_config.h"
#include "remoting/host/usage_stats_consent.h"
#include "remoting/host/win/security_descriptor.h"
namespace remoting {
namespace {
// The maximum size of the configuration file. "1MB ought to be enough" for any
// reasonable configuration we will ever need. 1MB is low enough to make the
// probability of out of memory situation fairly low. OOM is still possible and
// we will crash if it occurs.
const size_t kMaxConfigFileSize = 1024 * 1024;
// The host configuration file security descriptor that enables full access to
// Local System and built-in administrators only.
const char kConfigFileSecurityDescriptor[] =
"O:BAG:BAD:(A;;GA;;;SY)(A;;GA;;;BA)";
// The host configuration directory security descriptor that enables full access
// to Local System and built-in administrators, and read/execute access to
// built-in users (to allow traversal and reading the unprivileged config).
const char kConfigDirSecurityDescriptor[] =
"O:BAG:BAD:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGX;;;BU)";
const char kUnprivilegedConfigFileSecurityDescriptor[] =
"O:BAG:BAD:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GR;;;AU)";
// Helper to check if a handle refers to a reparse point (junction or symlink).
// This is used to prevent Local Privilege Escalation (LPE) via junction
// following.
bool IsHandleReparsePoint(HANDLE handle) {
BY_HANDLE_FILE_INFORMATION info;
if (!GetFileInformationByHandle(handle, &info)) {
PLOG(ERROR) << "GetFileInformationByHandle failed";
// Fail safe: return true if we can't verify the attributes.
return true;
}
return (info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
}
// Verifies that the final path of the handle matches the expected path.
// This prevents "intermediate junction" attacks where a component of the
// path above the leaf is a junction.
bool IsPathSafe(HANDLE handle, const base::FilePath& expected_path) {
std::vector<wchar_t> buffer(MAX_PATH);
DWORD result = GetFinalPathNameByHandleW(handle, buffer.data(),
static_cast<DWORD>(buffer.size()),
FILE_NAME_NORMALIZED);
if (result >= buffer.size()) {
buffer.resize(result);
result = GetFinalPathNameByHandleW(handle, buffer.data(),
static_cast<DWORD>(buffer.size()),
FILE_NAME_NORMALIZED);
}
if (result == 0 || result >= buffer.size()) {
PLOG(ERROR) << "GetFinalPathNameByHandleW failed";
return false;
}
std::wstring final_path(buffer.data(), result);
std::wstring expected_path_str = expected_path.value();
// GetFinalPathNameByHandle prepends \\?\ to the path.
if (base::StartsWith(final_path, L"\\\\?\\UNC\\", base::CompareCase::SENSITIVE)) {
final_path = L"\\\\" + final_path.substr(8);
} else if (base::StartsWith(final_path, L"\\\\?\\",
base::CompareCase::SENSITIVE)) {
final_path = final_path.substr(4);
}
// Use _wcsicmp for robust case-insensitive comparison on Windows.
if (_wcsicmp(final_path.c_str(), expected_path_str.c_str()) != 0) {
LOG(ERROR) << "Path mismatch detected. Expected: " << expected_path_str
<< ", Final: " << final_path;
return false;
}
return true;
}
// Reads and parses the configuration file securely without following junctions.
bool ReadConfig(const base::FilePath& filename, base::DictValue& config_out) {
// Use FILE_FLAG_OPEN_REPARSE_POINT to open the junction itself if it exists,
// rather than following it to a potentially sensitive target.
base::win::ScopedHandle file(CreateFileW(
filename.value().c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, nullptr));
if (!file.is_valid()) {
DWORD error = GetLastError();
if (error == ERROR_FILE_NOT_FOUND) {
LOG(INFO) << "'" << filename.value() << "' does not exist, skipping read.";
} else {
PLOG(ERROR) << "Failed to open '" << filename.value() << "'.";
}
return false;
}
if (IsHandleReparsePoint(file.Get())) {
LOG(ERROR) << "Config file '" << filename.value() << "' is a reparse point.";
return false;
}
if (!IsPathSafe(file.Get(), filename)) {
LOG(ERROR) << "Config file path is insecure: " << filename.value();
return false;
}
BY_HANDLE_FILE_INFORMATION info;
if (!GetFileInformationByHandle(file.Get(), &info)) {
PLOG(ERROR) << "GetFileInformationByHandle failed for '" << filename.value()
<< "'.";
return false;
}
uint64_t file_size =
(static_cast<uint64_t>(info.nFileSizeHigh) << 32) | info.nFileSizeLow;
if (file_size == 0) {
LOG(ERROR) << "Config file '" << filename.value() << "' is empty.";
return false;
}
if (file_size > kMaxConfigFileSize) {
LOG(ERROR) << "Config file '" << filename.value() << "' is too large.";
return false;
}
std::string file_content;
file_content.resize(static_cast<size_t>(file_size));
DWORD bytes_read;
if (!::ReadFile(file.Get(), file_content.data(),
static_cast<DWORD>(file_content.size()), &bytes_read,
nullptr)) {
PLOG(ERROR) << "Failed to read '" << filename.value() << "'.";
return false;
}
file_content.resize(bytes_read);
std::optional<base::DictValue> config = HostConfigFromJson(file_content);
if (!config.has_value()) {
LOG(ERROR) << "Config file: '" << filename.value()
<< "' is empty or corrupt.";
return false;
}
config_out = std::move(*config);
return true;
}
// Sets the Owner, Group, and DACL on a handle to prevent non-admins from
// tampering with it or reverting security settings.
bool SetDirAcl(HANDLE handle, const char* sddl) {
ScopedSd sd = ConvertSddlToSd(sddl);
if (!sd) {
PLOG(ERROR) << "Failed to convert SDDL to SD: " << sddl;
return false;
}
PACL dacl = nullptr;
BOOL dacl_present = FALSE;
BOOL dacl_defaulted = FALSE;
if (!GetSecurityDescriptorDacl(sd.get(), &dacl_present, &dacl,
&dacl_defaulted)) {
PLOG(ERROR) << "GetSecurityDescriptorDacl failed";
return false;
}
PSID owner = nullptr;
BOOL owner_defaulted = FALSE;
if (!GetSecurityDescriptorOwner(sd.get(), &owner, &owner_defaulted)) {
PLOG(ERROR) << "GetSecurityDescriptorOwner failed";
return false;
}
PSID group = nullptr;
BOOL group_defaulted = FALSE;
if (!GetSecurityDescriptorGroup(sd.get(), &group, &group_defaulted)) {
PLOG(ERROR) << "GetSecurityDescriptorGroup failed";
return false;
}
// Set Owner, Group, and DACL. Setting the Owner prevents an unprivileged
// creator of the directory from regaining control.
DWORD result = SetSecurityInfo(
handle, SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION |
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
owner, group, dacl, nullptr);
if (result != ERROR_SUCCESS) {
LOG(ERROR) << "SetSecurityInfo failed: " << result;
return false;
}
return true;
}
// Writes a config file atomically and safely.
bool WriteConfigSafe(const base::FilePath& target_path,
const std::string& content,
const char* sddl) {
base::FilePath config_dir = target_path.DirName();
// 1. Verify config_dir is not a reparse point.
// We omit FILE_SHARE_DELETE to prevent the directory from being renamed
// while we have it open, ensuring our later move operation is not redirected.
base::win::ScopedHandle dir_handle(CreateFileW(
config_dir.value().c_str(), FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
if (!dir_handle.is_valid()) {
PLOG(ERROR) << "Failed to open config directory: " << config_dir.value();
return false;
}
if (IsHandleReparsePoint(dir_handle.Get())) {
LOG(ERROR) << "Config directory is a reparse point: " << config_dir.value();
return false;
}
if (!IsPathSafe(dir_handle.Get(), config_dir)) {
LOG(ERROR) << "Config directory path is insecure: " << config_dir.value();
return false;
}
// 2. Create a secure temporary file with a random name.
ScopedSd sd = ConvertSddlToSd(sddl);
if (!sd) {
LOG(ERROR) << "Failed to convert SDDL to security descriptor.";
return false;
}
SECURITY_ATTRIBUTES sa = {sizeof(sa), sd.get(), FALSE};
base::FilePath temp_path;
base::win::ScopedHandle temp_file;
for (int i = 0; i < 5; ++i) {
std::string random_name =
base::UnguessableToken::Create().ToString() + ".json~";
temp_path = config_dir.AppendASCII(random_name);
// CREATE_NEW and FILE_FLAG_OPEN_REPARSE_POINT ensure we don't follow an
// existing junction or overwrite an existing file.
temp_file.Set(::CreateFileW(
temp_path.value().c_str(), GENERIC_WRITE, 0, &sa, CREATE_NEW,
FILE_FLAG_SEQUENTIAL_SCAN | FILE_FLAG_OPEN_REPARSE_POINT, nullptr));
if (temp_file.is_valid()) {
break;
}
if (::GetLastError() != ERROR_FILE_EXISTS) {
PLOG(ERROR) << "Failed to create secure temp file in "
<< config_dir.value();
return false;
}
}
if (!temp_file.is_valid()) {
LOG(ERROR) << "Failed to generate a unique secure temp file name.";
return false;
}
// 3. Write content and flush to disk.
DWORD written;
if (!::WriteFile(temp_file.Get(), content.c_str(),
static_cast<DWORD>(content.length()), &written, nullptr) ||
written != content.length()) {
PLOG(ERROR) << "Failed to write to temp file: " << temp_path.value();
temp_file.Close();
base::DeleteFile(temp_path);
return false;
}
if (!::FlushFileBuffers(temp_file.Get())) {
PLOG(ERROR) << "Failed to flush temp file buffers: " << temp_path.value();
temp_file.Close();
base::DeleteFile(temp_path);
return false;
}
temp_file.Close();
// 4. Atomic replacement.
// We use MoveFileExW with MOVEFILE_REPLACE_EXISTING because it is atomic
// on the same volume and preserves the security descriptor of the temporary
// file (unlike ReplaceFileW which inherits the target's ACLs).
if (!::MoveFileExW(temp_path.value().c_str(), target_path.value().c_str(),
MOVEFILE_REPLACE_EXISTING)) {
PLOG(ERROR) << "Failed to move temp file to target: " << target_path.value();
base::DeleteFile(temp_path);
return false;
}
return true;
}
// Writes the configuration file up to |kMaxConfigFileSize| in size.
bool WriteConfig(const base::FilePath& config_dir,
const base::DictValue& config) {
std::string config_json = HostConfigToJson(config);
if (config_json.length() > kMaxConfigFileSize) {
LOG(ERROR) << "Config is larger than the max size: " << kMaxConfigFileSize;
return false;
}
// Ensure the required fields are present.
if (!config.FindString(kHostIdConfigPath)) {
LOG(ERROR) << "Config is missing " << kHostIdConfigPath;
return false;
}
const std::string* host_owner = config.FindString(kHostOwnerConfigPath);
if (!host_owner) {
LOG(ERROR) << "Config is missing " << kHostOwnerConfigPath;
return false;
}
if (!config.FindString(kHostSecretHashConfigPath) &&
!IsGoogleEmail(*host_owner)) {
// PIN authentication is not needed for Google hosts so we only want to
// fail if a secret_hash value isn't present for a non-Google host.
LOG(ERROR) << "Config is missing " << kHostSecretHashConfigPath;
return false;
}
if (!config.FindString(kServiceAccountConfigPath) &&
!config.FindString(kDeprecatedXmppLoginConfigPath)) {
LOG(ERROR) << "Config is missing " << kServiceAccountConfigPath << " and "
<< kDeprecatedXmppLoginConfigPath;
return false;
}
// Write full configuration.
base::FilePath full_config_path = config_dir.Append(kDefaultHostConfigFile);
if (!WriteConfigSafe(full_config_path, config_json,
kConfigFileSecurityDescriptor)) {
return false;
}
// Extract the unprivileged fields from the configuration.
base::DictValue unprivileged_config;
for (const auto& key : DaemonController::GetUnprivilegedConfigKeys()) {
if (const base::Value* value = config.Find(key)) {
unprivileged_config.Set(key, value->Clone());
}
}
// Write the unprivileged configuration file.
base::FilePath unprivileged_config_path =
config_dir.Append(kDefaultUnprivilegedConfigFileName);
return WriteConfigSafe(unprivileged_config_path,
HostConfigToJson(unprivileged_config),
kUnprivilegedConfigFileSecurityDescriptor);
}
DaemonController::State ConvertToDaemonState(DWORD service_state) {
switch (service_state) {
case SERVICE_RUNNING:
return DaemonController::STATE_STARTED;
case SERVICE_CONTINUE_PENDING:
case SERVICE_START_PENDING:
return DaemonController::STATE_STARTING;
case SERVICE_PAUSE_PENDING:
case SERVICE_STOP_PENDING:
return DaemonController::STATE_STOPPING;
case SERVICE_PAUSED:
case SERVICE_STOPPED:
return DaemonController::STATE_STOPPED;
default:
NOTREACHED();
}
}
ScopedScHandle OpenService(DWORD access) {
// Open the service and query its current state.
ScopedScHandle scmanager(
::OpenSCManagerW(nullptr, SERVICES_ACTIVE_DATABASE,
SC_MANAGER_CONNECT | SC_MANAGER_ENUMERATE_SERVICE));
if (!scmanager.is_valid()) {
PLOG(ERROR) << "Failed to connect to the service control manager";
return ScopedScHandle();
}
ScopedScHandle service(
::OpenServiceW(scmanager.Get(), kWindowsServiceName, access));
if (!service.is_valid()) {
PLOG(ERROR) << "Failed to open the '" << kWindowsServiceName << "' service";
}
return service;
}
void InvokeCompletionCallback(DaemonController::CompletionCallback done,
bool success) {
DaemonController::AsyncResult async_result =
success ? DaemonController::RESULT_OK : DaemonController::RESULT_FAILED;
std::move(done).Run(async_result);
}
bool StartDaemon() {
DWORD access = SERVICE_CHANGE_CONFIG | SERVICE_QUERY_STATUS | SERVICE_START |
SERVICE_STOP;
ScopedScHandle service = OpenService(access);
if (!service.is_valid()) {
return false;
}
// Change the service start type to 'auto'.
if (!::ChangeServiceConfigW(service.Get(), SERVICE_NO_CHANGE,
SERVICE_AUTO_START, SERVICE_NO_CHANGE, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr)) {
PLOG(ERROR) << "Failed to change the '" << kWindowsServiceName
<< "' service start type to 'auto'";
return false;
}
// Start the service.
if (!StartService(service.Get(), 0, nullptr)) {
DWORD error = GetLastError();
if (error != ERROR_SERVICE_ALREADY_RUNNING) {
LOG(ERROR) << "Failed to start the '" << kWindowsServiceName
<< "' service: " << error;
return false;
}
}
return true;
}
bool StopDaemon() {
DWORD access = SERVICE_CHANGE_CONFIG | SERVICE_QUERY_STATUS | SERVICE_START |
SERVICE_STOP;
ScopedScHandle service = OpenService(access);
if (!service.is_valid()) {
return false;
}
// Change the service start type to 'manual'.
if (!::ChangeServiceConfigW(service.Get(), SERVICE_NO_CHANGE,
SERVICE_DEMAND_START, SERVICE_NO_CHANGE, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr)) {
PLOG(ERROR) << "Failed to change the '" << kWindowsServiceName
<< "' service start type to 'manual'";
return false;
}
// Stop the service.
SERVICE_STATUS status;
if (!ControlService(service.Get(), SERVICE_CONTROL_STOP, &status)) {
DWORD error = GetLastError();
if (error != ERROR_SERVICE_NOT_ACTIVE) {
LOG(ERROR) << "Failed to stop the '" << kWindowsServiceName
<< "' service: " << error;
return false;
}
}
return true;
}
} // namespace
DaemonControllerDelegateWin::DaemonControllerDelegateWin()
: config_dir_(GetConfigDir()) {}
DaemonControllerDelegateWin::DaemonControllerDelegateWin(
const base::FilePath& config_dir)
: config_dir_(config_dir) {}
DaemonControllerDelegateWin::~DaemonControllerDelegateWin() {}
DaemonController::State DaemonControllerDelegateWin::GetState() {
// TODO(alexeypa): Make the thread alertable, so we can switch to APC
// notifications rather than polling.
ScopedScHandle service = OpenService(SERVICE_QUERY_STATUS);
if (!service.is_valid()) {
return DaemonController::STATE_UNKNOWN;
}
SERVICE_STATUS status;
if (!::QueryServiceStatus(service.Get(), &status)) {
PLOG(ERROR) << "Failed to query the state of the '" << kWindowsServiceName
<< "' service";
return DaemonController::STATE_UNKNOWN;
}
return ConvertToDaemonState(status.dwCurrentState);
}
std::optional<base::DictValue> DaemonControllerDelegateWin::GetConfig() {
base::DictValue config;
if (!ReadConfig(config_dir_.Append(kDefaultUnprivilegedConfigFileName),
config)) {
return std::nullopt;
}
return std::move(config);
}
void DaemonControllerDelegateWin::UpdateConfig(
base::DictValue updated_config,
DaemonController::CompletionCallback done) {
// Get the old config.
base::DictValue config;
if (!ReadConfig(config_dir_.Append(kDefaultHostConfigFile), config)) {
InvokeCompletionCallback(std::move(done), false);
return;
}
// Merge items from the new config into the existing config.
config.Merge(std::move(updated_config));
// Write the updated config.
bool result = WriteConfig(config_dir_, config);
InvokeCompletionCallback(std::move(done), result);
}
void DaemonControllerDelegateWin::Stop(
DaemonController::CompletionCallback done) {
bool result = StopDaemon();
InvokeCompletionCallback(std::move(done), result);
}
DaemonController::UsageStatsConsent
DaemonControllerDelegateWin::GetUsageStatsConsent() {
DaemonController::UsageStatsConsent consent;
consent.supported = true;
consent.allowed = true;
consent.set_by_policy = false;
// Get the recorded user's consent.
bool allowed;
bool set_by_policy;
// If the user's consent is not recorded yet, assume that the user didn't
// consent to collecting crash dumps.
if (remoting::GetUsageStatsConsent(&allowed, &set_by_policy)) {
consent.allowed = allowed;
consent.set_by_policy = set_by_policy;
}
return consent;
}
bool DaemonControllerDelegateWin::is_privileged() const {
return base::IsCurrentProcessElevated();
}
void DaemonControllerDelegateWin::CheckPermission(
bool it2me,
DaemonController::BoolCallback callback) {
std::move(callback).Run(true);
}
void DaemonControllerDelegateWin::SetConfigAndStart(
base::DictValue config,
bool consent,
DaemonController::CompletionCallback done) {
// Record the user's consent.
if (!remoting::SetUsageStatsConsent(consent)) {
InvokeCompletionCallback(std::move(done), false);
return;
}
// Determine the config directory path and create it if necessary.
if (!base::PathExists(config_dir_)) {
if (!base::CreateDirectory(config_dir_)) {
PLOG(ERROR) << "Failed to create the config directory.";
InvokeCompletionCallback(std::move(done), false);
return;
}
}
// Open the directory handle securely (no junction following).
// We need READ_CONTROL | WRITE_DAC | WRITE_OWNER | FILE_READ_ATTRIBUTES
// to set the ACL and verify the path.
// We omit FILE_SHARE_DELETE to prevent the directory from being renamed
// during this operation.
base::win::ScopedHandle dir_handle(CreateFileW(
config_dir_.value().c_str(),
READ_CONTROL | WRITE_DAC | WRITE_OWNER | FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
if (!dir_handle.is_valid()) {
PLOG(ERROR) << "Failed to open config directory for ACL setting: "
<< config_dir_.value();
InvokeCompletionCallback(std::move(done), false);
return;
}
if (IsHandleReparsePoint(dir_handle.Get())) {
LOG(ERROR) << "Config directory is a reparse point: " << config_dir_.value();
InvokeCompletionCallback(std::move(done), false);
return;
}
if (!IsPathSafe(dir_handle.Get(), config_dir_)) {
LOG(ERROR) << "Config directory path is insecure: " << config_dir_.value();
InvokeCompletionCallback(std::move(done), false);
return;
}
// Set strict ACLs on the directory, including the Owner and Group.
if (!SetDirAcl(dir_handle.Get(), kConfigDirSecurityDescriptor)) {
InvokeCompletionCallback(std::move(done), false);
return;
}
// Set the configuration.
if (!WriteConfig(config_dir_, config)) {
InvokeCompletionCallback(std::move(done), false);
return;
}
// Start daemon.
InvokeCompletionCallback(std::move(done), StartDaemon());
}
scoped_refptr<DaemonController> DaemonController::Create() {
return new DaemonController(
base::WrapUnique(new DaemonControllerDelegateWin()));
}
} // namespace remoting