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 }