Skip to content

Instantly share code, notes, and snippets.

@KatelynHaworth
Created November 2, 2019 16:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save KatelynHaworth/57bd4bc16d9f82f7cecba945095aa83f to your computer and use it in GitHub Desktop.
Save KatelynHaworth/57bd4bc16d9f82f7cecba945095aa83f to your computer and use it in GitHub Desktop.
Reproduction code for golang/go#35314

Reproduction code for golang/go#35314

This gist contains example code to reproduce golang/go#35314, due to the privilege requirements of DuplicateTokenEx the code is designed to run as a service which then spawns a child process and logs the output of the child process to the Windows event log.

Written for go 1.13.4

Running the code

First compile the code to a binary

env GOOS=windows go build -o bugtest.exe service.go

Then copy the binary to a Windows machine and create a new service that uses the binary

New-Service -Name BugTestService -BinaryPathName <bugtest.exe location> -StartupType Manual

Finally, run the service and get the logs (The service start will error but will complete its job)

Start-Service -Name BugTestService
Get-EventLog -LogName Application -Source BugTestService | Select -ExpandProperty Message

Cleaning up

To remove the service run the following

Remove-Service -Name BugTestService
// +build windows
package main
import (
"fmt"
"math"
"os"
"os/exec"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc/eventlog"
)
func main() {
if len(os.Args) > 1 {
// Running as child process, print environment variables
fmt.Println("Environment variables: ", os.Environ())
return
}
log, err := eventlog.Open("BugTestService")
if err != nil {
return
}
execPath, err := os.Executable()
if err != nil {
return
}
output, err := withoutEnv(execPath)
if err != nil {
return
}
_ = log.Info(1, "Without Env Set: " + output)
output, err = withEnv(execPath)
if err != nil {
return
}
_ = log.Info(1, "With Env Set: " + output)
}
func withoutEnv(path string) (string, error) {
attr, _, err := getChildProcessSysProcAttr()
if err != nil {
return "", err
}
defer windows.Close(windows.Handle(attr.Token))
cmd := exec.Command(path, "child")
cmd.SysProcAttr = attr
defer windows.Close(windows.Handle(attr.Token))
output, err := cmd.Output()
if err != nil {
return "", err
}
return string(output), nil
}
func withEnv(path string) (string, error) {
attr, env, err := getChildProcessSysProcAttr()
if err != nil {
return "", err
}
defer windows.Close(windows.Handle(attr.Token))
cmd := exec.Command(path, "child")
cmd.SysProcAttr = attr
cmd.Env = env
defer windows.Close(windows.Handle(attr.Token))
output, err := cmd.Output()
if err != nil {
return "", err
}
return string(output), nil
}
// wtsEnumerateSessions queries the Windows kernel
// to obtain a list of active user sessions attached
// to the current Window Terminal Server
func wtsEnumerateSessions() ([]*windows.WTS_SESSION_INFO, error) {
var sessionPointer uintptr
var sessionCount uint32
err := windows.WTSEnumerateSessions(0, 0, 1, (**windows.WTS_SESSION_INFO)(unsafe.Pointer(&sessionPointer)), &sessionCount)
if err != nil {
return nil, fmt.Errorf("enumerate terminal server sessions: %w", err)
}
defer windows.WTSFreeMemory(sessionPointer)
sessions := make([]*windows.WTS_SESSION_INFO, sessionCount)
size := unsafe.Sizeof(windows.WTS_SESSION_INFO{})
for i := range sessions {
sessions[i] = (*windows.WTS_SESSION_INFO)(unsafe.Pointer(sessionPointer + (size * uintptr(i))))
}
return sessions, nil
}
// getCurrentUserSessionID enumerates the active
// terminal server sessions to find the active
// terminal session and returns the session ID.
//
// If no active session can be found it will return
// a value of MaxUint32.
func getCurrentUserSessionId() (uint32, error) {
sessionList, err := wtsEnumerateSessions()
if err != nil {
return 0, fmt.Errorf("obtain terminal server session list: %w", err)
}
for i := range sessionList {
if sessionList[i].State == windows.WTSActive {
return sessionList[i].SessionID, nil
}
}
return math.MaxUint32, nil
}
// duplicateUserTokenFromSession will obtain the token
// for the session ID provided, it will then duplicate
// the token with permissions to launch a process as the
// related user
func duplicateUserTokenFromSessionID(sessionID uint32) (syscall.Token, error) {
var impersonationToken, userToken windows.Token
if err := windows.WTSQueryUserToken(sessionID, &impersonationToken); err != nil {
return 0, fmt.Errorf("query user token for session ID: %w", err)
}
if err := windows.DuplicateTokenEx(impersonationToken, 0, nil, windows.SecurityImpersonation, windows.TokenPrimary, &userToken); err != nil {
return 0, fmt.Errorf("duplicate user token with desired security: %w", err)
}
if err := windows.CloseHandle(windows.Handle(impersonationToken)); err != nil {
return 0, fmt.Errorf("close handle for original user token: %w", err)
}
return syscall.Token(userToken), nil
}
//go:linkname environForSysProcAttr os.environForSysProcAttr
func environForSysProcAttr(sys *syscall.SysProcAttr) (env []string, err error)
// getChildProcessSysProcAttr will first obtain the
// current active session ID and will duplicate a user
// token from that session.
//
// With the user token it will construct process attributes
// to allow the process to be started in the user's execution
// context.
func getChildProcessSysProcAttr() (attr *syscall.SysProcAttr, env []string, err error) {
attr = &syscall.SysProcAttr{
HideWindow: true,
}
sessionID, err := getCurrentUserSessionId()
if err != nil {
return nil, nil, fmt.Errorf("get current user session ID: %w", err)
}
attr.Token, err = duplicateUserTokenFromSessionID(sessionID)
if err != nil {
return nil, nil, fmt.Errorf("duplicate user token from session ID: %w", err)
}
// ==========
// Makes it all work
// ===========
env, err = environForSysProcAttr(attr)
if err != nil {
return nil, nil, fmt.Errorf("load environment variables for user: %w", err)
}
return
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment