写点什么

构建重启后依然可用的 Windows 服务

作者:Michael Haephrati, Ruth Haephrati

  • 2023-03-09
    北京
  • 本文字数:15192 字

    阅读完需:约 50 分钟

构建重启后依然可用的Windows服务

当使用 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 会话环境中运行,所以它不会干扰应用程序的其他组件或会话。显然,我们期望服务会在计算机启动后也自动启动,我们马上就会讨论该问题。


进一步来讲,这里显然有一个问题:我们为什么需要持久化的服务?答案很明显,服务应该能够:


  • 持续在后台运行。

  • 在已登录用户的会话中,调用自身。

  • 作为一个看门狗(watchdog),确保给定的应用程序一直在运行。


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 servicevoid 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



你可以看到,我们有两个元素:


  • command 元素,这里是Install

  • argument 元素,通过哈希分隔符(#)连接至命令元素,应该是我们希望看门狗观察的可执行文件。


Service 首先会启动 SampleApp,从此之后,如果你尝试终止或杀死SampleApp的话,看门狗会在几秒钟后重启它。如果重启,关掉计算机并再次启动,你会发现 Service 会再次出现并启动SampleApp。这就是我们的 Service 的目标和功能。

卸载


最后,如果要停止和卸载服务,我们包含了一个uninstall.bat脚本,它如下所示:


sc stop sg_revealerservicesc delete sg_revealerservicetaskkill /f /im sampleapp.exetaskkill /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 支持,加强网络安全和管理功能

公众号推荐:

2024 年 1 月,InfoQ 研究中心重磅发布《大语言模型综合能力测评报告 2024》,揭示了 10 个大模型在语义理解、文学创作、知识问答等领域的卓越表现。ChatGPT-4、文心一言等领先模型在编程、逻辑推理等方面展现出惊人的进步,预示着大模型将在 2024 年迎来更广泛的应用和创新。关注公众号「AI 前线」,回复「大模型报告」免费获取电子版研究报告。

AI 前线公众号
2023-03-09 08:005269

评论 1 条评论

发布
用户头像
详细!
2023-03-09 09:41 · 陕西
回复
没有更多了
发现更多内容

android逆向之root方式注入apk

轻口味

android 9月日更

基于虹软SDK,适配Camera1、Camera2、CameraX,实现人脸识别(Android)

小驰笔记

android 音视频 人脸识别 引航计划

Python代码阅读(第30篇):找到列表中的奇偶异常项

Felix

Python 编程 Code Programing 阅读代码

太有用,Alibaba架构师十年心血熬成的435网络协议文档

程序员 编程语言 网络协议 TCP/IP

膜拜!不愧是阿里大牛总结的Java10W字面经,Github访问量已破百万

Java 程序员 架构 面试 计算机

Prometheus 2.30.0 新特性

耳东@Erdong

release Prometheus 9月日更

[外文资源]最好的 Golang 博客

baiyutang

golang 9月日更

VSCode 中,TS 提示 ”无法找到 *.vue 声明文件“ 的解决方案

编程三昧

vscode Vue3 ts 9月日更

2021年最新整理, C++ 学习资料,含C++ 11 / 14 / 17 / 20 / 23 新特性、入门教程、推荐书籍、优质文章、学习笔记、教学视频等

奔着腾讯去

c++

拒绝裸奔,为 Elasticsearch 设置账号密码(qbit)

qbit

https 安全 Kibana

java 虚拟机 GC :G1配置参数

风翱

GC 9月日更

「绝密档案」“爆料”完整秒杀架构的设计到技术关键点的“情报信息”

洛神灬殇

后端 秒杀系统 秒杀架构 秒杀架构设计 引航计划

阿里云天池赛题解析——深度学习篇重磅发布!

博文视点Broadview

netty系列之:分离websocket处理器

程序那些事

Java 架构 Netty 程序那些事

扒一扒面向对象编程的另一面

华为云开发者联盟

编程 面向对象 对象 对象编程

linux之登录式shell和非登录式shell

入门小站

Linux

详解html5新增的标签与css3中伪类和伪元素

你好bk

html5 css3 大前端 基础技能

被客户像小学生一样训话

boshi

创业

网站攻击到提权的全部过程

网络安全学海

黑客 网络安全 信息安全 WEB安全 漏洞分析

P8整理的OpenStack构架,希望能帮助到你

hanaper

乌镇回溯 | 构建网络空间命运共同体,旺链科技做了哪些?

旺链科技

数字经济 产业区块链 世界互联网大会

万亿养老市场如何抢占商机?云巢智慧康养物联网加速器,三招化解ISV痛点!

华为云开发者联盟

物联网 华为云 iotda 沃土云创计划 智慧康养

深入浅出Redis宝典,阿里架构师10年经验汇总,PDF免费分享

Java redis 架构

网络攻防学习笔记 Day150

穿过生命散发芬芳

9月日更 网络流量分析

在线黑客帝国文字效果生成工具

入门小站

工具

分布式系统都要遵守的CAP

卢卡多多

CAP 9月日更

多指标异常检测方法综述

云智慧AIOps社区

AIOPS 异常检测 技术学习 智能运维 指标

秋招如何抱佛脚?2021最新大厂Java面试真题合集(附权威答案)

Java 架构 面试 程序人生 编程语言

13个VSCode使用技巧,开启高效的开发模式

华为云开发者联盟

vscode 日志 开发 插件 Git存储库

月度发布 | 极狐GitLab14.3升级40+新功能!

极狐GitLab

带你了解数仓安全测试的TLS协议

华为云开发者联盟

安全 通信 密钥 SSL/TLS协议 加密通信

构建重启后依然可用的Windows服务_语言 & 开发_InfoQ精选文章