65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (19投票s)

2022 年 10 月 28 日

CPOL

11分钟阅读

viewsIcon

25725

downloadIcon

86

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 日:初始版本
© . All rights reserved.