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 23 using base::win::ScopedHandle; 24 25 namespace { 26 27 const 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. 33 const WINSTATIONINFOCLASS kCreateProcessPipeNameClass = 34 static_cast<WINSTATIONINFOCLASS>(0x21); 35 36 const int kPipeBusyWaitTimeoutMs = 2000; 37 const int kPipeConnectMaxAttempts = 3; 38 39 // Terminates the process and closes process and thread handles in 40 // |process_information| structure. 41 void 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|. 55 bool ConnectToExecutionServer(uint32 session_id, 56 base::win::ScopedHandle* pipe_out) { 57 base::string16 pipe_name; 58 59 // Use winsta!WinStationQueryInformationW() to determine the process creation 60 // pipe name for the session. 61 base::FilePath winsta_path( 62 base::GetNativeLibraryName(base::UTF8ToUTF16("winsta"))); 63 base::ScopedNativeLibrary winsta(winsta_path); 64 if (winsta.is_valid()) { 65 PWINSTATIONQUERYINFORMATIONW win_station_query_information = 66 reinterpret_cast<PWINSTATIONQUERYINFORMATIONW>( 67 winsta.GetFunctionPointer("WinStationQueryInformationW")); 68 if (win_station_query_information) { 69 wchar_t name[MAX_PATH]; 70 ULONG name_length; 71 if (win_station_query_information(0, 72 session_id, 73 kCreateProcessPipeNameClass, 74 name, 75 sizeof(name), 76 &name_length)) { 77 pipe_name.assign(name); 78 } 79 } 80 } 81 82 // Use the default pipe name if we couldn't query its name. 83 if (pipe_name.empty()) { 84 pipe_name = base::UTF8ToUTF16( 85 base::StringPrintf(kCreateProcessDefaultPipeNameFormat, session_id)); 86 } 87 88 // Try to connect to the named pipe. 89 base::win::ScopedHandle pipe; 90 for (int i = 0; i < kPipeConnectMaxAttempts; ++i) { 91 pipe.Set(CreateFile(pipe_name.c_str(), 92 GENERIC_READ | GENERIC_WRITE, 93 0, 94 NULL, 95 OPEN_EXISTING, 96 0, 97 NULL)); 98 if (pipe.IsValid()) { 99 break; 100 } 101 102 // Cannot continue retrying if error is something other than 103 // ERROR_PIPE_BUSY. 104 if (GetLastError() != ERROR_PIPE_BUSY) { 105 break; 106 } 107 108 // Cannot continue retrying if wait on pipe fails. 109 if (!WaitNamedPipe(pipe_name.c_str(), kPipeBusyWaitTimeoutMs)) { 110 break; 111 } 112 } 113 114 if (!pipe.IsValid()) { 115 PLOG(ERROR) << "Failed to connect to '" << pipe_name << "'"; 116 return false; 117 } 118 119 *pipe_out = pipe.Pass(); 120 return true; 121 } 122 123 // Copies the process token making it a primary impersonation token. 124 // The returned handle will have |desired_access| rights. 125 bool CopyProcessToken(DWORD desired_access, ScopedHandle* token_out) { 126 HANDLE temp_handle; 127 if (!OpenProcessToken(GetCurrentProcess(), 128 TOKEN_DUPLICATE | desired_access, 129 &temp_handle)) { 130 PLOG(ERROR) << "Failed to open process token"; 131 return false; 132 } 133 ScopedHandle process_token(temp_handle); 134 135 if (!DuplicateTokenEx(process_token.Get(), 136 desired_access, 137 NULL, 138 SecurityImpersonation, 139 TokenPrimary, 140 &temp_handle)) { 141 PLOG(ERROR) << "Failed to duplicate the process token"; 142 return false; 143 } 144 145 token_out->Set(temp_handle); 146 return true; 147 } 148 149 // Creates a copy of the current process with SE_TCB_NAME privilege enabled. 150 bool CreatePrivilegedToken(ScopedHandle* token_out) { 151 ScopedHandle privileged_token; 152 DWORD desired_access = TOKEN_ADJUST_PRIVILEGES | TOKEN_IMPERSONATE | 153 TOKEN_DUPLICATE | TOKEN_QUERY; 154 if (!CopyProcessToken(desired_access, &privileged_token)) { 155 return false; 156 } 157 158 // Get the LUID for the SE_TCB_NAME privilege. 159 TOKEN_PRIVILEGES state; 160 state.PrivilegeCount = 1; 161 state.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 162 if (!LookupPrivilegeValue(NULL, SE_TCB_NAME, &state.Privileges[0].Luid)) { 163 PLOG(ERROR) << "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.Get(), FALSE, &state, 0, NULL, 169 0)) { 170 PLOG(ERROR) << "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. 180 bool 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 PLOG(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 PLOG(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 PLOG(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. 254 bool 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 PLOG(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. 288 bool SendCreateProcessRequest( 289 HANDLE pipe, 290 const base::FilePath::StringType& application_name, 291 const base::CommandLine::StringType& command_line, 292 DWORD creation_flags, 293 const base::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 base::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 PLOG(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. 371 bool CreateRemoteSessionProcess( 372 uint32 session_id, 373 const base::FilePath::StringType& application_name, 374 const base::CommandLine::StringType& command_line, 375 DWORD creation_flags, 376 const base::char16* desktop_name, 377 PROCESS_INFORMATION* process_information_out) { 378 DCHECK_LT(base::win::GetVersion(), base::win::VERSION_VISTA); 379 380 base::win::ScopedHandle pipe; 381 if (!ConnectToExecutionServer(session_id, &pipe)) 382 return false; 383 384 if (!SendCreateProcessRequest(pipe.Get(), application_name, command_line, 385 creation_flags, desktop_name)) { 386 return false; 387 } 388 389 PROCESS_INFORMATION process_information; 390 if (!ReceiveCreateProcessResponse(pipe.Get(), &process_information)) 391 return false; 392 393 if (!ProcessCreateProcessResponse(creation_flags, &process_information)) { 394 CloseHandlesAndTerminateProcess(&process_information); 395 return false; 396 } 397 398 *process_information_out = process_information; 399 return true; 400 } 401 402 } // namespace 403 404 namespace remoting { 405 406 base::LazyInstance<base::Lock>::Leaky g_inherit_handles_lock = 407 LAZY_INSTANCE_INITIALIZER; 408 409 // Creates a copy of the current process token for the given |session_id| so 410 // it can be used to launch a process in that session. 411 bool CreateSessionToken(uint32 session_id, ScopedHandle* token_out) { 412 ScopedHandle session_token; 413 DWORD desired_access = TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID | 414 TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY; 415 if (!CopyProcessToken(desired_access, &session_token)) { 416 return false; 417 } 418 419 // Temporarily enable the SE_TCB_NAME privilege as it is required by 420 // SetTokenInformation(TokenSessionId). 421 ScopedHandle privileged_token; 422 if (!CreatePrivilegedToken(&privileged_token)) { 423 return false; 424 } 425 if (!ImpersonateLoggedOnUser(privileged_token.Get())) { 426 PLOG(ERROR) << "Failed to impersonate the privileged token"; 427 return false; 428 } 429 430 // Change the session ID of the token. 431 DWORD new_session_id = session_id; 432 if (!SetTokenInformation(session_token.Get(), 433 TokenSessionId, 434 &new_session_id, 435 sizeof(new_session_id))) { 436 PLOG(ERROR) << "Failed to change session ID of a token"; 437 438 // Revert to the default token. 439 CHECK(RevertToSelf()); 440 return false; 441 } 442 443 // Revert to the default token. 444 CHECK(RevertToSelf()); 445 446 *token_out = session_token.Pass(); 447 return true; 448 } 449 450 bool LaunchProcessWithToken(const base::FilePath& binary, 451 const base::CommandLine::StringType& command_line, 452 HANDLE user_token, 453 SECURITY_ATTRIBUTES* process_attributes, 454 SECURITY_ATTRIBUTES* thread_attributes, 455 bool inherit_handles, 456 DWORD creation_flags, 457 const base::char16* desktop_name, 458 ScopedHandle* process_out, 459 ScopedHandle* thread_out) { 460 base::FilePath::StringType application_name = binary.value(); 461 462 STARTUPINFOW startup_info; 463 memset(&startup_info, 0, sizeof(startup_info)); 464 startup_info.cb = sizeof(startup_info); 465 if (desktop_name) 466 startup_info.lpDesktop = const_cast<base::char16*>(desktop_name); 467 468 PROCESS_INFORMATION temp_process_info = {}; 469 BOOL result = CreateProcessAsUser(user_token, 470 application_name.c_str(), 471 const_cast<LPWSTR>(command_line.c_str()), 472 process_attributes, 473 thread_attributes, 474 inherit_handles, 475 creation_flags, 476 NULL, 477 NULL, 478 &startup_info, 479 &temp_process_info); 480 481 // CreateProcessAsUser will fail on XP and W2K3 with ERROR_PIPE_NOT_CONNECTED 482 // if the user hasn't logged to the target session yet. In such a case 483 // we try to talk to the execution server directly emulating what 484 // the undocumented and not-exported advapi32!CreateRemoteSessionProcessW() 485 // function does. The created process will run under Winlogon'a token instead 486 // of |user_token|. Since Winlogon runs as SYSTEM, this suits our needs. 487 if (!result && 488 GetLastError() == ERROR_PIPE_NOT_CONNECTED && 489 base::win::GetVersion() < base::win::VERSION_VISTA) { 490 DWORD session_id; 491 DWORD return_length; 492 result = GetTokenInformation(user_token, 493 TokenSessionId, 494 &session_id, 495 sizeof(session_id), 496 &return_length); 497 if (result && session_id != 0) { 498 result = CreateRemoteSessionProcess(session_id, 499 application_name, 500 command_line, 501 creation_flags, 502 desktop_name, 503 &temp_process_info); 504 } else { 505 // Restore the error status returned by CreateProcessAsUser(). 506 result = FALSE; 507 SetLastError(ERROR_PIPE_NOT_CONNECTED); 508 } 509 } 510 511 if (!result) { 512 PLOG(ERROR) << "Failed to launch a process with a user token"; 513 return false; 514 } 515 516 base::win::ScopedProcessInformation process_info(temp_process_info); 517 518 CHECK(process_info.IsValid()); 519 process_out->Set(process_info.TakeProcessHandle()); 520 thread_out->Set(process_info.TakeThreadHandle()); 521 return true; 522 } 523 524 } // namespace remoting 525