Skip to content

Instantly share code, notes, and snippets.

@mholt
Forked from KatelynHaworth/main.go
Created October 16, 2018 14:25
Show Gist options
  • Save mholt/f52b56653de12dd9075db1b2ce914bd8 to your computer and use it in GitHub Desktop.
Save mholt/f52b56653de12dd9075db1b2ce914bd8 to your computer and use it in GitHub Desktop.
Example of run an interactive process on the current user from system service on windows (Golang)
package main
import (
"github.com/kardianos/service"
"log"
"flag"
)
type Service struct {}
var logger service.Logger
func (*Service) Start(_ service.Service) error {
if err := StartProcessAsCurrentUser("notepad.exe", "", ""); err != nil {
return err
}
return nil
}
func (*Service) Stop(_ service.Service) error {
return nil
}
var serviceFlag = flag.String("service", "", "Control the service")
func main() {
svcConfig := &service.Config{
Name: "RunAsUserTest",
DisplayName: "Run As User Test",
Description: "Service to test launching programs as user from service",
}
svc := &Service{}
s, err := service.New(svc, svcConfig)
if err != nil {
log.Fatal(err)
}
flag.Parse()
if *serviceFlag != "" {
if err := service.Control(s, *serviceFlag); err != nil {
log.Fatal(err)
}
return
}
logger, err = s.Logger(nil)
if err != nil {
log.Fatal(err)
}
err = s.Run()
if err != nil {
logger.Error(err)
}
}
package main
import (
"fmt"
"unsafe"
"golang.org/x/sys/windows"
)
var (
modwtsapi32 *windows.LazyDLL = windows.NewLazySystemDLL("wtsapi32.dll")
modkernel32 *windows.LazyDLL = windows.NewLazySystemDLL("kernel32.dll")
modadvapi32 *windows.LazyDLL = windows.NewLazySystemDLL("advapi32.dll")
moduserenv *windows.LazyDLL = windows.NewLazySystemDLL("userenv.dll")
procWTSEnumerateSessionsW *windows.LazyProc = modwtsapi32.NewProc("WTSEnumerateSessionsW")
procWTSGetActiveConsoleSessionId *windows.LazyProc = modkernel32.NewProc("WTSGetActiveConsoleSessionId")
procWTSQueryUserToken *windows.LazyProc = modwtsapi32.NewProc("WTSQueryUserToken")
procDuplicateTokenEx *windows.LazyProc = modadvapi32.NewProc("DuplicateTokenEx")
procCreateEnvironmentBlock *windows.LazyProc = moduserenv.NewProc("CreateEnvironmentBlock")
procCreateProcessAsUser *windows.LazyProc = modadvapi32.NewProc("CreateProcessAsUserW")
)
const (
WTS_CURRENT_SERVER_HANDLE uintptr = 0
)
type WTS_CONNECTSTATE_CLASS int
const (
WTSActive WTS_CONNECTSTATE_CLASS = iota
WTSConnected
WTSConnectQuery
WTSShadow
WTSDisconnected
WTSIdle
WTSListen
WTSReset
WTSDown
WTSInit
)
type SECURITY_IMPERSONATION_LEVEL int
const (
SecurityAnonymous SECURITY_IMPERSONATION_LEVEL = iota
SecurityIdentification
SecurityImpersonation
SecurityDelegation
)
type TOKEN_TYPE int
const (
TokenPrimary TOKEN_TYPE = iota + 1
TokenImpersonazion
)
type SW int
const (
SW_HIDE SW = 0
SW_SHOWNORMAL = 1
SW_NORMAL = 1
SW_SHOWMINIMIZED = 2
SW_SHOWMAXIMIZED = 3
SW_MAXIMIZE = 3
SW_SHOWNOACTIVATE = 4
SW_SHOW = 5
SW_MINIMIZE = 6
SW_SHOWMINNOACTIVE = 7
SW_SHOWNA = 8
SW_RESTORE = 9
SW_SHOWDEFAULT = 10
SW_MAX = 1
)
type WTS_SESSION_INFO struct {
SessionID windows.Handle
WinStationName *uint16
State WTS_CONNECTSTATE_CLASS
}
const (
CREATE_UNICODE_ENVIRONMENT uint16 = 0x00000400
CREATE_NO_WINDOW = 0x08000000
CREATE_NEW_CONSOLE = 0x00000010
)
// GetCurrentUserSessionId will attempt to resolve
// the session ID of the user currently active on
// the system.
func GetCurrentUserSessionId() (windows.Handle, error) {
sessionList, err := WTSEnumerateSessions()
if err != nil {
return 0xFFFFFFFF, fmt.Errorf("get current user session token: %s", err)
}
for i := range sessionList {
if sessionList[i].State == WTSActive {
return sessionList[i].SessionID, nil
}
}
if sessionId, _, err := procWTSGetActiveConsoleSessionId.Call(); sessionId == 0xFFFFFFFF {
return 0xFFFFFFFF, fmt.Errorf("get current user session token: call native WTSGetActiveConsoleSessionId: %s", err)
} else {
return windows.Handle(sessionId), nil
}
}
// WTSEnumerateSession will call the native
// version for Windows and parse the result
// to a Golang friendly version
func WTSEnumerateSessions() ([]*WTS_SESSION_INFO, error) {
var (
sessionInformation windows.Handle = windows.Handle(0)
sessionCount int = 0
sessionList []*WTS_SESSION_INFO = make([]*WTS_SESSION_INFO, 0)
)
if returnCode, _, err := procWTSEnumerateSessionsW.Call(WTS_CURRENT_SERVER_HANDLE, 0, 1, uintptr(unsafe.Pointer(&sessionInformation)), uintptr(unsafe.Pointer(&sessionCount))); returnCode == 0 {
return nil, fmt.Errorf("call native WTSEnumerateSessionsW: %s", err)
}
structSize := unsafe.Sizeof(WTS_SESSION_INFO{})
current := uintptr(sessionInformation)
for i := 0; i < sessionCount; i++ {
sessionList = append(sessionList, (*WTS_SESSION_INFO)(unsafe.Pointer(current)))
current += structSize
}
return sessionList, nil
}
// DuplicateUserTokenFromSessionID will attempt
// to duplicate the user token for the user logged
// into the provided session ID
func DuplicateUserTokenFromSessionID(sessionId windows.Handle) (windows.Token, error) {
var (
impersonationToken windows.Handle = 0
userToken windows.Token = 0
)
if returnCode, _, err := procWTSQueryUserToken.Call(uintptr(sessionId), uintptr(unsafe.Pointer(&impersonationToken))); returnCode == 0 {
return 0xFFFFFFFF, fmt.Errorf("call native WTSQueryUserToken: %s", err)
}
if returnCode, _, err := procDuplicateTokenEx.Call(uintptr(impersonationToken), 0, 0, uintptr(SecurityImpersonation), uintptr(TokenPrimary), uintptr(unsafe.Pointer(&userToken))); returnCode == 0 {
return 0xFFFFFFFF, fmt.Errorf("call native DuplicateTokenEx: %s", err)
}
if err := windows.CloseHandle(impersonationToken); err != nil {
return 0xFFFFFFFF, fmt.Errorf("close windows handle used for token duplication: %s", err)
}
return userToken, nil
}
func StartProcessAsCurrentUser(appPath, cmdLine, workDir string) error {
var (
sessionId windows.Handle
userToken windows.Token
envInfo windows.Handle
startupInfo windows.StartupInfo
processInfo windows.ProcessInformation
commandLine uintptr = 0
workingDir uintptr = 0
err error
)
if sessionId, err = GetCurrentUserSessionId(); err != nil {
return err
}
if userToken, err = DuplicateUserTokenFromSessionID(sessionId); err != nil {
return fmt.Errorf("get duplicate user token for current user session: %s", err)
}
if returnCode, _, err := procCreateEnvironmentBlock.Call(uintptr(unsafe.Pointer(&envInfo)), uintptr(userToken), 0); returnCode == 0 {
return fmt.Errorf("create environment details for process: %s", err)
}
creationFlags := CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE
startupInfo.ShowWindow = SW_SHOW
startupInfo.Desktop = windows.StringToUTF16Ptr("winsta0\\default")
if len(cmdLine) > 0 {
commandLine = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(cmdLine)))
}
if len(workDir) > 0 {
workingDir = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(workDir)))
}
if returnCode, _, err := procCreateProcessAsUser.Call(
uintptr(userToken), uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(appPath))), commandLine, 0, 0, 0,
uintptr(creationFlags), uintptr(envInfo), workingDir, uintptr(unsafe.Pointer(&startupInfo)), uintptr(unsafe.Pointer(&processInfo)),
); returnCode == 0 {
return fmt.Errorf("create process as user: %s", err)
}
return nil
}
@BelodedAleksey
Copy link

BelodedAleksey commented Nov 28, 2019

I got error "WTSQueryUserToken: A required privilege is not held by the client". Can you please tell me what can i do to get code working?
I tried to execute RevertToSelf and OpenThreadToken before WTSQuery, but results are the same with error "WTSQueryUserToken: A required privilege is not held by the client"

@xplodwild
Copy link

xplodwild commented Oct 30, 2020

@BelodedAleksey Might be a bit late to the party, but if it can help someone, your service needs to be running as SYSTEM for this to work. Works for me.

@achilles4828
Copy link

To support that above statement, calling WTSQueryUserToken() requires your service process to have SE_TCB_NAME privilege enabled which is available to an NT USER(SYSTEM) which is part of the trusted computer base.

@huahouye
Copy link

huahouye commented Apr 16, 2021

After register this RunAsUserTest as a windows service by command : main.exe -service install and main.exe -service start, it won't be error like "WTSQueryUserToken: A required privilege is not held by the client", works fine, good job.

@Anton-go
Copy link

Hey!
I used your code in my project and modified it to start the process for all active users.
I noticed a memory leak when I repeatedly call WTSEnumerateSessionsW.
I looked at the MS documentation I suggested that I need to free the WTS_SESSION_INFO memory in the WTSEnumerateSessionsW function. I tried to free the WTS_SESSION_INFO memory area
by calling:
modwtsapi32a = syscall.NewLazyDLL ("wtsapi32.dll")
procWTSFreeMemory = modwtsapi32a.NewProc ("WTSFreeMemory")
syscall.Syscall (procWTSFreeMemory.Addr (), 1, uintptr (unsafe.Pointer (& sessionInformation)), 0, 0)

But the memory still leaks away. What else could be causing the leak?

@CCCougar
Copy link

Excellent gits!!!

But when I want to hide the console window using const CREATE_NO_WINDOW, it failed to compile:

error

And I searched MSDN:

dword

Then I changed uint16 to uint32, and it worked!

const (
	CREATE_UNICODE_ENVIRONMENT uint32 = 0x00000400 // changed from uint16 to uint32
	CREATE_NO_WINDOW                  = 0x08000000
	CREATE_NEW_CONSOLE                = 0x00000010
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment