1 /*
2  * Copyright 2015-2021 The Regents of the University of California
3  * All rights reserved.
4  *
5  * This file is part of Spoofer.
6  *
7  * Spoofer is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * Spoofer is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with Spoofer.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include <iostream>
22 #include <stdexcept>
23 #include "spoof_qt.h"
24 #include <QThread>
25 #include <QDir>
26 #include <QMutex>
27 #include <QWaitCondition>
28 #include <QCommandLineParser>
29 #include <QLockFile>
30 #include <windows.h> // GetConsoleMode(), CTRL_*_EVENT, etc
31 #include <shlobj.h> // SHGetFolderPath()
32 #include "../../config.h"
33 #include "appwin.h"
34 #include "common.h"
35 #include "ServiceStarterThread.h"
36 static const char cvsid[] ATR_USED = "$Id: appwin.cpp,v 1.39 2021/04/28 17:39:09 kkeys Exp $";
37 
isaconsole(HANDLE h)38 static bool isaconsole(HANDLE h)
39 {
40     DWORD mode;
41     return h != INVALID_HANDLE_VALUE && GetConsoleMode(h, &mode);
42 }
43 
AppWin(int & argc,char ** argv)44 AppWin::AppWin(int &argc, char **argv) : App(argc, argv),
45     winIn(GetStdHandle(STD_INPUT_HANDLE)),
46     winOut(GetStdHandle(STD_OUTPUT_HANDLE)),
47     winErr(GetStdHandle(STD_ERROR_HANDLE)),
48     installerProcess(INVALID_HANDLE_VALUE),
49     installerJob(INVALID_HANDLE_VALUE),
50     isService(false)
51 {
52     isInteractive = isaconsole(winIn) && isaconsole(winOut);
53 }
54 
dumpPaths() const55 void AppWin::dumpPaths() const
56 {
57     TCHAR szPath[MAX_PATH];
58     App::dumpPaths();
59     spout << "Windows paths:" << Qt_endl;
60 
61 #define dumpWinPath(id)   do { \
62     spout << #id << Qt_endl; \
63     if (SUCCEEDED(SHGetFolderPath(nullptr, CSIDL_##id, nullptr, 0, szPath))) \
64 	spout << "    " << QString::fromWCharArray(szPath) << Qt_endl; \
65 } while (0)
66 
67     dumpWinPath(APPDATA);
68     dumpWinPath(COMMON_APPDATA);
69     dumpWinPath(LOCAL_APPDATA);
70     dumpWinPath(PERSONAL);
71     dumpWinPath(COMMON_DOCUMENTS);
72     dumpWinPath(PROGRAM_FILES);
73     dumpWinPath(PROGRAM_FILES_COMMON);
74     dumpWinPath(PROGRAM_FILES_COMMONX86);
75 }
76 
chooseDataDir()77 QString AppWin::chooseDataDir()
78 {
79     // We want only system-wide dirs, but QStandardPaths::standardLocations()
80     // doesn't separate user dirs from system-wide dirs.
81     int nFolder = CSIDL_COMMON_APPDATA;
82     TCHAR szPath[MAX_PATH];
83     if (!SUCCEEDED(SHGetFolderPath(nullptr, nFolder, nullptr, 0, szPath))) {
84 	sperr << "can't get data folder path" << Qt_endl;
85 	return QString();
86     }
87     return QString::fromWCharArray(szPath) %
88 	QSL("\\") % QCoreApplication::organizationName() %
89 	QSL("\\") % QCoreApplication::applicationName();
90 }
91 
92 enum AppState { APPSTATE_NOTREADY, APPSTATE_READY, APPSTATE_DONE };
93 static QMutex appStateMutex;
94 static QWaitCondition appStateCond;
95 static AppState appState = APPSTATE_NOTREADY;
96 static ServiceStarterThread *starter = nullptr;
97 
98 static SERVICE_STATUS svcStatus;
99 static SERVICE_STATUS_HANDLE svcStatusHandle;
100 //static HANDLE svcStopEvent = nullptr;
101 
102 static void WINAPI SvcCtrlHandler(DWORD);
103 static void WINAPI SvcMainA(DWORD, LPSTR *);
104 
105 static void ReportSvcStatus(DWORD, DWORD, DWORD);
106 // static void SvcReportEvent(LPTSTR);
107 
108 static const SERVICE_TABLE_ENTRYA serviceTable[] = {
109     { const_cast<char *>(APPNAME), SvcMainA },
110     { nullptr, nullptr }
111 };
112 
ReportSvcStatus(DWORD state,DWORD exitCode,DWORD waitHint)113 static void ReportSvcStatus(DWORD state, DWORD exitCode, DWORD waitHint)
114 {
115     static DWORD checkpoint = 1;
116     svcStatus.dwCurrentState = state;
117     svcStatus.dwWin32ExitCode = exitCode;
118     svcStatus.dwWaitHint = waitHint;
119 
120     if (state == SERVICE_START_PENDING)
121 	svcStatus.dwControlsAccepted = 0;
122     else
123 	svcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE;
124 
125     if (state == SERVICE_START_PENDING || state == SERVICE_STOP_PENDING)
126 	svcStatus.dwCheckPoint = checkpoint++;
127     else
128 	svcStatus.dwCheckPoint = 0;
129 
130     SetServiceStatus(svcStatusHandle, &svcStatus);
131 }
132 
pause()133 void AppWin::pause()
134 {
135     ReportSvcStatus(SERVICE_PAUSE_PENDING, NO_ERROR, 0);
136     App::pause();
137     ReportSvcStatus(SERVICE_PAUSED, NO_ERROR, 0);
138 }
139 
resume()140 void AppWin::resume()
141 {
142     ReportSvcStatus(SERVICE_CONTINUE_PENDING, NO_ERROR, 0);
143     App::resume();
144     ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
145 }
146 
SvcCtrlHandler(DWORD ctrl)147 static void WINAPI SvcCtrlHandler(DWORD ctrl)
148 {
149     AppWin *app = static_cast<AppWin*>(QCoreApplication::instance());
150     switch (ctrl) {
151 	case SERVICE_CONTROL_STOP:
152 	    App::sperr << "received SERVICE_CONTROL_STOP" << Qt_endl;
153 	    ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
154 	    app->exit(0);
155 	    break;
156 	case SERVICE_CONTROL_SHUTDOWN:
157 	    App::sperr << "received SERVICE_CONTROL_SHUTDOWN" << Qt_endl;
158 	    ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
159 	    app->exit(0);
160 	    break;
161 	case SERVICE_CONTROL_PAUSE:
162 	    App::sperr << "received SERVICE_CONTROL_PAUSE" << Qt_endl;
163 	    app->pause();
164 	    break;
165 	case SERVICE_CONTROL_CONTINUE:
166 	    App::sperr << "received SERVICE_CONTROL_CONTINUE" << Qt_endl;
167 	    app->resume();
168 	    break;
169 	case SERVICE_CONTROL_INTERROGATE:
170 	    qDebug() << "received SERVICE_CONTROL_INTERROGATE";
171 	    break;
172 	default:
173 	    qDebug() << "received unknown SERVICE_CONTROL" << ctrl;
174 	    break;
175     }
176 }
177 
SvcMainA(DWORD argc,LPSTR * argv)178 static void SvcMainA(DWORD argc, LPSTR *argv)
179 {
180     // pass service arguments along to parseCommandLine()
181     if (argc) {
182 	QStringList *args = new QStringList();
183 	for (unsigned i = 0; i < argc; i++)
184 	    *args << QString::fromLocal8Bit(argv[i]);
185 	SpooferBase::args = args;
186     }
187 
188     // tell main thread it can start app.exec()
189     // qDebug() << "SvcMain sending READY";
190     appStateMutex.lock();
191     appState = APPSTATE_READY;
192     appStateCond.wakeAll();
193 
194     // wait for main thread to finish app.exec()
195     // qDebug() << "SvcMain waiting for DONE";
196     while (appState < APPSTATE_DONE) appStateCond.wait(&appStateMutex);
197     appStateMutex.unlock();
198     // qDebug() << "SvcMain got DONE";
199 }
200 
run()201 void ServiceStarterThread::run() {
202     AppWin *app = (AppWin*)QCoreApplication::instance();
203     app->isService = true;
204     if (StartServiceCtrlDispatcherA(serviceTable)) {
205 	// Ran as service; SvcMain has already run in another thread.
206     } else if (GetLastError() != ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) {
207 	// Failed to run as service.
208 	app->isService = false;
209 	qDebug() << "StartServiceCtrlDispatcher: " << getLastErrmsg();
210 	qDebug() << "starter sending DONE";
211 	appStateMutex.lock();
212 	appState = APPSTATE_DONE;
213 	appStateCond.wakeAll();
214 	appStateMutex.unlock();
215     } else {
216 	// We are running as a console app, not a service.
217 	app->isService = false;
218 	SvcMainA(0, nullptr);
219     }
220 }
221 
init(int & exitCode)222 bool AppWin::init(int &exitCode)
223 {
224     exitCode = 1; // default result is failure
225 
226     // App::exec() must be run in the main thread.  But Windows'
227     // StartServiceCtrlDispatcher() launches a new thread to run SvcMain(),
228     // and blocks its own thread until the SvcMain() thread exits.
229     // Solution:
230     // Main thread launches "Starter" thread, then waits for READY.
231     // Starter thread calls StartServiceCtrlDispatcher() and blocks.
232     // SvcMain() thread signals READY, then waits for DONE.
233     // Main thread wakes on READY, calls App::exec(), signals DONE, and exits.
234     // SvcMain() thread wakes on DONE and exits.
235     // Starter thread wakes on return of SvcMain(), and exits.
236 
237     // start a thread that starts a Windows Service
238     starter = new ServiceStarterThread(this);
239     starter->start();
240 
241     // wait for starter to tell us it's ready
242     qDebug() << "main waiting for READY";
243     appStateMutex.lock();
244     while (appState < APPSTATE_READY) appStateCond.wait(&appStateMutex);
245     appStateMutex.unlock();
246     if (appState == APPSTATE_DONE) { // error in starter
247 	qDebug() << "main got DONE";
248 	return false;
249     }
250     qDebug() << "main got READY";
251 
252     if (this->isService) {
253 	svcStatusHandle = RegisterServiceCtrlHandlerA(APPNAME, SvcCtrlHandler);
254 	if (!svcStatusHandle) {
255 	    // SvcReportEvent(TEXT("RegisterServiceCtrlHandler"));
256 	    qDebug() << "RegisterServiceCtrlHandler failed: " << getLastErrmsg();
257 	    return false;
258 	}
259 
260 	svcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
261 	svcStatus.dwServiceSpecificExitCode = 0;
262 
263 	ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);
264 	// TODO: use a lower waitHint here so control panel doesn't wait so
265 	// long; and, to make sure control panel doesn't give up too early, we
266 	// should call ReportSvcStatus(SERVICE_START_PENDING,...) occasionally
267 	// to increment checkpoint between now and readyService().  Then we
268 	// can lower the sleep in readyService().
269     }
270 
271     return App::init(exitCode);
272 }
273 
verifyDaemon(PROCESS_INFORMATION & procinfo)274 bool AppWin::verifyDaemon(PROCESS_INFORMATION &procinfo)
275 {
276     QLockFile lockFile(config->lockFileName());
277     qDebug().noquote() << "waiting for child to lock" <<
278 	QDir::toNativeSeparators(config->lockFileName());
279     qint64 qpid;
280     while (!lockFile.getLockInfo(&qpid, nullptr, nullptr) ||
281 	qpid != procinfo.dwProcessId) // child is not ready
282     {
283 	DWORD code;
284 	if (!GetExitCodeProcess(procinfo.hProcess, &code) || code != STILL_ACTIVE) {
285 	    sperr << "Detached process exited prematurely" << Qt_endl;
286 	    return false;
287 	}
288 	QThread::msleep(100);
289     }
290     return true;
291 }
292 
prestart(int & exitCode)293 bool AppWin::prestart(int &exitCode)
294 {
295     if (!this->isService && GetConsoleWindow() && this->optDetach) {
296 	// Start a child process with same command line as this one, but
297 	// detached, with stdout and stderr redirected to a log file.
298 	// Exit parent without waiting for child to finish.
299 	QString logname = AppLog::makeName();
300 
301 	STARTUPINFO startupinfo;
302 	memset(&startupinfo, 0, sizeof(startupinfo));
303 	startupinfo.cb = sizeof(STARTUPINFO);
304 	startupinfo.dwFlags = STARTF_USESTDHANDLES;
305 	startupinfo.hStdInput = INVALID_HANDLE_VALUE;
306 
307 #if 1 // Windows API
308 	SECURITY_ATTRIBUTES secattr;
309 	memset(&secattr, 0, sizeof(secattr));
310 	secattr.nLength = sizeof(secattr);
311 	secattr.bInheritHandle = true;
312 	startupinfo.hStdOutput = startupinfo.hStdError = CreateFileA(
313 	    qPrintable(logname), GENERIC_WRITE, FILE_SHARE_READ,
314 	    &secattr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
315 	if (startupinfo.hStdError == INVALID_HANDLE_VALUE) {
316 	    sperr << "log file " << logname << ": " << getLastErrmsg() << Qt_endl;
317 	    return false;
318 	}
319 #else // stdio API
320         FILE *out = fopen(qPrintable(logname), "wt");
321         if (!out) {
322             sperr << "log file " << logname << ": " << strerror(errno) << Qt_endl;
323 	    return false;
324 	}
325         startupinfo.hStdOutput = startupinfo.hStdError =
326 	    (HANDLE)_get_osfhandle(_fileno(out));
327 #endif
328 
329 	PROCESS_INFORMATION procinfo;
330 	TCHAR exename[MAX_PATH];
331 	GetModuleFileName(nullptr, exename, MAX_PATH);
332 	if (CreateProcess(exename, GetCommandLine(), nullptr, nullptr, TRUE,
333 	    DETACHED_PROCESS, nullptr, nullptr, &startupinfo, &procinfo))
334 	{
335 	    exitCode = SP_EXIT_OK; // success
336 	    qDebug() << "Detached; child pid =" << procinfo.dwProcessId;
337 	    sperr << "Log file: " << QDir::toNativeSeparators(logname) << Qt_endl;
338 	} else {
339 	    exitCode = static_cast<int>(getLastErr());
340 	    sperr << "Detach failed: " << getLastErrmsg() << Qt_endl;
341 	}
342 	pApplabel = QSL(" parent");
343 	exitCode = verifyDaemon(procinfo) ? SP_EXIT_OK : SP_EXIT_DAEMON_FAILED;
344 	return false; // skip app.exec()
345     }
346 
347     return true;
348 }
349 
readyService(int exitCode) const350 void AppWin::readyService(int exitCode) const
351 {
352     if (!isService) return;
353     ReportSvcStatus(SERVICE_RUNNING, (DWORD)exitCode, 0);
354     if (paused) {
355 	QThread::msleep(3100); // hack: give control panel a chance to see SERVICE_RUNNING before we switch to SERVICE_PAUSED.
356 	ReportSvcStatus(SERVICE_PAUSED, NO_ERROR, 0);
357     }
358 }
359 
endService(int exitCode)360 void AppWin::endService(int exitCode)
361 {
362     if (isService)
363 	ReportSvcStatus(SERVICE_STOPPED, (DWORD)exitCode, 0);
364     // tell starter we're done
365     qDebug() << "main sending DONE";
366     appStateMutex.lock();
367     appState = APPSTATE_DONE;
368     appStateCond.wakeAll();
369     appStateMutex.unlock();
370     starter->wait(1000);
371     delete starter;
372     App::endService(exitCode);
373 }
374 
end() const375 void AppWin::end() const
376 {
377     DWORD proclist[1];
378     DWORD numProcsOnConsole = GetConsoleProcessList(proclist, 1);
379     if (this->isInteractive && numProcsOnConsole == 1) {
380 	const char prompt[] = "Press enter.\n";
381 	char c;
382 	WriteFile(this->winErr, prompt, sizeof(prompt)-1, nullptr, nullptr);
383 	ReadFile(this->winIn, &c, 1, nullptr, nullptr);
384     }
385     App::end();
386 }
387 
ConsoleCtrlHandler(DWORD type)388 static BOOL ConsoleCtrlHandler(DWORD type)
389 {
390     const char *name;
391     QCoreApplication *app = QCoreApplication::instance();
392     switch (type) {
393 	case CTRL_C_EVENT:
394 	    name = "C";     break;
395 	case CTRL_CLOSE_EVENT:    // closed console or "End Task"
396 	    name = "Close"; break;
397 	case CTRL_BREAK_EVENT:
398 	    name = "Break"; break;
399 	case CTRL_LOGOFF_EVENT:
400 	    return true;
401 	case CTRL_SHUTDOWN_EVENT:
402 	    return true;
403 	default:
404 	    return true;
405     }
406     qDebug().nospace() << "Scheduler: caught Ctrl-" << name << ".";
407     app->exit(255); // request app to end its event loop
408     QThread::msleep(10000); // if app exits cleanly now, it'll kill this thread
409     return false; // app hasn't exited; kill all threads
410 }
411 
initSignals()412 bool AppWin::initSignals()
413 {
414     SetConsoleCtrlHandler((PHANDLER_ROUTINE)ConsoleCtrlHandler, TRUE);
415     return true;
416 }
417 
killProber()418 void AppWin::killProber()
419 {
420     // prober->terminate() does not work with non-gui processes like prober
421     prober->kill();
422 }
423 
424 #ifdef AUTOUPGRADE_ENABLED
installerIsRunning()425 bool AppWin::installerIsRunning()
426 {
427     DWORD exitCode;
428     if (!GetExitCodeProcess(installerProcess, &exitCode)) return false;
429     if (exitCode == STILL_ACTIVE) return true;
430     qWarning() << "installer exit code:" << exitCode;
431     return false;
432 }
433 
killInstaller()434 void AppWin::killInstaller()
435 {
436     TerminateJobObject(installerJob, 1);
437 }
438 
executeInstaller(const QString & installerName)439 void AppWin::executeInstaller(const QString &installerName)
440 {
441     LPWSTR comspec = _wgetenv(L"ComSpec");
442     std::wstring wInstaller = QDir::toNativeSeparators(installerName).toStdWString();
443     std::wstring wDataDir = QDir::toNativeSeparators(dataDir).toStdWString();
444     std::wstring wUpFin = QDir::toNativeSeparators(upFinName()).toStdWString();
445     std::wstring wVStr = upgradeInfo->vstr.toStdWString();
446     // Note: Windows cmd.exe syntax:
447     //   escape character is ^
448     //   like posix shell:  &&  ||  >  2>&1
449     //   ( ) groups commands.
450     //   & separates commands.
451     //   quotes and spaces are not removed (with /S).
452     //   %var% substitutions are done at parse time.
453     //   !var! substitutions are done at run time (with /V:ON).
454     //   "if (cond) (cmd1) else (cmd2)" does not seem to allow & after it.
455     //       "cmd0 && (cmd1) || (cmd2)" does allow & after it, but
456     //       executes cmd2 if cmd1 fails, which is not what we want.
457     std::wstring cmdline =
458 	L"cmd.exe /V:ON /S /C ( "
459 	L"set installer=" + wInstaller + L"& "
460 	L"echo Installing !installer!& "
461 	L"set FromTo=from " PACKAGE_VERSION " to " + wVStr + L"& "
462 	L"\"!installer!\" /S& "
463 	L"set status=!errorlevel!& "
464 	L"cmd /c exit !status! && ( " +  // essentially "if status == 0"
465 #ifndef UPGRADE_WITHOUT_DOWNLOAD
466 	(config->installerKeep() ? L"" : L"del \"!installer!\"& ") +
467 #endif
468 	L"set msg=Successfully upgraded !FromTo!"
469 	L")& "
470 	L"cmd /c exit !status! || ( " +  // essentially "if status != 0"
471 	L"set msg=Failed to upgrade !FromTo! ^(errorlevel !status!^)"
472 	L")& "
473 	L"echo !msg! at !DATE! !TIME!>\"" + wUpFin + L"\"& "
474 	L"echo !msg!&"
475 	L"exit !status!"
476 	L") >\"" + wDataDir + L"\\upgrade-log.txt\" 2>&1";
477     qDebug().noquote() << QSL("cmdline:") << QString::fromStdWString(cmdline);
478     STARTUPINFOW startupinfo;
479     memset(&startupinfo, 0, sizeof(startupinfo));
480     startupinfo.cb = sizeof(startupinfo);
481     PROCESS_INFORMATION procinfo;
482     if (CreateProcessW(comspec, const_cast<wchar_t*>(cmdline.data()), nullptr, nullptr, TRUE,
483 	DETACHED_PROCESS, nullptr, nullptr, &startupinfo, &procinfo))
484     {
485 	installerProcess = procinfo.hProcess;
486 	installerJob = CreateJobObjectA(nullptr, "spoofer-install-script");
487 	AssignProcessToJobObject(installerJob, installerProcess);
488 	// All children of installerProcess will also belong to installerJob.
489 	qDebug() << "executing cmd script, pid" << procinfo.dwProcessId;
490     } else {
491 	abortInstallation(QSL("Failed to execute upgrade script: ") % getLastErrmsg());
492 	return;
493     }
494 }
495 #endif // AUTOUPGRADE_ENABLED
496