Last active
August 8, 2024 15:32
-
-
Save petemoore/8e43861943d175076e4b365039c8d5cd to your computer and use it in GitHub Desktop.
Running process as interactive user from windows service with UAC process elevation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
"errors" | |
"fmt" | |
"log" | |
"os" | |
"os/exec" | |
"syscall" | |
"time" | |
"unsafe" | |
"golang.org/x/sys/windows/svc" | |
"golang.org/x/sys/windows/svc/eventlog" | |
"golang.org/x/sys/windows/svc/mgr" | |
) | |
var ( | |
advapi32 = syscall.NewLazyDLL("advapi32.dll") | |
kernel32 = syscall.NewLazyDLL("kernel32.dll") | |
userenv = syscall.NewLazyDLL("userenv.dll") | |
wtsapi32 = syscall.NewLazyDLL("wtsapi32.dll") | |
procGetUserProfileDirectory = userenv.NewProc("GetUserProfileDirectoryW") | |
procWTSGetActiveConsoleSessionId = kernel32.NewProc("WTSGetActiveConsoleSessionId") | |
procWTSQueryUserToken = wtsapi32.NewProc("WTSQueryUserToken") | |
procGetTokenInformation = advapi32.NewProc("GetTokenInformation") | |
) | |
const ( | |
CREATE_BREAKAWAY_FROM_JOB = 0x01000000 | |
CREATE_NEW_CONSOLE = 0x00000010 | |
CREATE_NEW_PROCESS_GROUP = 0x00000200 | |
TokenLinkedToken = 19 | |
) | |
type TOKEN_INFORMATION_CLASS uint32 | |
func main() { | |
serviceName := "test-linked-token" | |
isIntSess, err := svc.IsAnInteractiveSession() | |
if err != nil { | |
log.Fatalf("failed to determine if we are running in an interactive session: %v", err) | |
} | |
var logFilename string | |
if isIntSess { | |
logFilename = `C:\gopath\src\github.com\taskcluster\test-linked-token\interactive.log` | |
} else { | |
logFilename = `C:\gopath\src\github.com\taskcluster\test-linked-token\service.log` | |
} | |
logFile, err := os.Create(logFilename) | |
if err != nil { | |
log.Fatalf("could not create log file %v: %v", logFilename, err) | |
} | |
defer logFile.Close() | |
logger := log.New(logFile, "", log.LstdFlags) | |
if isIntSess { | |
logger.Printf("Running as an interactive process") | |
err = installService(serviceName, "Test Linked Token") | |
if err != nil { | |
logger.Fatalf("could not install service %v: %v", serviceName, err) | |
} | |
defer func() { | |
err = removeService(serviceName) | |
if err != nil { | |
logger.Fatalf("could not remove service %v: %v", serviceName, err) | |
} | |
}() | |
err = startService(serviceName) | |
if err != nil { | |
logger.Fatalf("could not start service %v: %v", serviceName, err) | |
} | |
logger.Printf("Started service") | |
return | |
} | |
logger.Printf("Running as a windows service") | |
RunAdminCommand(logger) | |
} | |
func RunAdminCommand(logger *log.Logger) { | |
token, err := InteractiveUserToken(1 * time.Minute) | |
if err != nil { | |
logger.Fatal(err) | |
} | |
linkedToken, err := GetLinkedToken(token) | |
if err != nil { | |
logger.Fatal(err) | |
} | |
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |
defer cancel() | |
cmd := exec.CommandContext(ctx, `C:\gopath\src\github.com\taskcluster\test-linked-token\admincommand.bat`) | |
cmd.Stdout = os.Stdout | |
cmd.Stderr = os.Stderr | |
// creationFlags := uint32(CREATE_NEW_PROCESS_GROUP | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB) | |
cmd.SysProcAttr = &syscall.SysProcAttr{ | |
// Token: syscall.Token(token), | |
Token: syscall.Token(linkedToken), | |
// CreationFlags: creationFlags, | |
} | |
err = cmd.Start() | |
if err != nil { | |
logger.Fatal(err) | |
} | |
logger.Printf("Waiting for command to finish...") | |
err = cmd.Wait() | |
logger.Printf("Command finished with error: %v", err) | |
} | |
// InteractiveUserToken returns a user token (security context) for the | |
// interactive desktop session attached to the default console (i.e. what would | |
// be seen on a display connected directly to the computer, rather than a | |
// remote RDP session). It must be called from a process which is running under | |
// LocalSystem account in order to have the necessary privileges (typically a | |
// Windows service). Since the service might be running before a local logon | |
// occurs, a timeout can be specified for waiting for a successful logon (via | |
// winlogon) to occur. The returned token can be used in e.g. | |
// CreateProcessAsUser system call, which allows e.g. a Windows service to run | |
// a process in the interactive desktop session, as if the logged in user had | |
// executed the process directly. The function additionally waits for the user | |
// profile directory to exist, before returning. | |
func InteractiveUserToken(timeout time.Duration) (hToken syscall.Handle, err error) { | |
deadline := time.Now().Add(timeout) | |
var sessionId uint32 | |
sessionId, err = WTSGetActiveConsoleSessionId() | |
if err == nil { | |
err = WTSQueryUserToken(sessionId, &hToken) | |
} | |
for err != nil { | |
if time.Now().After(deadline) { | |
return | |
} | |
time.Sleep(time.Second / 10) | |
sessionId, err = WTSGetActiveConsoleSessionId() | |
if err == nil { | |
err = WTSQueryUserToken(sessionId, &hToken) | |
} | |
} | |
// to be safe, let's make sure profile directory has already been created, | |
// to avoid likely race conditions outside of this function | |
var userProfileDir string | |
userProfileDir, err = ProfileDirectory(hToken) | |
if err == nil { | |
_, err = os.Stat(userProfileDir) | |
} | |
for err != nil { | |
if time.Now().After(deadline) { | |
return | |
} | |
time.Sleep(time.Second / 10) | |
userProfileDir, err = ProfileDirectory(hToken) | |
if err == nil { | |
_, err = os.Stat(userProfileDir) | |
} | |
} | |
return | |
} | |
// https://msdn.microsoft.com/en-us/library/aa383835(VS.85).aspx | |
// DWORD WTSGetActiveConsoleSessionId(void); | |
func WTSGetActiveConsoleSessionId() (sessionId uint32, err error) { | |
r1, _, _ := procWTSGetActiveConsoleSessionId.Call() | |
if r1 == 0xFFFFFFFF { | |
err = os.NewSyscallError("WTSGetActiveConsoleSessionId", errors.New("Got return value 0xFFFFFFFF from syscall WTSGetActiveConsoleSessionId")) | |
} else { | |
sessionId = uint32(r1) | |
} | |
return | |
} | |
// https://msdn.microsoft.com/en-us/library/aa383840(VS.85).aspx | |
// BOOL WTSQueryUserToken( | |
// _In_ ULONG SessionId, | |
// _Out_ PHANDLE phToken | |
// ); | |
func WTSQueryUserToken( | |
sessionId uint32, | |
phToken *syscall.Handle, | |
) (err error) { | |
r1, _, e1 := procWTSQueryUserToken.Call( | |
uintptr(sessionId), | |
uintptr(unsafe.Pointer(phToken)), | |
) | |
if r1 == 0 { | |
err = os.NewSyscallError("WTSQueryUserToken", e1) | |
} | |
return | |
} | |
// ProfileDirectory returns the profile directory of the user represented by | |
// the given user handle | |
func ProfileDirectory(hToken syscall.Handle) (string, error) { | |
lpcchSize := uint32(0) | |
GetUserProfileDirectory(hToken, nil, &lpcchSize) | |
u16 := make([]uint16, lpcchSize) | |
err := GetUserProfileDirectory(hToken, &u16[0], &lpcchSize) | |
// bad token? | |
if err != nil { | |
return "", err | |
} | |
return syscall.UTF16ToString(u16), nil | |
} | |
// https://msdn.microsoft.com/en-us/library/windows/desktop/bb762280(v=vs.85).aspx | |
// BOOL WINAPI GetUserProfileDirectory( | |
// _In_ HANDLE hToken, | |
// _Out_opt_ LPTSTR lpProfileDir, | |
// _Inout_ LPDWORD lpcchSize | |
// ); | |
func GetUserProfileDirectory( | |
hToken syscall.Handle, | |
lpProfileDir *uint16, | |
lpcchSize *uint32, | |
) (err error) { | |
r1, _, e1 := procGetUserProfileDirectory.Call( | |
uintptr(hToken), | |
uintptr(unsafe.Pointer(lpProfileDir)), | |
uintptr(unsafe.Pointer(lpcchSize)), | |
) | |
if r1 == 0 { | |
err = os.NewSyscallError("GetUserProfileDirectory", e1) | |
} | |
return | |
} | |
func installService(name, desc string) error { | |
exepath, err := os.Executable() | |
if err != nil { | |
return err | |
} | |
m, err := mgr.Connect() | |
if err != nil { | |
return err | |
} | |
defer m.Disconnect() | |
s, err := m.OpenService(name) | |
if err == nil { | |
s.Close() | |
return fmt.Errorf("service %s already exists", name) | |
} | |
s, err = m.CreateService(name, exepath, mgr.Config{DisplayName: desc}) | |
if err != nil { | |
return err | |
} | |
defer s.Close() | |
err = eventlog.InstallAsEventCreate(name, eventlog.Error|eventlog.Warning|eventlog.Info) | |
if err != nil { | |
s.Delete() | |
return fmt.Errorf("SetupEventLogSource() failed: %s", err) | |
} | |
return nil | |
} | |
func removeService(name string) error { | |
m, err := mgr.Connect() | |
if err != nil { | |
return err | |
} | |
defer m.Disconnect() | |
s, err := m.OpenService(name) | |
if err != nil { | |
return fmt.Errorf("service %s is not installed", name) | |
} | |
defer s.Close() | |
err = s.Delete() | |
if err != nil { | |
return err | |
} | |
err = eventlog.Remove(name) | |
if err != nil { | |
return fmt.Errorf("RemoveEventLogSource() failed: %s", err) | |
} | |
return nil | |
} | |
func startService(name string) error { | |
m, err := mgr.Connect() | |
if err != nil { | |
return err | |
} | |
defer m.Disconnect() | |
s, err := m.OpenService(name) | |
if err != nil { | |
return fmt.Errorf("could not access service: %v", err) | |
} | |
defer s.Close() | |
err = s.Start() | |
if err != nil { | |
return fmt.Errorf("could not start service: %v", err) | |
} | |
return nil | |
} | |
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa446671(v=vs.85).aspx | |
// BOOL WINAPI GetTokenInformation( | |
// _In_ HANDLE TokenHandle, | |
// _In_ TOKEN_INFORMATION_CLASS TokenInformationClass, | |
// _Out_opt_ LPVOID TokenInformation, | |
// _In_ DWORD TokenInformationLength, | |
// _Out_ PDWORD ReturnLength | |
// ); | |
func GetTokenInformation( | |
tokenHandle syscall.Handle, | |
tokenInformationClass TOKEN_INFORMATION_CLASS, | |
tokenInformation *byte, | |
tokenInformationLength uint32, | |
returnLength *uint32, | |
) (err error) { | |
r1, _, e1 := procGetTokenInformation.Call( | |
uintptr(tokenHandle), | |
uintptr(tokenInformationClass), | |
uintptr(unsafe.Pointer(tokenInformation)), | |
uintptr(tokenInformationLength), | |
uintptr(unsafe.Pointer(returnLength)), | |
) | |
if r1 == 0 { | |
err = os.NewSyscallError("GetTokenInformation", e1) | |
} | |
return | |
} | |
func GetLinkedToken(hToken syscall.Handle) (syscall.Handle, error) { | |
tokenInformationLength := uint32(0) | |
_ = GetTokenInformation(hToken, TokenLinkedToken, nil, 0, &tokenInformationLength) | |
tokenInformation := make([]byte, tokenInformationLength) | |
err := GetTokenInformation(hToken, TokenLinkedToken, &tokenInformation[0], tokenInformationLength, &tokenInformationLength) | |
if err != nil { | |
return 0, err | |
} | |
linkedTokenStruct := (*TOKEN_LINKED_TOKEN)(unsafe.Pointer(&tokenInformation[0])) | |
return linkedTokenStruct.LinkedToken, nil | |
} | |
// https://msdn.microsoft.com/en-us/library/windows/desktop/bb530719(v=vs.85).aspx | |
// typedef struct _TOKEN_LINKED_TOKEN { | |
// HANDLE LinkedToken; | |
// } TOKEN_LINKED_TOKEN, *PTOKEN_LINKED_TOKEN; | |
type TOKEN_LINKED_TOKEN struct { | |
LinkedToken syscall.Handle // HANDLE | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment