Windows 服务






4.87/5 (34投票s)
一篇关于 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 中运行。
自 Windows Vista、Windows Server 2008 及更高版本的 Windows 以来,操作系统将服务隔离在会话 0 中,并在其他会话中运行应用程序(每个登录用户一个会话),因此服务可以防止来自应用程序代码的攻击。
结果是,如果 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 版本,因为它存在一些错误。
清理
在应用程序终止之前,清理工作包括调用 CloseHandle 和 DestroyEnvironmentBlock。
处理各种场景
在构建了一个在用户会话中调用进程运行的服务骨架之后,最大的挑战是跟踪和处理各种场景,例如
- 切换用户 - 任何新用户登录或当前用户注销后以不同凭据登录的情况等等。
- 注销/登录 - 测试服务在注销和登录之间如何运行,即当 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 日:初始版本