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

Windows 服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (34投票s)

2013年8月23日

CPOL

6分钟阅读

viewsIcon

50622

一篇关于 NT 服务的最新文章。

引言

在查找有关 Windows 服务的文章时,我发现网上大部分信息都已经过时了。有许多文章和代码片段是在 2000 年之前发布的!例如,如果你在亚马逊上搜索相关书籍,你会发现:《Win32 系统服务》或《专业 NT 服务》,这两本书都是在 2000 年之前或左右出版的。因此,我决定自己研究一下 Windows 服务。自 Windows 7 以来,Windows 处理 Windows 服务的方式发生了变化,这对于理解如何正确编写和使用 Windows 服务并使其兼容所有操作系统版本也很重要。我还邀请您阅读一篇更高级的 Windows 服务文章

背景

NT 服务(也称为Windows 服务)是对特殊进程的称呼,该进程由 NT 内核的服务控制管理器加载,并在 Windows 启动后(用户登录前)立即在后台运行。服务主要用于执行核心和低级操作系统任务,例如 Web 服务、事件日志记录、文件服务、帮助和支持、打印、加密和错误报告。话虽如此,与许多人认为的不同,没有任何限制可以阻止任何人向服务添加任何其他功能,包括用户界面和普通应用程序所做的任何事情。

基础知识

在我们深入探讨复杂问题之前,让我们从基础开始。一个典型的基于服务的项目将从一个空的 Win32API 项目创建。

一些常量

首先,有几个常量很有用

SERVICE_STATUS        g_ServiceStatus = {0};
SERVICE_STATUS_HANDLE g_StatusHandle = NULL;
HANDLE                g_ServiceStopEvent = INVALID_HANDLE_VALUE; 

SERVICE_STATUS 将用于获取服务的当前状态。

SERVICE_STATUS_HANDLE 将用于保存当前状态。

参见

BOOL WINAPI SetServiceStatus(
  _In_  SERVICE_STATUS_HANDLE hServiceStatus,
  _In_  LPSERVICE_STATUS lpServiceStatus 
);

为我们的服务命名

#define SERVICE_NAME L"CodeProjectDemo" 

安装我们的服务

这是典型的安装代码

DWORD InstallTheService()
{
	SC_HANDLE schSCManager;
    	SC_HANDLE schService;
    	TCHAR szPath[MAX_PATH];
    	if(!GetModuleFileName(NULL, szPath, MAX_PATH))
    	{
		DWORD Ret = GetLastError();
        	printf("Cannot install service (%d)\n", Ret);
        	return Ret;
    	}
	// Get a handle to the SCM database.  
    	schSCManager = OpenSCManager( 
        NULL,                    // local computer
        NULL,                    // ServicesActive database 
        SC_MANAGER_ALL_ACCESS);  // full access rights 
 
    	if (NULL == schSCManager) 
    	{
		DWORD Ret = GetLastError();
        	printf("OpenSCManager failed (%d)\n", Ret);
        	return Ret;
    	}
    	// Create the service.
    	schService = CreateServiceW( 
        schSCManager,              // SCM database 
        SERVICE_NAME,              // name of service 
        SERVICE_NAME,              // service name to display 
        SERVICE_ALL_ACCESS,        // desired access 
        SERVICE_WIN32_OWN_PROCESS, // service type 
        SERVICE_AUTO_START,		   // start type 
        SERVICE_ERROR_NORMAL,      // error control type 
        szPath,                    // path to service's binary 
        NULL,                      // no load ordering group 
        NULL,                      // no tag identifier 
        NULL,                      // no dependencies 
        NULL,                      // LocalSystem account 
        NULL);                     // no password 
 
    	if (schService == NULL) 
    	{
		DWORD Ret = GetLastError();
		if (Ret != 1073)
		{
			printf("CreateService failed (%d)\n", Ret);
			CloseServiceHandle(schSCManager);
			return Ret;
		}
		else
		{
			printf("The service is exists\n");
		}
    	}
    	else
		printf("Service installed successfully\n");
    	CloseServiceHandle(schService); 
    	CloseServiceHandle(schSCManager);
	return 0;
}  

卸载服务

当您希望卸载服务时,您执行以下操作

定义 SCM 和服务的句柄

SC_HANDLE schSCManager; 
SC_HANDLE schService; 

获取 SCM 数据库的句柄

schSCManager = OpenSCManager( 
        NULL,                    // local computer
        NULL,                    // ServicesActive database 
        SC_MANAGER_ALL_ACCESS);  // full access rights 

打开 SCM

if (NULL == schSCManager)  
{ 
	DWORD Ret = GetLastError();
        printf("OpenSCManager failed (%d)\n", Ret);
        return Ret;
 }

获取服务的句柄

schService = OpenServiceW( 
        schSCManager,       // SCM database 
        SERVICE_NAME,       // name of service 
        DELETE);            // need delete access 

终止服务

if (schService == NULL)
{
	DWORD Ret = GetLastError();
        printf("OpenService failed (%d)\n", Ret);
        CloseServiceHandle(schSCManager);
        return Ret;
} 

删除服务

if (!DeleteService(schService) ) 
{
	DWORD Ret = GetLastError();
        printf("DeleteService failed (%d)\n", Ret);
}
else printf("Service deleted successfully\n");  

清理。

CloseServiceHandle(schService);  
CloseServiceHandle(schSCManager);  

服务隔离

由于 Windows 服务是在 SYSTEM 用户帐户下运行,而不是在任何其他用户帐户下运行,因此服务可以变得非常强大,并可能带来潜在的安全风险。正因为如此,Microsoft 引入了服务隔离。在此更改之前,所有服务都与应用程序一起在会话 0 中运行。

图 1 - 隔离前

自 Windows Vista、Windows Server 2008 及更高版本的 Windows 以来,操作系统将服务隔离在会话 0 中,并在其他会话中运行应用程序(每个登录用户一个会话),因此服务可以防止来自应用程序代码的攻击。

图 2 - 隔离后

结果是,如果 NT 服务试图访问剪贴板,截取活动屏幕快照或显示对话框,因为它无法访问任何已登录用户使用的空间,捕获的剪贴板数据将包含空内容,捕获的屏幕实际上将是一个空桌面图像,上面什么也没有显示,当显示对话框时,由于用户不在会话 0 中运行,他将看不到 UI,因此将无法提供服务所需的输入。为了响应,用户需要切换到不同的视图才能看到它。

为了根据操作系统版本判断我们是否需要假定服务将被隔离,我们可以创建以下参数

    BOOL api_bSessionIsolated = FALSE; //   are we isolated or not?  

然后,我们执行以下检查

    // Checking the current Windows version
    OSVERSIONINFO osVersionInfo = {0};
    osVersionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
    GetVersionEx(&osVersionInfo);
    if (osVersionInfo.dwMajorVersion >= 6)
        api_bSessionIsolated = TRUE;
    else
        api_bSessionIsolated = FALSE; 

隔离服务显示消息框

调用 WTSSendMessage

 BOOL WTSSendMessage(
  _In_   HANDLE hServer,
  _In_   DWORD SessionId,
  _In_   LPTSTR pTitle,
  _In_   DWORD TitleLength,
  _In_   LPTSTR pMessage,
  _In_   DWORD MessageLength,
  _In_   DWORD Style,
  _In_   DWORD Timeout,
  _Out_  DWORD *pResponse,
  _In_   BOOL bWait
);

与其他会话通信

为了与其他会话交互和通信,服务应使用 CreateProcessAsUser API 来创建一个代理,该代理将在用户的会话下运行所有用户相关的任务并与服务交互,而服务则在会话 0 下运行。

下面是正确实现该功能所需的步骤。

步骤 1:获取当前活动的 Windows 会话

这通过调用 WTSGetActiveConsoleSessionId 完成,该函数返回控制台(即机器键盘和显示器,而不是 WTS 会话)上当前活动的 Windows 会话 ID。

DWORD WTSGetActiveConsoleSessionId(void);  

我曾读过关于调用 WTSGetActiveConsoleSessionId失败或错误的案例(例如,当 NT 服务调用时它总是返回 0 的情况),因此我将介绍另一种方法,即枚举所有会话并找到处于 WTSConnected 状态的会话。

要理解此方法,我们首先需要了解每个会话的可能状态,这些状态在 Windows SDK 头文件中定义的 WTS_CONNECTIONSTATE_CLASS 中。

请参阅 c:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Include\WtsApi32.h

typedef enum _WTS_CONNECTSTATE_CLASS {
    WTSActive,              // User logged on to WinStation
    WTSConnected,           // WinStation connected to client
    WTSConnectQuery,        // In the process of connecting to client
    WTSShadow,              // Shadowing another WinStation
    WTSDisconnected,        // WinStation logged on without client
    WTSIdle,                // Waiting for client to connect
    WTSListen,              // WinStation is listening for connection
    WTSReset,               // WinStation is being reset
    WTSDown,                // WinStation is down due to error
    WTSInit,                // WinStation in initialization
} WTS_CONNECTSTATE_CLASS;    

所以我们的函数看起来像这样

DWORD WINAPI GetActiveSessionId()
{
    PWTS_SESSION_INFO pSessionInfo = 0;
        DWORD dwCount = 0;
        WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSessionInfo, &dwCount);
       DWORD dwActive;
        for (DWORD i = 0; i < dwCount; ++i)
        {
            WTS_SESSION_INFO si = pSessionInfo[i];
            if (WTSActive == si.State)
            { 
                    dwActive = si.SessionId;
            WriteToLog(L"Session ID = %d",dwActive);
                    break;
            }
        }     
    WTSFreeMemory(pSessionInfo);
    return dwActive;
} 

注意:我在所有代码中都添加了 WriteToLog,这对于将所有内容追踪到一个连续的日志文件非常有用。

在大多数情况下,当您登录且假设只有一个用户登录时,会话 ID 将是“1”。

但是我们需要枚举所有会话还是可以直接使用 WTSGetActiveConsoleSessionId

我的结论是 YES。我修改了我的函数如下,得到了相同的结果。

DWORD WINAPI GetActiveSessionId()
{
   DWORD dwActive;
   dwActive = WTSGetActiveConsoleSessionId();
   WriteToLog(L"Session ID according to WTSGetActiveConsoleSessionId is %d",dwActive);
 
    PWTS_SESSION_INFO pSessionInfo = 0;
    DWORD dwCount = 0;
    WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSessionInfo, &dwCount);
    for (DWORD i = 0; i < dwCount; ++i)
    {
        WTS_SESSION_INFO si = pSessionInfo[i];
        if (WTSActive == si.State)
        { 
            dwActive = si.SessionId;
            WriteToLog(L"Session ID = %d",dwActive);
            break;
        }
    }     
    WTSFreeMemory(pSessionInfo);
    return dwActive;
}

这意味着它看起来可能像这样

DWORD WINAPI GetActiveSessionId()
{ 
       DWORD dwActive;
       dwActive = WTSGetActiveConsoleSessionId(); 
    return dwActive; 
}

步骤 2:查询当前会话的令牌

接下来,我们调用 WTSQueryUserToken 以获取该会话的令牌。

BOOL WTSQueryUserToken(
  _In_   ULONG SessionId,
  _Out_  PHANDLE phToken 
);    

我们调用 WTSQueryUserToken,并向其传递上次调用的会话 ID

WTSQueryUserToken (GetActiveSessionId(), &hToken)    

替代方法

请注意,WTSQueryUserToken 只能从在 LocalSystem 帐户下运行的服务中调用。另一种方法是调用 OpenProcessToken

BOOL WINAPI OpenProcessToken(
  _In_   HANDLE ProcessHandle,
  _In_   DWORD DesiredAccess,
  _Out_  PHANDLE TokenHandle
);

进程句柄可以是当前进程或(几乎)始终运行的进程,即 explorer.exe

步骤 3:复制令牌

接下来,我们调用 DuplicateTokenEx 来复制令牌。

DuplicateTokenEx(hToken,MAXIMUM_ALLOWED,NULL,SecurityIdentification,
                 TokenPrimary, &hTokenDup); 

步骤 4:创建环境

接下来,我们通过调用 CreateEnvironmentBlock 为即将创建的新进程创建环境。

这是如何完成的

BOOL WINAPI CreateEnvironmentBlock(
  _Out_     LPVOID *lpEnvironment,
  _In_opt_  HANDLE hToken,
  _In_      BOOL bInherit 
); 

调用新进程

现在我们准备创建新进程,从服务中调用它,同时在活动用户帐户下创建它。我们通过调用 CreateProcessAsUser 来完成此操作。

BOOL WINAPI CreateProcessAsUser(
  _In_opt_     HANDLE hToken,
  _In_opt_     LPCTSTR lpApplicationName,
  _Inout_opt_  LPTSTR lpCommandLine,
  _In_opt_     LPSECURITY_ATTRIBUTES lpProcessAttributes,
  _In_opt_     LPSECURITY_ATTRIBUTES lpThreadAttributes,
  _In_         BOOL bInheritHandles,
  _In_         DWORD dwCreationFlags,
  _In_opt_     LPVOID lpEnvironment,
  _In_opt_     LPCTSTR lpCurrentDirectory,
  _In_         LPSTARTUPINFO lpStartupInfo,
  _Out_        LPPROCESS_INFORMATION lpProcessInformation
);

注意:建议使用 W 版本 (UNICODE),而不是 A 版本,因为它存在一些错误。

清理

在应用程序终止之前,清理工作包括调用 CloseHandleDestroyEnvironmentBlock

处理各种场景

在构建了一个在用户会话中调用进程运行的服务骨架之后,最大的挑战是跟踪和处理各种场景,例如

  • 切换用户 - 任何新用户登录或当前用户注销后以不同凭据登录的情况等等。
  • 注销/登录 - 测试服务在注销和登录之间如何运行,即当 Windows 登录屏幕出现时会发生什么(例如,包含服务部分的备份解决方案是否可以在此时继续向服务器发送文件)。注销后,系统会销毁与该用户关联的会话。WTSQueryUserToken
  • 重启(硬重启和软重启)——当用户会话进程正在运行,用户按下“重启”菜单或执行硬重启时会发生什么。
  • 开关机(硬开关机和软开关机)——当用户按下“关机”菜单,或者仅仅按下/按钮并执行硬关机时会发生什么。
  • Windows 更新 - 我们还需要考虑重新启动包括安装 Windows 更新的情况,这发生在实际重新启动开始之前以及 Windows 重新启动后,登录之前。

我的 WriteToLog 例程

最后,我想与您分享我的 WriteToLog 例程

void WriteToLog(LPCTSTR lpText, ...)
{
	FILE* file;
	CTime time = CTime::ApiGetCurrentLocalTime();
	CString strMsg;
	va_list ptr;
	va_start(ptr, lpText);
	strMsg.VFormat(lpText, ptr);
	CString strDate = time.FormatDate(_T("d/MM/yyyy"));
	CString strTime = time.FormatTime(_T("hh:mm:ss tt"));
	
	CString strTrace;
	strTrace.Format(_T("%s %s: %s"), (LPCTSTR)strTime, 
                   (LPCTSTR)strDate, (LPCTSTR)strMsg);
	
	file = _tfopen(LOG_FILENAME, L"a");
	if (file)
	{
		_ftprintf(file, _T("\n%s\n"), (LPCTSTR)strTrace);
		fclose(file);
	}
}

历史

  • 2013 年 8 月 23 日:初始版本
© . All rights reserved.