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