This article will try to explain how to run services in unprivileged contexts and how to access services from unprivileged accounts.

Note that absolutely none of this is authoritative or directly based on relevant documentation. It’s mostly what I found and figured out and guessed and (in some cases) made up. Some of it may be wrong or dangerous or lead to disaster or confusion. I am not taking responsibility here for anything, not even spelling. Read and digest at your own peril!

Using GeneralTestService as an example (download at download generaltestservice), I will try to explain how to run the service with appropriate privileges and how to access it from an unprivileged (restricted) account.

PS C:\GeneralTestService> dir


    Directory: C:\GeneralTestService


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         7/20/2024   6:57 PM          14848 GeneralTestService.exe
-a----         2/13/2025   1:00 PM             14 GeneralTestService.ps1


PS C:\GeneralTestService> cat .\GeneralTestService.ps1
whoami /groups
PS C:\GeneralTestService>

This shows the GeneralTestService.exe binary (sophisticated people call this an “image”) and a one-line PowerShell script the service will run to show the group members of the running account.

First step, install GeneralTestService:

PS C:\GeneralTestService> sc.exe create GeneralTestService binpath=C:\GeneralTestService\GeneralTestService.exe
[SC] CreateService SUCCESS
PS C:\GeneralTestService> sc.exe qc GeneralTestService
[SC] QueryServiceConfig SUCCESS

SERVICE_NAME: GeneralTestService
        TYPE               : 10  WIN32_OWN_PROCESS
        START_TYPE         : 3   DEMAND_START
        ERROR_CONTROL      : 1   NORMAL
        BINARY_PATH_NAME   : C:\GeneralTestService\GeneralTestService.exe
        LOAD_ORDER_GROUP   :
        TAG                : 0
        DISPLAY_NAME       : GeneralTestService
        DEPENDENCIES       :
        SERVICE_START_NAME : LocalSystem
PS C:\GeneralTestService>

This will create the GeneralTestService Windows service and prepare it run as LocalSystem. This is not desirable in the long run. However, start it once with those ridiculous rights to allow it to create registry keys for further configuration:

PS C:\GeneralTestService> sc.exe start GeneralTestService

SERVICE_NAME: GeneralTestService
        TYPE               : 10  WIN32_OWN_PROCESS
        STATE              : 2  START_PENDING
                                (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        CHECKPOINT         : 0x0
        WAIT_HINT          : 0x7d0
        PID                : 3188
        FLAGS              :
PS C:\GeneralTestService> Get-ItemProperty HKLM:\SOFTWARE\AB\GeneralTestService\generaltestservice.exe\

LogName         : Application
EventSourceName : GeneralTestService
TestScript      : A:\NotARealPath\TestScript.ps1
PSPath          : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\AB\GeneralTestService\generaltestservice.exe\
PSParentPath    : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\AB\GeneralTestService
PSChildName     : generaltestservice.exe
PSDrive         : HKLM
PSProvider      : Microsoft.PowerShell.Core\Registry

PS C:\GeneralTestService>

This will start the service and create a registry key
HKLM:\SOFTWARE\AB\GeneralTestService\generaltestservice.exe
(You can create copies of GeneralTestService.exe and install several instances that will result in several keys.)

Note that the service writes several entries to the Application event log:

PS C:\GeneralTestService> Get-WinEvent -LogName Application -MaxEvents 6


   ProviderName: GeneralTestService

TimeCreated                      Id LevelDisplayName Message
-----------                      -- ---------------- -------
2/16/2025 12:33:43 PM             0 Information      Service started successfully.
2/16/2025 12:33:43 PM             0 Information      That was all, folks.
2/16/2025 12:33:43 PM             0 Information      No test script is given and hence will not be executed. Find the script path at [HKLM:\SOFTWARE\AB\GeneralTestService\GeneralTestSer...
2/16/2025 12:33:43 PM             0 Information      I appear to hold the following privileges:...
2/16/2025 12:33:43 PM             0 Information      I am running and my image name is [GeneralTestService.exe]....
2/16/2025 12:33:43 PM             0 Error            I am beginning my run through the event log with an error message to be more noticeable and make things generally more interesting!


PS C:\GeneralTestService>

Point the TestScript property to the actual test script:

PS C:\GeneralTestService> Set-ItemProperty HKLM:\SOFTWARE\AB\GeneralTestService\generaltestservice.exe\ TestScript C:\GeneralTestService\GeneralTestService.ps1
PS C:\GeneralTestService> Get-ItemProperty HKLM:\SOFTWARE\AB\GeneralTestService\generaltestservice.exe\


LogName         : Application
EventSourceName : GeneralTestService
TestScript      : C:\GeneralTestService\GeneralTestService.ps1
PSPath          : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\AB\GeneralTestService\generaltestservice.exe\
PSParentPath    : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\AB\GeneralTestService
PSChildName     : generaltestservice.exe
PSDrive         : HKLM
PSProvider      : Microsoft.PowerShell.Core\Registry



PS C:\GeneralTestService>

Note the individual events GeneralTestService is causing:

1

I am beginning my run through the event log with an error message to be more noticeable and make things generally more interesting!

2

I am running and my image name is [GeneralTestService.exe].
I am running as [WORKGROUP\SYSTEM].

3

I appear to hold the following privileges:
SeAssignPrimaryTokenPrivilege
SeLockMemoryPrivilege
SeIncreaseQuotaPrivilege

(With LocalSystem this list goes on a bit.)

3

Test script [C:\GeneralTestService\GeneralTestService.ps1] found and it gives us this output and error:


GROUP INFORMATION
-----------------

Group Name                             Type             SID          Attributes                                        
====================================== ================ ============ ==================================================
BUILTIN\Administrators                 Alias            S-1-5-32-544 Enabled by default, Enabled group, Group owner    
Everyone                               Well-known group S-1-1-0      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Authenticated Users       Well-known group S-1-5-11     Mandatory group, Enabled by default, Enabled group
Mandatory Label\System Mandatory Level Label            S-1-16-16384                                                   

4

That was all, folks.

5

Service started successfully.

And when it is shut down, it writes a sixth and seventh message:

6

I am shutting down.

7

Service started successfully.


This service now runs with full system rights and absolutely shouldn’t.

There are several ways to tame the service, which fall into three broad categories:

  1. Deprivilege the service itself.
  2. Use a less privileged account to run the service.
  3. Use no account to run the service and assign further rights and privileges to the service itself.

The first way will continue running the service under the identity of the system (the computer) itself but with fewer privileges.


1.1. Reduce the privileges of the service itself

This can be done with the service regardless of the identity it runs under but does nothing to stop the service from abusing rights given to the running account in ACLs.

PS C:\GeneralTestService> sc.exe privs GeneralTestService SeShutdownPrivilege
[SC] ChangeServiceConfig2 SUCCESS
PS C:\GeneralTestService> Restart-Service GeneralTestService
PS C:\GeneralTestService> sc.exe qprivs GeneralTestService
[SC] QueryServiceConfig2 SUCCESS

SERVICE_NAME: GeneralTestService
        PRIVILEGES       : SeShutdownPrivilege
PS C:\GeneralTestService>

GeneralTestService now reports in the Application event log that the number of its privileges has been drastically reduced:

I appear to hold the following privileges:
SeShutdownPrivilege
SeChangeNotifyPrivilege

The system privileges are gone, only SeShutdownPrivilege and SeChangeNotifyPrivilege remain. (The reason SeChangeNotifyPrivilege remains is because absolutely everything has that privilege. It’s supposed to be this way.)

This is but an example, but this would make an efficient service to reboot a computer. Note that the service can still touch every object on the computer, including files, processes, registry keys etc.. Privilege dropping like this should only be used in combination with other restricting methods.


1.2. Use one of the less privileged versions of the local system account

There are two such: LocalService and NetworkService. LocalService has slightly more rights than NetworkService but NetworkService can access the network.

First, let’s remove the privilege restriction the service and then switch it to the LocalService account:

PS C:\GeneralTestService> sc.exe privs GeneralTestService /
[SC] ChangeServiceConfig2 SUCCESS
PS C:\GeneralTestService> sc.exe qprivs GeneralTestService
[SC] QueryServiceConfig2 SUCCESS

SERVICE_NAME: GeneralTestService
        PRIVILEGES       :
PS C:\GeneralTestService> sc.exe config GeneralTestService obj="NT AUTHORITY\LocalService"
[SC] ChangeServiceConfig SUCCESS
PS C:\GeneralTestService> Restart-Service GeneralTestService
PS C:\GeneralTestService>

The service now reports in the Application log:

I am running and my image name is [GeneralTestService.exe].
I am running as [NT AUTHORITY\LOCAL SERVICE].

and

I appear to hold the following privileges:
SeAssignPrimaryTokenPrivilege
SeIncreaseQuotaPrivilege
SeSystemtimePrivilege
SeAuditPrivilege
SeChangeNotifyPrivilege
SeImpersonatePrivilege
SeCreateGlobalPrivilege
SeIncreaseWorkingSetPrivilege
SeTimeZonePrivilege

The LocalService and NetworkService accounts are effective ways of reducing a service’s rights but still let it run under the identity of the computer itself.


1.3. Write-restrict the service

A third way, which like the first can be combined with other methods, is to write-restrict the service. When this is done, the service has all the read rights you would expect it to have, but cannot write anywhere not specifically allowed. (The way it works is that a group NT AUTHORITY\WRITE RESTRICTED is implicitly in a deny entry at the top of every ACL in the system but if that group is added with write permissions or full access of if the service’s account or the service itself is added with write permissions or full access, the deny entry is overruled, i.e. never reached.)

PS C:\GeneralTestService> sc.exe config GeneralTestService obj="NT AUTHORITY\SYSTEM"
[SC] ChangeServiceConfig SUCCESS
PS C:\GeneralTestService> sc.exe sidtype GeneralTestService restricted
[SC] ChangeServiceConfig2 SUCCESS
PS C:\GeneralTestService> Restart-Service GeneralTestService
PS C:\GeneralTestService>

This reconfigured the service to run under LocalSystem again but with write-restrictions.

The service now reports in the Application log as a group membership (a result of the whoami /groups command in GeneralTestService.ps1):

NT AUTHORITY\WRITE RESTRICTED Well-known group S-1-5-33 Mandatory group, Enabled by default, Enabled group

This group membership will now stop the service from using its system rights for writing, anywhere; unless that group, the service’s account, or the service itself is specifically allowed to write to the object.

Note that this often doesn’t work as some software vendors make depecrated API calls that require write access.

To undo this:

PS C:\GeneralTestService> sc.exe sidtype GeneralTestService unrestricted
[SC] ChangeServiceConfig2 SUCCESS
PS C:\GeneralTestService>

There is also a third sidtype “none”.

unrestricted:
The service has a security identifier NT Service\ServiceName

restricted:
The service has a security identifier NT Service\ServiceName and is a member of NT AUTHORITY\WRITE RESTRICTED

none:
The service does not have a security identifier referring to the service itself


2.1 Use a local user account to run the service

This the method that basically came to Windows from Unix and can usually be found with ported services like PostgreSQL.

It is really not recommended.

PS C:\GeneralTestService> net user

User accounts for \\CHAMPIGNAC

-------------------------------------------------------------------------------
Administrator            benoit                   DefaultAccount
Guest                    legrand                  luke
WDAGUtilityAccount
The command completed successfully.

PS C:\GeneralTestService> sc.exe config GeneralTestService obj=.\benoit password=SomeSecretPassword
[SC] ChangeServiceConfig SUCCESS
PS C:\GeneralTestService> sc.exe start GeneralTestService
[SC] StartService FAILED 1069:

The service did not start due to a logon failure.

PS C:\GeneralTestService>

But the local account has to be allowed to log on as a service. This can be done in secpol.msc or using the AccountRights tool from ABTokenTools (see Introduction).

PS C:\GeneralTestService> & 'C:\Program Files\ABTokenTools\AccountRights.exe' benoit
0
PS C:\GeneralTestService> & 'C:\Program Files\ABTokenTools\AccountRights.exe' benoit SeServiceLogonRight
0
SeServiceLogonRight
1
PS C:\GeneralTestService> sc.exe start GeneralTestService

SERVICE_NAME: GeneralTestService
        TYPE               : 10  WIN32_OWN_PROCESS
        STATE              : 2  START_PENDING
                                (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        CHECKPOINT         : 0x0
        WAIT_HINT          : 0x7d0
        PID                : 4736
        FLAGS              :
PS C:\GeneralTestService>


2.2 Use a domain user account to run the service

This is equally not recommended and in some sense worse than using a local account. Recall that any user account password is known to someone and in the case of a user account used to run a service it is probably known to several people. Those people now all have the same access to the computer as the service does, and it will be difficult finding out who (ab)used the service’s account when and to do what.

Note also that the Service Control Manager needs to know the password of the user account it uses to start a service. Hence a local or domain user account password are stored, encrypted, by the Service Control Manager. A local administrator can decrypt the password and, in case of a domain account, abuse it for lateral movement, i.e. they can then use it to access other computers to which the domain user account has access. It is therefor a very bad idea to use a domain user account to run a service anywhere.

Configuration of the service is done the same as for the local user account, just with the domain’s name (here “example”) replacing the “.”:

PS C:\GeneralTestService> sc.exe config GeneralTestService obj=example\benoit password=SomeEvenMoreSecretPassword


2.3 Use a domain service account to run the service

This is the recommended way to run a service on a domain-joined computer.

A domain service account, also known as a managed service account or group managed service account (gMSA) retrieves its current password from a domain controller when the service starts and there is no known (to the user) password for it. It also cannot log on interactively and is automatically assigned a few privileges required for normal services.

Prepare a managed service account in the domain:

PS C:\Users\Administrator> New-ADServiceAccount TestService -DNSHostName GeneralTestService.example.com
PS C:\Users\Administrator> Set-ADServiceAccount TestService -PrincipalsAllowedToRetrieveManagedPassword champignac$
PS C:\Users\Administrator>

And then configure the service to use the service account. Note that you likely have to reboot the target computer first.

PS C:\GeneralTestService> sc.exe config GeneralTestService obj=example\TestService$
[SC] ChangeServiceConfig SUCCESS
PS C:\GeneralTestService> sc.exe qmanagedaccount GeneralTestService
[SC] QueryServiceConfig2 SUCCESS

ACCOUNT MANAGED : TRUE
PS C:\GeneralTestService> sc.exe start GeneralTestService

SERVICE_NAME: GeneralTestService
        TYPE               : 10  WIN32_OWN_PROCESS
        STATE              : 2  START_PENDING
                                (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        CHECKPOINT         : 0x0
        WAIT_HINT          : 0x7d0
        PID                : 1512
        FLAGS              :
PS C:\GeneralTestService>

You want to make sure that sc.exe qmanagedaccount says “TRUE” to ensure that the system understood that this is a managed account without a password known to a user.

To make sure that a domain controller is reachable for password retrieval when the service starts, configure dependencies on relevant services with an automatic start:

PS C:\GeneralTestService> sc.exe config GeneralTestService depend=netlogon/w32time start=auto
[SC] ChangeServiceConfig SUCCESS
PS C:\GeneralTestService> sc.exe qc GeneralTestService
[SC] QueryServiceConfig SUCCESS

SERVICE_NAME: GeneralTestService
        TYPE               : 10  WIN32_OWN_PROCESS
        START_TYPE         : 2   AUTO_START
        ERROR_CONTROL      : 1   NORMAL
        BINARY_PATH_NAME   : C:\GeneralTestService\GeneralTestService.exe
        LOAD_ORDER_GROUP   :
        TAG                : 0
        DISPLAY_NAME       : GeneralTestService
        DEPENDENCIES       : netlogon
                           : w32time
        SERVICE_START_NAME : example\TestService$
PS C:\GeneralTestService>


3.1 Do not use an account to run the service

This is the recommended method to start a non-system service on a computer that is not domain-joined. It does not use an account at all but simply uses the service itself as a security principal which can then be added to groups and ACLs and can be assigned privileges.

PS C:\GeneralTestService> sc.exe config GeneralTestService obj="NT Service\GeneralTestService"
[SC] ChangeServiceConfig SUCCESS
PS C:\GeneralTestService> Restart-Service GeneralTestService
PS C:\GeneralTestService>

The event log now reports

I am running and my image name is [GeneralTestService.exe].
I am running as [NT Service\GeneralTestService].

and

I appear to hold the following privileges:
SeChangeNotifyPrivilege
SeImpersonatePrivilege
SeCreateGlobalPrivilege
SeIncreaseWorkingSetPrivilege

These are the three privileges a normal service needs plus SeChangeNotifyPrivilege which everyone has.

SeImpersonatePrivilege allows the service to impersonate users in threads, for example when a user uses the service and needs the services to act on his behalf.

SeCreateGlobalPrivilege allows the service to create objects that are visible in all sessions. If I ever find an example, I will write about it.

Quick reference table accounts for services

Security principal type Recommendations
System account For actual operating system services only, in short: don’t use this
Local service or network service account Use if a system account is needed but the service should run in a restricted mode
Local user account Use only if the service does not support anything else
Domain user account Do not use if you can at all avoid it
Domain managed service account Use if your computer is in a domain
Local virtual service account (service SID) Use if your computer is not in a domain
Unrestricted Add this to let the service have a service SID (you can add permissions specific to the service in ACLs using this SID)
Restricted Add this to disallow writing to anything except specifically allowed to the service (using the service SID, for example)
None Add this sidtype if the service should not have a service SID (this is not very compatible with using a local virtual service account)


Giving access to controlling the service to users or groups

In most cases you want to allow someone to start and stop or otherwise manipulate a service.

This can be done in four different ways:

  1. Make every user a member of Administrators. This is the preferred solution recommended by software vendors who really could not care less about security on their customers’ computers.
  2. Use a Just-Enough-Admin configuration to allow certain users to manipulate certain services. This can quickly become very complicated.
  3. Write another service to control the service. This is stupid.
  4. Set permissions on the service to allow a user, better a specific group, whatever access they need.

I will only describe the fourth method. This should be done when the service is being installed.

We are assuming the user benoit needs access.

PS C:\GeneralTestService> sc.exe sdshow GeneralTestService

D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCLCSWLOCRRC;;;IU)(A;;CCLCSWLOCRRC;;;SU)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)
PS C:\GeneralTestService>

The output is a Security Descriptor Definition Language string.

To read it, simply note that what comes after D: is the Discretionary Access Control List (DACL) and what comes after S: is the System Access Control List (SACL).

The DACL is relevant to us. We can ignore the SACL.

Each bracketed part in the DACL is a Access Control Entry (ACE) and reads like this when split at “;”:

  1. A for Allow, D for Deny
  2. empty/irrelevant
  3. Permissions as a number of string of characters
  4. empty/irrelevant
  5. empty/irrelevant
  6. Security Principal affected, given as Security Identifier (SID) or short name (“SY” is the system account)

To add a new group giving it the same rights as the Administrators group we need to create a new group and find the entry for Administrators. The short name for Administrators is BA (for Builtin\Administrators). Then we add another entry to the DACL with our new group as the Security Principal affected.

A group specifically created to give access to a certain object that way is called a Resource Group. Perhaps it should be given a clear, memorable name:

I now like RG_ServiceName-AccessMask.

PS C:\GeneralTestService> net localgroup RG_GeneralTestService-0x0030 /add
The command completed successfully.

PS C:\GeneralTestService> net localgroup RG_GeneralTestService-0x0030 /add benoit
The command completed successfully.

PS C:\GeneralTestService> Get-LocalGroup RG_GeneralTestService-0x0030

Name                     Description
----                     -----------
RG_GeneralTestService-0x0030


PS C:\GeneralTestService> Get-LocalGroup RG_GeneralTestService-0x0030|Format-List


Description     :
Name            : RG_GeneralTestService-0x0030
SID             : S-1-5-21-344341352-2539047333-2300305637-1007
PrincipalSource : Local
ObjectClass     : Group

PS C:\GeneralTestService> sc.exe sdshow GeneralTestService

D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCLCSWLOCRRC;;;IU)(A;;CCLCSWLOCRRC;;;SU)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)
PS C:\GeneralTestService> sc.exe sdset GeneralTestService "D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCLCSWLOCRRC;;;IU)(A;;CCLCSWLOCRRC;;;SU)(A;;0x30;;;S-1-5-21-344341352-2539047333-2300305637-1007)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)"
[SC] SetServiceObjectSecurity SUCCESS
PS C:\GeneralTestService> sc.exe sdshow GeneralTestService

D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCLCSWLOCRRC;;;IU)(A;;CCLCSWLOCRRC;;;SU)(A;;0x30;;;S-1-5-21-344341352-2539047333-2300305637-1007)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)
PS C:\GeneralTestService>

Note that the right to start the service (SERVICE_START) is 0x10 and the right to stop the service (SERVICE_STOP) is 0x20. We are granting 0x30 to allow both.

User benoit via the group RG_GeneralTestService-0x0030 now has the same right to start and stop the service.

Resource groups are a powerful tool to make granting access easier. Whenever you create an object, like a service, that is not a file, like a service, you should probably create one or two resource groups to manage access.


Next: Adding Windows Features