Introduction
While looking for articles about NT services I have found that most of the information online is out dated. There are many articles and code snippets which were published before 2000 ! For example, if you look for books at 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 NT Services. The changes in the way Windows handles NT Services since Windows 7 are also important for understanding the proper way to write and use NT Services and yet keep it compatible to all OS versions.
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 from 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 an 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:
<pre>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
Installing a 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 to 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;
}<span style="color: rgb(17, 17, 17); font-family: 'Segoe UI', Arial, sans-serif; font-size: 14px;"> </span>
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 NT 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.
Here are the steps that need to be taken 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 follow 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 form 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 hit 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);
}
}
Michael Haephrati, born in 1964, an entrepreneur, inventor and a musician. Haephrati worked on many ventures starting from HarmonySoft, designing Rashumon, the first Graphical Multi-lingual word processor for Amiga computer.
Worked with Amdocs and managed several software projects, among them one for the Ministry of Tourism in New Zealand. During 1995-1996 he worked as a Contractor with Apple at Cupertino. After returning to Israel, worked as a Project Manager with Top Image Systems (mostly with JCC, Nicosia), and then at a research institute made the fist steps developing the credit scoring field in Israel. He founded Target Scoring and developed a credit scoring system named ThiS, based on geographical statistical data, participating VISA CAL, Isracard, Bank Leumi and Bank Discount (Target Scoring, being the VP Business Development of a large Israeli institute).
During 2000, he founded Target Eye, and developed the first remote PC surveillance and monitoring system, named Target Eye.
Other ventures included: Data Cleansing (as part of the DataTune system which was implemented in many organizations.
Also a Code Project Member since Sunday, March 16, 2003 (10 years, 5 months)
20 Sep 2013: Best C++ article of August 2013
25 Jan 2013: Code Project - Best C++ article of December 2012
31 Dec 2012: CodeProject MVP 2013