致敬您的服务:创建持久的隔离 Windows 服务





5.00/5 (19投票s)
Windows 服务是强大的小程序,在 Windows 上开发时不可避免地会遇到它们
二等奖:2022 年 10 月最佳文章
服务与野兽
在为 Windows 编程时,不可避免地要接触 Windows 服务。我过去写过几篇关于服务的文章,例如这篇文章。似乎无论我与服务打交道多少,或者我认为我多么能处理它们,我总是会遇到更多未被记录或(如果我“幸运”)文档记录很差的问题、挑战和麻烦。其中一些问题是在 Microsoft 引入服务隔离时开始的。我遇到的最令人烦恼的问题之一是,当勾选了快速启动后,在关闭 PC 时无法重新启动服务。由于我找不到解决方案,我决定卷起袖子自己创建一个,这导致了我开发了一个持久化的服务。
致敬您的服务……
在我深入解释我的解决方案之前,让我们从基础开始,解释什么是服务,以及我们为什么首先需要使用 Windows 服务。
NT 服务(也称为 Windows 服务)是一种特殊进程,由 NT 内核的服务控制管理器加载,并在 Windows 启动后(在用户登录之前)在后台运行。我们使用服务来执行核心和低级操作系统任务,例如 Web 服务器、事件日志记录、文件服务器、帮助和支持、打印、加密以及错误报告。
除此之外,服务使我们能够创建长期运行的可执行应用程序。原因是服务在其自己的 Windows 会话环境中运行,因此它不会干扰您的应用程序的其他组件或会话。显然,服务应该在计算机启动后自动启动——我们稍后会讲到这一点。
进一步来说,显而易见的问题是——为什么我们需要持久化的服务?答案很清楚——我们需要服务来
- 始终运行。
- 在登录用户的会话下调用自身。
- 充当看门狗,确保给定的应用程序始终运行。
Windows 服务需要能够承受睡眠、休眠、重启和关机。然而,正如前面解释的,当勾选了“快速启动”后,PC 被关闭再重新打开时,存在特定的且危险的问题。在大多数这些情况下,服务未能重新启动。
由于我正在开发一款杀毒软件,它应该在重启或关机后自动重启,这个问题造成了一个我急于解决的严重问题。
坚持!好的服务……
为了创建近乎完美的持久化 Windows 服务,我必须先解决几个根本问题。
其中一个问题与服务隔离有关——隔离的服务无法访问与任何特定用户关联的任何上下文。我们的一个软件产品曾经将数据存储在 *c:\users\<用户名称>\appdata\local\* 中,但当它从我们的服务运行时,路径无效,因为服务运行在会话 0 中。此外,重启后,服务在任何用户登录之前启动——这导致了解决方案的第一部分:能够等待用户登录。
为了弄清楚如何做到这一点,我在这里发布了我的问题。
事实证明,这是一个没有完美解决方案的问题,但是,本文附带的代码已被使用并经过全面测试,没有出现问题。
基础知识
我的代码的结构和流程可能看起来很复杂,这是有原因的。在过去的 10 年里,服务变得与其他进程隔离。由于 Windows 服务以 *SYSTEM* 用户帐户运行,而不是其他任何用户帐户,并且运行是隔离的。
隔离的原因是因为服务可能很强大,并且可能存在潜在的安全风险。因此,Microsoft 引入了服务的隔离。在此更改之前,所有服务都与应用程序一起在会话 0 中运行。
在此更改之前,Windows 服务与其他程序并存。
然而,自 Windows Vista 以来的隔离之后,情况发生了变化。
我的代码背后的想法是让 Windows 服务通过调用CreateProcessAsUserW 来将自己作为用户启动,这将在后面详细介绍。
我的服务有几个命令,当使用这些命令行参数调用时,它会相应地执行操作。
#define SERVICE_COMMAND_INSTALL L"Install" // The command line argument
// for installing the service
#define SERVICE_COMMAND_LAUNCHER L"ServiceIsLauncher" // Launcher command for
// NT service
当调用 *SG_RevealerService* 时,有以下选项
选项 1 - 不带任何命令行参数调用 - 什么也不会发生。
选项 2 - 使用 `Install` 命令行参数调用
在这种情况下,服务将自行安装,如果在一个哈希(#)分隔符之后添加了一个有效的可执行文件路径,该可执行文件将启动,并且看门狗将使其保持运行。
然后,服务使用CreateProcessAsUserW() 运行自身,并且新进程以用户帐户运行。它使服务能够访问调用实例由于服务隔离而无法访问的上下文。
选项 3 - 使用 `ServiceIsLauncher` 命令行参数调用。然后将启动服务客户端主应用程序。此时,入口函数指示服务已在用户权限下启动。此时,您可以在任务管理器中看到两个 `SG_RevealerService` 实例:一个在 `SYSTEM` 下,另一个在当前登录用户下。
/*
RunHost
*/
BOOL RunHost(LPWSTR HostExePath,LPWSTR CommandLineArguments)
{
WriteToLog(L"RunHost '%s'",HostExePath);
STARTUPINFO startupInfo = {};
startupInfo.cb = sizeof(STARTUPINFO);
startupInfo.lpDesktop = (LPTSTR)_T("winsta0\\default");
HANDLE hToken = 0;
BOOL bRes = FALSE;
LPVOID pEnv = NULL;
CreateEnvironmentBlock(&pEnv, hToken, TRUE);
PROCESS_INFORMATION processInfoAgent = {};
PROCESS_INFORMATION processInfoHideProcess = {};
PROCESS_INFORMATION processInfoHideProcess32 = {};
if (PathFileExists(HostExePath))
{
std::wstring commandLine;
commandLine.reserve(1024);
commandLine += L"\"";
commandLine += HostExePath;
commandLine += L"\" \"";
commandLine += CommandLineArguments;
commandLine += L"\"";
WriteToLog(L"launch host with CreateProcessAsUser ... %s",
commandLine.c_str());
bRes = CreateProcessAsUserW(hToken, NULL, &commandLine[0],
NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS |
CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE |
CREATE_DEFAULT_ERROR_MODE, pEnv,
NULL, &startupInfo, &processInfoAgent);
if (bRes == FALSE)
{
DWORD dwLastError = ::GetLastError();
TCHAR lpBuffer[256] = _T("?");
if (dwLastError != 0) // Don't want to see an
// "operation done successfully" error ;-)
{
::FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, // It's a system error
NULL, // No string to be
// formatted needed
dwLastError, // Hey Windows: Please
// explain this error!
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Do it in the standard
// language
lpBuffer, // Put the message here
255, // Number of bytes to store the message
NULL);
}
WriteToLog(L"CreateProcessAsUser failed - Command Line = %s Error : %s",
commandLine, lpBuffer);
}
else
{
if (!writeStringInRegistry(HKEY_LOCAL_MACHINE,
(PWCHAR)SERVICE_REG_KEY, (PWCHAR)SERVICE_KEY_NAME, HostExePath))
{
WriteToLog(L"Failed to write registry");
}
}
}
else
{
WriteToLog(L"RunHost failed because path '%s' does not exists", HostExePath);
}
hPrevAppProcess = processInfoAgent.hProcess;
CloseHandle(hToken);
WriteToLog(L"Run host end!");
return bRes;
}
检测用户登录
第一个挑战是仅在用户登录时(如果登录)才开始某些操作。
为了检测用户登录,我们首先定义一个全局变量。
bool g_bLoggedIn = false;
当且仅当用户登录时,它将被设置为 `true`。
订阅登录事件
我定义了以下预处理器指令
#define EVENT_SUBSCRIBE_PATH L"Security"
#define EVENT_SUBSCRIBE_QUERY L"Event/System[EventID=4624]"
服务启动后,我们订阅登录事件,因此在用户登录的那一刻,我们会通过我们设置的回调函数收到警报,然后我们可以继续。
为了实现这一点,我们需要一个类来处理订阅的创建和等待事件回调。
class UserLoginListner
{
HANDLE hWait = NULL;
HANDLE hSubscription = NULL;
public:
~UserLoginListner()
{
CloseHandle(hWait);
EvtClose(hSubscription);
}
UserLoginListner()
{
const wchar_t* pwsPath = EVENT_SUBSCRIBE_PATH;
const wchar_t* pwsQuery = EVENT_SUBSCRIBE_QUERY;
hWait = CreateEvent(NULL, FALSE, FALSE, NULL);
hSubscription = EvtSubscribe(NULL, NULL,
pwsPath, pwsQuery,
NULL,
hWait,
(EVT_SUBSCRIBE_CALLBACK)UserLoginListner::SubscriptionCallback,
EvtSubscribeToFutureEvents);
if (hSubscription == NULL)
{
DWORD status = GetLastError();
if (ERROR_EVT_CHANNEL_NOT_FOUND == status)
WriteToLog(L"Channel %s was not found.\n", pwsPath);
else if (ERROR_EVT_INVALID_QUERY == status)
WriteToLog(L"The query \"%s\" is not valid.\n", pwsQuery);
else
WriteToLog(L"EvtSubscribe failed with %lu.\n", status);
CloseHandle(hWait);
}
}
接下来,我们需要一个函数来执行等待本身
void WaitForUserToLogIn()
{
WriteToLog(L"Waiting for a user to log in...");
WaitForSingleObject(hWait, INFINITE);
WriteToLog(L"Received a Logon event - a user has logged in");
}
我们还需要回调函数
static DWORD WINAPI SubscriptionCallback(EVT_SUBSCRIBE_NOTIFY_ACTION action, PVOID
pContext, EVT_HANDLE hEvent)
{
if (action == EvtSubscribeActionDeliver)
{
WriteToLog(L"SubscriptionCallback invoked.");
HANDLE Handle = (HANDLE)(LONG_PTR)pContext;
SetEvent(Handle);
}
return ERROR_SUCCESS;
}
然后,我们只需要添加一段包含以下几行的代码
WriteToLog(L"Launch client\n"); // launch client ...
{
UserLoginListner WaitTillAUserLogins;
WaitTillAUserLogins.WaitForUserToLogIn();
}
一旦我们到达这个块的末尾,我们就可以确信用户已经登录。
稍后在本文中,我将解释如何查找登录用户的帐户/用户名以及如何使用我的 `GetLoggedInUser()` 函数。
不是你,是我:冒充用户
当我们确定用户已登录时,我们需要冒充该用户。
以下函数可以完成这项工作。它不仅可以冒充用户,还可以调用CreateProcessAsUserW() 并以该用户的身份运行自身。
通过这样做,我们可以让服务访问用户的上下文,包括文档、桌面等,并允许服务使用 UI,而从会话 0 运行的服务无法做到这一点。
CreateProcessAsUserW 创建一个新进程及其主线程,该进程将在给定用户的上下文中运行。
//Function to run a process as active user from Windows service
void ImpersonateActiveUserAndRun()
{
DWORD session_id = -1;
DWORD session_count = 0;
WTS_SESSION_INFOW *pSession = NULL;
if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSession, &session_count))
{
WriteToLog(L"WTSEnumerateSessions - success");
}
else
{
WriteToLog(L"WTSEnumerateSessions - failed. Error %d",GetLastError());
return;
}
TCHAR szCurModule[MAX_PATH] = { 0 };
GetModuleFileName(NULL, szCurModule, MAX_PATH);
for (size_t i = 0; i < session_count; i++)
{
session_id = pSession[i].SessionId;
WTS_CONNECTSTATE_CLASS wts_connect_state = WTSDisconnected;
WTS_CONNECTSTATE_CLASS* ptr_wts_connect_state = NULL;
DWORD bytes_returned = 0;
if (::WTSQuerySessionInformation(
WTS_CURRENT_SERVER_HANDLE,
session_id,
WTSConnectState,
reinterpret_cast<LPTSTR*>(&ptr_wts_connect_state),
&bytes_returned))
{
wts_connect_state = *ptr_wts_connect_state;
::WTSFreeMemory(ptr_wts_connect_state);
if (wts_connect_state != WTSActive) continue;
}
else
{
continue;
}
HANDLE hImpersonationToken;
if (!WTSQueryUserToken(session_id, &hImpersonationToken))
{
continue;
}
//Get the actual token from impersonation one
DWORD neededSize1 = 0;
HANDLE *realToken = new HANDLE;
if (GetTokenInformation(hImpersonationToken,
(::TOKEN_INFORMATION_CLASS) TokenLinkedToken,
realToken, sizeof(HANDLE), &neededSize1))
{
CloseHandle(hImpersonationToken);
hImpersonationToken = *realToken;
}
else
{
continue;
}
HANDLE hUserToken;
if (!DuplicateTokenEx(hImpersonationToken,
TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS | MAXIMUM_ALLOWED,
NULL,
SecurityImpersonation,
TokenPrimary,
&hUserToken))
{
continue;
}
// Get user name of this process
WCHAR* pUserName;
DWORD user_name_len = 0;
if (WTSQuerySessionInformationW
(WTS_CURRENT_SERVER_HANDLE, session_id, WTSUserName, &pUserName, &user_name_len))
{
//Now we got the user name stored in pUserName
}
// Free allocated memory
if (pUserName) WTSFreeMemory(pUserName);
ImpersonateLoggedOnUser(hUserToken);
STARTUPINFOW StartupInfo;
GetStartupInfoW(&StartupInfo);
StartupInfo.cb = sizeof(STARTUPINFOW);
PROCESS_INFORMATION processInfo;
SECURITY_ATTRIBUTES Security1;
Security1.nLength = sizeof SECURITY_ATTRIBUTES;
SECURITY_ATTRIBUTES Security2;
Security2.nLength = sizeof SECURITY_ATTRIBUTES;
void* lpEnvironment = NULL;
// Obtain all needed necessary environment variables of the logged in user.
// They will then be passed to the new process we create.
BOOL resultEnv = CreateEnvironmentBlock(&lpEnvironment, hUserToken, FALSE);
if (!resultEnv)
{
WriteToLog(L"CreateEnvironmentBlock - failed. Error %d",GetLastError());
continue;
}
std::wstring commandLine;
commandLine.reserve(1024);
commandLine += L"\"";
commandLine += szCurModule;
commandLine += L"\" \"";
commandLine += SERVICE_COMMAND_Launcher;
commandLine += L"\"";
WCHAR PP[1024]; //path and parameters
ZeroMemory(PP, 1024 * sizeof WCHAR);
wcscpy_s(PP, commandLine.c_str());
// Next, we impersonate - by starting the process
// as if the current logged in user, has started it
BOOL result = CreateProcessAsUserW(hUserToken,
NULL,
PP,
NULL,
NULL,
FALSE,
NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE,
NULL,
NULL,
&StartupInfo,
&processInfo);
if (!result)
{
WriteToLog(L"CreateProcessAsUser - failed. Error %d",GetLastError());
}
else
{
WriteToLog(L"CreateProcessAsUser - success");
}
DestroyEnvironmentBlock(lpEnvironment);
CloseHandle(hImpersonationToken);
CloseHandle(hUserToken);
CloseHandle(realToken);
RevertToSelf();
}
WTSFreeMemory(pSession);
}
查找登录用户
为了查找登录用户的帐户名,我们使用以下函数
std::wstring GetLoggedInUser()
{
std::wstring user{L""};
WTS_SESSION_INFO *SessionInfo;
unsigned long SessionCount;
unsigned long ActiveSessionId = -1;
if(WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE,
0, 1, &SessionInfo, &SessionCount))
{
for (size_t i = 0; i < SessionCount; i++)
{
if (SessionInfo[i].State == WTSActive ||
SessionInfo[i].State == WTSConnected)
{
ActiveSessionId = SessionInfo[i].SessionId;
break;
}
}
wchar_t *UserName;
if (ActiveSessionId != -1)
{
unsigned long BytesReturned;
if (WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE,
ActiveSessionId, WTSUserName, &UserName, &BytesReturned))
{
user = UserName; // Now we have the logged in user name
WTSFreeMemory(UserName);
}
}
WTSFreeMemory(SessionInfo);
}
return user;
}
我们在服务启动后不久使用此函数,只要没有用户登录,此函数就会返回一个空字符串,并且在返回空字符串时,我们就知道应该等待。
看门狗是服务的最佳伙伴
服务非常适合与看门狗机制结合使用。
这样的机制将确保给定的应用程序始终运行,并且如果它异常关闭,它将重新启动。我们总是需要记住,用户可能只是选择“**退出**”菜单,在这种情况下,我们不希望重新启动进程,但是,如果进程通过“**任务管理器**”或其他任何方式停止,我们希望重新启动它。一个很好的例子是杀毒软件。我们希望避免恶意软件终止它本应检测到的杀毒软件。
为了实现这一点,我们需要服务向使用它的程序提供某种 API,以便当该程序的用户选择“**退出**”时,程序会通知服务其工作已完成,并且它可以自行卸载。
一些构建块
接下来,我将解释一些理解本文代码所需的构建块。
GetExePath
为了获取我们服务或任何可执行文件的路径,此函数将非常有用。
/**
* GetExePath() - returns the full path of the current executable.
*
* @param values - none.
* @return a std::wstring containing the full path of the current executable.
*/
std::wstring GetExePath()
{
wchar_t buffer[65536];
GetModuleFileName(NULL, buffer, sizeof(buffer) / sizeof(*buffer));
int pos = -1;
int index = 0;
while (buffer[index])
{
if (buffer[index] == L'\\' || buffer[index] == L'/')
{
pos = index;
}
index++;
}
buffer[pos + 1] = 0;
return buffer;
}
WriteLogFile
开发 Windows 服务(以及任何软件)时,拥有日志记录机制非常重要。我们有一个非常复杂的日志记录机制,但为了本文的目的,我添加了一个最小的日志记录函数 `WriteToLog`。它的工作方式类似于 `printf`,但发送给它的所有内容不仅会被格式化,还会存储在日志文件中,之后可以进行检查。
日志文件的路径(附加到日志文件,而不是路径)通常是服务 EXE 的路径,但是,由于服务隔离,在 PC 重启后的短时间内,此路径将更改为 *c:\Windows\System32*,而我们不希望这样,所以我们的日志函数会检查我们 EXE 的路径,并且不会假设当前目录在服务的整个生命周期内保持不变。
/**
* WriteToLog() - writes formatted text into a log file, and on screen (console)
*
* @param values - formatted text, such as L"The result is %d",result.
* @return - none
*/
void WriteToLog(LPCTSTR lpText, ...)
{
FILE *fp;
wchar_t log_file[MAX_PATH]{L""};
if(wcscmp(log_file,L"") == NULL)
{
wcscpy(log_file,GetExePath().c_str());
wcscat(log_file,L"log.txt");
}
// find gmt time, and store in buf_time
time_t rawtime;
struct tm* ptm;
wchar_t buf_time[DATETIME_BUFFER_SIZE];
time(&rawtime);
ptm = gmtime(&rawtime);
wcsftime(buf_time, sizeof(buf_time) / sizeof(*buf_time), L"%d.%m.%Y %H:%M", ptm);
// store passed messsage (lpText) to buffer_in
wchar_t buffer_in[BUFFER_SIZE];
va_list ptr;
va_start(ptr, lpText);
vswprintf(buffer_in, BUFFER_SIZE, lpText, ptr);
va_end(ptr);
// store output message to buffer_out - enabled multiple parameters in swprintf
wchar_t buffer_out[BUFFER_SIZE];
swprintf(buffer_out, BUFFER_SIZE, L"%s %s\n", buf_time, buffer_in);
_wfopen_s(&fp, log_file, L"a,ccs=UTF-8");
if (fp)
{
fwprintf(fp, L"%s\n", buffer_out);
fclose(fp);
}
wcscat(buffer_out,L"\n");HANDLE stdOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (stdOut != NULL && stdOut != INVALID_HANDLE_VALUE)
{
DWORD written = 0;
WriteConsole(stdOut, buffer_out, wcslen(buffer_out), &written, NULL);
}
}
更多构建块 - 注册表操作
这里有一些我们用于存储看门狗可执行文件路径的函数,以便当服务在 PC 关机或重启后重启时,它将拥有该路径。
BOOL CreateRegistryKey(HKEY hKeyParent, PWCHAR subkey)
{
DWORD dwDisposition; //Verify new key is created or open existing key
HKEY hKey;
DWORD Ret;
Ret =
RegCreateKeyEx(
hKeyParent,
subkey,
0,
NULL,
REG_OPTION_NON_VOLATILE,
KEY_ALL_ACCESS,
NULL,
&hKey,
&dwDisposition);
if (Ret != ERROR_SUCCESS)
{
WriteToLog(L"Error opening or creating new key\n");
return FALSE;
}
RegCloseKey(hKey); //close the key
return TRUE;
}
BOOL writeStringInRegistry(HKEY hKeyParent, PWCHAR subkey,
PWCHAR valueName, PWCHAR strData)
{
DWORD Ret;
HKEY hKey;
//Check if the registry exists
Ret = RegOpenKeyEx(
hKeyParent,
subkey,
0,
KEY_WRITE,
&hKey
);
if (Ret == ERROR_SUCCESS)
{
if (ERROR_SUCCESS !=
RegSetValueEx(
hKey,
valueName,
0,
REG_SZ,
(LPBYTE)(strData),
((((DWORD)lstrlen(strData) + 1)) * 2)))
{
RegCloseKey(hKey);
return FALSE;
}
RegCloseKey(hKey);
return TRUE;
}
return FALSE;
}
LONG GetStringRegKey(HKEY hKey, const std::wstring &strValueName,
std::wstring &strValue, const std::wstring &strDefaultValue)
{
strValue = strDefaultValue;
TCHAR szBuffer[MAX_PATH];
DWORD dwBufferSize = sizeof(szBuffer);
ULONG nError;
nError = RegQueryValueEx(hKey, strValueName.c_str(), 0, NULL,
(LPBYTE)szBuffer, &dwBufferSize);
if (nError == ERROR_SUCCESS)
{
strValue = szBuffer;
if (strValue.front() == _T('"') && strValue.back() == _T('"'))
{
strValue.erase(0, 1); // erase the first character
strValue.erase(strValue.size() - 1); // erase the last character
}
}
return nError;
}
BOOL readStringFromRegistry(HKEY hKeyParent, PWCHAR subkey,
PWCHAR valueName, std::wstring& readData)
{
HKEY hKey;
DWORD len = 1024;
DWORD readDataLen = len;
PWCHAR readBuffer = (PWCHAR)malloc(sizeof(PWCHAR) * len);
if (readBuffer == NULL)
return FALSE;
//Check if the registry exists
DWORD Ret = RegOpenKeyEx(
hKeyParent,
subkey,
0,
KEY_READ,
&hKey
);
if (Ret == ERROR_SUCCESS)
{
Ret = RegQueryValueEx(
hKey,
valueName,
NULL,
NULL,
(BYTE*)readBuffer,
&readDataLen
);
while (Ret == ERROR_MORE_DATA)
{
// Get a buffer that is big enough.
len += 1024;
readBuffer = (PWCHAR)realloc(readBuffer, len);
readDataLen = len;
Ret = RegQueryValueEx(
hKey,
valueName,
NULL,
NULL,
(BYTE*)readBuffer,
&readDataLen
);
}
if (Ret != ERROR_SUCCESS)
{
RegCloseKey(hKey);
return false;;
}
readData = readBuffer;
RegCloseKey(hKey);
return true;
}
else
{
return false;
}
}
检查我们的主机是否正在运行
一个关键能力是保护我们的 `SampleApp`(我们称之为“主机”),当它未运行时,重启它(因此是看门狗)。在实际应用中,我们会检查主机是否被用户终止(这是可以的),或者被恶意软件终止(这是不行的),如果是后者,则重启它(否则,用户只会选择“**退出**”,但应用程序会“纠缠不休”,并一遍又一遍地被执行)。
这是实现方式
我们创建一个 `Timer` 事件,并在给定的时间间隔(不应太频繁)检查主机进程是否正在运行,如果没有,则启动它。我们使用一个静态布尔标志(`is_running`),用于指示我们已经在此代码块中,因此它不会在处理前一个 `WM_TIMER` 事件的代码执行时被调用。这是我在 `WM_TIMER` 代码块中总是做的事情,因为当设置的定时器过于频繁时,在执行前一个 `WM_TIMER` 事件的代码时,代码块可能会被调用。
我们还通过检查 `g_bLoggedIn` 布尔标志来检查用户是否已登录。
case WM_TIMER:
{
if (is_running) break;
WriteToLog(L"Timer event");
is_running = true;
HANDLE hProcessSnap;
PROCESSENTRY32 pe32;
bool found{ false };
WriteToLog(L"Enumerating all processess...");
// Take a snapshot of all processes in the system.
hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE)
{
WriteToLog(L"Failed to call CreateToolhelp32Snapshot().
Error code %d",GetLastError());
is_running = false;
return 1;
}
// Set the size of the structure before using it.
pe32.dwSize = sizeof(PROCESSENTRY32);
// Retrieve information about the first process,
// and exit if unsuccessful
if (!Process32First(hProcessSnap, &pe32))
{
WriteToLog(L"Failed to call Process32First().
Error code %d",GetLastError());
CloseHandle(hProcessSnap); // clean the snapshot object
is_running=false;
break;
}
// Now walk the snapshot of processes, and
// display information about each process in turn
DWORD svchost_parent_pid = 0;
DWORD dllhost_parent_pid = 0;
std::wstring szPath = L"";
if (readStringFromRegistry(HKEY_LOCAL_MACHINE,
(PWCHAR)SERVICE_REG_KEY, (PWCHAR)SERVICE_KEY_NAME, szPath))
{
m_szExeToFind = szPath.substr(szPath.find_last_of(L"/\\") + 1); // The
// process name is the executable name only
m_szExeToRun = szPath; // The executable to run is the full path
}
else
{
WriteToLog(L"Error reading ExeToFind from the Registry");
}
do
{
if (wcsstr( m_szExeToFind.c_str(), pe32.szExeFile))
{
WriteToLog(L"%s is running",m_szExeToFind.c_str());
found = true;
is_running=false;
break;
}
if (!g_bLoggedIn)
{
WriteToLog(L"WatchDog isn't starting '%s'
because user isn't logged in",m_szExeToFind.c_str());
return 1;
}
}
while (Process32Next(hProcessSnap, &pe32));
if (!found)
{
WriteToLog(L"'%s' is not running. Need to start it",m_szExeToFind.c_str());
if (!m_szExeToRun.empty()) // Watch Dog start the host app
{
if (!g_bLoggedIn)
{
WriteToLog(L"WatchDog isn't starting '%s'
because user isn't logged in",m_szExeToFind.c_str());
return 1;
}
ImpersonateActiveUserAndRun();
RunHost((LPWSTR)m_szExeToRun.c_str(), (LPWSTR)L"");
}
else
{
WriteToLog(L"m_szExeToRun is empty");
}
}
CloseHandle(hProcessSnap);
}
is_running=false;
break;
如何测试服务
当我们想测试解决方案时,我们雇佣了 20 名合格且合作的测试人员。在工作过程中,更多的测试是成功的。在某个时候,它在我的 Surface Pro 笔记本电脑上完美运行,幸运的是,我的一位员工报告说,在他的 PC 上,关机后,服务没有启动,或者启动了但没有在环 3 下自行启动。这是个好消息,因为在开发过程中,当你怀疑有 bug 时,最糟糕的消息是找不到它也无法重现它。10% 的测试人员报告了问题。因此,此处发布的版本在他的 PC 上运行完美,但是,2% 的测试人员仍然偶尔报告问题。换句话说,`SampleApp` 在关闭 PC 并重新启动后不会启动。
以下是测试服务和看门狗的说明。
SampleApp
我包含了一个由 Visual Studio Wizard 生成的示例应用程序,作为由看门狗保持运行的“主机”应用程序。您可以单独运行它,它应该看起来像这样。这个应用程序没做什么。事实上,它什么都没做……
以下是测试服务和看门狗的说明。
从 CMD 运行
在管理员权限下打开 CMD。将当前目录更改为服务 EXE 所在的位置,然后键入
SG_RevealerService.exe Install#SampleApp.exe
正如你所见,我们有两个元素
- **命令**,即 `Install`,并用哈希(`#`)分隔符连接
- **参数**,它应该是任何你想让看门狗监视的可执行文件
服务将首先启动 `SampleApp`,然后从那时起,尝试终止或杀死 `SampleApp`,看门狗将在几秒钟后重新启动它。然后尝试重启,关闭 PC 再打开,看看服务是否回来并启动了 `SampleApp`。这总结了我们服务的目标和功能。
卸载
然后,要停止和卸载,我包含了 *uninstall.bat*,它如下所示
sc stop sg_revealerservice
sc delete sg_revealerservice
taskkill /f /im sampleapp.exe
taskkill /f /im sg_revealerservice.exe
致谢
我应该感谢 Microsoft 的David Delaune,他给了我一个非常有用的建议:订阅‘登录’事件,而不是每隔 X 秒检查一次用户是否已登录,因此我的代码也相应地更新了。
历史
- 2022 年 10 月 28 日:初始版本