github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/tools/runquiet/runquiet.go (about)

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  //
     4  // Windows utility for silently starting console processes
     5  // without showing a console.
     6  // Must be built with -ldflags "-H windowsgui"
     7  
     8  //go:build windows
     9  // +build windows
    10  
    11  package main
    12  
    13  import (
    14  	"errors"
    15  	"fmt"
    16  	"log"
    17  	"os"
    18  	"strings"
    19  	"syscall"
    20  	"time"
    21  	"unsafe"
    22  
    23  	"github.com/keybase/go-winio"
    24  	"golang.org/x/sys/windows"
    25  )
    26  
    27  const flagCreateNewConsole = 0x00000010
    28  
    29  var (
    30  	modadvapi32 *windows.LazyDLL = windows.NewLazySystemDLL("advapi32.dll")
    31  	moduser32   *windows.LazyDLL = windows.NewLazySystemDLL("user32.dll")
    32  
    33  	procDuplicateTokenEx         *windows.LazyProc = modadvapi32.NewProc("DuplicateTokenEx")
    34  	procGetShellWindow           *windows.LazyProc = moduser32.NewProc("GetShellWindow")
    35  	procGetWindowThreadProcessId *windows.LazyProc = moduser32.NewProc("GetWindowThreadProcessId")
    36  	procCreateProcessWithTokenW  *windows.LazyProc = modadvapi32.NewProc("CreateProcessWithTokenW")
    37  )
    38  
    39  func GetWindowThreadProcessId(hwnd syscall.Handle) uint32 {
    40  	var processID uint32
    41  	_, _, _ = procGetWindowThreadProcessId.Call(
    42  		uintptr(hwnd),
    43  		uintptr(unsafe.Pointer(&processID)))
    44  
    45  	return processID
    46  }
    47  
    48  const SecurityImpersonation = 2
    49  
    50  type TokenType uint32
    51  
    52  const (
    53  	TokenPrimary TokenType = 1
    54  	_            TokenType = 2 // TokenImpersonation
    55  )
    56  
    57  func DuplicateTokenEx(hExistingToken windows.Token, dwDesiredAccess uint32, lpTokenAttributes *syscall.SecurityAttributes, impersonationLevel uint32, tokenType TokenType, phNewToken *windows.Token) (err error) {
    58  	r1, _, e1 := syscall.Syscall6(procDuplicateTokenEx.Addr(), 6, uintptr(hExistingToken), uintptr(dwDesiredAccess), uintptr(unsafe.Pointer(lpTokenAttributes)), uintptr(impersonationLevel), uintptr(tokenType), uintptr(unsafe.Pointer(phNewToken)))
    59  	if r1 == 0 {
    60  		if e1 != 0 {
    61  			err = syscall.Errno(e1)
    62  		} else {
    63  			err = syscall.EINVAL
    64  		}
    65  	}
    66  	return
    67  }
    68  
    69  // makeCmdLine builds a command line out of args by escaping "special"
    70  // characters and joining the arguments with spaces.
    71  func makeCmdLine(args []string) string {
    72  	var s string
    73  	for _, v := range args {
    74  		if s != "" {
    75  			s += " "
    76  		}
    77  		s += syscall.EscapeArg(v)
    78  	}
    79  	return s
    80  }
    81  
    82  func CreateProcessWithTokenW(hToken syscall.Token, argv []string, attr *syscall.ProcAttr) (pid int, handle uintptr, err error) {
    83  	var sys = attr.Sys
    84  
    85  	var cmdline string
    86  	// Windows CreateProcess takes the command line as a single string:
    87  	// use attr.CmdLine if set, else build the command line by escaping
    88  	// and joining each argument with spaces
    89  	if sys.CmdLine != "" {
    90  		cmdline = sys.CmdLine
    91  	} else {
    92  		cmdline = makeCmdLine(argv)
    93  	}
    94  
    95  	var argvp *uint16
    96  	if len(cmdline) != 0 {
    97  		argvp, err = syscall.UTF16PtrFromString(cmdline)
    98  		if err != nil {
    99  			return 0, 0, err
   100  		}
   101  	}
   102  
   103  	si := new(syscall.StartupInfo)
   104  	si.Cb = uint32(unsafe.Sizeof(*si))
   105  	if sys.HideWindow {
   106  		si.Flags |= syscall.STARTF_USESHOWWINDOW
   107  		si.ShowWindow = syscall.SW_HIDE
   108  	}
   109  
   110  	pi := new(syscall.ProcessInformation)
   111  
   112  	flags := sys.CreationFlags | syscall.CREATE_UNICODE_ENVIRONMENT
   113  	r1, _, e1 := procCreateProcessWithTokenW.Call(
   114  		uintptr(hToken),                // HANDLE                hToken,
   115  		0,                              // DWORD                 dwLogonFlags,
   116  		uintptr(0),                     // LPCWSTR               lpApplicationName,
   117  		uintptr(unsafe.Pointer(argvp)), // LPWSTR                lpCommandLine,
   118  		uintptr(flags),                 // DWORD                 dwCreationFlags,
   119  		uintptr(0),                     // LPVOID                lpEnvironment,
   120  		uintptr(0),                     // LPCWSTR               lpCurrentDirectory,
   121  		uintptr(unsafe.Pointer(si)),    // LPSTARTUPINFOW        lpStartupInfo,
   122  		uintptr(unsafe.Pointer(pi)),    // LPPROCESS_INFORMATION lpProcessInformation
   123  	)
   124  
   125  	if r1 != 0 {
   126  		e1 = nil
   127  	}
   128  
   129  	return int(pi.ProcessId), uintptr(pi.Process), e1
   130  }
   131  
   132  // Protection against running elevated: attempt to run as regular user instead.
   133  // Assume an error means we weren't elevated, which should be the normal case.
   134  // see https://blogs.msdn.microsoft.com/aaron_margosis/2009/06/06/faq-how-do-i-start-a-program-as-the-desktop-user-from-an-elevated-app/
   135  func getUserToken() (syscall.Token, error) {
   136  	var err error
   137  	var shellWindow uintptr
   138  	if shellWindow, _, err = procGetShellWindow.Call(); shellWindow == 0 {
   139  		return 0, fmt.Errorf("call native GetShellWindow: %s", err)
   140  	}
   141  
   142  	processID := GetWindowThreadProcessId(syscall.Handle(shellWindow))
   143  
   144  	if processID == 0 {
   145  		return 0, errors.New("can't get desktop window proc ID")
   146  	}
   147  
   148  	h, e := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, false, processID)
   149  	if e != nil {
   150  		return 0, fmt.Errorf("OpenProcess error: %s %d", e, processID)
   151  	}
   152  	defer syscall.CloseHandle(h)
   153  
   154  	var token, dupToken windows.Token
   155  	err = windows.OpenProcessToken(windows.Handle(h), windows.TOKEN_DUPLICATE, &token)
   156  	if err != nil {
   157  		return 0, err
   158  	}
   159  	const TOKEN_ADJUST_SESSIONID = 256
   160  	dwTokenRights := uint32(syscall.TOKEN_QUERY | syscall.TOKEN_ASSIGN_PRIMARY | syscall.TOKEN_DUPLICATE | syscall.TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID)
   161  	err = DuplicateTokenEx(token, dwTokenRights, nil, SecurityImpersonation, TokenPrimary, &dupToken)
   162  	if err != nil || dupToken == 0 {
   163  		return 0, fmt.Errorf("DuplicateTokenEx error: %s", err.Error())
   164  	}
   165  
   166  	return syscall.Token(dupToken), nil
   167  }
   168  
   169  // elevated means we try running as user
   170  func doRun(elevated bool) error {
   171  	argsIndex := 1 // 0 is the name of this program, 1 is either the one to launch or the "wait" option
   172  
   173  	if len(os.Args) < 2 {
   174  		log.Fatal("ERROR: no arguments. Use [-wait] programname [arg arg arg]\n")
   175  	}
   176  
   177  	// Do this awkward thing so we can pass along the rest of the command line as-is
   178  
   179  	doWait := false
   180  	doHide := true
   181  	for i := 1; i < 3 && (i+1) < len(os.Args); i++ {
   182  		if strings.EqualFold(os.Args[argsIndex], "-wait") {
   183  			argsIndex++
   184  			doWait = true
   185  		} else if strings.EqualFold(os.Args[argsIndex], "-show") {
   186  			argsIndex++
   187  			doHide = false
   188  		}
   189  	}
   190  	attr := &syscall.ProcAttr{
   191  		Files: []uintptr{uintptr(syscall.Stdin), uintptr(syscall.Stdout), uintptr(syscall.Stderr)},
   192  		Env:   syscall.Environ(),
   193  		Sys: &syscall.SysProcAttr{
   194  			HideWindow:    doHide,
   195  			CreationFlags: flagCreateNewConsole,
   196  		},
   197  	}
   198  	fmt.Printf("Launching %s with args %v\n", os.Args[argsIndex], os.Args[argsIndex:])
   199  
   200  	var err error
   201  	var pid int
   202  
   203  	if elevated {
   204  		token, _ := getUserToken()
   205  
   206  		pid, _, err = CreateProcessWithTokenW(token, os.Args[argsIndex:], attr)
   207  		defer syscall.CloseHandle(syscall.Handle(token))
   208  		if err != nil {
   209  			fmt.Printf("CreateProcessWithTokenW error: %s\n", err.Error())
   210  			return err
   211  		}
   212  	} else {
   213  		pid, _, err = syscall.StartProcess(os.Args[argsIndex], os.Args[argsIndex:], attr)
   214  	}
   215  
   216  	if err != nil {
   217  		fmt.Printf("StartProcess error: %s\n", err.Error())
   218  		return err
   219  	} else if doWait {
   220  		p, err := os.FindProcess(pid)
   221  		if err != nil {
   222  			fmt.Printf("Launcher can't find %d\n", pid)
   223  			return err
   224  		}
   225  
   226  		timeout := make(chan time.Time, 1)
   227  
   228  		go func() {
   229  			pstate, err := p.Wait()
   230  
   231  			if err == nil && pstate.Success() {
   232  				time.Sleep(100 * time.Millisecond)
   233  			} else {
   234  				fmt.Printf("Unsuccessful wait: Error %v, pstate %v\n", err, *pstate)
   235  			}
   236  			timeout <- time.Now()
   237  		}()
   238  
   239  		// Only wait 15 seconds because an erroring command was shown to hang
   240  		// up an installer on Win7
   241  		select {
   242  		case _ = <-timeout:
   243  			// success
   244  		case <-time.After(15 * time.Second):
   245  			fmt.Println("timed out")
   246  		}
   247  	}
   248  	return nil
   249  }
   250  
   251  func main() {
   252  	// RunWithPrivilege enables a single privilege for a function call.
   253  	// SeIncreaseQuotaPrivilege will only work when elevated
   254  	err := winio.RunWithPrivilege("SeIncreaseQuotaPrivilege", func() error {
   255  		result := doRun(true)
   256  		if result != nil {
   257  			// Print this failure but not the RunWithPrivilege result, since
   258  			// RunWithPrivilege failure is the normal case
   259  			fmt.Printf("De-elevation failure: %v\n", result)
   260  		}
   261  		return result
   262  	})
   263  	if err != nil {
   264  		// This means we weren't elevated, or de-elevation failed. Run without.
   265  		doRun(false)
   266  	}
   267  }