当使用 C++为 Windows 编程时,使用 Windows 服务(Windows Services)几乎是难以避免的。在微软 Windows 操作系统中,Windows 服务发挥着重要的作用,它们能够创建和管理长时间运行的进程,这些进程能够在睡眠、休眠、重启和关机的过程中幸存下来。但是,如果无法做到这一点会怎样呢?在选中快速启动(Fast Startup)时,关闭计算机会导致服务无法重启,这会给程序带来灾难性的后果。微软在 Windows Vista 中引入的Service Isolation可能会导致这类灾难性的后果,在本文中将会阐述如何解决它。
感谢 Windows 服务
多年以来,我们一直在使用 Windows 服务,但是不管我们觉得有多么了解服务,或者有多么自信能够处理它,却始终会遇到更多的问题、挑战和麻烦。其中有些问题根本是没有文档的,或者我们“幸运”一点的话,会有一点糟糕的文档。
自从微软引入服务隔离之后,我们遇到的最令人恼火的问题之一就是当快速启动选中时,计算机关闭后,无法重启服务。鉴于我们没有找到现成的解决方案,所以我们决定自动动手实现一个,这促成了持久化服务的开发。
但是,在深入研究和解释我们的解决方案之前,我们首先从基础知识开始,解释什么是服务,以及为什么要使用 Windows 服务。
NT 服务(也叫做 Windows 服务)指的是由 NT 内核的服务控制管理器(Service Control Manager)加载的特殊进程,它会在 Windows 启动(在用户登录前)立即在后台运行。我们使用服务来执行核心和底层的操作系统任务,比如 Web 服务、事件日志、文件服务、帮助和支持、打印、加密和错误报告。
此外,服务使我们能够创建可执行的、长时间运行的应用程序。原因在于服务会在自己的 Windows 会话环境中运行,所以它不会干扰应用程序的其他组件或会话。显然,我们期望服务会在计算机启动后也自动启动,我们马上就会讨论该问题。
进一步来讲,这里显然有一个问题:我们为什么需要持久化的服务?答案很明显,服务应该能够:
Windows 服务需要能够在睡眠、休眠、重启和关机时依然能够存活。但是,正如前文所述,当选中“快速启动”时,计算机关机再启动的话,会出现一些特定的危险问题。在大多数场景中,服务无法重新启动。
因为我们正在开发的是一个反病毒软件,它应该在重启或关机后重新启动,这种情况造成了一个严重的问题,我们迫切需要解决它。
实现良好的服务
为了创建近乎完美的持久化 Windows 服务,我们必须首先解决几个底层的问题。
其中一个问题与服务隔离有关,被隔离的服务无法访问与任何特定用户相关的上下文。我们某个软件产品将数据存储到了c:\users\<USER NAME>\appdata\local\
中,但是当它从我们的服务中运行的话,这个路径就是无效的,因为服务是在 Session 0 中运行的。除此之外,在重启后,服务会在所有用户登录之前启动,这形成了解决方案的第一部分:等待用户登录。
为了弄清如何做到这一点,我们在这里发布了遇到的问题。
事实证明,这是一个没有完美解决方案的问题,但是,本文附带的代码已经得到了应用,并且经过了全面的测试,没有任何的问题。
基础知识
我们的代码结构和流程可能看起来很复杂,但是这是有一定原因的。在过去的十年间,服务已经与其他进程隔离。从那时开始,Windows 服务会在SYSTEM
用户账号下运行,而不是其他的用户账号,并且是隔离运行的。
隔离运行的原因在于,服务的功能很强大,可能是潜在的安全风险。正因为如此,微软引入了服务隔离。在这个变化之前,所有的服务会与应用一起在 Session 0 中运行。
但是,在引入了隔离之后(这是在 Windows Vista 中引入的),情况发生了变化。我们的代码背后的想法是通过调用CreateProcessAsUserW
,让 Windows 服务以某个用户的身份启动自己,这一点将在后文详细阐述。我们的服务叫做SG_RevealerService
,它有多个命令,当使用如下的命令行参数调用时,它们会采取相应的行为。
#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
命令行参数进行调用。在这种情况下,服务将自行安装,如果在哈希分隔符(#)添加了有效的可执行路径,服务将会启动,Windows 看门狗会保持其一直运行。
然后,Service 会使用CreateProcessAsUserW()
运行自身,新的进程会在用户账号下运行。这给了 Service 访问上下文的能力,因为 Service Isolation,调用实例是无法访问该上下文的。
选项 3:使用 ServiceIsLauncher 命令行参数进行调用。服务客户端主应用将会启动。此时,入口函数表明服务已经以当前用户的权限启动了自身。现在,在 Task Manager 中,我们会看到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
。
订阅登录事件
我们定义了如下的Preprocesor Directives:
#define EVENT_SUBSCRIBE_PATH L"Security"
#define EVENT_SUBSCRIBE_QUERY L"Event/System[EventID=4624]"
复制代码
当 Service 启动后,我们订阅登录事件,所以当用户登录时,我们会通过设置的回调函数得到一个告警,然后我们就可以继续后面的操作了。为了实现这一点,我们需要一个类来处理订阅的创建并等待事件回调。
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()
,以该用户的身份运行自己。通过这种方式,我们能够让服务访问用户的上下文,包括文档、桌面等,并允许服务使用用户界面,这对于从 Session 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;
}
复制代码
在服务启动后不久,我们就要使用该函数。只要没有用户登录,这个函数就会返回一个空字符串,如果这样的话,我们就知道应该继续等待。
看门狗是 Service 的好朋友
Service 与看门狗机制协同使用是很理想的方案。
这种机制将确保一个给定应用始终处于运行状态,如果它异常关闭的话,看门狗会重新启动它。我们要始终记住,如果用户通过Quit
退出的话,我们不希望重启进程。但是,如果进程是通过Task Manager
或其他方式被停掉的,我们会希望重启它。一个很好的例子是反病毒程序。我们想要确保恶意软件不能终止本应检测它的反病毒程序。
为了实现这一点,我们需要该 Service 为使用它的程序提供某种 API,当该程序的用户选择“Quit”,程序会告知 Service,程序的工作已经完成了,Service 可以卸载自己了。
一些构建基块
接下来,我们介绍一些构建基块,要理解本文的代码,它们是必备的。
GetExePath
为了获取我们的 Service 或其他可执行文件的路径,如下的函数是非常便利的。
/**
* 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 Service 时(以及其他任何软件),拥有一个日志机制都是很重要的。我们有一个非常复杂的日志机制,但是就本文而言,我添加了一个最小的日志函数,名为WriteToLog
。它的运行机制类似于printf
,但是所有发送给它的内容不仅会被格式化,还会存储在一个日志文件中,以备日后检查。这个日志文件的大小会不断增长,因为会有新的日志条目追加到上面。
日志文件的路径,通常会位于 Service 的 EXE 的路径,但是,由于 Service Isolation,在重启计算机后的一小段时间内,这个路径会变成 c:\Windows\System32
,我们并不希望如此。所以,我们的日志函数会检查 exe 的路径,并且不会假设Current Directory在 Service 的生命周期内会保持不变。
/**
* 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);
}
}
复制代码
更多的构建基块:注册表相关的内容
下面是一些我们用来存储看门狗可执行文件路径的函数,所以当计算机重启后,Service 重新启动时,就能使用该路径。
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;
}
}
复制代码
检查宿主(Host)是否在运行
本文中的程序有一项核心能力,那就是保护我们的SampleApp
(我们将其称为宿主),当它未运行时,就重新启动它(所以叫做看门狗)。在真实场景中,我们会检查宿主是被用户终止的(这是允许的),还是被恶意软件终止的(这是不允许的),在后一种情况下,我们将会重启它(否则,如果用户选择Quit
,但应用程序将继续“骚扰”系统并反复执行)。
如下是它如何实现的:
我们创建了一个Timer
事件,每隔一定的时间(不应该过于频繁),我们会检查宿主的进程是否在运行,如果没有的话,我们就启动它。我们使用了一个静态布尔型标记(is_running
),用来表明我们已经处于该代码块中了,所以在处理过程中时,能够避免再次调用。这是在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()) // watchdog 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;
复制代码
如何测试 Service
当我们想要测试这个解决方案时,我们雇佣了 20 个资深的和协作的测试人员。在整个工作过程中,越来多的测试均成功了。在某些时候,它在我们自己的 Surface Pro 笔记本电脑上运行地非常完美,但是,我们的一位员工报告说,在他的计算机上,在关闭之后,服务没有再次启动,或者在Ring 3下没有启动自身。这是一个好消息,因为在开发过程中,当你怀疑某个地方存在缺陷的时候,最糟糕的事情就是无法找到它,也无法重现它。总而言之,10%的测试者报告了问题。因此,这里发布的版本在我们员工的电脑上运行完美,然而 2%的测试者仍然不时报告问题。换句话说,SampleApp
在关闭计算机并打开后无法启动。
如下是对测试服务和看门狗的说明。
SampleApp
我们包含了一个由 Visual Studio Wizard 生成的样例应用,作为“宿主”应用,它会被看门狗确保一直运行。你可以单独运行它,外观如下面的图片所示。该应用没有做太多的事情。实际上,它一无是处……
在后面的内容中,我们将提供测试服务和看门狗的指南。你可以在GitHub下载源码。
从 CMD 中运行
以管理员身份打开 CMD。将当前目录变更至 Service 的 EXE 所在的路径并输入:
SG_RevealerService.exe Install#SampleApp.exe
你可以看到,我们有两个元素:
Service 首先会启动 SampleApp,从此之后,如果你尝试终止或杀死SampleApp
的话,看门狗会在几秒钟后重启它。如果重启,关掉计算机并再次启动,你会发现 Service 会再次出现并启动SampleApp
。这就是我们的 Service 的目标和功能。
卸载
最后,如果要停止和卸载服务,我们包含了一个uninstall.bat
脚本,它如下所示:
sc stop sg_revealerservice
sc delete sg_revealerservice
taskkill /f /im sampleapp.exe
taskkill /f /im sg_revealerservice.exe
复制代码
结论
Windows Service 在微软 Windows 操作系统中起着关键作用,它支持创建和管理长期运行的进程。
在有些场景下,如果勾选了“快速启动”,在正常关闭并重启计算机后,服务往往无法重启。
本文的目的是创建一个持久化的服务,在 Windows 重新启动或关机后,能够始终运行并重新启动。
其中一个主要的问题与 Service Isolation 有关。隔离本身(在 Windows Vista 版本中引入)是很重要和强大的,然而,当我们需要与用户空间交互时,这会产生一些限制。
当服务重新启动时,我们希望它能与用户空间进行交互,然而它不能发生地太早(在任何用户登录之前)。不过,你可以通过订阅登录事件来解决这个问题。
Service 与看门狗机制协同使用是很理想的方案。这种机制能够确保给定的应用一直在运行,并且在异常关闭时,它将重新启动。我们在前面描述的方法的基础上,成功地开发了这个机制,这使得它可以一直运行,在用户登录时得到提醒,并且能够与用户空间进行交互。
定时器事件能够用来监控被观察进程的运行。
在开发过程中,好的日志机制始终是非常有用的,我们可以使用简单的日志工具,并在需要的时候,使用更为复杂的工具。
最终的解决方案必须要进行测试。代码被确认并验证可以运行后,多达 2%的测试人员依然可能会报告错误,这是有一定原因的。
作者简介:
Michael Haephrati 是 Secured Globe, Inc.的联合创始人和首席执行官,该公司于 2008 年与他的妻子 Ruth Haephrati 一起创建。Michael 是一位音乐作曲家、发明家,也是一位专门从事软件开发和信息安全的专家。凭借 30 多年的经验,Michael 形成了独特的视角,将技术和创新结合起来,并强调终端用户的体验。多年来,Michael 领导了各种客户的创新项目和技术。他是“Learning C++”(https://www.manning.com/books/learning-c-plus-plus)的作者,该书由 Manning Publications 出版。
Ruth Haephrati 是 Secured Globe, Inc.的联合创始人和首席执行官,该公司于 2008 年与她的丈夫 Michael Haephrati 一起创建。Ruth 是一位作家、演讲者、企业家、网络安全和网络取证专家。在过去的 25 年里,Ruth 与微软和 IBM 等领先公司合作,担任顾问和 C++实践专家。她最近参与了为一个国际客户开发的最先进的反恶意软件技术。在业余时间,Ruth 是一位插画家、画家、野生动物摄影师和世界旅行者。
原文链接:
The Service and the Beast: Building a Windows Service that Does Not Fail to Restart
相关阅读:
Windows 11发布重大更新:ChatGPT版Bing集成到任务栏中,可快速访问AI聊天功能
Kubernetes 1.26 版本正式发布:改进 Windows 支持,加强网络安全和管理功能
评论 1 条评论