blob: 6cb7e3168c4836f3f80d90ac7a3db0a2e9273111 [file] [log] [blame]
// Copyright (c) 2012 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 "remoting/host/win/launch_process_with_token.h"
#include <windows.h>
#include <winternl.h>
#include <limits>
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "base/rand_util.h"
#include "base/scoped_native_library.h"
#include "base/strings/string16.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/scoped_handle.h"
#include "base/win/scoped_process_information.h"
#include "base/win/windows_version.h"
using base::win::ScopedHandle;
namespace {
const char kCreateProcessDefaultPipeNameFormat[] =
"\\\\.\\Pipe\\TerminalServer\\SystemExecSrvr\\%d";
// Undocumented WINSTATIONINFOCLASS value causing
// winsta!WinStationQueryInformationW() to return the name of the pipe for
// requesting cross-session process creation.
const WINSTATIONINFOCLASS kCreateProcessPipeNameClass =
static_cast<WINSTATIONINFOCLASS>(0x21);
const int kPipeBusyWaitTimeoutMs = 2000;
const int kPipeConnectMaxAttempts = 3;
// Terminates the process and closes process and thread handles in
// |process_information| structure.
void CloseHandlesAndTerminateProcess(PROCESS_INFORMATION* process_information) {
if (process_information->hThread) {
CloseHandle(process_information->hThread);
process_information->hThread = NULL;
}
if (process_information->hProcess) {
TerminateProcess(process_information->hProcess, CONTROL_C_EXIT);
CloseHandle(process_information->hProcess);
process_information->hProcess = NULL;
}
}
// Connects to the executor server corresponding to |session_id|.
bool ConnectToExecutionServer(uint32 session_id,
base::win::ScopedHandle* pipe_out) {
base::string16 pipe_name;
// Use winsta!WinStationQueryInformationW() to determine the process creation
// pipe name for the session.
base::FilePath winsta_path(base::GetNativeLibraryName(UTF8ToUTF16("winsta")));
base::ScopedNativeLibrary winsta(winsta_path);
if (winsta.is_valid()) {
PWINSTATIONQUERYINFORMATIONW win_station_query_information =
static_cast<PWINSTATIONQUERYINFORMATIONW>(
winsta.GetFunctionPointer("WinStationQueryInformationW"));
if (win_station_query_information) {
wchar_t name[MAX_PATH];
ULONG name_length;
if (win_station_query_information(0,
session_id,
kCreateProcessPipeNameClass,
name,
sizeof(name),
&name_length)) {
pipe_name.assign(name);
}
}
}
// Use the default pipe name if we couldn't query its name.
if (pipe_name.empty()) {
pipe_name = UTF8ToUTF16(
base::StringPrintf(kCreateProcessDefaultPipeNameFormat, session_id));
}
// Try to connect to the named pipe.
base::win::ScopedHandle pipe;
for (int i = 0; i < kPipeConnectMaxAttempts; ++i) {
pipe.Set(CreateFile(pipe_name.c_str(),
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0,
NULL));
if (pipe.IsValid()) {
break;
}
// Cannot continue retrying if error is something other than
// ERROR_PIPE_BUSY.
if (GetLastError() != ERROR_PIPE_BUSY) {
break;
}
// Cannot continue retrying if wait on pipe fails.
if (!WaitNamedPipe(pipe_name.c_str(), kPipeBusyWaitTimeoutMs)) {
break;
}
}
if (!pipe.IsValid()) {
LOG_GETLASTERROR(ERROR) << "Failed to connect to '" << pipe_name << "'";
return false;
}
*pipe_out = pipe.Pass();
return true;
}
// Copies the process token making it a primary impersonation token.
// The returned handle will have |desired_access| rights.
bool CopyProcessToken(DWORD desired_access, ScopedHandle* token_out) {
HANDLE temp_handle;
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_DUPLICATE | desired_access,
&temp_handle)) {
LOG_GETLASTERROR(ERROR) << "Failed to open process token";
return false;
}
ScopedHandle process_token(temp_handle);
if (!DuplicateTokenEx(process_token,
desired_access,
NULL,
SecurityImpersonation,
TokenPrimary,
&temp_handle)) {
LOG_GETLASTERROR(ERROR) << "Failed to duplicate the process token";
return false;
}
token_out->Set(temp_handle);
return true;
}
// Creates a copy of the current process with SE_TCB_NAME privilege enabled.
bool CreatePrivilegedToken(ScopedHandle* token_out) {
ScopedHandle privileged_token;
DWORD desired_access = TOKEN_ADJUST_PRIVILEGES | TOKEN_IMPERSONATE |
TOKEN_DUPLICATE | TOKEN_QUERY;
if (!CopyProcessToken(desired_access, &privileged_token)) {
return false;
}
// Get the LUID for the SE_TCB_NAME privilege.
TOKEN_PRIVILEGES state;
state.PrivilegeCount = 1;
state.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!LookupPrivilegeValue(NULL, SE_TCB_NAME, &state.Privileges[0].Luid)) {
LOG_GETLASTERROR(ERROR) <<
"Failed to lookup the LUID for the SE_TCB_NAME privilege";
return false;
}
// Enable the SE_TCB_NAME privilege.
if (!AdjustTokenPrivileges(privileged_token, FALSE, &state, 0, NULL, 0)) {
LOG_GETLASTERROR(ERROR) <<
"Failed to enable SE_TCB_NAME privilege in a token";
return false;
}
*token_out = privileged_token.Pass();
return true;
}
// Fills the process and thread handles in the passed |process_information|
// structure and resume the process if the caller didn't want to suspend it.
bool ProcessCreateProcessResponse(DWORD creation_flags,
PROCESS_INFORMATION* process_information) {
// The execution server does not return handles to the created process and
// thread.
if (!process_information->hProcess) {
// N.B. PROCESS_ALL_ACCESS is different in XP and Vista+ versions of
// the SDK. |desired_access| below is effectively PROCESS_ALL_ACCESS from
// the XP version of the SDK.
DWORD desired_access =
STANDARD_RIGHTS_REQUIRED |
SYNCHRONIZE |
PROCESS_TERMINATE |
PROCESS_CREATE_THREAD |
PROCESS_SET_SESSIONID |
PROCESS_VM_OPERATION |
PROCESS_VM_READ |
PROCESS_VM_WRITE |
PROCESS_DUP_HANDLE |
PROCESS_CREATE_PROCESS |
PROCESS_SET_QUOTA |
PROCESS_SET_INFORMATION |
PROCESS_QUERY_INFORMATION |
PROCESS_SUSPEND_RESUME;
process_information->hProcess =
OpenProcess(desired_access,
FALSE,
process_information->dwProcessId);
if (!process_information->hProcess) {
LOG_GETLASTERROR(ERROR) << "Failed to open the process "
<< process_information->dwProcessId;
return false;
}
}
if (!process_information->hThread) {
// N.B. THREAD_ALL_ACCESS is different in XP and Vista+ versions of
// the SDK. |desired_access| below is effectively THREAD_ALL_ACCESS from
// the XP version of the SDK.
DWORD desired_access =
STANDARD_RIGHTS_REQUIRED |
SYNCHRONIZE |
THREAD_TERMINATE |
THREAD_SUSPEND_RESUME |
THREAD_GET_CONTEXT |
THREAD_SET_CONTEXT |
THREAD_QUERY_INFORMATION |
THREAD_SET_INFORMATION |
THREAD_SET_THREAD_TOKEN |
THREAD_IMPERSONATE |
THREAD_DIRECT_IMPERSONATION;
process_information->hThread =
OpenThread(desired_access,
FALSE,
process_information->dwThreadId);
if (!process_information->hThread) {
LOG_GETLASTERROR(ERROR) << "Failed to open the thread "
<< process_information->dwThreadId;
return false;
}
}
// Resume the thread if the caller didn't want to suspend the process.
if ((creation_flags & CREATE_SUSPENDED) == 0) {
if (!ResumeThread(process_information->hThread)) {
LOG_GETLASTERROR(ERROR) << "Failed to resume the thread "
<< process_information->dwThreadId;
return false;
}
}
return true;
}
// Receives the response to a remote process create request.
bool ReceiveCreateProcessResponse(
HANDLE pipe,
PROCESS_INFORMATION* process_information_out) {
struct CreateProcessResponse {
DWORD size;
BOOL success;
DWORD last_error;
PROCESS_INFORMATION process_information;
};
DWORD bytes;
CreateProcessResponse response;
if (!ReadFile(pipe, &response, sizeof(response), &bytes, NULL)) {
LOG_GETLASTERROR(ERROR) << "Failed to receive CreateProcessAsUser response";
return false;
}
// The server sends the data in one chunk so if we didn't received a complete
// answer something bad happend and there is no point in retrying.
if (bytes != sizeof(response)) {
SetLastError(ERROR_RECEIVE_PARTIAL);
return false;
}
if (!response.success) {
SetLastError(response.last_error);
return false;
}
*process_information_out = response.process_information;
return true;
}
// Sends a remote process create request to the execution server.
bool SendCreateProcessRequest(
HANDLE pipe,
const base::FilePath::StringType& application_name,
const CommandLine::StringType& command_line,
DWORD creation_flags,
const char16* desktop_name) {
// |CreateProcessRequest| structure passes the same parameters to
// the execution server as CreateProcessAsUser() function does. Strings are
// stored as wide strings immediately after the structure. String pointers are
// represented as byte offsets to string data from the beginning of
// the structure.
struct CreateProcessRequest {
DWORD size;
DWORD process_id;
BOOL use_default_token;
HANDLE token;
LPWSTR application_name;
LPWSTR command_line;
SECURITY_ATTRIBUTES process_attributes;
SECURITY_ATTRIBUTES thread_attributes;
BOOL inherit_handles;
DWORD creation_flags;
LPVOID environment;
LPWSTR current_directory;
STARTUPINFOW startup_info;
PROCESS_INFORMATION process_information;
};
base::string16 desktop;
if (desktop_name)
desktop = desktop_name;
// Allocate a large enough buffer to hold the CreateProcessRequest structure
// and three NULL-terminated string parameters.
size_t size = sizeof(CreateProcessRequest) + sizeof(wchar_t) *
(application_name.size() + command_line.size() + desktop.size() + 3);
scoped_ptr<char[]> buffer(new char[size]);
memset(buffer.get(), 0, size);
// Marshal the input parameters.
CreateProcessRequest* request =
reinterpret_cast<CreateProcessRequest*>(buffer.get());
request->size = size;
request->process_id = GetCurrentProcessId();
request->use_default_token = TRUE;
// Always pass CREATE_SUSPENDED to avoid a race between the created process
// exiting too soon and OpenProcess() call below.
request->creation_flags = creation_flags | CREATE_SUSPENDED;
request->startup_info.cb = sizeof(request->startup_info);
size_t buffer_offset = sizeof(CreateProcessRequest);
request->application_name = reinterpret_cast<LPWSTR>(buffer_offset);
std::copy(application_name.begin(),
application_name.end(),
reinterpret_cast<wchar_t*>(buffer.get() + buffer_offset));
buffer_offset += (application_name.size() + 1) * sizeof(wchar_t);
request->command_line = reinterpret_cast<LPWSTR>(buffer_offset);
std::copy(command_line.begin(),
command_line.end(),
reinterpret_cast<wchar_t*>(buffer.get() + buffer_offset));
buffer_offset += (command_line.size() + 1) * sizeof(wchar_t);
request->startup_info.lpDesktop =
reinterpret_cast<LPWSTR>(buffer_offset);
std::copy(desktop.begin(),
desktop.end(),
reinterpret_cast<wchar_t*>(buffer.get() + buffer_offset));
// Pass the request to create a process in the target session.
DWORD bytes;
if (!WriteFile(pipe, buffer.get(), size, &bytes, NULL)) {
LOG_GETLASTERROR(ERROR) << "Failed to send CreateProcessAsUser request";
return false;
}
return true;
}
// Requests the execution server to create a process in the specified session
// using the default (i.e. Winlogon) token. This routine relies on undocumented
// OS functionality and will likely not work on anything but XP or W2K3.
bool CreateRemoteSessionProcess(
uint32 session_id,
const base::FilePath::StringType& application_name,
const CommandLine::StringType& command_line,
DWORD creation_flags,
const char16* desktop_name,
PROCESS_INFORMATION* process_information_out)
{
DCHECK_LT(base::win::GetVersion(), base::win::VERSION_VISTA);
base::win::ScopedHandle pipe;
if (!ConnectToExecutionServer(session_id, &pipe))
return false;
if (!SendCreateProcessRequest(pipe, application_name, command_line,
creation_flags, desktop_name)) {
return false;
}
PROCESS_INFORMATION process_information;
if (!ReceiveCreateProcessResponse(pipe, &process_information))
return false;
if (!ProcessCreateProcessResponse(creation_flags, &process_information)) {
CloseHandlesAndTerminateProcess(&process_information);
return false;
}
*process_information_out = process_information;
return true;
}
} // namespace
namespace remoting {
base::LazyInstance<base::Lock>::Leaky g_inherit_handles_lock =
LAZY_INSTANCE_INITIALIZER;
// Creates a copy of the current process token for the given |session_id| so
// it can be used to launch a process in that session.
bool CreateSessionToken(uint32 session_id, ScopedHandle* token_out) {
ScopedHandle session_token;
DWORD desired_access = TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID |
TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY;
if (!CopyProcessToken(desired_access, &session_token)) {
return false;
}
// Temporarily enable the SE_TCB_NAME privilege as it is required by
// SetTokenInformation(TokenSessionId).
ScopedHandle privileged_token;
if (!CreatePrivilegedToken(&privileged_token)) {
return false;
}
if (!ImpersonateLoggedOnUser(privileged_token)) {
LOG_GETLASTERROR(ERROR) <<
"Failed to impersonate the privileged token";
return false;
}
// Change the session ID of the token.
DWORD new_session_id = session_id;
if (!SetTokenInformation(session_token,
TokenSessionId,
&new_session_id,
sizeof(new_session_id))) {
LOG_GETLASTERROR(ERROR) << "Failed to change session ID of a token";
// Revert to the default token.
CHECK(RevertToSelf());
return false;
}
// Revert to the default token.
CHECK(RevertToSelf());
*token_out = session_token.Pass();
return true;
}
bool LaunchProcessWithToken(const base::FilePath& binary,
const CommandLine::StringType& command_line,
HANDLE user_token,
SECURITY_ATTRIBUTES* process_attributes,
SECURITY_ATTRIBUTES* thread_attributes,
bool inherit_handles,
DWORD creation_flags,
const char16* desktop_name,
ScopedHandle* process_out,
ScopedHandle* thread_out) {
base::FilePath::StringType application_name = binary.value();
STARTUPINFOW startup_info;
memset(&startup_info, 0, sizeof(startup_info));
startup_info.cb = sizeof(startup_info);
if (desktop_name)
startup_info.lpDesktop = const_cast<char16*>(desktop_name);
PROCESS_INFORMATION temp_process_info = {};
BOOL result = CreateProcessAsUser(user_token,
application_name.c_str(),
const_cast<LPWSTR>(command_line.c_str()),
process_attributes,
thread_attributes,
inherit_handles,
creation_flags,
NULL,
NULL,
&startup_info,
&temp_process_info);
// CreateProcessAsUser will fail on XP and W2K3 with ERROR_PIPE_NOT_CONNECTED
// if the user hasn't logged to the target session yet. In such a case
// we try to talk to the execution server directly emulating what
// the undocumented and not-exported advapi32!CreateRemoteSessionProcessW()
// function does. The created process will run under Winlogon'a token instead
// of |user_token|. Since Winlogon runs as SYSTEM, this suits our needs.
if (!result &&
GetLastError() == ERROR_PIPE_NOT_CONNECTED &&
base::win::GetVersion() < base::win::VERSION_VISTA) {
DWORD session_id;
DWORD return_length;
result = GetTokenInformation(user_token,
TokenSessionId,
&session_id,
sizeof(session_id),
&return_length);
if (result && session_id != 0) {
result = CreateRemoteSessionProcess(session_id,
application_name,
command_line,
creation_flags,
desktop_name,
&temp_process_info);
} else {
// Restore the error status returned by CreateProcessAsUser().
result = FALSE;
SetLastError(ERROR_PIPE_NOT_CONNECTED);
}
}
if (!result) {
LOG_GETLASTERROR(ERROR) <<
"Failed to launch a process with a user token";
return false;
}
base::win::ScopedProcessInformation process_info(temp_process_info);
CHECK(process_info.IsValid());
process_out->Set(process_info.TakeProcessHandle());
thread_out->Set(process_info.TakeThreadHandle());
return true;
}
} // namespace remoting