1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "remoting/host/win/launch_process_with_token.h"
6
7#include <windows.h>
8#include <winternl.h>
9
10#include <limits>
11
12#include "base/logging.h"
13#include "base/memory/scoped_ptr.h"
14#include "base/rand_util.h"
15#include "base/scoped_native_library.h"
16#include "base/strings/string16.h"
17#include "base/strings/stringprintf.h"
18#include "base/strings/utf_string_conversions.h"
19#include "base/win/scoped_handle.h"
20#include "base/win/scoped_process_information.h"
21#include "base/win/windows_version.h"
22
23using base::win::ScopedHandle;
24
25namespace {
26
27const char kCreateProcessDefaultPipeNameFormat[] =
28    "\\\\.\\Pipe\\TerminalServer\\SystemExecSrvr\\%d";
29
30// Undocumented WINSTATIONINFOCLASS value causing
31// winsta!WinStationQueryInformationW() to return the name of the pipe for
32// requesting cross-session process creation.
33const WINSTATIONINFOCLASS kCreateProcessPipeNameClass =
34    static_cast<WINSTATIONINFOCLASS>(0x21);
35
36const int kPipeBusyWaitTimeoutMs = 2000;
37const int kPipeConnectMaxAttempts = 3;
38
39// Terminates the process and closes process and thread handles in
40// |process_information| structure.
41void CloseHandlesAndTerminateProcess(PROCESS_INFORMATION* process_information) {
42  if (process_information->hThread) {
43    CloseHandle(process_information->hThread);
44    process_information->hThread = NULL;
45  }
46
47  if (process_information->hProcess) {
48    TerminateProcess(process_information->hProcess, CONTROL_C_EXIT);
49    CloseHandle(process_information->hProcess);
50    process_information->hProcess = NULL;
51  }
52}
53
54// Connects to the executor server corresponding to |session_id|.
55bool ConnectToExecutionServer(uint32 session_id,
56                              base::win::ScopedHandle* pipe_out) {
57  string16 pipe_name;
58
59  // Use winsta!WinStationQueryInformationW() to determine the process creation
60  // pipe name for the session.
61  base::FilePath winsta_path(base::GetNativeLibraryName(UTF8ToUTF16("winsta")));
62  base::ScopedNativeLibrary winsta(winsta_path);
63  if (winsta.is_valid()) {
64    PWINSTATIONQUERYINFORMATIONW win_station_query_information =
65        static_cast<PWINSTATIONQUERYINFORMATIONW>(
66            winsta.GetFunctionPointer("WinStationQueryInformationW"));
67    if (win_station_query_information) {
68      wchar_t name[MAX_PATH];
69      ULONG name_length;
70      if (win_station_query_information(0,
71                                        session_id,
72                                        kCreateProcessPipeNameClass,
73                                        name,
74                                        sizeof(name),
75                                        &name_length)) {
76        pipe_name.assign(name);
77      }
78    }
79  }
80
81  // Use the default pipe name if we couldn't query its name.
82  if (pipe_name.empty()) {
83    pipe_name = UTF8ToUTF16(
84        base::StringPrintf(kCreateProcessDefaultPipeNameFormat, session_id));
85  }
86
87  // Try to connect to the named pipe.
88  base::win::ScopedHandle pipe;
89  for (int i = 0; i < kPipeConnectMaxAttempts; ++i) {
90    pipe.Set(CreateFile(pipe_name.c_str(),
91                        GENERIC_READ | GENERIC_WRITE,
92                        0,
93                        NULL,
94                        OPEN_EXISTING,
95                        0,
96                        NULL));
97    if (pipe.IsValid()) {
98      break;
99    }
100
101    // Cannot continue retrying if error is something other than
102    // ERROR_PIPE_BUSY.
103    if (GetLastError() != ERROR_PIPE_BUSY) {
104      break;
105    }
106
107    // Cannot continue retrying if wait on pipe fails.
108    if (!WaitNamedPipe(pipe_name.c_str(), kPipeBusyWaitTimeoutMs)) {
109      break;
110    }
111  }
112
113  if (!pipe.IsValid()) {
114    LOG_GETLASTERROR(ERROR) << "Failed to connect to '" << pipe_name << "'";
115    return false;
116  }
117
118  *pipe_out = pipe.Pass();
119  return true;
120}
121
122// Copies the process token making it a primary impersonation token.
123// The returned handle will have |desired_access| rights.
124bool CopyProcessToken(DWORD desired_access, ScopedHandle* token_out) {
125  ScopedHandle process_token;
126  if (!OpenProcessToken(GetCurrentProcess(),
127                        TOKEN_DUPLICATE | desired_access,
128                        process_token.Receive())) {
129    LOG_GETLASTERROR(ERROR) << "Failed to open process token";
130    return false;
131  }
132
133  ScopedHandle copied_token;
134  if (!DuplicateTokenEx(process_token,
135                        desired_access,
136                        NULL,
137                        SecurityImpersonation,
138                        TokenPrimary,
139                        copied_token.Receive())) {
140    LOG_GETLASTERROR(ERROR) << "Failed to duplicate the process token";
141    return false;
142  }
143
144  *token_out = copied_token.Pass();
145  return true;
146}
147
148// Creates a copy of the current process with SE_TCB_NAME privilege enabled.
149bool CreatePrivilegedToken(ScopedHandle* token_out) {
150  ScopedHandle privileged_token;
151  DWORD desired_access = TOKEN_ADJUST_PRIVILEGES | TOKEN_IMPERSONATE |
152                         TOKEN_DUPLICATE | TOKEN_QUERY;
153  if (!CopyProcessToken(desired_access, &privileged_token)) {
154    return false;
155  }
156
157  // Get the LUID for the SE_TCB_NAME privilege.
158  TOKEN_PRIVILEGES state;
159  state.PrivilegeCount = 1;
160  state.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
161  if (!LookupPrivilegeValue(NULL, SE_TCB_NAME, &state.Privileges[0].Luid)) {
162    LOG_GETLASTERROR(ERROR) <<
163        "Failed to lookup the LUID for the SE_TCB_NAME privilege";
164    return false;
165  }
166
167  // Enable the SE_TCB_NAME privilege.
168  if (!AdjustTokenPrivileges(privileged_token, FALSE, &state, 0, NULL, 0)) {
169    LOG_GETLASTERROR(ERROR) <<
170        "Failed to enable SE_TCB_NAME privilege in a token";
171    return false;
172  }
173
174  *token_out = privileged_token.Pass();
175  return true;
176}
177
178// Fills the process and thread handles in the passed |process_information|
179// structure and resume the process if the caller didn't want to suspend it.
180bool ProcessCreateProcessResponse(DWORD creation_flags,
181                                  PROCESS_INFORMATION* process_information) {
182  // The execution server does not return handles to the created process and
183  // thread.
184  if (!process_information->hProcess) {
185    // N.B. PROCESS_ALL_ACCESS is different in XP and Vista+ versions of
186    // the SDK. |desired_access| below is effectively PROCESS_ALL_ACCESS from
187    // the XP version of the SDK.
188    DWORD desired_access =
189        STANDARD_RIGHTS_REQUIRED |
190        SYNCHRONIZE |
191        PROCESS_TERMINATE |
192        PROCESS_CREATE_THREAD |
193        PROCESS_SET_SESSIONID |
194        PROCESS_VM_OPERATION |
195        PROCESS_VM_READ |
196        PROCESS_VM_WRITE |
197        PROCESS_DUP_HANDLE |
198        PROCESS_CREATE_PROCESS |
199        PROCESS_SET_QUOTA |
200        PROCESS_SET_INFORMATION |
201        PROCESS_QUERY_INFORMATION |
202        PROCESS_SUSPEND_RESUME;
203    process_information->hProcess =
204        OpenProcess(desired_access,
205                    FALSE,
206                    process_information->dwProcessId);
207    if (!process_information->hProcess) {
208      LOG_GETLASTERROR(ERROR) << "Failed to open the process "
209                              << process_information->dwProcessId;
210      return false;
211    }
212  }
213
214  if (!process_information->hThread) {
215    // N.B. THREAD_ALL_ACCESS is different in XP and Vista+ versions of
216    // the SDK. |desired_access| below is effectively THREAD_ALL_ACCESS from
217    // the XP version of the SDK.
218    DWORD desired_access =
219        STANDARD_RIGHTS_REQUIRED |
220        SYNCHRONIZE |
221        THREAD_TERMINATE |
222        THREAD_SUSPEND_RESUME |
223        THREAD_GET_CONTEXT |
224        THREAD_SET_CONTEXT |
225        THREAD_QUERY_INFORMATION |
226        THREAD_SET_INFORMATION |
227        THREAD_SET_THREAD_TOKEN |
228        THREAD_IMPERSONATE |
229        THREAD_DIRECT_IMPERSONATION;
230    process_information->hThread =
231        OpenThread(desired_access,
232                   FALSE,
233                   process_information->dwThreadId);
234    if (!process_information->hThread) {
235      LOG_GETLASTERROR(ERROR) << "Failed to open the thread "
236                              << process_information->dwThreadId;
237      return false;
238    }
239  }
240
241  // Resume the thread if the caller didn't want to suspend the process.
242  if ((creation_flags & CREATE_SUSPENDED) == 0) {
243    if (!ResumeThread(process_information->hThread)) {
244      LOG_GETLASTERROR(ERROR) << "Failed to resume the thread "
245                              << process_information->dwThreadId;
246      return false;
247    }
248  }
249
250  return true;
251}
252
253// Receives the response to a remote process create request.
254bool ReceiveCreateProcessResponse(
255    HANDLE pipe,
256    PROCESS_INFORMATION* process_information_out) {
257  struct CreateProcessResponse {
258    DWORD size;
259    BOOL success;
260    DWORD last_error;
261    PROCESS_INFORMATION process_information;
262  };
263
264  DWORD bytes;
265  CreateProcessResponse response;
266  if (!ReadFile(pipe, &response, sizeof(response), &bytes, NULL)) {
267    LOG_GETLASTERROR(ERROR) << "Failed to receive CreateProcessAsUser response";
268    return false;
269  }
270
271  // The server sends the data in one chunk so if we didn't received a complete
272  // answer something bad happend and there is no point in retrying.
273  if (bytes != sizeof(response)) {
274    SetLastError(ERROR_RECEIVE_PARTIAL);
275    return false;
276  }
277
278  if (!response.success) {
279    SetLastError(response.last_error);
280    return false;
281  }
282
283  *process_information_out = response.process_information;
284  return true;
285}
286
287// Sends a remote process create request to the execution server.
288bool SendCreateProcessRequest(
289    HANDLE pipe,
290    const base::FilePath::StringType& application_name,
291    const CommandLine::StringType& command_line,
292    DWORD creation_flags,
293    const char16* desktop_name) {
294  // |CreateProcessRequest| structure passes the same parameters to
295  // the execution server as CreateProcessAsUser() function does. Strings are
296  // stored as wide strings immediately after the structure. String pointers are
297  // represented as byte offsets to string data from the beginning of
298  // the structure.
299  struct CreateProcessRequest {
300    DWORD size;
301    DWORD process_id;
302    BOOL use_default_token;
303    HANDLE token;
304    LPWSTR application_name;
305    LPWSTR command_line;
306    SECURITY_ATTRIBUTES process_attributes;
307    SECURITY_ATTRIBUTES thread_attributes;
308    BOOL inherit_handles;
309    DWORD creation_flags;
310    LPVOID environment;
311    LPWSTR current_directory;
312    STARTUPINFOW startup_info;
313    PROCESS_INFORMATION process_information;
314  };
315
316  string16 desktop;
317  if (desktop_name)
318    desktop = desktop_name;
319
320  // Allocate a large enough buffer to hold the CreateProcessRequest structure
321  // and three NULL-terminated string parameters.
322  size_t size = sizeof(CreateProcessRequest) + sizeof(wchar_t) *
323      (application_name.size() + command_line.size() + desktop.size() + 3);
324  scoped_ptr<char[]> buffer(new char[size]);
325  memset(buffer.get(), 0, size);
326
327  // Marshal the input parameters.
328  CreateProcessRequest* request =
329      reinterpret_cast<CreateProcessRequest*>(buffer.get());
330  request->size = size;
331  request->process_id = GetCurrentProcessId();
332  request->use_default_token = TRUE;
333  // Always pass CREATE_SUSPENDED to avoid a race between the created process
334  // exiting too soon and OpenProcess() call below.
335  request->creation_flags = creation_flags | CREATE_SUSPENDED;
336  request->startup_info.cb = sizeof(request->startup_info);
337
338  size_t buffer_offset = sizeof(CreateProcessRequest);
339
340  request->application_name = reinterpret_cast<LPWSTR>(buffer_offset);
341  std::copy(application_name.begin(),
342            application_name.end(),
343            reinterpret_cast<wchar_t*>(buffer.get() + buffer_offset));
344  buffer_offset += (application_name.size() + 1) * sizeof(wchar_t);
345
346  request->command_line = reinterpret_cast<LPWSTR>(buffer_offset);
347  std::copy(command_line.begin(),
348            command_line.end(),
349            reinterpret_cast<wchar_t*>(buffer.get() + buffer_offset));
350  buffer_offset += (command_line.size() + 1) * sizeof(wchar_t);
351
352  request->startup_info.lpDesktop =
353      reinterpret_cast<LPWSTR>(buffer_offset);
354  std::copy(desktop.begin(),
355            desktop.end(),
356            reinterpret_cast<wchar_t*>(buffer.get() + buffer_offset));
357
358  // Pass the request to create a process in the target session.
359  DWORD bytes;
360  if (!WriteFile(pipe, buffer.get(), size, &bytes, NULL)) {
361    LOG_GETLASTERROR(ERROR) << "Failed to send CreateProcessAsUser request";
362    return false;
363  }
364
365  return true;
366}
367
368// Requests the execution server to create a process in the specified session
369// using the default (i.e. Winlogon) token. This routine relies on undocumented
370// OS functionality and will likely not work on anything but XP or W2K3.
371bool CreateRemoteSessionProcess(
372    uint32 session_id,
373    const base::FilePath::StringType& application_name,
374    const CommandLine::StringType& command_line,
375    DWORD creation_flags,
376    const char16* desktop_name,
377    PROCESS_INFORMATION* process_information_out)
378{
379  DCHECK_LT(base::win::GetVersion(), base::win::VERSION_VISTA);
380
381  base::win::ScopedHandle pipe;
382  if (!ConnectToExecutionServer(session_id, &pipe))
383    return false;
384
385  if (!SendCreateProcessRequest(pipe, application_name, command_line,
386                                creation_flags, desktop_name)) {
387    return false;
388  }
389
390  PROCESS_INFORMATION process_information;
391  if (!ReceiveCreateProcessResponse(pipe, &process_information))
392    return false;
393
394  if (!ProcessCreateProcessResponse(creation_flags, &process_information)) {
395    CloseHandlesAndTerminateProcess(&process_information);
396    return false;
397  }
398
399  *process_information_out = process_information;
400  return true;
401}
402
403} // namespace
404
405namespace remoting {
406
407base::LazyInstance<base::Lock>::Leaky g_inherit_handles_lock =
408    LAZY_INSTANCE_INITIALIZER;
409
410// Creates a copy of the current process token for the given |session_id| so
411// it can be used to launch a process in that session.
412bool CreateSessionToken(uint32 session_id, ScopedHandle* token_out) {
413  ScopedHandle session_token;
414  DWORD desired_access = TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID |
415                         TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY;
416  if (!CopyProcessToken(desired_access, &session_token)) {
417    return false;
418  }
419
420  // Temporarily enable the SE_TCB_NAME privilege as it is required by
421  // SetTokenInformation(TokenSessionId).
422  ScopedHandle privileged_token;
423  if (!CreatePrivilegedToken(&privileged_token)) {
424    return false;
425  }
426  if (!ImpersonateLoggedOnUser(privileged_token)) {
427    LOG_GETLASTERROR(ERROR) <<
428        "Failed to impersonate the privileged token";
429    return false;
430  }
431
432  // Change the session ID of the token.
433  DWORD new_session_id = session_id;
434  if (!SetTokenInformation(session_token,
435                           TokenSessionId,
436                           &new_session_id,
437                           sizeof(new_session_id))) {
438    LOG_GETLASTERROR(ERROR) << "Failed to change session ID of a token";
439
440    // Revert to the default token.
441    CHECK(RevertToSelf());
442    return false;
443  }
444
445  // Revert to the default token.
446  CHECK(RevertToSelf());
447
448  *token_out = session_token.Pass();
449  return true;
450}
451
452bool LaunchProcessWithToken(const base::FilePath& binary,
453                            const CommandLine::StringType& command_line,
454                            HANDLE user_token,
455                            SECURITY_ATTRIBUTES* process_attributes,
456                            SECURITY_ATTRIBUTES* thread_attributes,
457                            bool inherit_handles,
458                            DWORD creation_flags,
459                            const char16* desktop_name,
460                            ScopedHandle* process_out,
461                            ScopedHandle* thread_out) {
462  base::FilePath::StringType application_name = binary.value();
463
464  STARTUPINFOW startup_info;
465  memset(&startup_info, 0, sizeof(startup_info));
466  startup_info.cb = sizeof(startup_info);
467  if (desktop_name)
468    startup_info.lpDesktop = const_cast<char16*>(desktop_name);
469
470  base::win::ScopedProcessInformation process_info;
471  BOOL result = CreateProcessAsUser(user_token,
472                                    application_name.c_str(),
473                                    const_cast<LPWSTR>(command_line.c_str()),
474                                    process_attributes,
475                                    thread_attributes,
476                                    inherit_handles,
477                                    creation_flags,
478                                    NULL,
479                                    NULL,
480                                    &startup_info,
481                                    process_info.Receive());
482
483  // CreateProcessAsUser will fail on XP and W2K3 with ERROR_PIPE_NOT_CONNECTED
484  // if the user hasn't logged to the target session yet. In such a case
485  // we try to talk to the execution server directly emulating what
486  // the undocumented and not-exported advapi32!CreateRemoteSessionProcessW()
487  // function does. The created process will run under Winlogon'a token instead
488  // of |user_token|. Since Winlogon runs as SYSTEM, this suits our needs.
489  if (!result &&
490      GetLastError() == ERROR_PIPE_NOT_CONNECTED &&
491      base::win::GetVersion() < base::win::VERSION_VISTA) {
492    DWORD session_id;
493    DWORD return_length;
494    result = GetTokenInformation(user_token,
495                                 TokenSessionId,
496                                 &session_id,
497                                 sizeof(session_id),
498                                 &return_length);
499    if (result && session_id != 0) {
500      result = CreateRemoteSessionProcess(session_id,
501                                          application_name,
502                                          command_line,
503                                          creation_flags,
504                                          desktop_name,
505                                          process_info.Receive());
506    } else {
507      // Restore the error status returned by CreateProcessAsUser().
508      result = FALSE;
509      SetLastError(ERROR_PIPE_NOT_CONNECTED);
510    }
511  }
512
513  if (!result) {
514    LOG_GETLASTERROR(ERROR) <<
515        "Failed to launch a process with a user token";
516    return false;
517  }
518
519  CHECK(process_info.IsValid());
520  process_out->Set(process_info.TakeProcessHandle());
521  thread_out->Set(process_info.TakeThreadHandle());
522  return true;
523}
524
525} // namespace remoting
526