568 lines
17 KiB
Go
568 lines
17 KiB
Go
// Licensed to Elasticsearch B.V. under one or more contributor
|
|
// license agreements. See the NOTICE file distributed with
|
|
// this work for additional information regarding copyright
|
|
// ownership. Elasticsearch B.V. licenses this file to you under
|
|
// the Apache License, Version 2.0 (the "License"); you may
|
|
// not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
// +build windows
|
|
|
|
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"strconv"
|
|
"syscall"
|
|
"time"
|
|
"unicode/utf16"
|
|
"unsafe"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/sys/windows"
|
|
"golang.org/x/sys/windows/registry"
|
|
|
|
"github.com/elastic/beats/libbeat/common"
|
|
"github.com/elastic/beats/libbeat/logp"
|
|
"github.com/elastic/beats/winlogbeat/sys"
|
|
"github.com/elastic/gosigar"
|
|
)
|
|
|
|
// Windows API calls
|
|
//sys _OpenSCManager(machineName *uint16, databaseName *uint16, desiredAcces ServiceSCMAccessRight) (handle ServiceDatabaseHandle, err error) = advapi32.OpenSCManagerW
|
|
//sys _EnumServicesStatusEx(handle ServiceDatabaseHandle, infoLevel ServiceInfoLevel, serviceType ServiceType, serviceState ServiceEnumState, services *byte, bufSize uint32, bytesNeeded *uint32, servicesReturned *uint32, resumeHandle *uintptr, groupName *uintptr) (err error) [failretval==0] = advapi32.EnumServicesStatusExW
|
|
//sys _OpenService(handle ServiceDatabaseHandle, serviceName *uint16, desiredAccess ServiceAccessRight) (serviceHandle ServiceHandle, err error) = advapi32.OpenServiceW
|
|
//sys _QueryServiceConfig(serviceHandle ServiceHandle, serviceConfig *byte, bufSize uint32, bytesNeeded *uint32) (err error) [failretval==0] = advapi32.QueryServiceConfigW
|
|
//sys _QueryServiceConfig2(serviceHandle ServiceHandle, infoLevel ServiceConfigInformation, configBuffer *byte, bufSize uint32, bytesNeeded *uint32) (err error) [failretval==0] = advapi32.QueryServiceConfig2W
|
|
//sys _CloseServiceHandle(handle uintptr) (err error) = advapi32.CloseServiceHandle
|
|
|
|
var (
|
|
sizeofEnumServiceStatusProcess = (int)(unsafe.Sizeof(EnumServiceStatusProcess{}))
|
|
)
|
|
|
|
type ServiceDatabaseHandle uintptr
|
|
|
|
type ServiceHandle uintptr
|
|
|
|
type ProcessHandle uintptr
|
|
|
|
type ServiceConfigInformation uint32
|
|
|
|
const (
|
|
ServiceConfigDelayedAutoStartInfo ServiceConfigInformation = 3
|
|
ServiceConfigDescription ServiceConfigInformation = 1
|
|
ServiceConfigFailureActions ServiceConfigInformation = 2
|
|
ServiceConfigFailureActionsFlag ServiceConfigInformation = 4
|
|
ServiceConfigPreferredNode ServiceConfigInformation = 9
|
|
ServiceConfigPreshutdownInfo ServiceConfigInformation = 7
|
|
ServiceConfigRequiredPrivilegesInfo ServiceConfigInformation = 6
|
|
ServiceConfigServiceSidInfo ServiceConfigInformation = 5
|
|
ServiceConfigTriggerInfo ServiceConfigInformation = 8
|
|
ServiceConfigLaunchProtected ServiceConfigInformation = 12
|
|
)
|
|
|
|
type serviceDelayedAutoStartInfo struct {
|
|
delayedAutoStart bool
|
|
}
|
|
|
|
type serviceTriggerInfo struct {
|
|
cTriggers uint32
|
|
pTriggers uintptr
|
|
pReserved uintptr
|
|
}
|
|
|
|
var serviceStates = map[ServiceState]string{
|
|
ServiceContinuePending: "Continuing",
|
|
ServicePausePending: "Pausing",
|
|
ServicePaused: "Paused",
|
|
ServiceRunning: "Running",
|
|
ServiceStartPending: "Starting",
|
|
ServiceStopPending: "Stopping",
|
|
ServiceStopped: "Stopped",
|
|
}
|
|
|
|
const (
|
|
StartTypeBoot ServiceStartType = iota
|
|
StartTypeSystem
|
|
StartTypeAutomatic
|
|
StartTypeManual
|
|
StartTypeDisabled
|
|
StartTypeAutomaticDelayed
|
|
StartTypeAutomaticTriggered
|
|
StartTypeAutomaticDelayedTriggered
|
|
StartTypeManualTriggered
|
|
)
|
|
|
|
var serviceStartTypes = map[ServiceStartType]string{
|
|
StartTypeBoot: "Boot",
|
|
StartTypeSystem: "System",
|
|
StartTypeAutomatic: "Automatic",
|
|
StartTypeManual: "Manual",
|
|
StartTypeDisabled: "Disabled",
|
|
StartTypeAutomaticDelayed: "Automatic (Delayed)",
|
|
StartTypeAutomaticTriggered: "Automatic (Triggered)",
|
|
StartTypeAutomaticDelayedTriggered: "Automatic (Delayed, Triggered)",
|
|
StartTypeManualTriggered: "Manual (Triggered)",
|
|
}
|
|
|
|
func (startType ServiceStartType) String() string {
|
|
return serviceStartTypes[startType]
|
|
}
|
|
|
|
func (state ServiceState) String() string {
|
|
if val, ok := serviceStates[state]; ok {
|
|
return val
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// errorNames is mapping of errno values to names.
|
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms681383(v=vs.85).aspx
|
|
var errorNames = map[uint32]string{
|
|
1077: "ERROR_SERVICE_NEVER_STARTED",
|
|
}
|
|
|
|
type ServiceStatus struct {
|
|
DisplayName string
|
|
ServiceName string
|
|
CurrentState string
|
|
StartType ServiceStartType
|
|
PID uint32 // ID of the associated process.
|
|
Uptime time.Duration
|
|
ExitCode uint32 // Exit code for stopped services.
|
|
}
|
|
|
|
type ServiceReader struct {
|
|
handle ServiceDatabaseHandle
|
|
state ServiceEnumState
|
|
guid string // Host's MachineGuid value (a unique ID for the host).
|
|
ids map[string]string // Cache of service IDs.
|
|
protectedServices map[string]struct{}
|
|
}
|
|
|
|
var InvalidServiceDatabaseHandle = ^ServiceDatabaseHandle(0)
|
|
var InvalidServiceHandle = ^ServiceHandle(0)
|
|
|
|
func OpenSCManager(machineName string, databaseName string, desiredAccess ServiceSCMAccessRight) (ServiceDatabaseHandle, error) {
|
|
var machineNamePtr *uint16
|
|
if machineName != "" {
|
|
var err error
|
|
machineNamePtr, err = syscall.UTF16PtrFromString(machineName)
|
|
if err != nil {
|
|
return InvalidServiceDatabaseHandle, err
|
|
}
|
|
}
|
|
|
|
var databaseNamePtr *uint16
|
|
if databaseName != "" {
|
|
var err error
|
|
databaseNamePtr, err = syscall.UTF16PtrFromString(databaseName)
|
|
if err != nil {
|
|
return InvalidServiceDatabaseHandle, err
|
|
}
|
|
}
|
|
|
|
handle, err := _OpenSCManager(machineNamePtr, databaseNamePtr, desiredAccess)
|
|
if err != nil {
|
|
return InvalidServiceDatabaseHandle, ServiceErrno(err.(syscall.Errno))
|
|
}
|
|
|
|
return handle, nil
|
|
}
|
|
|
|
func OpenService(handle ServiceDatabaseHandle, serviceName string, desiredAccess ServiceAccessRight) (ServiceHandle, error) {
|
|
var serviceNamePtr *uint16
|
|
if serviceName != "" {
|
|
var err error
|
|
serviceNamePtr, err = syscall.UTF16PtrFromString(serviceName)
|
|
if err != nil {
|
|
return InvalidServiceHandle, err
|
|
}
|
|
}
|
|
|
|
serviceHandle, err := _OpenService(handle, serviceNamePtr, desiredAccess)
|
|
if err != nil {
|
|
return InvalidServiceHandle, ServiceErrno(err.(syscall.Errno))
|
|
}
|
|
|
|
return serviceHandle, nil
|
|
}
|
|
|
|
func QueryServiceConfig2(serviceHandle ServiceHandle, infoLevel ServiceConfigInformation) ([]byte, error) {
|
|
var buffer []byte
|
|
|
|
for {
|
|
var bytesNeeded uint32
|
|
var bufPtr *byte
|
|
if len(buffer) > 0 {
|
|
bufPtr = &buffer[0]
|
|
}
|
|
|
|
if err := _QueryServiceConfig2(serviceHandle, infoLevel, bufPtr, uint32(len(buffer)), &bytesNeeded); err != nil {
|
|
if ServiceErrno(err.(syscall.Errno)) == SERVICE_ERROR_INSUFFICIENT_BUFFER {
|
|
// Increase buffer size and retry.
|
|
buffer = make([]byte, len(buffer)+int(bytesNeeded))
|
|
continue
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
return buffer, nil
|
|
}
|
|
|
|
func getServiceStates(handle ServiceDatabaseHandle, state ServiceEnumState, protectedServices map[string]struct{}) ([]ServiceStatus, error) {
|
|
var servicesReturned uint32
|
|
var servicesBuffer []byte
|
|
|
|
for {
|
|
var bytesNeeded uint32
|
|
var buf *byte
|
|
if len(servicesBuffer) > 0 {
|
|
buf = &servicesBuffer[0]
|
|
}
|
|
|
|
if err := _EnumServicesStatusEx(handle, ScEnumProcessInfo, ServiceWin32, state, buf, uint32(len(servicesBuffer)), &bytesNeeded, &servicesReturned, nil, nil); err != nil {
|
|
if ServiceErrno(err.(syscall.Errno)) == SERVICE_ERROR_MORE_DATA {
|
|
// Increase buffer size and retry.
|
|
servicesBuffer = make([]byte, len(servicesBuffer)+int(bytesNeeded))
|
|
continue
|
|
}
|
|
return nil, ServiceErrno(err.(syscall.Errno))
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
// Windows appears to tack on a single byte null terminator to the UTF-16
|
|
// strings, but we are expecting either no null terminator or \u0000 (an
|
|
// even number of bytes).
|
|
if len(servicesBuffer)%2 != 0 && servicesBuffer[len(servicesBuffer)-1] == 0 {
|
|
servicesBuffer = servicesBuffer[:len(servicesBuffer)-1]
|
|
}
|
|
|
|
var services []ServiceStatus
|
|
for i := 0; i < int(servicesReturned); i++ {
|
|
serviceTemp := (*EnumServiceStatusProcess)(unsafe.Pointer(&servicesBuffer[i*sizeofEnumServiceStatusProcess]))
|
|
|
|
service, err := getServiceInformation(serviceTemp, servicesBuffer, handle, protectedServices)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
services = append(services, service)
|
|
}
|
|
|
|
return services, nil
|
|
}
|
|
|
|
func getServiceInformation(rawService *EnumServiceStatusProcess, servicesBuffer []byte, handle ServiceDatabaseHandle, protectedServices map[string]struct{}) (ServiceStatus, error) {
|
|
service := ServiceStatus{
|
|
PID: rawService.ServiceStatusProcess.DwProcessId,
|
|
}
|
|
|
|
// Read null-terminated UTF16 strings from the buffer.
|
|
serviceNameOffset := uintptr(unsafe.Pointer(rawService.LpServiceName)) - (uintptr)(unsafe.Pointer(&servicesBuffer[0]))
|
|
displayNameOffset := uintptr(unsafe.Pointer(rawService.LpDisplayName)) - (uintptr)(unsafe.Pointer(&servicesBuffer[0]))
|
|
|
|
strBuf := new(bytes.Buffer)
|
|
if err := sys.UTF16ToUTF8Bytes(servicesBuffer[displayNameOffset:], strBuf); err != nil {
|
|
return service, err
|
|
}
|
|
service.DisplayName = strBuf.String()
|
|
|
|
strBuf.Reset()
|
|
if err := sys.UTF16ToUTF8Bytes(servicesBuffer[serviceNameOffset:], strBuf); err != nil {
|
|
return service, err
|
|
}
|
|
service.ServiceName = strBuf.String()
|
|
|
|
var state string
|
|
|
|
if stat, ok := serviceStates[ServiceState(rawService.ServiceStatusProcess.DwCurrentState)]; ok {
|
|
state = stat
|
|
} else {
|
|
state = "Can not define State"
|
|
}
|
|
service.CurrentState = state
|
|
|
|
// Exit code.
|
|
service.ExitCode = rawService.ServiceStatusProcess.DwWin32ExitCode
|
|
if service.ExitCode == uint32(windows.ERROR_SERVICE_SPECIFIC_ERROR) {
|
|
service.ExitCode = rawService.ServiceStatusProcess.DwServiceSpecificExitCode
|
|
}
|
|
|
|
serviceHandle, err := OpenService(handle, service.ServiceName, ServiceQueryConfig)
|
|
if err != nil {
|
|
return service, err
|
|
}
|
|
|
|
defer CloseServiceHandle(serviceHandle)
|
|
|
|
// Get detailed information
|
|
if err := getAdditionalServiceInfo(serviceHandle, &service); err != nil {
|
|
return service, err
|
|
}
|
|
|
|
// Get optional information
|
|
if err := getOptionalServiceInfo(serviceHandle, &service); err != nil {
|
|
return service, err
|
|
}
|
|
|
|
//Get uptime for service
|
|
if ServiceState(rawService.ServiceStatusProcess.DwCurrentState) != ServiceStopped {
|
|
processUpTime, err := getServiceUptime(rawService.ServiceStatusProcess.DwProcessId)
|
|
if err != nil {
|
|
if _, ok := protectedServices[service.ServiceName]; errors.Cause(err) == syscall.ERROR_ACCESS_DENIED && !ok {
|
|
protectedServices[service.ServiceName] = struct{}{}
|
|
logp.Warn("Uptime for service %v is not available because of insufficient rights", service.ServiceName)
|
|
} else {
|
|
return service, err
|
|
}
|
|
}
|
|
service.Uptime = processUpTime / time.Millisecond
|
|
}
|
|
|
|
return service, nil
|
|
}
|
|
|
|
// getServiceUptime returns the uptime for process
|
|
func getServiceUptime(processID uint32) (time.Duration, error) {
|
|
var processCreationTime gosigar.ProcTime
|
|
|
|
err := processCreationTime.Get(int(processID))
|
|
if err != nil {
|
|
return time.Duration(processCreationTime.StartTime), err
|
|
}
|
|
|
|
uptime := time.Since(time.Unix(0, int64(processCreationTime.StartTime)*int64(time.Millisecond)))
|
|
|
|
return uptime, nil
|
|
}
|
|
|
|
func getAdditionalServiceInfo(serviceHandle ServiceHandle, service *ServiceStatus) error {
|
|
var buffer []byte
|
|
|
|
for {
|
|
var bytesNeeded uint32
|
|
var bufPtr *byte
|
|
if len(buffer) > 0 {
|
|
bufPtr = &buffer[0]
|
|
}
|
|
|
|
if err := _QueryServiceConfig(serviceHandle, bufPtr, uint32(len(buffer)), &bytesNeeded); err != nil {
|
|
if ServiceErrno(err.(syscall.Errno)) == SERVICE_ERROR_INSUFFICIENT_BUFFER {
|
|
// Increase buffer size and retry.
|
|
buffer = make([]byte, len(buffer)+int(bytesNeeded))
|
|
continue
|
|
}
|
|
return ServiceErrno(err.(syscall.Errno))
|
|
}
|
|
serviceQueryConfig := (*QueryServiceConfig)(unsafe.Pointer(&buffer[0]))
|
|
service.StartType = ServiceStartType(serviceQueryConfig.DwStartType)
|
|
break
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getOptionalServiceInfo(serviceHandle ServiceHandle, service *ServiceStatus) error {
|
|
// Get information if the service is started delayed or triggered. Only valid for automatic or manual services. So filter them first.
|
|
if service.StartType == StartTypeAutomatic || service.StartType == StartTypeManual {
|
|
var delayedInfo *serviceDelayedAutoStartInfo
|
|
if service.StartType == StartTypeAutomatic {
|
|
delayedInfoBuffer, err := QueryServiceConfig2(serviceHandle, ServiceConfigDelayedAutoStartInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
delayedInfo = (*serviceDelayedAutoStartInfo)(unsafe.Pointer(&delayedInfoBuffer[0]))
|
|
}
|
|
|
|
// Get information if the service is triggered.
|
|
triggeredInfoBuffer, err := QueryServiceConfig2(serviceHandle, ServiceConfigTriggerInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
triggeredInfo := (*serviceTriggerInfo)(unsafe.Pointer(&triggeredInfoBuffer[0]))
|
|
|
|
if service.StartType == StartTypeAutomatic {
|
|
if triggeredInfo.cTriggers > 0 && delayedInfo.delayedAutoStart {
|
|
service.StartType = StartTypeAutomaticDelayedTriggered
|
|
} else if triggeredInfo.cTriggers > 0 {
|
|
service.StartType = StartTypeAutomaticTriggered
|
|
} else if delayedInfo.delayedAutoStart {
|
|
service.StartType = StartTypeAutomaticDelayed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if service.StartType == StartTypeManual && triggeredInfo.cTriggers > 0 {
|
|
service.StartType = StartTypeManualTriggered
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (reader *ServiceReader) Close() error {
|
|
return CloseServiceDatabaseHandle(reader.handle)
|
|
}
|
|
|
|
func CloseServiceDatabaseHandle(handle ServiceDatabaseHandle) error {
|
|
if err := _CloseServiceHandle(uintptr(handle)); err != nil {
|
|
return ServiceErrno(err.(syscall.Errno))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func CloseServiceHandle(handle ServiceHandle) error {
|
|
if err := _CloseServiceHandle(uintptr(handle)); err != nil {
|
|
return ServiceErrno(err.(syscall.Errno))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func NewServiceReader() (*ServiceReader, error) {
|
|
hndl, err := OpenSCManager("", "", ScManagerEnumerateService|ScManagerConnect)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "initialization failed")
|
|
}
|
|
|
|
guid, err := getMachineGUID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r := &ServiceReader{
|
|
handle: hndl,
|
|
state: ServiceStateAll,
|
|
guid: guid,
|
|
ids: map[string]string{},
|
|
protectedServices: map[string]struct{}{},
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func (reader *ServiceReader) Read() ([]common.MapStr, error) {
|
|
services, err := getServiceStates(reader.handle, reader.state, reader.protectedServices)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]common.MapStr, 0, len(services))
|
|
|
|
for _, service := range services {
|
|
ev := common.MapStr{
|
|
"id": reader.getServiceID(service.ServiceName),
|
|
"display_name": service.DisplayName,
|
|
"name": service.ServiceName,
|
|
"state": service.CurrentState,
|
|
"start_type": service.StartType.String(),
|
|
}
|
|
|
|
if service.CurrentState == "Stopped" {
|
|
ev.Put("exit_code", getErrorCode(service.ExitCode))
|
|
}
|
|
|
|
if service.PID > 0 {
|
|
ev.Put("pid", service.PID)
|
|
}
|
|
|
|
if service.Uptime > 0 {
|
|
if _, err = ev.Put("uptime.ms", service.Uptime); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
result = append(result, ev)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// getServiceID returns a unique ID for the service that is derived from the
|
|
// machine's GUID and the service's name.
|
|
func (reader *ServiceReader) getServiceID(name string) string {
|
|
// hash returns a base64 encoded sha256 hash that is truncated to 10 chars.
|
|
hash := func(v string) string {
|
|
sum := sha256.Sum256([]byte(v))
|
|
base64Hash := base64.RawURLEncoding.EncodeToString(sum[:])
|
|
return base64Hash[:10]
|
|
}
|
|
|
|
id, found := reader.ids[name]
|
|
if !found {
|
|
id = hash(reader.guid + name)
|
|
reader.ids[name] = id
|
|
}
|
|
|
|
return id
|
|
}
|
|
|
|
func (e ServiceErrno) Error() string {
|
|
// If the value is not one of the known Service errors then assume its a
|
|
// general windows error.
|
|
if _, found := serviceErrors[e]; !found {
|
|
return syscall.Errno(e).Error()
|
|
}
|
|
|
|
// Use FormatMessage to convert the service errno to a string.
|
|
var flags uint32 = syscall.FORMAT_MESSAGE_FROM_SYSTEM | syscall.FORMAT_MESSAGE_ARGUMENT_ARRAY | syscall.FORMAT_MESSAGE_IGNORE_INSERTS
|
|
b := make([]uint16, 300)
|
|
n, err := windows.FormatMessage(flags, modadvapi32.Handle(), uint32(e), 0, b, nil)
|
|
if err != nil {
|
|
return "service error #" + strconv.Itoa(int(e))
|
|
}
|
|
|
|
// Trim terminating \r and \n
|
|
for ; n > 0 && (b[n-1] == '\n' || b[n-1] == '\r'); n-- {
|
|
}
|
|
return string(utf16.Decode(b[:n]))
|
|
}
|
|
|
|
// getMachineGUID returns the machine's GUID value which is unique to a Windows
|
|
// installation.
|
|
func getMachineGUID() (string, error) {
|
|
const key = registry.LOCAL_MACHINE
|
|
const path = `SOFTWARE\Microsoft\Cryptography`
|
|
const name = "MachineGuid"
|
|
|
|
k, err := registry.OpenKey(key, path, registry.READ|registry.WOW64_64KEY)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, `failed to open HKLM\%v`, path)
|
|
}
|
|
|
|
guid, _, err := k.GetStringValue(name)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, `failed to get value of HKLM\%v\%v`, path, name)
|
|
}
|
|
|
|
return guid, nil
|
|
}
|
|
|
|
func getErrorCode(errno uint32) string {
|
|
name, found := errorNames[errno]
|
|
if found {
|
|
return name
|
|
}
|
|
return strconv.Itoa(int(errno))
|
|
}
|