blob: 0082f637c964dd1c7c805c316b40171928fe684d [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/elevation_service/elevated_recovery_impl.h"
#include <objbase.h>
#include <string>
#include <utility>
#include "base/base_paths.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/command_line.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/strings/utf_string_conversions.h"
#include "base/version.h"
#include "base/win/scoped_process_information.h"
#include "chrome/install_static/install_util.h"
#include "third_party/zlib/google/zip.h"
namespace elevation_service {
namespace {
// The hard-coded Recovery subdirectory where the CRX is unpacked and executed.
constexpr base::FilePath::CharType kRecoveryDirectory[] =
FILE_PATH_LITERAL("ChromeRecovery");
// The hard-coded Recovery executable. This file comes from the CRX, and we
// create an elevated process from it.
constexpr base::FilePath::CharType kRecoveryExeName[] =
FILE_PATH_LITERAL("ChromeRecovery.exe");
// The hard-coded SHA256 of the SubjectPublicKeyInfo used to sign the Recovery
// CRX which contains ChromeRecovery.exe.
std::vector<uint8_t> GetRecoveryCRXHash() {
return std::vector<uint8_t>{0x5f, 0x94, 0xe0, 0x3c, 0x64, 0x30, 0x9f, 0xbc,
0xfe, 0x00, 0x9a, 0x27, 0x3e, 0x52, 0xbf, 0xa5,
0x84, 0xb9, 0xb3, 0x75, 0x07, 0x29, 0xde, 0xfa,
0x32, 0x76, 0xd9, 0x93, 0xb5, 0xa3, 0xce, 0x02};
}
// This function is only meant to be called when a Windows API function errors
// out, and the corresponding ::GetLastResult is expected to be set to an error.
// There could be cases where ::GetLastError() is not set correctly, this
// function returns E_FAIL in those cases.
HRESULT HRESULTFromLastError() {
const auto error_code = ::GetLastError();
return (error_code != ERROR_SUCCESS) ? HRESULT_FROM_WIN32(error_code)
: E_FAIL;
}
// Opens and returns the COM caller's |process| given the process id, or the
// current process if |proc_id| is 0.
HRESULT OpenCallingProcess(uint32_t proc_id, base::Process* process) {
DCHECK(process);
if (!proc_id) {
*process = base::Process::Current();
return S_OK;
}
HRESULT hr = ::CoImpersonateClient();
if (FAILED(hr))
return hr;
base::ScopedClosureRunner revert_to_self(
base::BindOnce(base::IgnoreResult(&::CoRevertToSelf)));
*process = base::Process::OpenWithAccess(proc_id, PROCESS_DUP_HANDLE);
return process->IsValid() ? S_OK : HRESULTFromLastError();
}
// Validates the provided CRX using the |crx_hash|, and if validation succeeds,
// unpacks the CRX under |unpack_under_path|. Returns the unpacked CRX
// directory in |unpacked_crx_dir|.
HRESULT ValidateAndUnpackCRX(const base::FilePath& from_crx_path,
const crx_file::VerifierFormat& crx_format,
const std::vector<uint8_t>& crx_hash,
const base::FilePath& unpack_under_path,
base::ScopedTempDir* unpacked_crx_dir) {
DCHECK(unpacked_crx_dir);
base::ScopedTempDir to_dir;
if (!to_dir.CreateUniqueTempDirUnderPath(unpack_under_path))
return HRESULTFromLastError();
const base::FilePath to_crx_path =
to_dir.GetPath().Append(from_crx_path.BaseName());
if (!base::CopyFile(from_crx_path, to_crx_path))
return HRESULTFromLastError();
std::string public_key;
if (crx_file::Verify(to_crx_path, crx_format, {crx_hash}, {}, &public_key,
nullptr) != crx_file::VerifierResult::OK_FULL) {
return CRYPT_E_NO_MATCH;
}
if (!zip::Unzip(to_crx_path, to_dir.GetPath()))
return E_UNEXPECTED;
LOG_IF(WARNING, !base::DeleteFile(to_crx_path, false));
LOG_IF(WARNING, !unpacked_crx_dir->Set(to_dir.Take()));
return S_OK;
}
// Runs the executable |path_and_name| with the provided |args|. The returned
// |proc_handle| is a process handle that is valid for the |calling_process|
// process.
HRESULT LaunchCmd(const base::CommandLine& command_line,
const base::Process& calling_process,
base::win::ScopedHandle* proc_handle) {
DCHECK(!command_line.GetCommandLineString().empty());
DCHECK(proc_handle);
base::LaunchOptions options = {};
options.feedback_cursor_off = true;
base::GetTempDir(&options.current_directory);
base::Process proc = base::LaunchProcess(command_line, options);
if (!proc.IsValid())
return HRESULTFromLastError();
HANDLE duplicate_proc_handle = nullptr;
constexpr DWORD desired_access =
PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE;
bool res = ::DuplicateHandle(
::GetCurrentProcess(), // Current process.
proc.Handle(), // Process handle to duplicate.
calling_process.Handle(), // Process receiving the handle.
&duplicate_proc_handle, // Duplicated handle.
desired_access, // Access requested for the new handle.
FALSE, // Don't inherit the new handle.
0) != 0;
if (!res)
return HRESULTFromLastError();
proc_handle->Set(duplicate_proc_handle);
return S_OK;
}
HRESULT ValidateCRXArgs(const base::string16& browser_appid,
const base::string16& browser_version,
const base::string16& session_id) {
if (!browser_appid.empty()) {
GUID guid = {};
HRESULT hr = ::IIDFromString(browser_appid.c_str(), &guid);
if (FAILED(hr))
return hr;
}
const base::Version version(base::UTF16ToASCII(browser_version));
if (!version.IsValid())
return E_INVALIDARG;
GUID session_guid = {};
return ::IIDFromString(session_id.c_str(), &session_guid);
}
// Deletes all the files and subdirectories within |directory_path|. Errors are
// ignored.
void DeleteDirectoryFiles(const base::FilePath& directory_path) {
base::FileEnumerator file_enum(
directory_path, false,
base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES);
for (base::FilePath current = file_enum.Next(); !current.empty();
current = file_enum.Next()) {
base::DeleteFile(current, true);
}
}
// Schedules deletion after reboot of |dir_name| as well as all the files and
// subdirectories within |dir_name|. Errors are ignored.
void ScheduleDirectoryForDeletion(const base::FilePath& dir_name) {
// First schedule all the files within |dir_name| for deletion.
base::FileEnumerator file_enum(dir_name, false, base::FileEnumerator::FILES);
for (base::FilePath file = file_enum.Next(); !file.empty();
file = file_enum.Next()) {
base::DeleteFileAfterReboot(file);
}
// Then recurse to all the subdirectories.
base::FileEnumerator dir_enum(dir_name, false,
base::FileEnumerator::DIRECTORIES);
for (base::FilePath sub_dir = dir_enum.Next(); !sub_dir.empty();
sub_dir = dir_enum.Next()) {
ScheduleDirectoryForDeletion(sub_dir);
}
// Now schedule the empty directory itself.
base::DeleteFileAfterReboot(dir_name);
}
// Returns the ChromeRecovery directory under Google\Chrome. For machine
// installs, this directory is under %ProgramFiles%, which is writeable only by
// adminstrators. We use this secure directory to validate and unpack the CRX to
// prevent tampering.
HRESULT GetChromeRecoveryDirectory(base::FilePath* dir) {
base::FilePath recovery_dir;
if (!base::PathService::Get(base::DIR_EXE, &recovery_dir))
return HRESULTFromLastError();
recovery_dir = recovery_dir.DirName().DirName().Append(kRecoveryDirectory);
*dir = std::move(recovery_dir);
return S_OK;
}
} // namespace
HRESULT CleanupChromeRecoveryDirectory() {
base::FilePath recovery_dir;
HRESULT hr = GetChromeRecoveryDirectory(&recovery_dir);
if (FAILED(hr))
return hr;
DeleteDirectoryFiles(recovery_dir);
return S_OK;
}
HRESULT RunChromeRecoveryCRX(const base::FilePath& crx_path,
const base::string16& browser_appid,
const base::string16& browser_version,
const base::string16& session_id,
uint32_t caller_proc_id,
base::win::ScopedHandle* proc_handle) {
if (crx_path.empty() || !proc_handle)
return E_INVALIDARG;
HRESULT hr = ValidateCRXArgs(browser_appid, browser_version, session_id);
if (FAILED(hr))
return hr;
base::CommandLine args(base::CommandLine::NO_PROGRAM);
if (!browser_appid.empty())
args.AppendSwitchNative("appguid", browser_appid);
args.AppendSwitchNative("browser-version", browser_version);
args.AppendSwitchNative("sessionid", session_id);
args.AppendSwitch("system");
base::FilePath unpack_dir;
hr = GetChromeRecoveryDirectory(&unpack_dir);
if (FAILED(hr))
return hr;
return RunCRX(crx_path, args,
crx_file::VerifierFormat::CRX3_WITH_PUBLISHER_PROOF,
GetRecoveryCRXHash(), unpack_dir,
base::FilePath(kRecoveryExeName), caller_proc_id, proc_handle);
}
HRESULT RunCRX(const base::FilePath& crx_path,
const base::CommandLine& args,
const crx_file::VerifierFormat& crx_format,
const std::vector<uint8_t>& crx_hash,
const base::FilePath& unpack_under_path,
const base::FilePath& exe_filename,
uint32_t caller_proc_id,
base::win::ScopedHandle* proc_handle) {
DCHECK(proc_handle);
DCHECK(!crx_path.empty());
DCHECK(!crx_hash.empty());
DCHECK(!unpack_under_path.empty());
DCHECK(!exe_filename.empty());
base::Process calling_process;
HRESULT hr = OpenCallingProcess(caller_proc_id, &calling_process);
if (FAILED(hr))
return hr;
base::ScopedTempDir unpacked_crx_dir;
hr = ValidateAndUnpackCRX(crx_path, crx_format, crx_hash, unpack_under_path,
&unpacked_crx_dir);
if (FAILED(hr))
return hr;
const base::FilePath path_and_name =
unpacked_crx_dir.GetPath().Append(exe_filename);
base::CommandLine command_line(path_and_name);
command_line.AppendArguments(args, false);
base::win::ScopedHandle scoped_proc_handle;
hr = LaunchCmd(command_line, calling_process, &scoped_proc_handle);
if (FAILED(hr))
return hr;
ScheduleDirectoryForDeletion(unpacked_crx_dir.Take());
*proc_handle = std::move(scoped_proc_handle);
return hr;
}
} // namespace elevation_service