Introduction
During my career in banking companies, I spent a lot of time developing software components that had to start running at machine startup without any user logon. This meant writing a Windows service. Moreover I had to also provide a GUI that should allow users to change some basic configuration settings.
With the introduction of .NET Framework, to write a system service has become much simpler than before. This becomes possible with the use of classes in the System.ServiceProcess
namespace. Until Windows XP, there were no strict security rules to conform to and above all service and user process were freely able to share resources with not many problems. But with the release of the next generation of Windows, some of these tasks become more complex to realize and developers have to spend a lot of effort to make it work again. In this article, I want to share with you the skeleton that I've used to upgrade some services and make them Vista ready like my GekoMirror application.
The Architecture
The architecture is strictly client-server, where the server part runs in a Windows service and the client is a window form that allow the users to consume the service and to configure its behavior. First of all, it is important to remember that in the new releases of Windows, the client and the service often run in different security contexts: Windows service runs with “Local System” or “Network Service” permissions, and the GUI runs using the logged users security context. I suggest you to read about the new features introduced with the release of Windows VISTA like the User Access Control (UAC). You can find a useful document at this link. You will also be interested in how Windows restricts access to core system resources (folders, registry, system objects). Our service must be patched in order to satisfy these new requirements.
Implementation
The communication between GUI and the service is built using a named-pipe, the information exchange is text based with a “command line” syntax: The client sends string
messages into the pipe with the format “command param1 param2 …
” and waits for a response from the server. Data and configuration persistence are implemented making use of files and the system registry.
Guidelines Vista Ready
Let’s assume that our service is running as local system and the GUI as logged user. Then the service can access with read and write permission the %programfiles% folder and the LocalMachine registry hive, differently the GUI, that runs with a low level security token, cannot. So, every time the GUI needs to make some permanent changes to the configuration, it has to request the service to do the work for it. Moreover when service creates the named pipe where the client sends commands into, the default operating system behavior is to grant only the read access to non administrator users. Then it is necessary to explicitly grant “standard users” to write into the pipe.
In general the guidelines are:
On Server Side
- Do all the work needed to create, modify and delete files or folders in the core system folders (%systemroot%, %programfiles%).
- Do all the work needed to create, modify and delete registry hive or values.
- Grant explicitly to the standard users the right permissions to the system objects. (pipe, timers, mutex, …).
On Client Side
- Ask the service to do all those tasks that it cannot do.
- Ask the service to return all the configuration settings that the client needs.
Applying these “little” corrections, you can patch your service/client application to work with the next generation of Windows.
Source Code
The source code is broken into three parts:
- A common part that is used to work with pipe: “Interop” class
- A server part that manages commands execution, the client requests and service commands: “
CommandEngine
” and “Server
” classes - A client part that is a window form: “
frmClient
” class
Points of Interest
Service Threading
One interesting point is the use of threading to answer system commands. When system calls the “OnStart
” subroutine, a new thread is created and the thread function will stay “alive” with an infinite loop until it will be killed. Otherwise when a stop request is made, all active client connections are terminated and both the working thread and the thread that listens for new client connection stop.
Public Class MyService
Private ps As Server
Private st As Thread
Protected Overrides Sub OnStart(ByVal args() As String)
Try
ps = New Server()
AddHandler ps.MessageReceived, AddressOf ReceiveMessage
st = New Thread(AddressOf ps.Start)
st.Start()
Catch ex As Exception
EventLog.WriteEntry("MyService", _
"An error occurred creating server class." & vbCrLf & ex.Message, _
EventLogEntryType.Error)
Throw ex
End Try
End Sub
Protected Overrides Sub OnStop()
If st.ThreadState <> Threading.ThreadState.Unstarted Then
ps.Stop()
Try
st.Join(10000)
EventLog.WriteEntry("MyService", _
"Server thread is terminated successfully.", _
EventLogEntryType.Information)
Catch ex As Exception
st.Abort()
EventLog.WriteEntry("MyService", _
"An error occurred terminating server thread." & vbCrLf & ex.Message, _
EventLogEntryType.Error)
End Try
End If
End Sub
[…]
End Class
Class Server
Public Sub Start()
Me._listenThread = New Thread(New ThreadStart(AddressOf ListenForClients))
Me._listenThread.Start()
End Sub
Public Sub [Stop]()
For Each c As ClientInfo In Me._clients
Interop.DisconnectNamedPipe(c.handle)
c.handle.Close()
Next
Interop.DisconnectNamedPipe(Me._new_clientHandle)
Me._new_clientHandle.Close()
Me._listenThread.Abort()
End Sub
[…]
End Class
Pipe Security
Another point of interest is creating the pipe and how write permissions are granted to standard users. To create a named pipe with a modified security attribute, I used the “Security Descriptor String” notation and created a security descriptor with the function ConvertStringSecurityDescriptorToSecurityDescriptor()
.
Dim sd As IntPtr
Dim sds As String
Dim sdl As Integer
sds = "D:(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;BA)(A;;FA;;;BU)"
rc = Interop.ConvertStringSecurityDescriptorToSecurityDescriptor(sds, 1, sd, sdl)
If Not rc Then
EventLog.WriteEntry("MyService", _
"ConvertStringSecurityDescriptorToSecurityDescriptor:" & _
Marshal.GetHRForLastWin32Error, _
EventLogEntryType.Error)
Exit Sub
End If
Dim sa As Interop.SECURITY_ATTRIBUTES
sa = New Interop.SECURITY_ATTRIBUTES
sa.nLength = Marshal.SizeOf(GetType(Interop.SECURITY_ATTRIBUTES))
sa.bInheritHandle = 1
sa.lpSecurityDescriptor = sd
Me._new_clientHandle = Interop.CreateNamedPipe( _
Interop.PIPE_NAME, _
Interop.DUPLEX Or Interop.FILE_FLAG_OVERLAPPED, _
0, _
255, _
Interop.BUFFER_SIZE, _
Interop.BUFFER_SIZE, _
0, _
sa)
If (Me._new_clientHandle.IsInvalid) Then
Exit Sub
End If
Command Parsing & Executions
As mentioned before, client/sever message exchange has a “command line” syntax. The client sends a message to the server in the format “command argument1 argument2 …
” and waits for a not empty response. Pay attention to return something also after an exception, otherwise client process may hang.
Public Class CommandEngine
Public Class Commands
Public Const CMD_ADD = "add"
Public Const CMD_MUL = "mul"
Public Const CMD_DIV = "div"
Public Const CMD_SUB = "sub"
End Class
Public Const CMD_ERROR = "ERROR."
Public Const CMD_NOTFOUND = "NOT FOUND."
Public Shared Function ExecuteCommand(ByVal cmd As String) As String
Dim r As String
Dim x As Double
Dim y As Double
r = String.Empty
x = 0
y = 0
Dim args As String()
args = cmd.ToLower.Split(" ")
If args.Length <> 3 Then
Return CMD_ERROR
End If
x = args(1)
y = args(2)
If args(0) = Commands.CMD_ADD Then
Try
r = (x + y).ToString
Catch ex As Exception
Return CMD_ERROR
End Try
ElseIf args(0) = Commands.CMD_SUB Then
Try
r = (x - y).ToString
Catch ex As Exception
Return CMD_ERROR
End Try
ElseIf args(0) = Commands.CMD_MUL Then
Try
r = (x * y).ToString
Catch ex As Exception
Return CMD_ERROR
End Try
ElseIf args(0) = Commands.CMD_DIV Then
Try
r = (x / y).ToString
Catch ex As Exception
Return CMD_ERROR
End Try
Else
Return CMD_NOTFOUND
End If
Return r
End Function
End Class
Running the Sample
The sample consists of a service executable Myservice.exe and a GUI client application Client.exe. To run the sample, you need to install the service using the “installutil” command. You must run the “Visual Studio command prompt” with administrative privileges and execute the utility in the executables folder of the project:
C:\Temp\VBClient-Service\MyService\bin\Debug> Installutil MyService.exe
Conclusion
Windows VISTA represents a big step forward in security management, but many efforts are required to patch some backward compatibility issues. Have a good upgrade.