blob: 8e10f09fc572ecadcb6ab0f93dae1a864028ab05 [file] [log] [blame]
// 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/chrome_cleaner/os/system_util_cleaner.h"
#include <windows.h>
#include <accctrl.h>
#include <aclapi.h>
#include <lmcons.h>
#include <shellapi.h>
#include <memory>
#include <utility>
#include <vector>
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/process/process_info.h"
#include "base/process/process_iterator.h"
#include "base/stl_util.h"
#include "base/strings/string16.h"
#include "base/strings/string_util.h"
#include "base/threading/simple_thread.h"
#include "base/win/scoped_com_initializer.h"
#include "base/win/scoped_handle.h"
#include "base/win/windows_version.h"
#include "chrome/chrome_cleaner/constants/chrome_cleaner_switches.h"
#include "chrome/chrome_cleaner/constants/quarantine_constants.h"
#include "chrome/chrome_cleaner/os/disk_util.h"
#include "chrome/chrome_cleaner/os/scoped_service_handle.h"
#include "chrome/chrome_cleaner/os/system_util.h"
#include "sandbox/win/src/acl.h"
#include "sandbox/win/src/sid.h"
namespace chrome_cleaner {
namespace {
// The size of strings to be passed down to user info API.
const size_t kAccountSidMaxStringSize = UNLEN + 1;
// The number of iterations to poll if a service is stopped correctly.
const unsigned int kMaxServiceQueryIterations = 100;
// The number of iteration to poll if a service exists.
const unsigned int kMaxServiceExistIterations = 5;
// The sleep time in ms between each poll.
const unsigned int kServiceQueryWaitTimeMs = 100;
// The number of iterations to poll if a process is stopped correctly.
const unsigned int kMaxProcessQueryIterations = 50;
// The sleep time in ms between each poll.
const unsigned int kProcessQueryWaitTimeMs = 100;
// Class used to run ShellExecuteEx() in a separate thread where COM is
// STA-initialized.
class LaunchElevatedProcessThreadDelegate
: public base::DelegateSimpleThread::Delegate {
public:
LaunchElevatedProcessThreadDelegate(const base::CommandLine& command_line,
HWND hwnd)
: command_line_(command_line), hwnd_(hwnd) {}
void Run() override {
// ShellExecuteEx() must run in a STA-initialized thread. The
// ScopedCOMInitializer object's default constructor initializes COM as an
// STA.
//
// The code that calls ShellExecuteEx was inspired by
// https://cs-staging.chromium.org/webrtc/src/base/process/launch.h?l=261
base::win::ScopedCOMInitializer scoped_com_initializer;
if (!scoped_com_initializer.Succeeded()) {
PLOG(ERROR) << "Failed to initialize COM when launching elevated process";
return;
}
const base::string16 file = command_line_.GetProgram().value();
const base::string16 arguments = command_line_.GetArgumentsString();
SHELLEXECUTEINFO shex_info = {};
shex_info.cbSize = sizeof(shex_info);
shex_info.fMask = SEE_MASK_NOCLOSEPROCESS;
shex_info.hwnd = hwnd_;
shex_info.lpVerb = L"runas";
shex_info.lpFile = file.c_str();
shex_info.lpParameters = arguments.c_str();
shex_info.nShow = SW_SHOWNORMAL;
if (::ShellExecuteEx(&shex_info))
privileged_process_ = base::Process(shex_info.hProcess);
else
PLOG(ERROR) << "Failed to launch elevated process";
}
// Must be called only after the simple thread object has been joined.
base::Process GetProcess() { return std::move(privileged_process_); }
private:
const base::CommandLine command_line_;
const HWND hwnd_;
base::Process privileged_process_;
};
// Return true if running as the system user.
bool IsRunningAsSystem() {
wchar_t name[kAccountSidMaxStringSize];
DWORD name_size = kAccountSidMaxStringSize;
CHECK(::GetUserName(name, &name_size));
return ::wcscmp(name, L"SYSTEM") == 0;
}
// Get the type of elevation of the current process.
HRESULT GetElevationType(PTOKEN_ELEVATION_TYPE elevation) {
DCHECK(elevation);
*elevation = TokenElevationTypeDefault;
HANDLE token = nullptr;
if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &token))
return HRESULT_FROM_WIN32(GetLastError());
base::win::ScopedHandle process_token(token);
DWORD size = 0;
TOKEN_ELEVATION_TYPE elevation_type;
if (!::GetTokenInformation(token, TokenElevationType, &elevation_type,
sizeof(elevation_type), &size)) {
return HRESULT_FROM_WIN32(GetLastError());
}
*elevation = elevation_type;
return S_OK;
}
// Verify if the current process is running with admin rights or not.
bool CheckForAdminRights() {
if (IsRunningAsSystem()) {
return true;
} else {
if (base::GetCurrentProcessIntegrityLevel() == base::HIGH_INTEGRITY)
return true;
TOKEN_ELEVATION_TYPE elevation = TokenElevationTypeDefault;
return SUCCEEDED(GetElevationType(&elevation)) &&
elevation == TokenElevationTypeFull;
}
}
bool SetRightsPrivileges(const wchar_t* privilege_name, bool enable) {
HANDLE token = nullptr;
if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES,
&token)) {
PLOG(WARNING) << "Failed to OpenProcessToken of CurrentProcess.";
return false;
}
base::win::ScopedHandle process_token(token);
LUID previous_rights = {};
if (!::LookupPrivilegeValue(nullptr, privilege_name, &previous_rights)) {
PLOG(WARNING) << "Failed to LookupPrivilegeValue.";
return false;
}
TOKEN_PRIVILEGES token_privileges;
token_privileges.PrivilegeCount = 1;
token_privileges.Privileges[0].Luid = previous_rights;
if (enable)
token_privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
else
token_privileges.Privileges[0].Attributes = 0;
if (!::AdjustTokenPrivileges(process_token.Get(), FALSE, &token_privileges,
sizeof(TOKEN_PRIVILEGES), nullptr, nullptr)) {
PLOG(WARNING) << "Failed to AdjustTokenPrivileges.";
return false;
}
return true;
}
bool IsPrivilegeEnabled(const wchar_t* privilege_name) {
HANDLE token = nullptr;
if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_READ, &token)) {
PLOG(ERROR) << "Can't open process for TOKEN_READ.";
return false;
}
base::win::ScopedHandle process_token(token);
LUID rights = {};
if (!::LookupPrivilegeValue(nullptr, privilege_name, &rights)) {
PLOG(ERROR) << "Can't lookup privilege value.";
return false;
}
// Get the list of privileges in the token.
DWORD size = 0;
::GetTokenInformation(token, TokenPrivileges, nullptr, 0, &size);
std::unique_ptr<BYTE[]> token_privileges_bytes(new BYTE[size]);
TOKEN_PRIVILEGES* token_privileges =
reinterpret_cast<TOKEN_PRIVILEGES*>(token_privileges_bytes.get());
if (!::GetTokenInformation(token, TokenPrivileges, token_privileges, size,
&size)) {
PLOG(ERROR) << "Can't GetTokenInformation.";
return false;
}
for (size_t i = 0; i < token_privileges->PrivilegeCount; ++i) {
if (token_privileges->Privileges[i].Luid.LowPart == rights.LowPart &&
token_privileges->Privileges[i].Luid.HighPart == rights.HighPart) {
return token_privileges->Privileges[i].Attributes == SE_PRIVILEGE_ENABLED;
}
}
return false;
}
bool SendStopToService(const wchar_t* service_name) {
DCHECK(service_name);
LOG(INFO) << "Stopping service '" << service_name << "'.";
ScopedServiceHandle service;
if (!service.OpenService(service_name, SC_MANAGER_ALL_ACCESS, SERVICE_STOP))
return false;
// If the service doesn't exist, assume it's stopped.
if (!service.IsValid())
return true;
// Stop the service.
SERVICE_STATUS service_state;
if (!::ControlService(service.get(), SERVICE_CONTROL_STOP, &service_state)) {
if (::GetLastError() == ERROR_SERVICE_NOT_ACTIVE) {
LOG(INFO) << "Service '" << service_name << "' is not active.";
return true;
}
PLOG(WARNING) << "Control service failed: could not stop the service.";
return false;
}
return true;
}
bool GetQuarantineFolderPath(base::FilePath* output_path) {
DCHECK(output_path);
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(kQuarantineDirSwitch)) {
*output_path = command_line->GetSwitchValuePath(kQuarantineDirSwitch);
} else {
base::FilePath product_path;
if (!GetAppDataProductDirectory(&product_path)) {
LOG(ERROR) << "Failed to get AppData product directory.";
return false;
}
*output_path = product_path.Append(kQuarantineFolder);
}
return true;
}
} // namespace
bool AcquireDebugRightsPrivileges() {
return SetRightsPrivileges(SE_DEBUG_NAME, true);
}
bool ReleaseDebugRightsPrivileges() {
return SetRightsPrivileges(SE_DEBUG_NAME, false);
}
bool HasDebugRightsPrivileges() {
return IsPrivilegeEnabled(SE_DEBUG_NAME);
}
bool HasAdminRights() {
static bool elevated = CheckForAdminRights();
return elevated;
}
bool IsProcessRunning(const wchar_t* executable) {
base::NamedProcessIterator iter(executable, nullptr);
const base::ProcessEntry* entry = iter.NextProcessEntry();
return entry != nullptr;
}
bool WaitForProcessesStopped(const wchar_t* executable) {
DCHECK(executable);
LOG(INFO) << "Wait for processes '" << executable << "'.";
// Wait until the process is completely stopped.
for (unsigned int iteration = 0; iteration < kMaxProcessQueryIterations;
++iteration) {
if (!IsProcessRunning(executable))
return true;
::Sleep(kProcessQueryWaitTimeMs);
}
// The process didn't terminate.
LOG(ERROR) << "Cannot stop process '" << executable << "', timeout.";
return false;
}
bool WaitForServiceDeleted(const wchar_t* service_name) {
for (unsigned int iteration = 0; iteration < kMaxServiceExistIterations;
++iteration) {
if (!DoesServiceExist(service_name))
return true;
::Sleep(kServiceQueryWaitTimeMs);
}
return false;
}
bool DoesServiceExist(const wchar_t* service_name) {
ScopedServiceHandle service;
if (!service.OpenService(service_name, SC_MANAGER_ALL_ACCESS,
SERVICE_QUERY_STATUS)) {
return false;
}
return service.IsValid();
}
bool WaitForServiceStopped(const wchar_t* service_name) {
DCHECK(service_name);
LOG(INFO) << "Wait for service '" << service_name << "'.";
ScopedServiceHandle service;
if (!service.OpenService(service_name, SC_MANAGER_ALL_ACCESS,
SERVICE_QUERY_STATUS)) {
return false;
}
// If the service doesn't exist, assume it's stopped.
if (!service.IsValid())
return true;
// Wait until the service is completely stopped.
for (unsigned int iteration = 0; iteration < kMaxServiceQueryIterations;
++iteration) {
SERVICE_STATUS_PROCESS service_state = {};
DWORD needed_bytes = 0;
if (!::QueryServiceStatusEx(service.get(), SC_STATUS_PROCESS_INFO,
reinterpret_cast<LPBYTE>(&service_state),
sizeof(SERVICE_STATUS_PROCESS),
&needed_bytes)) {
PLOG(ERROR) << "QueryServiceStatusEx failed for service '" << service_name
<< "'.";
return false;
}
if (service_state.dwCurrentState == SERVICE_STOPPED)
return true;
if (service_state.dwCurrentState != SERVICE_STOP_PENDING &&
service_state.dwCurrentState != SERVICE_RUNNING) {
LOG(ERROR) << "Cannot stop service '" << service_name << "'"
<< ", current service state '" << service_state.dwCurrentState
<< "'.";
return false;
}
::Sleep(kServiceQueryWaitTimeMs);
}
// The service didn't terminate.
LOG(ERROR) << "Cannot stop service '" << service_name << "', timeout.";
return false;
}
bool StopService(const wchar_t* service_name) {
if (!SendStopToService(service_name))
return false;
if (!WaitForServiceStopped(service_name))
return false;
return true;
}
bool DeleteService(const wchar_t* service_name) {
DCHECK(service_name);
LOG(INFO) << "Delete service '" << service_name << "'.";
// Attempt to stop the service before deleting it, but don't worry if it
// doesn't stop.
StopService(service_name);
ScopedServiceHandle service;
if (!service.OpenService(service_name, SC_MANAGER_ALL_ACCESS, DELETE))
return false;
// If the service doesn't exist, assume it does not need to be deleted.
if (!service.IsValid())
return true;
if (!::DeleteService(service.get())) {
if (::GetLastError() == ERROR_SERVICE_MARKED_FOR_DELETE) {
LOG(WARNING) << "Service '" << service_name
<< "' has already been marked for deletion.";
return true;
}
PLOG(WARNING) << "DeleteService failed for service '" << service_name
<< "'.";
return false;
}
return true;
}
// This sets up COM security to allow NetworkService, LocalService, and System
// to call back into the process. It is largely inspired by
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa378987.aspx
// static
bool InitializeCOMSecurity() {
// Create the security descriptor explicitly as follows because
// CoInitializeSecurity() will not accept the relative security descriptors
// returned by ConvertStringSecurityDescriptorToSecurityDescriptor().
const size_t kSidCount = 5;
uint64_t* sids[kSidCount][(SECURITY_MAX_SID_SIZE + sizeof(uint64_t) - 1) /
sizeof(uint64_t)] = {
{}, {}, {}, {}, {},
};
// These are ordered by most interesting ones to try first.
WELL_KNOWN_SID_TYPE sid_types[kSidCount] = {
WinBuiltinAdministratorsSid, // administrator group security identifier
WinLocalServiceSid, // local service security identifier
WinNetworkServiceSid, // network service security identifier
WinSelfSid, // personal account security identifier
WinLocalSystemSid, // local system security identifier
};
// This creates a security descriptor that is equivalent to the following
// security descriptor definition language (SDDL) string:
// O:BAG:BAD:(A;;0x1;;;LS)(A;;0x1;;;NS)(A;;0x1;;;PS)
// (A;;0x1;;;SY)(A;;0x1;;;BA)
// Initialize the security descriptor.
SECURITY_DESCRIPTOR security_desc = {};
if (!::InitializeSecurityDescriptor(&security_desc,
SECURITY_DESCRIPTOR_REVISION))
return false;
DCHECK_EQ(kSidCount, base::size(sids));
DCHECK_EQ(kSidCount, base::size(sid_types));
for (size_t i = 0; i < kSidCount; ++i) {
DWORD sid_bytes = sizeof(sids[i]);
if (!::CreateWellKnownSid(sid_types[i], nullptr, sids[i], &sid_bytes))
return false;
}
// Setup the access control entries (ACE) for COM. You may need to modify
// the access permissions for your application. COM_RIGHTS_EXECUTE and
// COM_RIGHTS_EXECUTE_LOCAL are the minimum access rights required.
EXPLICIT_ACCESS explicit_access[kSidCount] = {};
DCHECK_EQ(kSidCount, base::size(sids));
DCHECK_EQ(kSidCount, base::size(explicit_access));
for (size_t i = 0; i < kSidCount; ++i) {
explicit_access[i].grfAccessPermissions =
COM_RIGHTS_EXECUTE | COM_RIGHTS_EXECUTE_LOCAL;
explicit_access[i].grfAccessMode = SET_ACCESS;
explicit_access[i].grfInheritance = NO_INHERITANCE;
explicit_access[i].Trustee.pMultipleTrustee = nullptr;
explicit_access[i].Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
explicit_access[i].Trustee.TrusteeForm = TRUSTEE_IS_SID;
explicit_access[i].Trustee.TrusteeType = TRUSTEE_IS_GROUP;
explicit_access[i].Trustee.ptstrName = reinterpret_cast<LPTSTR>(sids[i]);
}
// Create an access control list (ACL) using this ACE list, if this succeeds
// make sure to ::LocalFree(acl).
ACL* acl = nullptr;
DWORD acl_result = ::SetEntriesInAcl(base::size(explicit_access),
explicit_access, nullptr, &acl);
if (acl_result != ERROR_SUCCESS || acl == nullptr)
return false;
HRESULT hr = E_FAIL;
// Set the security descriptor owner and group to Administrators and set the
// discretionary access control list (DACL) to the ACL.
if (::SetSecurityDescriptorOwner(&security_desc, sids[0], FALSE) &&
::SetSecurityDescriptorGroup(&security_desc, sids[0], FALSE) &&
::SetSecurityDescriptorDacl(&security_desc, TRUE, acl, FALSE)) {
// Initialize COM. You may need to modify the parameters of
// CoInitializeSecurity() for your application. Note that an
// explicit security descriptor is being passed down.
hr = ::CoInitializeSecurity(
&security_desc, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_IMP_LEVEL_IDENTIFY, nullptr,
EOAC_DISABLE_AAA | EOAC_NO_CUSTOM_MARSHAL, nullptr);
}
::LocalFree(acl);
return SUCCEEDED(hr);
}
base::Process LaunchElevatedProcessWithAssociatedWindow(
const base::CommandLine& command_line,
HWND hwnd) {
LaunchElevatedProcessThreadDelegate runner(command_line, hwnd);
base::DelegateSimpleThread thread(&runner, "ElevatedProcessLauncher");
thread.Start();
thread.Join();
return runner.GetProcess();
}
// Only allow administrator group to access the quarantine folder.
bool InitializeQuarantineFolder(base::FilePath* output_quarantine_path) {
DCHECK(output_quarantine_path);
base::FilePath quarantine_path;
if (!GetQuarantineFolderPath(&quarantine_path)) {
LOG(ERROR) << "Failed to get quarantine folder path.";
return false;
}
if (!base::DirectoryExists(quarantine_path) &&
!base::CreateDirectory(quarantine_path)) {
LOG(ERROR) << "Failed to create quarantine folder.";
return false;
}
sandbox::Sid admin_sid(WinBuiltinAdministratorsSid);
if (!admin_sid.IsValid()) {
LOG(ERROR) << "Failed to get administrator sid.";
return false;
}
PACL dacl;
if (!sandbox::AddSidToDacl(admin_sid, /*old_dacl=*/nullptr, SET_ACCESS,
GENERIC_ALL, &dacl)) {
LOG(ERROR) << "Failed to create ACLs.";
return false;
}
DWORD result_code = ERROR_SUCCESS;
// |PROTECTED_DACL_SECURITY_INFORMATION| will remove inherited ACLs.
result_code = ::SetNamedSecurityInfo(
const_cast<wchar_t*>(quarantine_path.value().c_str()), SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION |
PROTECTED_DACL_SECURITY_INFORMATION,
admin_sid.GetPSID(), /*psidGroup=*/nullptr, dacl, /*pSacl=*/nullptr);
::LocalFree(dacl);
if (result_code != ERROR_SUCCESS) {
LOG(ERROR) << "Failed to set ACLs to quarantine folder.";
return false;
}
*output_quarantine_path = quarantine_path;
return true;
}
} // namespace chrome_cleaner