Reason for Project
A while back I wrote a little utility to temporarily turn off the Microsoft Security Essentials Anti-Virus / Anti-Malware service.
In January of 2013 I discovered that my application no longer worked due to an Access denied error. I started to investigate why and discovered my application no longer had the “Rights” to shut down the service even though it was “Run As Admin”.
In order to understand what rights I or an application I wrote would have over a service I needed to see what Users and Rights were assigned to the service. That would also explain why I no longer could control the service thru the built in Service Controller.
Back story
Beginning with Windows Vista we now have Service isolation, Service Hardening and the ability to assign a SID to the service to help secure it more. You also need to secure the registry where the information about your service is listed so it can not be changed by unauthorized users or code.
The service can be set up with “Default permissions” or set with “Special permissions”, effectively locking it down to help keep users or malicious code from messing with it.
This is just one of many articles on the subject. Services Hardening in Windows Vista here.
So services just keep getting better protection to help keep malicious code from stopping or compromising a service.
The Code
For several months I have been looking into finding a way to get the security descriptor from a service but no sample code could be found at the time. I asked the question on Code Project but did not get an answer.
The .Net framework has a built in way to get at the security descriptor for the Registry,Folder, and Files but after going thru the Object browser (.Net 3.5) for some time I could not find a way to do it without creating the needed wrapper classes like was done for the other items.
So I decided to try to Platform Invoke with the “QueryServiceObjectSecurity” function here.
After several attempts and asking another question on Code Project and not getting an answer I kept digging and am now close but not quite there yet on getting it to work.
Now that I have a working model I can see what I am missing and possibly get it to work.
In the mean time while still researching how to do it I stumbled across a question that was asked on Stack Overflow here titled “Getting Win32_Service security descriptor using VBScript”. That question gave me a starting point to try VB script. Further research landed me on the MSDN page where I think it was originally taken from. here. WMI Security Descriptor Objects.
After seeing that, I was able to work out the following VB Script to get the Security Descriptor for all services.
VBScript
'Tested Script Still works with the values commented out.
'SE_OWNER_DEFAULTED = &h1
'SE_GROUP_DEFAULTED = &h2
'SE_DACL_PRESENT = &h4
'SE_DACL_DEFAULTED = &h8 'ACCESS_ALLOWED_ACE_TYPE = &h0 'ACCESS_DENIED_ACE_TYPE = &h1
strComputer = "." Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate, (Security)}!\\" & strComputer & "\root\cimv2")
' Get an instance of Win32_SecurityDescriptorHelper Set objHelper = GetObject( _
"winmgmts:root\cimv2:Win32_SecurityDescriptorHelper" )
Set colServices = objWMIService.ExecQuery _
("Select * from Win32_Service")
For Each objService in colServices
Wscript.Echo "Name: " & objService.Name
' Get security descriptor for Service Return = objService.GetSecurityDescriptor( objSD )
If ( return <> 0 ) Then
WScript.Echo "Could not get security descriptor: " & Return
wscript.Quit Return End If If ( return = 1 ) Then
WScript.Echo "The request is not supported: " & Return
wscript.Quit Return End If If ( return = 2 ) Then
WScript.Echo "The user did not have the necessary access: " & Return
wscript.Quit Return End If If ( return = 8 ) Then
WScript.Echo "Interactive process: " & Return
wscript.Quit Return End If If ( return = 9 ) Then
WScript.Echo "The name specified was not valid: " & Return
wscript.Quit Return End If If ( return = 21 ) Then
WScript.Echo "The parameters passed to the service are not valid: " & Return
wscript.Quit Return End If ' Convert Service security descriptor from
' Win32_SecurityDescriptor format to SDDL format
Return = objHelper.Win32SDToSDDL( objSD,SDDLstring )
If ( Return <> 0 ) Then
WScript.Echo "Could not convert to SDDL: " & Return
WScript.Quit Return End If
WScript.Echo SDDLstring
WScript.Echo ""
WScript.Echo "" Next
This script requires the “Security” Keyword and to be run as Admin for it to work.
That little script took several hours to work out once I got a clue on what needed to go into it.
For the most part it is self explanatory.
The part I had trouble understanding was the Object, “objSD”.
As it turn out that is the Security Descriptor itself. That was the out Object from the call to GetSecurityDescriptor
it was also an In Object to “Win32SDToSDDL
”. I did not totally understand this until I completed the VB.NET version of this coming up next.
The script version can be modified to just check one service name or, used “as is” , to get all of them.
VB.NET
The VB.NET version was a little tougher. I first tried to work it out by starting out with the code generated from my GUI WMI Code Creator but that wasn’t working.
I could probably count on one hand the number of times in the last fifteen years that I needed to “Invoke” a method in a WMI class in order to get the information I needed. So I have to relearn how every time.
Here is the Final Project:
And here is what it looks like after formatting by hand in the textbox.
As you can see you get the Owner, Group, DACL, and the SACL.
Let's start with where the program starts Filling in the combo box.
I wanted to test two different ways to see how they worked.
The first is getting the list by using the “System.ServiceProcess” class.
Remember you have to add that “reference” before you can “Imports” it or use it in your code.
Private Sub FillServiceNames()
Try
Dim svcs() As ServiceController = ServiceController.GetServices()
Dim svcCtlr As ServiceController
For Each svcCtlr In svcs
cbServiceNames.Items.Add(svcCtlr.ServiceName)
count = count + 1 Next
lblCount.Text = "Services Found: " & count.ToString
Catch ex As Exception
MsgBox(ex.Message)
End Try
End Sub
It is pretty straight forward.
Next is using WMI to fill the Combo Box.
Private Sub FillComboBox()
Try
Dim searcher As New ManagementObjectSearcher( _
"Root\CIMV2", _
"SELECT * FROM Win32_Service")
For Each queryObj As ManagementObject In searcher.Get()
cbServiceNames.Items.Add(queryObj("Name"))
count = count + 1 Next
lblCount.Text = "Services Found: " & count.ToString
Catch ex As Exception
MsgBox(ex.Message)
End Try
End Sub
They both make a call to get a list of services and then do a for each loop and then fill the Combo Box with the service name.
That was the easy part.
When I first couldn’t figure out the proper way to invoke a method using trial and error, I tried to use the “Strongly Typed” classes generated by “MgmtClassGen.exe” to get a better idea of what they were wanting, but I was still getting errors and could not figure out some of the required parameters. After several hours of that I tried the (Original) WMI Code creator. It has a section that generates code that works with methods. The code produced by that was not working and there was one parameter that I just could not figure out what it was supposed to be.
My next Idea was to turn to a program called “WMI Delphi Code Creator” found here.
I was given a link to it after I posted my article on my GUI WMI Code Creator, which does not do methods.
That program was able to produce code using methods like the (Original) WMI Code Creator but it was in C#, so I had to convert it to VB.Net.
So those that want a C# version may want to check it out.
(Side Note on that program, if you don’t want it calling home for updates then uncheck that option in the options section. Also they have a install and standalone version)
After generating the two required methods to get the security descriptor and to transform it from a Win32SD to a SDDL string form I dropped the code into the project and started piecing it together.
After several hours of working with the code and reading more on it and fixing several items that used the same names in both generated methods, the code finally went all of the way thru and output to the text box.
I was so happy that it stopped crashing that at first I didn’t understand the results that were returned.
There was no SDDL String returned and I had two different error codes retuned that was not listed in the normal WMI return Codes. The first one returned from the “GetSecurityDescriptor” method was this, 2147943714 (Converted)0×80070522.
As it turns out it is a privilege not held error from the system not WMI.
So back to the “Strongly Typed” classes to see if I could figure out what I was missing. I found this in the Function “GetSecurityDescriptor
”.
Dim EnablePrivileges As Boolean = PrivateLateBoundObject.Scope.Options.EnablePrivileges
PrivateLateBoundObject.Scope.Options.EnablePrivileges = true
That gave me the answer.
So without further ado, here is the final code.
Try Dim strbldr As New StringBuilder
Dim svcName As String
If cbServiceNames.SelectedIndex = Nothing Then
MsgBox("No Service Name Was Selected")
Exit Sub Else
svcName = cbServiceNames.SelectedItem.ToString
End If Dim ComputerName As String = "localhost" Dim Scope As ManagementScope
If Not ComputerName.Equals("localhost", StringComparison.OrdinalIgnoreCase) Then
Dim Conn As New ConnectionOptions()
Conn.Username = ""
Conn.Password = ""
Conn.Authority = "ntlmdomain:DOMAIN"
Scope = New ManagementScope([String].Format("\\{0}\root\CIMV2", ComputerName), Conn)
Else
Scope = New ManagementScope([String].Format("\\{0}\root\CIMV2", ComputerName), Nothing)
Scope.Options.EnablePrivileges = True
Scope.Connect()
Dim Options As New ObjectGetOptions()
Dim Path As New ManagementPath("Win32_Service.Name=" & "'" & svcName & "'")
Dim ClassInstance As New ManagementObject(Scope, Path, Options)
Dim inParams As ManagementBaseObject = ClassInstance.GetMethodParameters("GetSecurityDescriptor")
Dim outParams As ManagementBaseObject = _
ClassInstance.InvokeMethod("GetSecurityDescriptor", inParams, Nothing)
Dim operrHex As String = String.Format("0x{0:X2}", outParams("ReturnValue"))
Select Case outParams("ReturnValue")
Case 0
Case 2 MsgBox("Error Code 2" & _
vbNewLine & "The user does not have access to the requested information.")
Exit Sub
Case 8 MsgBox("Error Code 8" & vbNewLine & "Unknown failure.")
Exit Sub
Case 9 MsgBox("Error Code 9" & vbNewLine & "The user does not have adequate privileges.")
Exit Sub
Case 21 MsgBox("Error Code 21" & vbNewLine & "The specified parameter is invalid")
Exit Sub
Case Else MsgBox("Error Not Listed" & vbNewLine _
& "Error Code" & vbNewLine _
& outParams("ReturnValue").ToString _
& vbNewLine & "Hex:" _
& vbNewLine & operrHex)
Exit Sub
End Select
Dim Path2 As New ManagementPath("Win32_SecurityDescriptorHelper")
Dim ClassInstance2 As New ManagementClass(Path2)
Dim inParams2 As ManagementBaseObject = ClassInstance2.GetMethodParameters("Win32SDToSDDL")
inParams2("Descriptor") = outParams("Descriptor")
Dim outParams2 As ManagementBaseObject = _
ClassInstance2.InvokeMethod("Win32SDToSDDL", inParams2, Nothing)
strbldr.AppendLine("Security Descriptor in SDDL Format")
strbldr.AppendLine()
strbldr.AppendLine("Service Name:" & svcName)
strbldr.AppendLine()
Dim opstr As String If outParams2("SDDL") = Nothing Then
opstr = "SDDL String is nothing" Else
opstr = outParams2("SDDL")
End If
strbldr.AppendLine(opstr)
tbOutput.Text = strbldr.ToString
Catch ex As Exception
MsgBox(ex.Message)
End Try
The first part of the code just checks to see if a service name was selected from the combo box. If it was it sets “svcName
” to the name that was selected.
Next starts the generated code. This code is setup for Local and Remote use, I left it as is.
This part checks to see if you are connecting to WMI on the local system or on the network if yes then it inputs the network Credentials if not then it doesn’t use that parameter thus it is set to “Nothing” versus “Conn” as above.
As I hinted to above, The whole trick to get this to work with out that error above is to use the “Scope.Options.EnablePrivileges = True
” setting. Then Scope.Connect()
.
Next we set up to connect to the given service name and call the “GetSecurityDescriptor
” method.
This is the one of the lines that gave me so much trouble.
Dim outParams As ManagementBaseObject = _
ClassInstance.InvokeMethod("GetSecurityDescriptor", inParams, Nothing)
In VBScript it is.
What is objSD
? it is not Declared anywhere like VB.NET. But it just works in the script.
And the WMI Code Creator Output.
Dim outParams As ManagementBaseObject = _
classInstance.InvokeMethod("GetSecurityDescriptor", Nothing, Nothing)
I couldn’t figure out what they were wanting for a “ManagementBaseObject” when I was working with the “Strongly Typed” classes.
The next line is just there so that if you receive an error other than the normal ones in the list it will get the hex version of the Error and add that to a message box.
Next we set up the Select Case
statement using the values that would be output. Next we set up for the conversion.
Now we are at the line that I had so much trouble understanding what they were wanting.
inParams2("Descriptor") = outParams("Descriptor")
In VBScript:
Return = objHelper.Win32SDToSDDL( objSD,SDDLstring )
In the WMI Code Creator you had to give a name, who knew what it was supposed to be.
inParams("Descriptor") = InsertNameHere
The WMI Code Creator listed it as type “Object” but that was not very helpful.
By the “objSD
” in the script I assumed it was a Security Descriptor Object but didn’t know that was the Object you worked with. So it is the out parameter of the first call. In this case it was called amazingly, outParams
.
The last parts just sets up the string builder to get the name of the selected service.
Then verifies there is actually an output for the SDDL String.
Then finally write the information out to the textbox.
Once you understand what the input and output is expecting then it makes the job easier.
Later I may still go back and see if I can complete the Platform Invoke version just to see if I can figure it out now that I know what is involved using other methods.
I hope you learned something because I did.
SC.exe Utility
To get the DACL using this utility use the command
sc sdshow ServiceName
To get the SID of a service use the command
sc.exe showsid ServiceName
Note: this command uses Undocumented API calls to create a SID from the ServiceName.
Final Notes
These functions should only work on Vista and above.
References