| // 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/credential_provider/gaiacp/os_process_manager.h" |
| |
| #include <Windows.h> |
| #include <Winternl.h> |
| |
| #include <MDMRegistration.h> |
| #include <Shellapi.h> // For CommandLineToArgvW() |
| #include <Shlobj.h> |
| #include <aclapi.h> |
| #include <dpapi.h> |
| #include <sddl.h> |
| #include <security.h> |
| #include <userenv.h> |
| #include <wincred.h> |
| |
| #include <atlconv.h> |
| |
| #include <malloc.h> |
| #include <memory.h> |
| #include <stdlib.h> |
| |
| #include <iomanip> |
| #include <memory> |
| |
| #include "base/command_line.h" |
| #include "base/files/file_path.h" |
| #include "base/macros.h" |
| #include "base/process/launch.h" |
| #include "base/scoped_native_library.h" |
| #include "base/stl_util.h" |
| #include "base/strings/stringprintf.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" |
| |
| typedef NTSTATUS(FAR WINAPI* NtOpenDirectoryObjectPfn)( |
| OUT PHANDLE DirectoryHandle, |
| IN ACCESS_MASK DesiredAccess, |
| IN POBJECT_ATTRIBUTES ObjectAttributes); |
| |
| namespace credential_provider { |
| |
| namespace { |
| |
| void InitUnicodeString(const wchar_t* string, UNICODE_STRING* uicode_string) { |
| uicode_string->Buffer = const_cast<wchar_t*>(string); |
| uicode_string->Length = |
| static_cast<USHORT>(wcslen(uicode_string->Buffer) * sizeof(wchar_t)); |
| uicode_string->MaximumLength = uicode_string->Length + sizeof(wchar_t); |
| } |
| |
| HRESULT GetTokenLogonSID(const base::win::ScopedHandle& token, PSID* sid) { |
| LOGFN(INFO); |
| DCHECK(sid); |
| |
| // TODO: make more robust by asking for needed length first. |
| char buffer[256]; |
| DWORD returned_length; |
| if (!::GetTokenInformation(token.Get(), TokenLogonSid, &buffer, |
| base::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(INFO); |
| 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(INFO) << "session=" << session_id; |
| |
| UNICODE_STRING name; |
| wchar_t name_buffer[64]; |
| if (session_id == 0) { |
| _snwprintf_s(name_buffer, base::size(name_buffer), L"\\BaseNamedObjects"); |
| } else { |
| _snwprintf_s(name_buffer, base::size(name_buffer), |
| L"\\Sessions\\%d\\BaseNamedObjects", session_id); |
| } |
| InitUnicodeString(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) { |
| HRESULT hr = HRESULT_FROM_NT(err); |
| LOGFN(ERROR) << "SetSecurityInfo hr=" << putHR(hr); |
| return hr; |
| } |
| |
| return S_OK; |
| } |
| |
| HRESULT AllowLogonSIDOnWinSta0(PSID sid) { |
| LOGFN(INFO); |
| |
| 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) { |
| HRESULT 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(INFO); |
| |
| 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) { |
| HRESULT 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()) { |
| HRESULT 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) { |
| // CreateProcessWithTokenW() expects the command line to be non-const, so make |
| // a copy here. |
| // |
| // 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 same way and fails when the first arg is |
| // double quoted. Therefore the command line is built manually here. |
| base::string16 unquoted_cmdline; |
| base::StringAppendF(&unquoted_cmdline, L"\"%ls\"", |
| command_line.GetProgram().value().c_str()); |
| 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 |