Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / All-Topics

Get the Security Descriptor of a Windows Service With WMI

5.00/5 (2 votes)
5 Jun 2013CPOL9 min read 23.6K   196  
Get the Security Descriptor of a Windows Service with WMI.

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:

SvcSDDL

And here is what it looks like after formatting by hand in the textbox.

SvcSDDLFormated

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.

VB
Private Sub FillServiceNames()

    'Use ServiceProcess class to fill in the names
    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.

VB
Private Sub FillComboBox()
    ' Use WMI to fill in the Names
    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”.

VB
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.

VB
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
        'Error returned without EnablePrivleges 2147943714 (Converted)0x80070522 End If

    Scope.Connect()
    Dim Options As New ObjectGetOptions()
    'Dim Path As New ManagementPath("Win32_Service.Name=""AdobeARMservice""")
    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 ' No Problem continue on.
        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 '******************** 'Start the conversion '********************
    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.

VB
Dim outParams As ManagementBaseObject = _
   ClassInstance.InvokeMethod("GetSecurityDescriptor", inParams, Nothing)

In VBScript it is.

VB
' Get security descriptor for Service Return = objService.GetSecurityDescriptor( objSD )

What is objSD? it is not Declared anywhere like VB.NET. But it just works in the script.

And the WMI Code Creator Output.

VB
' Execute the method and obtain the return values.
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.

VB
inParams2("Descriptor") = outParams("Descriptor")

In VBScript:

VB
Return = objHelper.Win32SDToSDDL( objSD,SDDLstring )

In the WMI Code Creator you had to give a name, who knew what it was supposed to be.

VB
' Add the input parameters.
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)