[关闭]
@levinzhang 2023-02-26T13:20:30.000000Z 字数 18835 阅读 115

服务与野兽:构建重启后依然可用的Windows服务

by

摘要:

在微软Windows操作系统中,Windows服务发挥着重要的作用,能够创建和管理长时间运行的进程。但是,当启用“Fast Startup”时,在正常关机并启动计算机后,服务可能无法重新启动。本文的目标在于创建一个持久化的服务,当Windows重启或关闭后,服务始终能够运行并重启。


当使用C++为Windows编程时,使用Windows服务(Windows Services)几乎是难以避免的。在微软Windows操作系统中,Windows服务发挥着重要的作用,它们能够创建和管理长时间运行的进程,这些进程能够在睡眠、休眠、重启和关机的过程中幸存下来。但是,如果无法做到这一点会怎样呢?在选中快速启动(Fast Startup)时,关闭计算机会导致服务无法重启,这会给程序带来灾难性的后果。微软在Windows Vista中引入的Service Isolation可能会导致这类灾难性的后果,在本文中将会阐述如何解决它。

感谢服务

多年以来,我们一直在使用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,它有多个命令,当使用如下的命令行参数调用时,它们会采取相应的行为。

  1. #define SERVICE_COMMAND_INSTALL L"Install" // The command line argument
  2. // for installing the service
  3. #define SERVICE_COMMAND_LAUNCHER L"ServiceIsLauncher" // Launcher command for
  4. // NT service

当调用SG_RevealerService时,有三个选项:

选项1:不带有任何命令行参数进行调用。在这种情况下什么都不会发生。

选项2:带有Install命令行参数进行调用。在这种情况下,服务将自行安装,如果在哈希分隔符(#)添加了有效的可执行路径,服务将会启动,Windows看门狗会保持其一直运行。

然后,Service会使用CreateProcessAsUserW()运行自身,新的进程会在用户账号下运行。这给了Service访问上下文的能力,因为Service Isolation,调用实例是无法访问该上下文的。

选项3:使用ServiceIsLauncher命令行参数进行调用。服务客户端主应用将会启动。此时,入口函数表明服务已经以当前用户的权限启动了自身。现在,在Task Manager中,我们会看到SG_RevealerService的两个实例,其中一个在SYSTEM用户下,另一个在当前登录用户下。

  1. /*
  2. RunHost
  3. */
  4. BOOL RunHost(LPWSTR HostExePath,LPWSTR CommandLineArguments)
  5. {
  6. WriteToLog(L"RunHost '%s'",HostExePath);
  7. STARTUPINFO startupInfo = {};
  8. startupInfo.cb = sizeof(STARTUPINFO);
  9. startupInfo.lpDesktop = (LPTSTR)_T("winsta0\\default");
  10. HANDLE hToken = 0;
  11. BOOL bRes = FALSE;
  12. LPVOID pEnv = NULL;
  13. CreateEnvironmentBlock(&pEnv, hToken, TRUE);
  14. PROCESS_INFORMATION processInfoAgent = {};
  15. PROCESS_INFORMATION processInfoHideProcess = {};
  16. PROCESS_INFORMATION processInfoHideProcess32 = {};
  17. if (PathFileExists(HostExePath))
  18. {
  19. std::wstring commandLine;
  20. commandLine.reserve(1024);
  21. commandLine += L"\"";
  22. commandLine += HostExePath;
  23. commandLine += L"\" \"";
  24. commandLine += CommandLineArguments;
  25. commandLine += L"\"";
  26. WriteToLog(L"launch host with CreateProcessAsUser ... %s",
  27. commandLine.c_str());
  28. bRes = CreateProcessAsUserW(hToken, NULL, &commandLine[0],
  29. NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS |
  30. CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE |
  31. CREATE_DEFAULT_ERROR_MODE, pEnv,
  32. NULL, &startupInfo, &processInfoAgent);
  33. if (bRes == FALSE)
  34. {
  35. DWORD dwLastError = ::GetLastError();
  36. TCHAR lpBuffer[256] = _T("?");
  37. if (dwLastError != 0) // Don't want to see an
  38. // "operation done successfully" error ;-)
  39. {
  40. ::FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, // It's a system error
  41. NULL, // No string to be
  42. // formatted needed
  43. dwLastError, // Hey Windows: Please
  44. // explain this error!
  45. MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Do it in the standard
  46. // language
  47. lpBuffer, // Put the message here
  48. 255, // Number of bytes to store the message
  49. NULL);
  50. }
  51. WriteToLog(L"CreateProcessAsUser failed - Command Line = %s Error : %s",
  52. commandLine, lpBuffer);
  53. }
  54. else
  55. {
  56. if (!writeStringInRegistry(HKEY_LOCAL_MACHINE,
  57. (PWCHAR)SERVICE_REG_KEY, (PWCHAR)SERVICE_KEY_NAME, HostExePath))
  58. {
  59. WriteToLog(L"Failed to write registry");
  60. }
  61. }
  62. }
  63. else
  64. {
  65. WriteToLog(L"RunHost failed because path '%s' does not exists", HostExePath);
  66. }
  67. hPrevAppProcess = processInfoAgent.hProcess;
  68. CloseHandle(hToken);
  69. WriteToLog(L"Run host end!");
  70. return bRes;
  71. }

探测用户登录

第一个挑战是仅在用户登录时,才启动一些动作。为了探测用户的登录,我们首先定义一个全局变量。

  1. bool g_bLoggedIn = false;

当用户登录时,它的值应该被设置为true

订阅登录事件

我们定义了如下的Preprocesor Directives

  1. #define EVENT_SUBSCRIBE_PATH L"Security"
  2. #define EVENT_SUBSCRIBE_QUERY L"Event/System[EventID=4624]"

当Service启动后,我们订阅登录事件,所以当用户登录时,我们会通过设置的回调函数得到一个告警,然后我们就可以继续后面的操作了。为了实现这一点,我们需要一个类来处理订阅的创建并等待事件回调。

  1. class UserLoginListner
  2. {
  3. HANDLE hWait = NULL;
  4. HANDLE hSubscription = NULL;
  5. public:
  6. ~UserLoginListner()
  7. {
  8. CloseHandle(hWait);
  9. EvtClose(hSubscription);
  10. }
  11. UserLoginListner()
  12. {
  13. const wchar_t* pwsPath = EVENT_SUBSCRIBE_PATH;
  14. const wchar_t* pwsQuery = EVENT_SUBSCRIBE_QUERY;
  15. hWait = CreateEvent(NULL, FALSE, FALSE, NULL);
  16. hSubscription = EvtSubscribe(NULL, NULL,
  17. pwsPath, pwsQuery,
  18. NULL,
  19. hWait,
  20. (EVT_SUBSCRIBE_CALLBACK)UserLoginListner::SubscriptionCallback,
  21. EvtSubscribeToFutureEvents);
  22. if (hSubscription == NULL)
  23. {
  24. DWORD status = GetLastError();
  25. if (ERROR_EVT_CHANNEL_NOT_FOUND == status)
  26. WriteToLog(L"Channel %s was not found.\n", pwsPath);
  27. else if (ERROR_EVT_INVALID_QUERY == status)
  28. WriteToLog(L"The query \"%s\" is not valid.\n", pwsQuery);
  29. else
  30. WriteToLog(L"EvtSubscribe failed with %lu.\n", status);
  31. CloseHandle(hWait);
  32. }
  33. }

然后,我们需要一个函数实现等待:

  1. void WaitForUserToLogIn()
  2. {
  3. WriteToLog(L"Waiting for a user to log in...");
  4. WaitForSingleObject(hWait, INFINITE);
  5. WriteToLog(L"Received a Logon event - a user has logged in");
  6. }

我们还需要一个回调函数:

  1. static DWORD WINAPI SubscriptionCallback(EVT_SUBSCRIBE_NOTIFY_ACTION action, PVOID
  2. pContext, EVT_HANDLE hEvent)
  3. {
  4. if (action == EvtSubscribeActionDeliver)
  5. {
  6. WriteToLog(L"SubscriptionCallback invoked.");
  7. HANDLE Handle = (HANDLE)(LONG_PTR)pContext;
  8. SetEvent(Handle);
  9. }
  10. return ERROR_SUCCESS;
  11. }

接下来,需要做的就是添加具有如下内容的代码块:

  1. WriteToLog(L"Launch client\n"); // launch client ...
  2. {
  3. UserLoginListner WaitTillAUserLogins;
  4. WaitTillAUserLogins.WaitForUserToLogIn();
  5. }

到达代码块的底部时,我们就可以确信一个用户已经登录了。

在本文后面的内容中,我们将会介绍如何检索登录用户的账号/用户名,以及如何使用GetLoggedInUser()函数。

冒充用户

当确定一个用户已经登录时,我们需要冒充他们。

如下的函数完成了这项工作。它不仅冒充了用户,还调用了CreateProcessAsUserW(),以该用户的身份运行自己。通过这种方式,我们能够让服务访问用户的上下文,包括文档、桌面等,并允许服务使用用户界面,这对于从Session 0运行服务来讲是无法实现的。

CreateProcessAsUserW创建了一个新的进程及其主线程,它会在给定用户的上下文中运行。

  1. //Function to run a process as active user from Windows service
  2. void ImpersonateActiveUserAndRun()
  3. {
  4. DWORD session_id = -1;
  5. DWORD session_count = 0;
  6. WTS_SESSION_INFOW *pSession = NULL;
  7. if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSession, &session_count))
  8. {
  9. WriteToLog(L"WTSEnumerateSessions - success");
  10. }
  11. else
  12. {
  13. WriteToLog(L"WTSEnumerateSessions - failed. Error %d",GetLastError());
  14. return;
  15. }
  16. TCHAR szCurModule[MAX_PATH] = { 0 };
  17. GetModuleFileName(NULL, szCurModule, MAX_PATH);
  18. for (size_t i = 0; i < session_count; i++)
  19. {
  20. session_id = pSession[i].SessionId;
  21. WTS_CONNECTSTATE_CLASS wts_connect_state = WTSDisconnected;
  22. WTS_CONNECTSTATE_CLASS* ptr_wts_connect_state = NULL;
  23. DWORD bytes_returned = 0;
  24. if (::WTSQuerySessionInformation(
  25. WTS_CURRENT_SERVER_HANDLE,
  26. session_id,
  27. WTSConnectState,
  28. reinterpret_cast<LPTSTR*>(&ptr_wts_connect_state),
  29. &bytes_returned))
  30. {
  31. wts_connect_state = *ptr_wts_connect_state;
  32. ::WTSFreeMemory(ptr_wts_connect_state);
  33. if (wts_connect_state != WTSActive) continue;
  34. }
  35. else
  36. {
  37. continue;
  38. }
  39. HANDLE hImpersonationToken;
  40. if (!WTSQueryUserToken(session_id, &hImpersonationToken))
  41. {
  42. continue;
  43. }
  44. //Get the actual token from impersonation one
  45. DWORD neededSize1 = 0;
  46. HANDLE *realToken = new HANDLE;
  47. if (GetTokenInformation(hImpersonationToken, (::TOKEN_INFORMATION_CLASS) TokenLinkedToken, realToken, sizeof(HANDLE), &neededSize1))
  48. {
  49. CloseHandle(hImpersonationToken);
  50. hImpersonationToken = *realToken;
  51. }
  52. else
  53. {
  54. continue;
  55. }
  56. HANDLE hUserToken;
  57. if (!DuplicateTokenEx(hImpersonationToken,
  58. TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS | MAXIMUM_ALLOWED,
  59. NULL,
  60. SecurityImpersonation,
  61. TokenPrimary,
  62. &hUserToken))
  63. {
  64. continue;
  65. }
  66. // Get user name of this process
  67. WCHAR* pUserName;
  68. DWORD user_name_len = 0;
  69. if (WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, session_id, WTSUserName, &pUserName, &user_name_len))
  70. {
  71. //Now we got the user name stored in pUserName
  72. }
  73. // Free allocated memory
  74. if (pUserName) WTSFreeMemory(pUserName);
  75. ImpersonateLoggedOnUser(hUserToken);
  76. STARTUPINFOW StartupInfo;
  77. GetStartupInfoW(&StartupInfo);
  78. StartupInfo.cb = sizeof(STARTUPINFOW);
  79. PROCESS_INFORMATION processInfo;
  80. SECURITY_ATTRIBUTES Security1;
  81. Security1.nLength = sizeof SECURITY_ATTRIBUTES;
  82. SECURITY_ATTRIBUTES Security2;
  83. Security2.nLength = sizeof SECURITY_ATTRIBUTES;
  84. void* lpEnvironment = NULL;
  85. // Obtain all needed necessary environment variables of the logged in user.
  86. // They will then be passed to the new process we create.
  87. BOOL resultEnv = CreateEnvironmentBlock(&lpEnvironment, hUserToken, FALSE);
  88. if (!resultEnv)
  89. {
  90. WriteToLog(L"CreateEnvironmentBlock - failed. Error %d",GetLastError());
  91. continue;
  92. }
  93. std::wstring commandLine;
  94. commandLine.reserve(1024);
  95. commandLine += L"\"";
  96. commandLine += szCurModule;
  97. commandLine += L"\" \"";
  98. commandLine += SERVICE_COMMAND_Launcher;
  99. commandLine += L"\"";
  100. WCHAR PP[1024]; //path and parameters
  101. ZeroMemory(PP, 1024 * sizeof WCHAR);
  102. wcscpy_s(PP, commandLine.c_str());
  103. // Next we impersonate - by starting the process as if the current logged in user, has started it
  104. BOOL result = CreateProcessAsUserW(hUserToken,
  105. NULL,
  106. PP,
  107. NULL,
  108. NULL,
  109. FALSE,
  110. NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE,
  111. NULL,
  112. NULL,
  113. &StartupInfo,
  114. &processInfo);
  115. if (!result)
  116. {
  117. WriteToLog(L"CreateProcessAsUser - failed. Error %d",GetLastError());
  118. }
  119. else
  120. {
  121. WriteToLog(L"CreateProcessAsUser - success");
  122. }
  123. DestroyEnvironmentBlock(lpEnvironment);
  124. CloseHandle(hImpersonationToken);
  125. CloseHandle(hUserToken);
  126. CloseHandle(realToken);
  127. RevertToSelf();
  128. }
  129. WTSFreeMemory(pSession);
  130. }

寻找已登录的用户

为了寻找已登录用户的账号名,我们会使用如下的函数:

  1. std::wstring GetLoggedInUser()
  2. {
  3. std::wstring user{L""};
  4. WTS_SESSION_INFO *SessionInfo;
  5. unsigned long SessionCount;
  6. unsigned long ActiveSessionId = -1;
  7. if(WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE,
  8. 0, 1, &SessionInfo, &SessionCount))
  9. {
  10. for (size_t i = 0; i < SessionCount; i++)
  11. {
  12. if (SessionInfo[i].State == WTSActive ||
  13. SessionInfo[i].State == WTSConnected)
  14. {
  15. ActiveSessionId = SessionInfo[i].SessionId;
  16. break;
  17. }
  18. }
  19. wchar_t *UserName;
  20. if (ActiveSessionId != -1)
  21. {
  22. unsigned long BytesReturned;
  23. if (WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE,
  24. ActiveSessionId, WTSUserName, &UserName, &BytesReturned))
  25. {
  26. user = UserName; // Now we have the logged in user name
  27. WTSFreeMemory(UserName);
  28. }
  29. }
  30. WTSFreeMemory(SessionInfo);
  31. }
  32. return user;
  33. }

在服务启动后不久,我们就要使用该函数。只要没有用户登录,这个函数就会返回一个空字符串,如果这样的话,我们就知道应该继续等待。

看门狗是Service的好朋友

Service与看门狗机制协同使用是很理想的方案。

这种机制将确保一个给定应用始终处于运行状态,如果它异常关闭的话,看门狗会重新启动它。我们要始终记住,如果用户通过Quit退出的话,我们不希望重启进程。但是,如果进程是通过Task Manager或其他方式被停掉的,我们会希望重启它。一个很好的例子是反病毒程序。我们想要确保恶意软件不能终止本应检测它的反病毒程序。

为了实现这一点,我们需要该Service为使用它的程序提供某种API,当该程序的用户选择“Quit”,程序会告知Service,程序的工作已经完成了,Service可以卸载自己了。

一些构建基块

接下来,我们介绍一些构建基块,要理解本文的代码,它们是必备的。

GetExePath

为了获取我们的Service或其他可执行文件的路径,如下的函数是非常便利的。

  1. /**
  2. * GetExePath() - returns the full path of the current executable.
  3. *
  4. * @param values - none.
  5. * @return a std::wstring containing the full path of the current executable.
  6. */
  7. std::wstring GetExePath()
  8. {
  9. wchar_t buffer[65536];
  10. GetModuleFileName(NULL, buffer, sizeof(buffer) / sizeof(*buffer));
  11. int pos = -1;
  12. int index = 0;
  13. while (buffer[index])
  14. {
  15. if (buffer[index] == L'\\' || buffer[index] == L'/')
  16. {
  17. pos = index;
  18. }
  19. index++;
  20. }
  21. buffer[pos + 1] = 0;
  22. return buffer;
  23. }

WriteLogFile

当开发Windows Service时(以及其他任何软件),拥有一个日志机制都是很重要的。我们有一个非常复杂的日志机制,但是就本文而言,我添加了一个最小的日志函数,名为WriteToLog。它的运行机制类似于printf,但是所有发送给它的内容不仅会被格式化,还会存储在一个日志文件中,以备日后检查。这个日志文件的大小会不断增长,因为会有新的日志条目追加到上面。

日志文件的路径,通常会位于Service的EXE的路径,但是,由于Service Isolation,在重启计算机后的一小段时间内,这个路径会变成 c:\Windows\System32,我们并不希望如此。所以,我们的日志函数会检查exe的路径,并且不会假设Current Directory在Service的生命周期内会保持不变。

  1. /**
  2. * WriteToLog() - writes formatted text into a log file, and on screen (console)
  3. *
  4. * @param values - formatted text, such as L"The result is %d",result.
  5. * @return - none
  6. */
  7. void WriteToLog(LPCTSTR lpText, ...)
  8. {
  9. FILE *fp;
  10. wchar_t log_file[MAX_PATH]{L""};
  11. if(wcscmp(log_file,L"") == NULL)
  12. {
  13. wcscpy(log_file,GetExePath().c_str());
  14. wcscat(log_file,L"log.txt");
  15. }
  16. // find gmt time, and store in buf_time
  17. time_t rawtime;
  18. struct tm* ptm;
  19. wchar_t buf_time[DATETIME_BUFFER_SIZE];
  20. time(&rawtime);
  21. ptm = gmtime(&rawtime);
  22. wcsftime(buf_time, sizeof(buf_time) / sizeof(*buf_time), L"%d.%m.%Y %H:%M", ptm);
  23. // store passed messsage (lpText) to buffer_in
  24. wchar_t buffer_in[BUFFER_SIZE];
  25. va_list ptr;
  26. va_start(ptr, lpText);
  27. vswprintf(buffer_in, BUFFER_SIZE, lpText, ptr);
  28. va_end(ptr);
  29. // store output message to buffer_out - enabled multiple parameters in swprintf
  30. wchar_t buffer_out[BUFFER_SIZE];
  31. swprintf(buffer_out, BUFFER_SIZE, L"%s %s\n", buf_time, buffer_in);
  32. _wfopen_s(&fp, log_file, L"a,ccs=UTF-8");
  33. if (fp)
  34. {
  35. fwprintf(fp, L"%s\n", buffer_out);
  36. fclose(fp);
  37. }
  38. wcscat(buffer_out,L"\n");HANDLE stdOut = GetStdHandle(STD_OUTPUT_HANDLE);
  39. if (stdOut != NULL && stdOut != INVALID_HANDLE_VALUE)
  40. {
  41. DWORD written = 0;
  42. WriteConsole(stdOut, buffer_out, wcslen(buffer_out), &written, NULL);
  43. }
  44. }

更多的构建基块:注册表相关的内容

下面是一些我们用来存储看门狗可执行文件路径的函数,所以当计算机重启后,Service重新启动时,就能使用该路径。

  1. BOOL CreateRegistryKey(HKEY hKeyParent, PWCHAR subkey)
  2. {
  3. DWORD dwDisposition; //Verify new key is created or open existing key
  4. HKEY hKey;
  5. DWORD Ret;
  6. Ret =
  7. RegCreateKeyEx(
  8. hKeyParent,
  9. subkey,
  10. 0,
  11. NULL,
  12. REG_OPTION_NON_VOLATILE,
  13. KEY_ALL_ACCESS,
  14. NULL,
  15. &hKey,
  16. &dwDisposition);
  17. if (Ret != ERROR_SUCCESS)
  18. {
  19. WriteToLog(L"Error opening or creating new key\n");
  20. return FALSE;
  21. }
  22. RegCloseKey(hKey); //close the key
  23. return TRUE;
  24. }
  25. BOOL writeStringInRegistry(HKEY hKeyParent, PWCHAR subkey,
  26. PWCHAR valueName, PWCHAR strData)
  27. {
  28. DWORD Ret;
  29. HKEY hKey;
  30. //Check if the registry exists
  31. Ret = RegOpenKeyEx(
  32. hKeyParent,
  33. subkey,
  34. 0,
  35. KEY_WRITE,
  36. &hKey
  37. );
  38. if (Ret == ERROR_SUCCESS)
  39. {
  40. if (ERROR_SUCCESS !=
  41. RegSetValueEx(
  42. hKey,
  43. valueName,
  44. 0,
  45. REG_SZ,
  46. (LPBYTE)(strData),
  47. ((((DWORD)lstrlen(strData) + 1)) * 2)))
  48. {
  49. RegCloseKey(hKey);
  50. return FALSE;
  51. }
  52. RegCloseKey(hKey);
  53. return TRUE;
  54. }
  55. return FALSE;
  56. }
  57. LONG GetStringRegKey(HKEY hKey, const std::wstring &strValueName,
  58. std::wstring &strValue, const std::wstring &strDefaultValue)
  59. {
  60. strValue = strDefaultValue;
  61. TCHAR szBuffer[MAX_PATH];
  62. DWORD dwBufferSize = sizeof(szBuffer);
  63. ULONG nError;
  64. nError = RegQueryValueEx(hKey, strValueName.c_str(), 0, NULL,
  65. (LPBYTE)szBuffer, &dwBufferSize);
  66. if (nError == ERROR_SUCCESS)
  67. {
  68. strValue = szBuffer;
  69. if (strValue.front() == _T('"') && strValue.back() == _T('"'))
  70. {
  71. strValue.erase(0, 1); // erase the first character
  72. strValue.erase(strValue.size() - 1); // erase the last character
  73. }
  74. }
  75. return nError;
  76. }
  77. BOOL readStringFromRegistry(HKEY hKeyParent, PWCHAR subkey,
  78. PWCHAR valueName, std::wstring& readData)
  79. {
  80. HKEY hKey;
  81. DWORD len = 1024;
  82. DWORD readDataLen = len;
  83. PWCHAR readBuffer = (PWCHAR)malloc(sizeof(PWCHAR) * len);
  84. if (readBuffer == NULL)
  85. return FALSE;
  86. //Check if the registry exists
  87. DWORD Ret = RegOpenKeyEx(
  88. hKeyParent,
  89. subkey,
  90. 0,
  91. KEY_READ,
  92. &hKey
  93. );
  94. if (Ret == ERROR_SUCCESS)
  95. {
  96. Ret = RegQueryValueEx(
  97. hKey,
  98. valueName,
  99. NULL,
  100. NULL,
  101. (BYTE*)readBuffer,
  102. &readDataLen
  103. );
  104. while (Ret == ERROR_MORE_DATA)
  105. {
  106. // Get a buffer that is big enough.
  107. len += 1024;
  108. readBuffer = (PWCHAR)realloc(readBuffer, len);
  109. readDataLen = len;
  110. Ret = RegQueryValueEx(
  111. hKey,
  112. valueName,
  113. NULL,
  114. NULL,
  115. (BYTE*)readBuffer,
  116. &readDataLen
  117. );
  118. }
  119. if (Ret != ERROR_SUCCESS)
  120. {
  121. RegCloseKey(hKey);
  122. return false;;
  123. }
  124. readData = readBuffer;
  125. RegCloseKey(hKey);
  126. return true;
  127. }
  128. else
  129. {
  130. return false;
  131. }
  132. }

检查宿主(Host)是否在运行

本文中的程序有一项核心能力,那就是保护我们的SampleApp(我们将其称为宿主),当它未运行时,就重新启动它(所以叫做看门狗)。在真实场景中,我们会检查宿主是被用户终止的(这是允许的),还是被恶意软件终止的(这是不允许的),在后一种情况下,我们将会重启它(否则,如果用户选择Quit,但应用程序将继续“骚扰”系统并反复执行)。

如下是它如何实现的:

我们创建了一个Timer事件,每隔一定的时间(不应该过于频繁),我们会检查宿主的进程是否在运行,如果没有的话,我们就启动它。我们使用了一个静态布尔型标记(is_running),用来表明我们已经处于该代码块中了,所以在处理过程中时,能够避免再次调用。这是在WM_TIMER代码块中始终要做的事情,因为当定时器设置的频率过高的话,代码块在调用时,前一个WM_TIMER事件的代码依然在执行。

我们还通过检查g_bLoggedIn布尔标记来判断是否有用户登录。

  1. case WM_TIMER:
  2. {
  3. if (is_running) break;
  4. WriteToLog(L"Timer event");
  5. is_running = true;
  6. HANDLE hProcessSnap;
  7. PROCESSENTRY32 pe32;
  8. bool found{ false };
  9. WriteToLog(L"Enumerating all processess...");
  10. // Take a snapshot of all processes in the system.
  11. hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  12. if (hProcessSnap == INVALID_HANDLE_VALUE)
  13. {
  14. WriteToLog(L"Failed to call CreateToolhelp32Snapshot(). Error code %d",GetLastError());
  15. is_running = false;
  16. return 1;
  17. }
  18. // Set the size of the structure before using it.
  19. pe32.dwSize = sizeof(PROCESSENTRY32);
  20. // Retrieve information about the first process,
  21. // and exit if unsuccessful
  22. if (!Process32First(hProcessSnap, &pe32))
  23. {
  24. WriteToLog(L"Failed to call Process32First(). Error code %d",GetLastError());
  25. CloseHandle(hProcessSnap); // clean the snapshot object
  26. is_running=false;
  27. break;
  28. }
  29. // Now walk the snapshot of processes, and
  30. // display information about each process in turn
  31. DWORD svchost_parent_pid = 0;
  32. DWORD dllhost_parent_pid = 0;
  33. std::wstring szPath = L"";
  34. if (readStringFromRegistry(HKEY_LOCAL_MACHINE, (PWCHAR)SERVICE_REG_KEY, (PWCHAR)SERVICE_KEY_NAME, szPath))
  35. {
  36. m_szExeToFind = szPath.substr(szPath.find_last_of(L"/\\") + 1); // The process name is the executable name only
  37. m_szExeToRun = szPath; // The executable to run is the full path
  38. }
  39. else
  40. {
  41. WriteToLog(L"Error reading ExeToFind from the Registry");
  42. }
  43. do
  44. {
  45. if (wcsstr( m_szExeToFind.c_str(), pe32.szExeFile))
  46. {
  47. WriteToLog(L"%s is running",m_szExeToFind.c_str());
  48. found = true;
  49. is_running=false;
  50. break;
  51. }
  52. if (!g_bLoggedIn)
  53. {
  54. WriteToLog(L"WatchDog isn't starting '%s' because user isn't logged in",m_szExeToFind.c_str());
  55. return 1;
  56. }
  57. }
  58. while (Process32Next(hProcessSnap, &pe32));
  59. if (!found)
  60. {
  61. WriteToLog(L"'%s' is not running. Need to start it",m_szExeToFind.c_str());
  62. if (!m_szExeToRun.empty()) // watchdog start the host app
  63. {
  64. if (!g_bLoggedIn)
  65. {
  66. WriteToLog(L"WatchDog isn't starting '%s' because user isn't logged in",m_szExeToFind.c_str());
  67. return 1;
  68. }
  69. ImpersonateActiveUserAndRun();
  70. RunHost((LPWSTR)m_szExeToRun.c_str(), (LPWSTR)L"");
  71. }
  72. else
  73. {
  74. WriteToLog(L"m_szExeToRun is empty");
  75. }
  76. }
  77. CloseHandle(hProcessSnap);
  78. }
  79. is_running=false;
  80. 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脚本,它如下所示:

  1. sc stop sg_revealerservice
  2. sc delete sg_revealerservice
  3. taskkill /f /im sampleapp.exe
  4. taskkill /f /im sg_revealerservice.exe

结论

关于作者

Michael Haephrati

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

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

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注