chromium/chrome/credential_provider/gaiacp/os_process_manager.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/credential_provider/gaiacp/os_process_manager.h"

#include <Windows.h>

#include <MDMRegistration.h>
#include <Shellapi.h>  // For CommandLineToArgvW()
#include <Shlobj.h>
#include <Winternl.h>
#include <aclapi.h>
#include <atlconv.h>
#include <dpapi.h>
#include <malloc.h>
#include <memory.h>
#include <sddl.h>
#include <security.h>
#include <stdlib.h>
#include <userenv.h>
#include <wincred.h>

#include <iomanip>
#include <memory>

#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/process/launch.h"
#include "base/scoped_native_library.h"
#include "base/strings/strcat_win.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/registry.h"
#include "base/win/scoped_process_information.h"
#include "base/win/win_util.h"
#include "chrome/credential_provider/common/gcp_strings.h"
#include "chrome/credential_provider/gaiacp/gcp_utils.h"
#include "chrome/credential_provider/gaiacp/logging.h"
#include "chrome/credential_provider/gaiacp/scoped_handle.h"

typedef NTSTATUS(FAR WINAPI* NtOpenDirectoryObjectPfn)(
    OUT PHANDLE DirectoryHandle,
    IN ACCESS_MASK DesiredAccess,
    IN POBJECT_ATTRIBUTES ObjectAttributes);

namespace credential_provider {

namespace {

HRESULT GetTokenLogonSID(const base::win::ScopedHandle& token, PSID* sid) {
  LOGFN(VERBOSE);
  DCHECK(sid);

  // TODO: make more robust by asking for needed length first.
  char buffer[256];
  DWORD returned_length;
  if (!::GetTokenInformation(token.Get(), TokenLogonSid, &buffer,
                             std::size(buffer), &returned_length)) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "GetTokenInformation hr=" << putHR(hr);
    return hr;
  }

  // Make sure the data returned is correct.
  TOKEN_GROUPS* groups = reinterpret_cast<TOKEN_GROUPS*>(buffer);
  if (groups->GroupCount != 1) {
    LOGFN(ERROR) << "GetTokenInformation count=" << groups->GroupCount;
    return E_UNEXPECTED;
  }

  if ((groups->Groups[0].Attributes & SE_GROUP_LOGON_ID) != SE_GROUP_LOGON_ID) {
    LOGFN(ERROR) << "GetTokenInformation not a logon sid attr="
                 << std::setbase(16) << groups->Groups[0].Attributes;
    return E_UNEXPECTED;
  }

  if (!::IsValidSid(groups->Groups[0].Sid)) {
    LOGFN(ERROR) << "GetTokenInformation not valid sid attr="
                 << std::setbase(16) << groups->Groups[0].Attributes;
    return E_UNEXPECTED;
  }

  DWORD length = ::GetLengthSid(groups->Groups[0].Sid);
  *sid = reinterpret_cast<SID*>(LocalAlloc(LMEM_FIXED, length));
  if (*sid == nullptr) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "LocalAlloc sid hr=" << putHR(hr);
    return hr;
  }

  ::CopySid(length, *sid, groups->Groups[0].Sid);
  return S_OK;
}

HRESULT AddAllowedACE(ACL* dacl,
                      DWORD flags,
                      DWORD access_mask,
                      PSID sid,
                      ACL** new_dacl) {
  LOGFN(VERBOSE);
  DCHECK(new_dacl);

  ACL_SIZE_INFORMATION si;
  if (!::GetAclInformation(dacl, &si, sizeof(si), AclSizeInformation)) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "GetAclInformation hr=" << putHR(hr);
    return hr;
  }

  // Allocate memory for the existing DACL plus the new entry.
  DWORD new_dacl_size = si.AclBytesInUse + sizeof(ACCESS_ALLOWED_ACE) +
                        ::GetSidLengthRequired(SID_MAX_SUB_AUTHORITIES);
  std::unique_ptr<ACL, decltype(&LocalFree)> local_dacl(
      reinterpret_cast<ACL*>(::LocalAlloc(GPTR, new_dacl_size)), ::LocalFree);
  if (local_dacl == nullptr) {
    HRESULT hr = E_OUTOFMEMORY;
    LOGFN(ERROR) << "LocalAlloc ACL hr=" << putHR(hr);
    return hr;
  }

  if (!::InitializeAcl(local_dacl.get(), new_dacl_size, ACL_REVISION)) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "InitializeAcl hr=" << putHR(hr);
    return hr;
  }

  bool inserted = false;
  for (DWORD i = 0; i < si.AceCount; ++i) {
    ACE_HEADER* ace;
    if (!GetAce(dacl, i, reinterpret_cast<void**>(&ace))) {
      HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
      LOGFN(ERROR) << "GetAe i=" << i << " hr=" << putHR(hr);
      return hr;
    }

    if (!inserted && ace->AceFlags & INHERITED_ACE) {
      if (!::AddAccessAllowedAceEx(local_dacl.get(), ACL_REVISION, flags,
                                   access_mask, sid)) {
        HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
        LOGFN(ERROR) << "AddAccessAllowedAceEx i=" << i << " hr=" << putHR(hr);
        return hr;
      }

      inserted = true;
    }

    if (!::AddAce(local_dacl.get(), ACL_REVISION, MAXWORD, ace, ace->AceSize)) {
      HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
      LOGFN(ERROR) << "AddAce i=" << i << " hr=" << putHR(hr);
      return hr;
    }
  }

  if (!inserted) {
    if (!::AddAccessAllowedAceEx(local_dacl.get(), ACL_REVISION, flags,
                                 access_mask, sid)) {
      HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
      LOGFN(ERROR) << "AddAccessAllowedAceEx hr=" << putHR(hr);
      return hr;
    }
  }

  *new_dacl = local_dacl.release();
  return S_OK;
}

HRESULT AllowLogonSIDOnLocalBasedNamedObjects(PSID sid) {
  HMODULE ntdll = ::GetModuleHandleW(L"ntdll.dll");  // Don't close handle.
  if (ntdll == nullptr) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "GetModuleHandleW hr=" << putHR(hr);
    return hr;
  }

  NtOpenDirectoryObjectPfn NtOpenDirectoryObject =
      reinterpret_cast<NtOpenDirectoryObjectPfn>(
          ::GetProcAddress(ntdll, "NtOpenDirectoryObject"));
  if (NtOpenDirectoryObject == nullptr) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "GetProcAddress hr=" << putHR(hr);
    return hr;
  }

  DWORD session_id;
  if (!::ProcessIdToSessionId(GetCurrentProcessId(), &session_id)) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "ProcessIdToSessionId hr=" << putHR(hr);
    return hr;
  }

  LOGFN(VERBOSE) << "session=" << session_id;

  UNICODE_STRING name;
  wchar_t name_buffer[64];
  if (session_id == 0) {
    _snwprintf_s(name_buffer, std::size(name_buffer), L"\\BaseNamedObjects");
  } else {
    _snwprintf_s(name_buffer, std::size(name_buffer),
                 L"\\Sessions\\%d\\BaseNamedObjects", session_id);
  }
  InitWindowsStringWithString(name_buffer, &name);

  OBJECT_ATTRIBUTES oa;
  oa.Length = sizeof(oa);
  oa.RootDirectory = nullptr;
  oa.ObjectName = &name;
  oa.Attributes = 0;
  oa.SecurityDescriptor = nullptr;
  oa.SecurityQualityOfService = nullptr;

  const ACCESS_MASK kDesiredAccess =
      DIRECTORY_TRAVERSE | READ_CONTROL | WRITE_DAC;
  base::win::ScopedHandle::Handle handle;
  NTSTATUS sts = (*NtOpenDirectoryObject)(&handle, kDesiredAccess, &oa);
  if (sts != STATUS_SUCCESS) {
    HRESULT hr = HRESULT_FROM_NT(sts);
    LOGFN(ERROR) << "NtOpenDirectoryObject hr=" << putHR(hr);
    return hr;
  }
  base::win::ScopedHandle dir_handle(handle);

  PSECURITY_DESCRIPTOR sd;
  ACL* dacl;  // Not owned.
  DWORD err = ::GetSecurityInfo(dir_handle.Get(), SE_WINDOW_OBJECT,
                                DACL_SECURITY_INFORMATION, nullptr, nullptr,
                                &dacl, nullptr, &sd);
  if (err != ERROR_SUCCESS) {
    HRESULT hr = HRESULT_FROM_NT(err);
    LOGFN(ERROR) << "GetSecurityInfo hr=" << putHR(hr);
    return hr;
  }

  const DWORD kDesiredSidAccess = DIRECTORY_QUERY | DIRECTORY_TRAVERSE |
                                  DIRECTORY_CREATE_OBJECT |
                                  DIRECTORY_CREATE_SUBDIRECTORY;
  ACL* new_dacl = nullptr;
  HRESULT hr = AddAllowedACE(dacl, NO_PROPAGATE_INHERIT_ACE, kDesiredSidAccess,
                             sid, &new_dacl);
  ::LocalFree(sd);  // This "frees" dacl too.
  if (FAILED(hr)) {
    LOGFN(ERROR) << "AddAllowedACE 0 hr=" << putHR(hr);
    return hr;
  }

  err = ::SetSecurityInfo(dir_handle.Get(), SE_WINDOW_OBJECT,
                          DACL_SECURITY_INFORMATION, nullptr, nullptr, new_dacl,
                          nullptr);
  ::LocalFree(new_dacl);
  if (err != ERROR_SUCCESS) {
    hr = HRESULT_FROM_NT(err);
    LOGFN(ERROR) << "SetSecurityInfo hr=" << putHR(hr);
    return hr;
  }

  return S_OK;
}

HRESULT AllowLogonSIDOnWinSta0(PSID sid) {
  LOGFN(VERBOSE);

  ScopedWindowStationHandle winsta0(
      ::OpenWindowStationW(L"WinSta0", FALSE, READ_CONTROL | WRITE_DAC));
  if (!winsta0.IsValid()) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "OpenWindowStation hr=" << putHR(hr);
    return hr;
  }

  PSECURITY_DESCRIPTOR sd;
  ACL* dacl;  // Not owned.
  DWORD err = ::GetSecurityInfo(winsta0.Get(), SE_WINDOW_OBJECT,
                                DACL_SECURITY_INFORMATION, nullptr, nullptr,
                                &dacl, nullptr, &sd);
  if (err != ERROR_SUCCESS) {
    HRESULT hr = HRESULT_FROM_NT(err);
    LOGFN(ERROR) << "GetSecurityInfo hr=" << putHR(hr);
    return hr;
  }

  // Add DACL entries.  This is the minimum set of access rights needed for
  // a simple MFC app to run.
  const DWORD kDesiredAccess =
      WINSTA_ACCESSGLOBALATOMS | WINSTA_READSCREEN | WINSTA_EXITWINDOWS |
      READ_CONTROL |
      // The below are needed to run Chrome.  In particular,
      // WINSTA_WRITEATTRIBUTES is needed so that keyboard shortcuts works.
      // WINSTA_CREATEDESKTOP is needed in order for Chrome's sandboxing
      // to work.
      WINSTA_CREATEDESKTOP | WINSTA_READATTRIBUTES | WINSTA_WRITEATTRIBUTES;
  ACL* new_dacl = nullptr;
  HRESULT hr = AddAllowedACE(dacl, NO_PROPAGATE_INHERIT_ACE, kDesiredAccess,
                             sid, &new_dacl);
  LocalFree(sd);  // This "frees" dacl too.
  if (FAILED(hr)) {
    LOGFN(ERROR) << "AddAllowedACE 0 hr=" << putHR(hr);
    return hr;
  }

  err = ::SetSecurityInfo(winsta0.Get(), SE_WINDOW_OBJECT,
                          DACL_SECURITY_INFORMATION, nullptr, nullptr, new_dacl,
                          nullptr);
  ::LocalFree(new_dacl);
  if (err != ERROR_SUCCESS) {
    hr = HRESULT_FROM_NT(err);
    LOGFN(ERROR) << "SetSecurityInfo hr=" << putHR(hr);
    return hr;
  }

  // Usually a window station also sets inherit-only permissions for apps that
  // create new desktops.  However, the gaia logon app is not expected to do
  // that (nor is the logon session being given CREATEDESKTOP permission above
  // anyway) so not setting any inherited permissions for the logon session.

  return S_OK;
}

HDESK GetAndAllowLogonSIDOnDesktop(const wchar_t* desktop_name,
                                   PSID sid,
                                   DWORD desired_access) {
  LOGFN(VERBOSE);

  const DWORD kDesiredAccess =
      desired_access | READ_CONTROL | WRITE_DAC | DESKTOP_CREATEWINDOW;
  ScopedDesktopHandle desktop(
      ::OpenDesktop(desktop_name, 0, FALSE, kDesiredAccess));
  if (!desktop.IsValid()) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) {
      desktop.Set(::CreateDesktop(desktop_name, nullptr, nullptr, 0,
                                  kDesiredAccess, nullptr));
      if (!desktop.IsValid()) {
        hr = HRESULT_FROM_WIN32(::GetLastError());
        LOGFN(ERROR) << "CreateDesktop hr=" << putHR(hr);
        return nullptr;
      }
    } else {
      LOGFN(ERROR) << "OpenDesktop hr=" << putHR(hr);
      return nullptr;
    }
  }

  PSECURITY_DESCRIPTOR sd;
  ACL* dacl;  // Not owned.
  DWORD err = ::GetSecurityInfo(desktop.Get(), SE_WINDOW_OBJECT,
                                DACL_SECURITY_INFORMATION, nullptr, nullptr,
                                &dacl, nullptr, &sd);
  if (err != ERROR_SUCCESS) {
    HRESULT hr = HRESULT_FROM_NT(err);
    LOGFN(ERROR) << "GetSecurityInfo hr=" << putHR(hr);
    return nullptr;
  }

  // Add DACL entries.  This is the minimum set of access rights needed for
  // a simple MFC app to run.
  const DWORD kAccessMask =
      DESKTOP_CREATEWINDOW | DESKTOP_CREATEMENU | DESKTOP_HOOKCONTROL |
      DESKTOP_ENUMERATE | DESKTOP_READOBJECTS | DESKTOP_WRITEOBJECTS |
      READ_CONTROL |
      // This permission is needed specifically by Chrome to run due to the
      // sandboxing it does with its processes.
      DESKTOP_SWITCHDESKTOP;
  ACL* new_dacl = nullptr;
  HRESULT hr = AddAllowedACE(dacl, 0, kAccessMask, sid, &new_dacl);
  ::LocalFree(sd);  // This "frees" dacl too.
  if (FAILED(hr)) {
    LOGFN(ERROR) << "AddAllowedACE 0 hr=" << putHR(hr);
    return nullptr;
  }

  err = ::SetSecurityInfo(desktop.Get(), SE_WINDOW_OBJECT,
                          DACL_SECURITY_INFORMATION, nullptr, nullptr, new_dacl,
                          nullptr);
  ::LocalFree(new_dacl);
  if (err != ERROR_SUCCESS) {
    hr = HRESULT_FROM_NT(err);
    LOGFN(ERROR) << "SetSecurityInfo hr=" << putHR(hr);
    return nullptr;
  }

  return desktop.Take();
}

// Sets up the minimum required privileges or ACLs needed for the logon SID
// to run the logon stub process in a restricted environment.
HRESULT SetupPermissionsForLogonSid(PSID sid) {
  HRESULT hr = AllowLogonSIDOnLocalBasedNamedObjects(sid);
  if (FAILED(hr)) {
    LOGFN(ERROR) << "AllowLogonSIDOnLocalBasedNamedObjects hr=" << putHR(hr);
    return hr;
  }

  // Assume current window station is "WinSta0", which it should be for
  // winlogon.exe.
  if (kDesktopName[0] != 0) {
    // Add logon SID to the ACL of WinSta0.
    hr = AllowLogonSIDOnWinSta0(sid);
    if (FAILED(hr)) {
      LOGFN(ERROR) << "AllowLogonSIDOnWinSta0 hr=" << putHR(hr);
      return hr;
    }

    ScopedDesktopHandle desktop;
    desktop.Set(
        GetAndAllowLogonSIDOnDesktop(kDesktopName, sid, DESKTOP_SWITCHDESKTOP));
    if (!desktop.IsValid()) {
      hr = HRESULT_FROM_WIN32(::GetLastError());
      LOGFN(ERROR) << "GetAndAllowLogonSIDOnDesktop hr=" << putHR(hr);
      return hr;
    }
  }

  return S_OK;
}

}  // namespace

// static
OSProcessManager** OSProcessManager::GetInstanceStorage() {
  static OSProcessManager* instance = new OSProcessManager();
  return &instance;
}

// static
OSProcessManager* OSProcessManager::Get() {
  return *GetInstanceStorage();
}

// static
void OSProcessManager::SetInstanceForTesting(OSProcessManager* instance) {
  *GetInstanceStorage() = instance;
}

OSProcessManager::~OSProcessManager() {}

HRESULT OSProcessManager::GetTokenLogonSID(const base::win::ScopedHandle& token,
                                           PSID* sid) {
  return ::credential_provider::GetTokenLogonSID(token, sid);
}

HRESULT OSProcessManager::SetupPermissionsForLogonSid(PSID sid) {
  return ::credential_provider::SetupPermissionsForLogonSid(sid);
}

HRESULT OSProcessManager::CreateProcessWithToken(
    const base::win::ScopedHandle& logon_token,
    const base::CommandLine& command_line,
    _STARTUPINFOW* startupinfo,
    base::win::ScopedProcessInformation* procinfo) {
  // CreateProcessWithTokenW() expects the command line to be non-const, so make
  // a copy here.
  std::unique_ptr<wchar_t, void (*)(void*)>
      cmdline(wcsdup(command_line.GetCommandLineString().c_str()), std::free);
  PROCESS_INFORMATION temp_procinfo = {};
  if (!::CreateProcessWithTokenW(logon_token.Get(),
                                 LOGON_WITH_PROFILE,
                                 command_line.GetProgram().value().c_str(),
                                 cmdline.get(),
                                 CREATE_SUSPENDED,
                                 nullptr,  // environment
                                 nullptr,  // current directory
                                 startupinfo, &temp_procinfo)) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    return hr;
  }
  procinfo->Set(temp_procinfo);
  return S_OK;
}

HRESULT OSProcessManager::CreateRunningProcess(
    const base::CommandLine& command_line,
    _STARTUPINFOW* startupinfo,
    base::win::ScopedProcessInformation* procinfo) {
  // command_line.GetCommandLineString() is not used here because it quotes the
  // command line to follow the command line rules of Microsoft C/C++ startup
  // code.  However this function is called to execute rundll32 which parses
  // command lines in a special way and fails when the first arg is double
  // quoted.  Therefore the command line is built manually here.
  std::wstring unquoted_cmdline =
      base::StrCat({L"\"", command_line.GetProgram().value(), L"\""});
  for (const auto& arg : command_line.GetArgs()) {
    unquoted_cmdline.append(FILE_PATH_LITERAL(" "));
    unquoted_cmdline.append(arg);
  }

  for (const auto& switch_value : command_line.GetSwitches()) {
    unquoted_cmdline.append(L" --");
    unquoted_cmdline.append(base::UTF8ToWide(switch_value.first));
    if (switch_value.second.empty())
      continue;
    unquoted_cmdline.append(L"=");
    unquoted_cmdline.append(switch_value.second);
  }

  base::LaunchOptions options;

  // If stdio handles are being passed to the process, make sure they are
  // included in the inherited list.  This assumes the handles are already
  // marked as inheritable.
  if ((startupinfo->dwFlags & STARTF_USESTDHANDLES) == STARTF_USESTDHANDLES) {
    options.stdin_handle = startupinfo->hStdInput;
    options.stdout_handle = startupinfo->hStdOutput;
    options.stderr_handle = startupinfo->hStdError;
    options.handles_to_inherit.push_back(startupinfo->hStdInput);
    options.handles_to_inherit.push_back(startupinfo->hStdOutput);
    options.handles_to_inherit.push_back(startupinfo->hStdError);
  }

  base::Process process(base::LaunchProcess(unquoted_cmdline, options));
  return process.IsValid() ? S_OK : E_FAIL;
}

}  // namespace credential_provider