Skip to content

Instantly share code, notes, and snippets.

@KatelynHaworth
Created November 2, 2019 16:48

Revisions

  1. KatelynHaworth created this gist Nov 2, 2019.
    37 changes: 37 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,37 @@
    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

    ```bash
    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

    ```powershell
    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)

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

    ### Cleaning up

    To remove the service run the following

    ```powershell
    Remove-Service -Name BugTestService
    ```
    185 changes: 185 additions & 0 deletions service.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,185 @@
    // +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
    }