diff --git a/windows/svc/example/main.go b/windows/svc/example/main.go index dc96c081..9bf9bbdc 100644 --- a/windows/svc/example/main.go +++ b/windows/svc/example/main.go @@ -36,11 +36,11 @@ func usage(errmsg string) { func main() { const svcName = "myservice" - isIntSess, err := svc.IsAnInteractiveSession() + inService, err := svc.IsWindowsService() if err != nil { - log.Fatalf("failed to determine if we are running in an interactive session: %v", err) + log.Fatalf("failed to determine if we are running in service: %v", err) } - if !isIntSess { + if inService { runService(svcName, false) return } diff --git a/windows/svc/security.go b/windows/svc/security.go index 65025998..da6df1d3 100644 --- a/windows/svc/security.go +++ b/windows/svc/security.go @@ -7,6 +7,10 @@ package svc import ( + "errors" + "syscall" + "unsafe" + "golang.org/x/sys/windows" ) @@ -23,6 +27,8 @@ func allocSid(subAuth0 uint32) (*windows.SID, error) { // IsAnInteractiveSession determines if calling process is running interactively. // It queries the process token for membership in the Interactive group. // http://stackoverflow.com/questions/2668851/how-do-i-detect-that-my-application-is-running-as-service-or-in-an-interactive-s +// +// Deprecated: Use IsWindowsService instead. func IsAnInteractiveSession() (bool, error) { interSid, err := allocSid(windows.SECURITY_INTERACTIVE_RID) if err != nil { @@ -57,3 +63,95 @@ func IsAnInteractiveSession() (bool, error) { } return false, nil } + +var ( + ntdll = windows.NewLazySystemDLL("ntdll.dll") + _NtQueryInformationProcess = ntdll.NewProc("NtQueryInformationProcess") + + kernel32 = windows.NewLazySystemDLL("kernel32.dll") + _QueryFullProcessImageNameA = kernel32.NewProc("QueryFullProcessImageNameA") +) + +// IsWindowsService reports whether the process is currently executing +// as a Windows service. +func IsWindowsService() (bool, error) { + // This code was copied from runtime.isWindowsService function. + + // The below technique looks a bit hairy, but it's actually + // exactly what the .NET framework does for the similarly named function: + // https://github.com/dotnet/extensions/blob/f4066026ca06984b07e90e61a6390ac38152ba93/src/Hosting/WindowsServices/src/WindowsServiceHelpers.cs#L26-L31 + // Specifically, it looks up whether the parent process has session ID zero + // and is called "services". + const _CURRENT_PROCESS = ^uintptr(0) + // pbi is a PROCESS_BASIC_INFORMATION struct, where we just care about + // the 6th pointer inside of it, which contains the pid of the process + // parent: + // https://github.com/wine-mirror/wine/blob/42cb7d2ad1caba08de235e6319b9967296b5d554/include/winternl.h#L1294 + var pbi [6]uintptr + var pbiLen uint32 + r0, _, _ := syscall.Syscall6(_NtQueryInformationProcess.Addr(), 5, _CURRENT_PROCESS, 0, uintptr(unsafe.Pointer(&pbi[0])), uintptr(unsafe.Sizeof(pbi)), uintptr(unsafe.Pointer(&pbiLen)), 0) + if r0 != 0 { + return false, errors.New("NtQueryInformationProcess failed: error=" + itoa(int(r0))) + } + var psid uint32 + err := windows.ProcessIdToSessionId(uint32(pbi[5]), &psid) + if err != nil { + return false, err + } + if psid != 0 { + // parent session id should be 0 for service process + return false, nil + } + + pproc, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pbi[5])) + if err != nil { + return false, err + } + defer windows.CloseHandle(pproc) + + // exeName gets the path to the executable image of the parent process + var exeName [261]byte + exeNameLen := uint32(len(exeName) - 1) + r0, _, e0 := syscall.Syscall6(_QueryFullProcessImageNameA.Addr(), 4, uintptr(pproc), 0, uintptr(unsafe.Pointer(&exeName[0])), uintptr(unsafe.Pointer(&exeNameLen)), 0, 0) + if r0 == 0 { + if e0 != 0 { + return false, e0 + } else { + return false, syscall.EINVAL + } + } + const ( + servicesLower = "services.exe" + servicesUpper = "SERVICES.EXE" + ) + i := int(exeNameLen) - 1 + j := len(servicesLower) - 1 + if i < j { + return false, nil + } + for { + if j == -1 { + return i == -1 || exeName[i] == '\\', nil + } + if exeName[i] != servicesLower[j] && exeName[i] != servicesUpper[j] { + return false, nil + } + i-- + j-- + } +} + +func itoa(val int) string { // do it here rather than with fmt to avoid dependency + if val < 0 { + return "-" + itoa(-val) + } + var buf [32]byte // big enough for int64 + i := len(buf) - 1 + for val >= 10 { + buf[i] = byte(val%10 + '0') + i-- + val /= 10 + } + buf[i] = byte(val + '0') + return string(buf[i:]) +} diff --git a/windows/svc/svc_test.go b/windows/svc/svc_test.go index 3a80e480..7e870535 100644 --- a/windows/svc/svc_test.go +++ b/windows/svc/svc_test.go @@ -132,3 +132,23 @@ func TestExample(t *testing.T) { t.Errorf("%q string does not contain %q", string(out), want) } } + +func TestIsAnInteractiveSession(t *testing.T) { + isInteractive, err := svc.IsAnInteractiveSession() + if err != nil { + t.Fatal(err) + } + if !isInteractive { + t.Error("IsAnInteractiveSession retuns false when running interactively.") + } +} + +func TestIsWindowsService(t *testing.T) { + isSvc, err := svc.IsWindowsService() + if err != nil { + t.Fatal(err) + } + if isSvc { + t.Error("IsWindowsService retuns true when not running in a service.") + } +} diff --git a/windows/syscall_windows.go b/windows/syscall_windows.go index 30ef6410..65ca6d20 100644 --- a/windows/syscall_windows.go +++ b/windows/syscall_windows.go @@ -270,6 +270,7 @@ func NewCallbackCDecl(fn interface{}) uintptr { //sys RegEnumKeyEx(key Handle, index uint32, name *uint16, nameLen *uint32, reserved *uint32, class *uint16, classLen *uint32, lastWriteTime *Filetime) (regerrno error) = advapi32.RegEnumKeyExW //sys RegQueryValueEx(key Handle, name *uint16, reserved *uint32, valtype *uint32, buf *byte, buflen *uint32) (regerrno error) = advapi32.RegQueryValueExW //sys GetCurrentProcessId() (pid uint32) = kernel32.GetCurrentProcessId +//sys ProcessIdToSessionId(pid uint32, sessionid *uint32) (err error) = kernel32.ProcessIdToSessionId //sys GetConsoleMode(console Handle, mode *uint32) (err error) = kernel32.GetConsoleMode //sys SetConsoleMode(console Handle, mode uint32) (err error) = kernel32.SetConsoleMode //sys GetConsoleScreenBufferInfo(console Handle, info *ConsoleScreenBufferInfo) (err error) = kernel32.GetConsoleScreenBufferInfo diff --git a/windows/zsyscall_windows.go b/windows/zsyscall_windows.go index f1ae76b3..f6cef2b9 100644 --- a/windows/zsyscall_windows.go +++ b/windows/zsyscall_windows.go @@ -180,6 +180,7 @@ var ( procRegEnumKeyExW = modadvapi32.NewProc("RegEnumKeyExW") procRegQueryValueExW = modadvapi32.NewProc("RegQueryValueExW") procGetCurrentProcessId = modkernel32.NewProc("GetCurrentProcessId") + procProcessIdToSessionId = modkernel32.NewProc("ProcessIdToSessionId") procGetConsoleMode = modkernel32.NewProc("GetConsoleMode") procSetConsoleMode = modkernel32.NewProc("SetConsoleMode") procGetConsoleScreenBufferInfo = modkernel32.NewProc("GetConsoleScreenBufferInfo") @@ -1943,6 +1944,18 @@ func GetCurrentProcessId() (pid uint32) { return } +func ProcessIdToSessionId(pid uint32, sessionid *uint32) (err error) { + r1, _, e1 := syscall.Syscall(procProcessIdToSessionId.Addr(), 2, uintptr(pid), uintptr(unsafe.Pointer(sessionid)), 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + func GetConsoleMode(console Handle, mode *uint32) (err error) { r1, _, e1 := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(console), uintptr(unsafe.Pointer(mode)), 0) if r1 == 0 {