1 /*
2 * barrier -- mouse and keyboard sharing utility
3 * Copyright (C) 2012-2016 Symless Ltd.
4 * Copyright (C) 2009 Chris Schoeneman
5 *
6 * This package is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * found in the file LICENSE that should have accompanied this file.
9 *
10 * This package is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include "platform/MSWindowsWatchdog.h"
20
21 #include "ipc/IpcLogOutputter.h"
22 #include "ipc/IpcServer.h"
23 #include "ipc/IpcMessage.h"
24 #include "ipc/Ipc.h"
25 #include "barrier/App.h"
26 #include "barrier/ArgsBase.h"
27 #include "mt/Thread.h"
28 #include "arch/win32/ArchDaemonWindows.h"
29 #include "arch/win32/XArchWindows.h"
30 #include "arch/Arch.h"
31 #include "base/log_outputters.h"
32 #include "base/TMethodJob.h"
33 #include "base/Log.h"
34 #include "common/Version.h"
35
36 #include <sstream>
37 #include <UserEnv.h>
38 #include <Shellapi.h>
39
40 #define MAXIMUM_WAIT_TIME 3
41 enum {
42 kOutputBufferSize = 4096
43 };
44
45 typedef VOID (WINAPI *SendSas)(BOOL asUser);
46
activeDesktopName()47 std::string activeDesktopName()
48 {
49 const std::size_t BufferLength = 1024;
50 std::string name;
51 HDESK desk = OpenInputDesktop(0, FALSE, GENERIC_READ);
52 if (desk != NULL) {
53 TCHAR buffer[BufferLength];
54 if (GetUserObjectInformation(desk, UOI_NAME, buffer, BufferLength - 1, NULL) == TRUE)
55 name = buffer;
56 CloseDesktop(desk);
57 }
58 LOG((CLOG_DEBUG "found desktop name: %.64s", name.c_str()));
59 return name;
60 }
61
MSWindowsWatchdog(bool daemonized,bool autoDetectCommand,IpcServer & ipcServer,IpcLogOutputter & ipcLogOutputter)62 MSWindowsWatchdog::MSWindowsWatchdog(
63 bool daemonized,
64 bool autoDetectCommand,
65 IpcServer& ipcServer,
66 IpcLogOutputter& ipcLogOutputter) :
67 m_thread(NULL),
68 m_autoDetectCommand(autoDetectCommand),
69 m_monitoring(true),
70 m_commandChanged(false),
71 m_stdOutWrite(NULL),
72 m_stdOutRead(NULL),
73 m_ipcServer(ipcServer),
74 m_ipcLogOutputter(ipcLogOutputter),
75 m_elevateProcess(false),
76 m_processFailures(0),
77 m_processRunning(false),
78 m_fileLogOutputter(NULL),
79 m_autoElevated(false),
80 m_daemonized(daemonized)
81 {
82 }
83
84 void
startAsync()85 MSWindowsWatchdog::startAsync()
86 {
87 m_thread = new Thread(new TMethodJob<MSWindowsWatchdog>(
88 this, &MSWindowsWatchdog::mainLoop, nullptr));
89
90 m_outputThread = new Thread(new TMethodJob<MSWindowsWatchdog>(
91 this, &MSWindowsWatchdog::outputLoop, nullptr));
92 }
93
94 void
stop()95 MSWindowsWatchdog::stop()
96 {
97 m_monitoring = false;
98
99 m_thread->wait(5);
100 delete m_thread;
101
102 m_outputThread->wait(5);
103 delete m_outputThread;
104 }
105
106 HANDLE
duplicateProcessToken(HANDLE process,LPSECURITY_ATTRIBUTES security)107 MSWindowsWatchdog::duplicateProcessToken(HANDLE process, LPSECURITY_ATTRIBUTES security)
108 {
109 HANDLE sourceToken;
110
111 BOOL tokenRet = OpenProcessToken(
112 process,
113 TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS,
114 &sourceToken);
115
116 if (!tokenRet) {
117 LOG((CLOG_ERR "could not open token, process handle: %d", process));
118 throw XArch(new XArchEvalWindows());
119 }
120
121 LOG((CLOG_DEBUG "got token %i, duplicating", sourceToken));
122
123 HANDLE newToken;
124 BOOL duplicateRet = DuplicateTokenEx(
125 sourceToken, TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS, security,
126 SecurityImpersonation, TokenPrimary, &newToken);
127
128 if (!duplicateRet) {
129 LOG((CLOG_ERR "could not duplicate token %i", sourceToken));
130 throw XArch(new XArchEvalWindows());
131 }
132
133 LOG((CLOG_DEBUG "duplicated, new token: %i", newToken));
134 return newToken;
135 }
136
137 HANDLE
getUserToken(LPSECURITY_ATTRIBUTES security)138 MSWindowsWatchdog::getUserToken(LPSECURITY_ATTRIBUTES security)
139 {
140 // always elevate if we are at the vista/7 login screen. we could also
141 // elevate for the uac dialog (consent.exe) but this would be pointless,
142 // since barrier would re-launch as non-elevated after the desk switch,
143 // and so would be unusable with the new elevated process taking focus.
144 if (m_elevateProcess || m_autoElevated) {
145 LOG((CLOG_DEBUG "getting elevated token, %s",
146 (m_elevateProcess ? "elevation required" : "at login screen")));
147
148 HANDLE process;
149 if (!m_session.isProcessInSession("winlogon.exe", &process)) {
150 throw XMSWindowsWatchdogError("cannot get user token without winlogon.exe");
151 }
152
153 return duplicateProcessToken(process, security);
154 } else {
155 LOG((CLOG_DEBUG "getting non-elevated token"));
156 return m_session.getUserToken(security);
157 }
158 }
159
160 void
mainLoop(void *)161 MSWindowsWatchdog::mainLoop(void*)
162 {
163 shutdownExistingProcesses();
164
165 SendSas sendSasFunc = NULL;
166 HINSTANCE sasLib = LoadLibrary("sas.dll");
167 if (sasLib) {
168 LOG((CLOG_DEBUG "found sas.dll"));
169 sendSasFunc = (SendSas)GetProcAddress(sasLib, "SendSAS");
170 }
171
172 SECURITY_ATTRIBUTES saAttr;
173 saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
174 saAttr.bInheritHandle = TRUE;
175 saAttr.lpSecurityDescriptor = NULL;
176
177 if (!CreatePipe(&m_stdOutRead, &m_stdOutWrite, &saAttr, 0)) {
178 throw XArch(new XArchEvalWindows());
179 }
180
181 ZeroMemory(&m_processInfo, sizeof(PROCESS_INFORMATION));
182
183 while (m_monitoring) {
184 try {
185
186 if (m_processRunning && getCommand().empty()) {
187 LOG((CLOG_INFO "process started but command is empty, shutting down"));
188 shutdownExistingProcesses();
189 m_processRunning = false;
190 continue;
191 }
192
193 if (m_processFailures != 0) {
194 // increasing backoff period, maximum of 10 seconds.
195 int timeout = (m_processFailures * 2) < 10 ? (m_processFailures * 2) : 10;
196 LOG((CLOG_INFO "backing off, wait=%ds, failures=%d", timeout, m_processFailures));
197 ARCH->sleep(timeout);
198 }
199
200 if (!getCommand().empty() && ((m_processFailures != 0) || m_session.hasChanged() || m_commandChanged)) {
201 startProcess();
202 }
203
204 if (m_processRunning && !isProcessActive()) {
205
206 m_processFailures++;
207 m_processRunning = false;
208
209 LOG((CLOG_WARN "detected application not running, pid=%d",
210 m_processInfo.dwProcessId));
211 }
212
213 if (sendSasFunc != NULL) {
214
215 HANDLE sendSasEvent = CreateEvent(NULL, FALSE, FALSE, "Global\\SendSAS");
216 if (sendSasEvent != NULL) {
217
218 // use SendSAS event to wait for next session (timeout 1 second).
219 if (WaitForSingleObject(sendSasEvent, 1000) == WAIT_OBJECT_0) {
220 LOG((CLOG_DEBUG "calling SendSAS"));
221 sendSasFunc(FALSE);
222 }
223
224 CloseHandle(sendSasEvent);
225 continue;
226 }
227 }
228
229 // if the sas event failed, wait by sleeping.
230 ARCH->sleep(1);
231
232 }
233 catch (std::exception& e) {
234 LOG((CLOG_ERR "failed to launch, error: %s", e.what()));
235 m_processFailures++;
236 m_processRunning = false;
237 continue;
238 }
239 catch (...) {
240 LOG((CLOG_ERR "failed to launch, unknown error."));
241 m_processFailures++;
242 m_processRunning = false;
243 continue;
244 }
245 }
246
247 if (m_processRunning) {
248 LOG((CLOG_DEBUG "terminated running process on exit"));
249 shutdownProcess(m_processInfo.hProcess, m_processInfo.dwProcessId, 20);
250 }
251
252 LOG((CLOG_DEBUG "watchdog main thread finished"));
253 }
254
255 bool
isProcessActive()256 MSWindowsWatchdog::isProcessActive()
257 {
258 DWORD exitCode;
259 GetExitCodeProcess(m_processInfo.hProcess, &exitCode);
260 return exitCode == STILL_ACTIVE;
261 }
262
263 void
setFileLogOutputter(FileLogOutputter * outputter)264 MSWindowsWatchdog::setFileLogOutputter(FileLogOutputter* outputter)
265 {
266 m_fileLogOutputter = outputter;
267 }
268
269 void
startProcess()270 MSWindowsWatchdog::startProcess()
271 {
272 if (m_command.empty()) {
273 throw XMSWindowsWatchdogError("cannot start process, command is empty");
274 }
275
276 m_commandChanged = false;
277
278 if (m_processRunning) {
279 LOG((CLOG_DEBUG "closing existing process to make way for new one"));
280 shutdownProcess(m_processInfo.hProcess, m_processInfo.dwProcessId, 20);
281 m_processRunning = false;
282 }
283
284 m_session.updateActiveSession();
285
286 BOOL createRet;
287 if (!m_daemonized) {
288 createRet = doStartProcessAsSelf(m_command);
289 } else {
290 m_autoElevated = activeDesktopName() != "Default";
291
292 SECURITY_ATTRIBUTES sa{ 0 };
293 HANDLE userToken = getUserToken(&sa);
294 m_elevateProcess = m_autoElevated ? m_autoElevated : m_elevateProcess;
295 m_autoElevated = false;
296
297 // patch by Jack Zhou and Henry Tung
298 // set UIAccess to fix Windows 8 GUI interaction
299 // http://symless.com/spit/issues/details/3338/#c70
300 DWORD uiAccess = 1;
301 SetTokenInformation(userToken, TokenUIAccess, &uiAccess, sizeof(DWORD));
302
303 createRet = doStartProcessAsUser(m_command, userToken, &sa);
304 }
305
306 if (!createRet) {
307 LOG((CLOG_ERR "could not launch"));
308 DWORD exitCode = 0;
309 GetExitCodeProcess(m_processInfo.hProcess, &exitCode);
310 LOG((CLOG_ERR "exit code: %d", exitCode));
311 throw XArch(new XArchEvalWindows);
312 }
313 else {
314 // wait for program to fail.
315 ARCH->sleep(1);
316 if (!isProcessActive()) {
317 throw XMSWindowsWatchdogError("process immediately stopped");
318 }
319
320 m_processRunning = true;
321 m_processFailures = 0;
322
323 LOG((CLOG_DEBUG "started process, session=%i, elevated: %s, command=%s",
324 m_session.getActiveSessionId(),
325 m_elevateProcess ? "yes" : "no",
326 m_command.c_str()));
327 }
328 }
329
doStartProcessAsSelf(std::string & command)330 BOOL MSWindowsWatchdog::doStartProcessAsSelf(std::string& command)
331 {
332 DWORD creationFlags =
333 NORMAL_PRIORITY_CLASS |
334 CREATE_NO_WINDOW |
335 CREATE_UNICODE_ENVIRONMENT;
336
337 STARTUPINFO si;
338 ZeroMemory(&si, sizeof(STARTUPINFO));
339 si.cb = sizeof(STARTUPINFO);
340 si.lpDesktop = "winsta0\\Default"; // TODO: maybe this should be \winlogon if we have logonui.exe?
341 si.hStdError = m_stdOutWrite;
342 si.hStdOutput = m_stdOutWrite;
343 si.dwFlags |= STARTF_USESTDHANDLES;
344
345 LOG((CLOG_INFO "starting new process as self"));
346 return CreateProcess(NULL, LPSTR(command.c_str()), NULL, NULL, FALSE, creationFlags, NULL, NULL, &si, &m_processInfo);
347 }
348
doStartProcessAsUser(std::string & command,HANDLE userToken,LPSECURITY_ATTRIBUTES sa)349 BOOL MSWindowsWatchdog::doStartProcessAsUser(std::string& command, HANDLE userToken,
350 LPSECURITY_ATTRIBUTES sa)
351 {
352 // clear, as we're reusing process info struct
353 ZeroMemory(&m_processInfo, sizeof(PROCESS_INFORMATION));
354
355 STARTUPINFO si;
356 ZeroMemory(&si, sizeof(STARTUPINFO));
357 si.cb = sizeof(STARTUPINFO);
358 si.lpDesktop = "winsta0\\Default"; // TODO: maybe this should be \winlogon if we have logonui.exe?
359 si.hStdError = m_stdOutWrite;
360 si.hStdOutput = m_stdOutWrite;
361 si.dwFlags |= STARTF_USESTDHANDLES;
362
363 LPVOID environment;
364 BOOL blockRet = CreateEnvironmentBlock(&environment, userToken, FALSE);
365 if (!blockRet) {
366 LOG((CLOG_ERR "could not create environment block"));
367 throw XArch(new XArchEvalWindows);
368 }
369
370 DWORD creationFlags =
371 NORMAL_PRIORITY_CLASS |
372 CREATE_NO_WINDOW |
373 CREATE_UNICODE_ENVIRONMENT;
374
375 // re-launch in current active user session
376 LOG((CLOG_INFO "starting new process as privileged user"));
377 BOOL createRet = CreateProcessAsUser(
378 userToken, NULL, LPSTR(command.c_str()),
379 sa, NULL, TRUE, creationFlags,
380 environment, NULL, &si, &m_processInfo);
381
382 DestroyEnvironmentBlock(environment);
383 CloseHandle(userToken);
384
385 return createRet;
386 }
387
388 void
setCommand(const std::string & command,bool elevate)389 MSWindowsWatchdog::setCommand(const std::string& command, bool elevate)
390 {
391 LOG((CLOG_INFO "service command updated"));
392 m_command = command;
393 m_elevateProcess = elevate;
394 m_commandChanged = true;
395 m_processFailures = 0;
396 }
397
398 std::string
getCommand() const399 MSWindowsWatchdog::getCommand() const
400 {
401 if (!m_autoDetectCommand) {
402 return m_command;
403 }
404
405 // seems like a fairly convoluted way to get the process name
406 const char* launchName = App::instance().argsBase().m_exename.c_str();
407 std::string args = ARCH->commandLine();
408
409 // build up a full command line
410 std::stringstream cmdTemp;
411 cmdTemp << launchName << args;
412
413 std::string cmd = cmdTemp.str();
414
415 size_t i;
416 std::string find = "--relaunch";
417 while ((i = cmd.find(find)) != std::string::npos) {
418 cmd.replace(i, find.length(), "");
419 }
420
421 return cmd;
422 }
423
424 void
outputLoop(void *)425 MSWindowsWatchdog::outputLoop(void*)
426 {
427 // +1 char for \0
428 CHAR buffer[kOutputBufferSize + 1];
429
430 while (m_monitoring) {
431
432 DWORD bytesRead;
433 BOOL success = ReadFile(m_stdOutRead, buffer, kOutputBufferSize, &bytesRead, NULL);
434
435 // assume the process has gone away? slow down
436 // the reads until another one turns up.
437 if (!success || bytesRead == 0) {
438 ARCH->sleep(1);
439 }
440 else {
441 buffer[bytesRead] = '\0';
442 m_ipcLogOutputter.write(kINFO, buffer);
443 if (m_fileLogOutputter != NULL) {
444 m_fileLogOutputter->write(kINFO, buffer);
445 }
446 }
447 }
448 }
449
450 void
shutdownProcess(HANDLE handle,DWORD pid,int timeout)451 MSWindowsWatchdog::shutdownProcess(HANDLE handle, DWORD pid, int timeout)
452 {
453 DWORD exitCode;
454 GetExitCodeProcess(handle, &exitCode);
455 if (exitCode != STILL_ACTIVE) {
456 return;
457 }
458
459 IpcShutdownMessage shutdown;
460 m_ipcServer.send(shutdown, kIpcClientNode);
461
462 // wait for process to exit gracefully.
463 double start = ARCH->time();
464 while (true) {
465
466 GetExitCodeProcess(handle, &exitCode);
467 if (exitCode != STILL_ACTIVE) {
468 // yay, we got a graceful shutdown. there should be no hook in use errors!
469 LOG((CLOG_INFO "process %d was shutdown gracefully", pid));
470 break;
471 }
472 else {
473
474 double elapsed = (ARCH->time() - start);
475 if (elapsed > timeout) {
476 // if timeout reached, kill forcefully.
477 // calling TerminateProcess on barrier is very bad!
478 // it causes the hook DLL to stay loaded in some apps,
479 // making it impossible to start barrier again.
480 LOG((CLOG_WARN "shutdown timed out after %d secs, forcefully terminating", (int)elapsed));
481 TerminateProcess(handle, kExitSuccess);
482 break;
483 }
484
485 ARCH->sleep(1);
486 }
487 }
488 }
489
490 void
shutdownExistingProcesses()491 MSWindowsWatchdog::shutdownExistingProcesses()
492 {
493 // first we need to take a snapshot of the running processes
494 HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
495 if (snapshot == INVALID_HANDLE_VALUE) {
496 LOG((CLOG_ERR "could not get process snapshot"));
497 throw XArch(new XArchEvalWindows);
498 }
499
500 PROCESSENTRY32 entry;
501 entry.dwSize = sizeof(PROCESSENTRY32);
502
503 // get the first process, and if we can't do that then it's
504 // unlikely we can go any further
505 BOOL gotEntry = Process32First(snapshot, &entry);
506 if (!gotEntry) {
507 LOG((CLOG_ERR "could not get first process entry"));
508 throw XArch(new XArchEvalWindows);
509 }
510
511 // now just iterate until we can find winlogon.exe pid
512 DWORD pid = 0;
513 while (gotEntry) {
514
515 // make sure we're not checking the system process
516 if (entry.th32ProcessID != 0) {
517
518 if (_stricmp(entry.szExeFile, "barrierc.exe") == 0 ||
519 _stricmp(entry.szExeFile, "barriers.exe") == 0) {
520
521 HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, entry.th32ProcessID);
522 shutdownProcess(handle, entry.th32ProcessID, 10);
523 }
524 }
525
526 // now move on to the next entry (if we're not at the end)
527 gotEntry = Process32Next(snapshot, &entry);
528 if (!gotEntry) {
529
530 DWORD err = GetLastError();
531 if (err != ERROR_NO_MORE_FILES) {
532
533 // only worry about error if it's not the end of the snapshot
534 LOG((CLOG_ERR "could not get subsiquent process entry"));
535 throw XArch(new XArchEvalWindows);
536 }
537 }
538 }
539
540 CloseHandle(snapshot);
541 m_processRunning = false;
542 }
543