Since most of the information I found online about Windows services was outdated, this article is my attempt to give more up to date information about the topic.
Introduction
While looking for articles about Windows services, I found that most of the information online is outdated. There are many articles and code snippets which were published before 2000 ! For example, if you look for books on Amazon, you will find: Win32 System Service or Professional NT Service, both published before or near 2000, So I decided to write my own research about Windows Services. The changes in the way Windows handles Windows Services since Windows 7 are also important for understanding the proper way to write and use Windows Services and yet keep it compatible to all OS versions. I invite you to read also a more advanced article about Windows Services.
Background
NT Service (also known as Windows Service) is the term given to special process which is loaded by the Service Control Manager of the NT kernel and runs in the background right after Windows starts (before users log on). Services are needed mostly to perform core and low level OS tasks, such as Web serving, event logging, file serving, help and support, printing, cryptography, and error reporting. That being said, and unlike what many people think, there isn't any limitation preventing anyone to add any other functionality to a service including user interface and anything else that normal applications do.
The Basics
Before we go ahead to complex issues, let's start with the basics. A typical service based project will be created from an empty Win32API project.
Some Constants
First, there are several constants which are useful:
SERVICE_STATUS g_ServiceStatus = {0};
SERVICE_STATUS_HANDLE g_StatusHandle = NULL;
HANDLE g_ServiceStopEvent = INVALID_HANDLE_VALUE;
SERVICE_STATUS
will be used to obtain the current status of the service.
SERVICE_STATUS_HANDLE
will be used to hold the current status.
See:
BOOL WINAPI SetServiceStatus(
_In_ SERVICE_STATUS_HANDLE hServiceStatus,
_In_ LPSERVICE_STATUS lpServiceStatus
);
Giving a Name to Our Service
#define SERVICE_NAME L"CodeProjectDemo"
Installing Our Service
Here is a typical code for installation:
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;
}
schSCManager = OpenSCManager(
NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (NULL == schSCManager)
{
DWORD Ret = GetLastError();
printf("OpenSCManager failed (%d)\n", Ret);
return Ret;
}
schService = CreateServiceW(
schSCManager, SERVICE_NAME, SERVICE_NAME, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, szPath, NULL, NULL, NULL, NULL, NULL);
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;
}
Uninstalling a Service
When you wish to uninstall a service, you do the following:
Define the Handles for the SCM and the Service
SC_HANDLE schSCManager;
SC_HANDLE schService;
Get a Handle to the SCM Database
schSCManager = OpenSCManager(
NULL, NULL, SC_MANAGER_ALL_ACCESS);
Open SCM
if (NULL == schSCManager)
{
DWORD Ret = GetLastError();
printf("OpenSCManager failed (%d)\n", Ret);
return Ret;
}
Get a Handle to the Service
schService = OpenServiceW(
schSCManager, SERVICE_NAME, DELETE);
Terminate the Service
if (schService == NULL)
{
DWORD Ret = GetLastError();
printf("OpenService failed (%d)\n", Ret);
CloseServiceHandle(schSCManager);
return Ret;
}
Delete the Service
if (!DeleteService(schService) )
{
DWORD Ret = GetLastError();
printf("DeleteService failed (%d)\n", Ret);
}
else printf("Service deleted successfully\n");
Cleanup
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);
Service Isolation
Since Windows Services operate under the SYSTEM
user account as opposed to any other user account exists, services can become powerful and can be a potential security risk. Because of that, Microsoft introduced isolation of services. Before that change, all services ran in Session 0 along with applications.
Image 1 - Before isolation
Since Windows Vista, Windows Server 2008, and later versions of Windows, the operating system isolates services in Session 0 and runs applications in other sessions, a session per logged user, so services are protected from attacks that originate in application code.
Image 2 - After isolation
As a result, if an NT Service tries to access the Clipboard, take a snapshot of the active screen or displays a dialog box, since it can't access any space used by any of the logged users, captured Clipboard data will contain nothing, captured screen will be in fact an empty desktop like image with nothing shown on it and when a dialog box is displayed, since the user is not running in Session 0, he will not see the UI and therefore will not be able to provide the input that the service is looking for. In order to respond, the user will need to switch to a different view to see it.
To know whether we need to assume the service will be isolated, based on the OS version, we can create the following argument:
BOOL api_bSessionIsolated = FALSE;
Then, we do the following check:
OSVERSIONINFO osVersionInfo = {0};
osVersionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
GetVersionEx(&osVersionInfo);
if (osVersionInfo.dwMajorVersion >= 6)
api_bSessionIsolated = TRUE;
else
api_bSessionIsolated = FALSE;
Displaying a Message Box by an Isolated Service
Call 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
);
Communicating with Other Sessions
In order to interact and communicate with other sessions, the service should use the CreateProcessAsUser
API in order to create an Agent which will run all user related tasks under the user's session and will interact with the service, while it runs under session 0
.
Given below are the steps that need to be followed to implement that properly.
Step 1: Obtaining the Current Active Windows Session
That is done by calling WTSGetActiveConsoleSessionId which returns the ID of the current active Windows session at the console (i.e., the machine keyboard and display, as opposed to WTS sessions).
DWORD WTSGetActiveConsoleSessionId(void);
I have read about failure or errors when calling WTSGetActiveConsoleSessionId (for example, cases in which it always returns 0 when invoked by an NT Service) so I will introduce another option which would be enumerating all sessions and finding the one that is in WTSConnected
state.
To understand that method, we first need to understand the possible states of each session which are defined in the WTS_CONNECTIONSTATE_CLASS which can be found the Windows SDK header files.
See c:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Include\WtsApi32.h.
typedef enum _WTS_CONNECTSTATE_CLASS {
WTSActive, WTSConnected, WTSConnectQuery, WTSShadow, WTSDisconnected, WTSIdle, WTSListen, WTSReset, WTSDown, WTSInit, } WTS_CONNECTSTATE_CLASS;
So our function will look like this:
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;
}
Note: I add WriteToLog
to anything I code, which is great to trace anything into one continuous log file.
In most cases, while you are logged in and assuming only one user is logged in, the session ID will be "1
".
But do we need to enumerate all sessions or can we just use WTSGetActiveConsoleSessionId
?
My conclusion is YES
. I changed my function as follows and got the same results.
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;
}
which means that it can just look like this:
DWORD WINAPI GetActiveSessionId()
{
DWORD dwActive;
dwActive = WTSGetActiveConsoleSessionId();
return dwActive;
}
Step 2: Querying the Token of the Current Session
Next, we call WTSQueryUserToken to get the token for that session.
BOOL WTSQueryUserToken(
_In_ ULONG SessionId,
_Out_ PHANDLE phToken
);
We call WTSQueryUserToken
passing to it the session ID from the last call:
WTSQueryUserToken (GetActiveSessionId(), &hToken)
Alternative Method
Please note that WTSQueryUserToken can only be called from services running under LocalSystem
account. An alternative would be calling OpenProcessToken.
BOOL WINAPI OpenProcessToken(
_In_ HANDLE ProcessHandle,
_In_ DWORD DesiredAccess,
_Out_ PHANDLE TokenHandle
);
The process handle can be the current process or a process that (almost) always runs, explorer.exe.
Step 3: Duplicating the Token
Next, we call DuplicateTokenEx to duplicate the token.
DuplicateTokenEx(hToken,MAXIMUM_ALLOWED,NULL,SecurityIdentification,
TokenPrimary, &hTokenDup);
Step 4: Creating the Environment
Next, we create the environment for the new process to be created by calling CreateEnvironmentBlock.
Here is how that is done:
BOOL WINAPI CreateEnvironmentBlock(
_Out_ LPVOID *lpEnvironment,
_In_opt_ HANDLE hToken,
_In_ BOOL bInherit
);
Invoking the New Process
Now we are ready to create the new process, invoking it from the service and yet creating it under the active user's account. We do that by calling 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
);
Note: It is advised to use the W version (UNICODE) and not the A version, as it has some bugs.
Cleaning Up
Before termination of our application, the cleanup includes calling CloseHandle and DestroyEnvironmentBlock.
Handling Various Scenarios
After building a skeleton of a service which invokes a process to run in the user's session, the big challenge is to keep track and address a wide range of scenarios such as:
- Switching users - any case in which a new user logs in or the current user logs out and then logs in under a different credentials, etc.
- Log off / on - testing how the service operates between a log out and a log in, i.e., what is done when the Windows log in screen appears. (for example, could backup solutions, which contain a Service part, continue to send files to the server at that time). After logging off, the system destroys the session associated with that user.WTSQueryUserToken
- Restart (hard and soft) - what happens when the user's session process is running and the end, user presses the "Restart" menu or performs a hard restart.
- Turn PC off and on (hard and soft) - what happens when the user presses the "Turn off" menu, or just hits the On/Off button and performs a hard turn off.
- Windows updates - we need to add to that cases where restart includes installing Windows update, which takes place before restart actually starts and after Windows starts again, just before log in.
My WriteToLog Routine
Finally, I wanted to share with you my WriteToLog
routine:
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);
}
}
History
- 23rd August, 2013: Initial version