blob: 16eb628dbbaa9c552937968a751ea527de1a02eb [file] [log] [blame]
// Copyright 2019 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.
// GoogleUpdateSetup.exe is the first exe that is run when chrome is being
// installed. It has two main jobs:
// 1) unpack the resources (possibly decompressing some)
// 2) run the real installer (updater.exe) with appropriate flags (--install).
//
// All files needed by the updater are archived together as an uncompressed
// LZMA file, which is further compressed as one file, and inserted as a
// binary resource in the resource section of the setup program.
#include "chrome/updater/win/installer/installer.h"
// #define needed to link in RtlGenRandom(), a.k.a. SystemFunction036. See the
// "Community Additions" comment on MSDN here:
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa387694.aspx
#define SystemFunction036 NTAPI SystemFunction036
#include <NTSecAPI.h>
#undef SystemFunction036
#include <sddl.h>
#include <shellapi.h>
#include <stddef.h>
#include <stdlib.h>
#include <initializer_list>
// TODO(sorin): remove the dependencies on //base/ to reduce the code size.
#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 "chrome/installer/util/lzma_util.h"
#include "chrome/installer/util/self_cleaning_temp_dir.h"
#include "chrome/installer/util/util_constants.h"
#include "chrome/updater/win/installer/configuration.h"
#include "chrome/updater/win/installer/installer_constants.h"
#include "chrome/updater/win/installer/pe_resource.h"
#include "chrome/updater/win/installer/regkey.h"
namespace updater {
namespace {
// Initializes |temp_path| to "Temp" within the target directory, and
// |unpack_path| to a random directory beginning with "source" within
// |temp_path|. Returns false on error.
bool CreateTemporaryAndUnpackDirectories(
installer::SelfCleaningTempDir* temp_path, base::FilePath* unpack_path) {
DCHECK(temp_path && unpack_path);
base::FilePath temp_dir;
if (!base::PathService::Get(base::DIR_TEMP, &temp_dir)) return false;
if (!temp_path->Initialize(temp_dir, kTempPrefix)) {
PLOG(ERROR) << "Could not create temporary path.";
return false;
}
VLOG(1) << "Created path " << temp_path->path().value();
if (!base::CreateTemporaryDirInDir(temp_path->path(), L"source",
unpack_path)) {
PLOG(ERROR) << "Could not create temporary path for unpacked archive.";
return false;
}
return true;
}
} // namespace
using PathString = StackString<MAX_PATH>;
// This structure passes data back and forth for the processing
// of resource callbacks.
struct Context {
// Input to the call back method. Specifies the dir to save resources into.
const wchar_t* base_path = nullptr;
// First output from call back method. Specifies the path of resource archive.
PathString* updater_resource_path = nullptr;
};
// Calls CreateProcess with good default parameters and waits for the process to
// terminate returning the process exit code. In case of CreateProcess failure,
// returns a results object with the provided codes as follows:
// - ERROR_FILE_NOT_FOUND: (file_not_found_code, attributes of setup.exe).
// - ERROR_PATH_NOT_FOUND: (path_not_found_code, attributes of setup.exe).
// - Otherwise: (generic_failure_code, CreateProcess error code).
// In case of error waiting for the process to exit, returns a results object
// with (WAIT_FOR_PROCESS_FAILED, last error code). Otherwise, returns a results
// object with the subprocess's exit code.
ProcessExitResult RunProcessAndWait(const wchar_t* exe_path, wchar_t* cmdline) {
STARTUPINFOW si = {sizeof(si)};
PROCESS_INFORMATION pi = {0};
if (!::CreateProcess(exe_path, cmdline, nullptr, nullptr, FALSE,
CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) {
// Split specific failure modes. If the process couldn't be launched because
// its file/path couldn't be found, report its attributes in ExtraCode1.
// This will help diagnose the prevalence of launch failures due to Image
// File Execution Options tampering. See https://crbug.com/672813 for more
// details.
const DWORD last_error = ::GetLastError();
const DWORD attributes = ::GetFileAttributes(exe_path);
switch (last_error) {
case ERROR_FILE_NOT_FOUND:
return ProcessExitResult(RUN_SETUP_FAILED_FILE_NOT_FOUND, attributes);
case ERROR_PATH_NOT_FOUND:
return ProcessExitResult(RUN_SETUP_FAILED_PATH_NOT_FOUND, attributes);
default:
break;
}
// Lump all other errors into a distinct failure bucket.
return ProcessExitResult(RUN_SETUP_FAILED_COULD_NOT_CREATE_PROCESS,
last_error);
}
::CloseHandle(pi.hThread);
DWORD exit_code = SUCCESS_EXIT_CODE;
DWORD wr = ::WaitForSingleObject(pi.hProcess, INFINITE);
if (WAIT_OBJECT_0 != wr || !::GetExitCodeProcess(pi.hProcess, &exit_code)) {
// Note: We've assumed that WAIT_OBJCT_0 != wr means a failure. The call
// could return a different object but since we never spawn more than one
// sub-process at a time that case should never happen.
return ProcessExitResult(WAIT_FOR_PROCESS_FAILED, ::GetLastError());
}
::CloseHandle(pi.hProcess);
return ProcessExitResult(exit_code);
}
// Windows defined callback used in the EnumResourceNames call. For each
// matching resource found, the callback is invoked and at this point we write
// it to disk. We expect resource names to start with the 'updater' prefix.
// Any other name is treated as an error.
BOOL CALLBACK OnResourceFound(HMODULE module, const wchar_t* type,
wchar_t* name, LONG_PTR context) {
Context* ctx = reinterpret_cast<Context*>(context);
if (!ctx) return FALSE;
if (!StrStartsWith(name, kUpdaterArchivePrefix)) return FALSE;
PEResource resource(name, type, module);
if (!resource.IsValid() || resource.Size() < 1) return FALSE;
PathString full_path;
if (!full_path.assign(ctx->base_path) || !full_path.append(name) ||
!resource.WriteToDisk(full_path.get())) {
return FALSE;
}
if (!ctx->updater_resource_path->assign(full_path.get())) return FALSE;
return TRUE;
}
// Finds and writes to disk resources of type 'B7' (7zip archive). Returns false
// if there is a problem in writing any resource to disk.
ProcessExitResult UnpackBinaryResources(const Configuration& configuration,
HMODULE module,
const wchar_t* base_path,
PathString* archive_path) {
// Prepare the input to OnResourceFound method that needs a location where
// it will write all the resources.
Context context = {base_path, archive_path};
// Get the resources of type 'B7' (7zip archive).
if (!::EnumResourceNames(module, kLZMAResourceType, OnResourceFound,
reinterpret_cast<LONG_PTR>(&context))) {
return ProcessExitResult(UNABLE_TO_EXTRACT_ARCHIVE, ::GetLastError());
}
if (archive_path->length() == 0)
return ProcessExitResult(UNABLE_TO_EXTRACT_ARCHIVE);
ProcessExitResult exit_code = ProcessExitResult(SUCCESS_EXIT_CODE);
return exit_code;
}
// Executes updater.exe, waits for it to finish and returns the exit code.
ProcessExitResult RunSetup(const Configuration& configuration,
const wchar_t* setup_path) {
PathString setup_exe;
if (*setup_path != L'\0') {
if (!setup_exe.assign(setup_path))
return ProcessExitResult(COMMAND_STRING_OVERFLOW);
}
CommandString cmd_line;
// Put the quoted path to setup.exe in cmd_line first.
if (!cmd_line.assign(L"\"") || !cmd_line.append(setup_exe.get()) ||
!cmd_line.append(L"\"")) {
return ProcessExitResult(COMMAND_STRING_OVERFLOW);
}
if (!cmd_line.append(L" --install --enable-logging --v=1"))
return ProcessExitResult(COMMAND_STRING_OVERFLOW);
return RunProcessAndWait(setup_exe.get(), cmd_line.get());
}
// Returns true if the supplied path supports ACLs.
bool IsAclSupportedForPath(const wchar_t* path) {
PathString volume;
DWORD flags = 0;
return ::GetVolumePathName(path, volume.get(),
static_cast<DWORD>(volume.capacity())) &&
::GetVolumeInformation(volume.get(), nullptr, 0, nullptr, nullptr,
&flags, nullptr, 0) &&
(flags & FILE_PERSISTENT_ACLS);
}
// Retrieves the SID of the default owner for objects created by this user
// token (accounting for different behavior under UAC elevation, etc.).
// NOTE: On success the |sid| parameter must be freed with LocalFree().
bool GetCurrentOwnerSid(wchar_t** sid) {
HANDLE token;
if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &token))
return false;
DWORD size = 0;
bool result = false;
// We get the TokenOwner rather than the TokenUser because e.g. under UAC
// elevation we want the admin to own the directory rather than the user.
::GetTokenInformation(token, TokenOwner, nullptr, 0, &size);
if (size && GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
if (TOKEN_OWNER* owner =
reinterpret_cast<TOKEN_OWNER*>(::LocalAlloc(LPTR, size))) {
if (::GetTokenInformation(token, TokenOwner, owner, size, &size))
result = !!::ConvertSidToStringSid(owner->Owner, sid);
::LocalFree(owner);
}
}
::CloseHandle(token);
return result;
}
// Populates |sd| suitable for use when creating directories within |path| with
// ACLs allowing access to only the current owner, admin, and system.
// NOTE: On success the |sd| parameter must be freed with LocalFree().
bool SetSecurityDescriptor(const wchar_t* path, PSECURITY_DESCRIPTOR* sd) {
*sd = nullptr;
// We succeed without doing anything if ACLs aren't supported.
if (!IsAclSupportedForPath(path)) return true;
wchar_t* sid = nullptr;
if (!GetCurrentOwnerSid(&sid)) return false;
// The largest SID is under 200 characters, so 300 should give enough slack.
StackString<300> sddl;
bool result = sddl.append(
L"D:PAI" // Protected, auto-inherited DACL.
L"(A;;FA;;;BA)" // Admin: Full control.
L"(A;OIIOCI;GA;;;BA)"
L"(A;;FA;;;SY)" // System: Full control.
L"(A;OIIOCI;GA;;;SY)"
L"(A;OIIOCI;GA;;;CO)" // Owner: Full control.
L"(A;;FA;;;") &&
sddl.append(sid) && sddl.append(L")");
if (result) {
result = !!::ConvertStringSecurityDescriptorToSecurityDescriptor(
sddl.get(), SDDL_REVISION_1, sd, nullptr);
}
::LocalFree(sid);
return result;
}
// Creates a temporary directory under |base_path| and returns the full path
// of created directory in |work_dir|. If successful return true, otherwise
// false. When successful, the returned |work_dir| will always have a trailing
// backslash and this function requires that |base_path| always includes a
// trailing backslash as well.
// We do not use GetTempFileName here to avoid running into AV software that
// might hold on to the temp file as soon as we create it and then we can't
// delete it and create a directory in its place. So, we use our own mechanism
// for creating a directory with a hopefully-unique name. In the case of a
// collision, we retry a few times with a new name before failing.
bool CreateWorkDir(const wchar_t* base_path, PathString* work_dir,
ProcessExitResult* exit_code) {
*exit_code = ProcessExitResult(PATH_STRING_OVERFLOW);
if (!work_dir->assign(base_path) || !work_dir->append(kTempPrefix))
return false;
// Store the location where we'll append the id.
size_t end = work_dir->length();
// Check if we'll have enough buffer space to continue.
// The name of the directory will use up 11 chars and then we need to append
// the trailing backslash and a terminator. We've already added the prefix
// to the buffer, so let's just make sure we've got enough space for the rest.
if ((work_dir->capacity() - end) < (_countof("fffff.tmp") + 1)) return false;
// Add an ACL if supported by the filesystem. Otherwise system-level installs
// are potentially vulnerable to file squatting attacks.
SECURITY_ATTRIBUTES sa = {};
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
if (!SetSecurityDescriptor(base_path, &sa.lpSecurityDescriptor)) {
*exit_code =
ProcessExitResult(UNABLE_TO_SET_DIRECTORY_ACL, ::GetLastError());
return false;
}
unsigned int id;
*exit_code = ProcessExitResult(UNABLE_TO_GET_WORK_DIRECTORY);
for (int max_attempts = 10; max_attempts; --max_attempts) {
::RtlGenRandom(&id, sizeof(id)); // Try a different name.
// This converts 'id' to a string in the format "78563412" on windows
// because of little endianness, but we don't care since it's just
// a name. Since we checked capacity at the front end, we don't need to
// duplicate it here.
HexEncode(&id, sizeof(id), work_dir->get() + end,
work_dir->capacity() - end);
// We only want the first 5 digits to remain within the 8.3 file name
// format (compliant with previous implementation).
work_dir->truncate_at(end + 5);
// for consistency with the previous implementation which relied on
// GetTempFileName, we append the .tmp extension.
work_dir->append(L".tmp");
if (::CreateDirectory(work_dir->get(),
sa.lpSecurityDescriptor ? &sa : nullptr)) {
// Yay! Now let's just append the backslash and we're done.
work_dir->append(L"\\");
*exit_code = ProcessExitResult(SUCCESS_EXIT_CODE);
break;
}
}
if (sa.lpSecurityDescriptor) LocalFree(sa.lpSecurityDescriptor);
return exit_code->IsSuccess();
}
// Creates and returns a temporary directory in |work_dir| that can be used to
// extract updater payload. |work_dir| ends with a path separator.
bool GetWorkDir(HMODULE module, PathString* work_dir,
ProcessExitResult* exit_code) {
PathString base_path;
DWORD len =
::GetTempPath(static_cast<DWORD>(base_path.capacity()), base_path.get());
if (!len || len >= base_path.capacity() ||
!CreateWorkDir(base_path.get(), work_dir, exit_code)) {
// Problem creating the work dir under TEMP path, so try using the
// current directory as the base path.
len = ::GetModuleFileName(module, base_path.get(),
static_cast<DWORD>(base_path.capacity()));
if (len >= base_path.capacity() || !len)
return false; // Can't even get current directory? Return an error.
wchar_t* name = GetNameFromPathExt(base_path.get(), len);
if (name == base_path.get())
return false; // There was no directory in the string! Bail out.
*name = L'\0';
*exit_code = ProcessExitResult(SUCCESS_EXIT_CODE);
return CreateWorkDir(base_path.get(), work_dir, exit_code);
}
return true;
}
// Returns true for ".." and "." directories.
bool IsCurrentOrParentDirectory(const wchar_t* dir) {
return dir && dir[0] == L'.' &&
(dir[1] == L'\0' || (dir[1] == L'.' && dir[2] == L'\0'));
}
ProcessExitResult WMain(HMODULE module) {
ProcessExitResult exit_code = ProcessExitResult(SUCCESS_EXIT_CODE);
// Parse configuration from the command line and resources.
Configuration configuration;
if (!configuration.Initialize(module))
return ProcessExitResult(GENERIC_INITIALIZATION_FAILURE, ::GetLastError());
// Exit early if an invalid switch was found on the command line.
if (configuration.has_invalid_switch())
return ProcessExitResult(INVALID_OPTION);
// First get a path where we can extract the resource payload, which is
// a compressed LZMA archive of a single file.
base::ScopedTempDir base_path_owner;
PathString base_path;
if (!GetWorkDir(module, &base_path, &exit_code)) return exit_code;
if (!base_path_owner.Set(base::FilePath(base_path.get()))) {
::DeleteFile(base_path.get());
return ProcessExitResult(static_cast<DWORD>(installer::TEMP_DIR_FAILED));
}
PathString compressed_archive;
exit_code = UnpackBinaryResources(configuration, module, base_path.get(),
&compressed_archive);
// Create a temp folder where the archives are unpacked.
base::FilePath unpack_path;
installer::SelfCleaningTempDir temp_path;
if (!CreateTemporaryAndUnpackDirectories(&temp_path, &unpack_path))
return ProcessExitResult(static_cast<DWORD>(installer::TEMP_DIR_FAILED));
// Unpack the compressed archive to extract the uncompressed archive file.
UnPackStatus unpack_status = UNPACK_NO_ERROR;
int32_t ntstatus = 0;
auto lzma_result =
UnPackArchive(base::FilePath(compressed_archive.get()), unpack_path,
nullptr, &unpack_status, &ntstatus);
if (lzma_result)
return ProcessExitResult(static_cast<DWORD>(installer::UNPACKING_FAILED));
// Unpack the uncompressed archive to extract the updater files.
base::FilePath uncompressed_archive =
unpack_path.Append(FILE_PATH_LITERAL("updater.7z"));
lzma_result = UnPackArchive(uncompressed_archive, unpack_path, nullptr,
&unpack_status, &ntstatus);
if (lzma_result)
return ProcessExitResult(static_cast<DWORD>(installer::UNPACKING_FAILED));
// While unpacking the binaries, we paged in a whole bunch of memory that
// we don't need anymore. Let's give it back to the pool before running
// setup.
::SetProcessWorkingSetSize(::GetCurrentProcess(), static_cast<SIZE_T>(-1),
static_cast<SIZE_T>(-1));
PathString setup_path;
if (!setup_path.assign(unpack_path.value().c_str()) ||
!setup_path.append(L"\\bin\\updater.exe")) {
exit_code = ProcessExitResult(PATH_STRING_OVERFLOW);
}
if (exit_code.IsSuccess())
exit_code = RunSetup(configuration, setup_path.get());
return exit_code;
}
} // namespace updater