Skip to content

Instantly share code, notes, and snippets.

@jerblack
Last active May 19, 2024 09:08
Show Gist options
  • Save jerblack/d0eb182cc5a1c1d92d92a4c4fcc416c6 to your computer and use it in GitHub Desktop.
Save jerblack/d0eb182cc5a1c1d92d92a4c4fcc416c6 to your computer and use it in GitHub Desktop.
Relaunch Windows Golang program with UAC elevation when admin rights needed.

I'm buiding a command line tool in Go that has an option to install itself as a service on Windows, which it needs admin rights for. I wanted to be able to have it reliably detect if it was running as admin already and if not, relaunch itself as admin. When the user runs the tool with the specific switch to trigger this functionality (-install or -uninstall in my case) they are prompted by UAC (User Account Control) to run the program as admin, which allows the tool to relaunch itself with the necessary rights.

To detect if I was admin, I tried the method described here first:
https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
This wasn't accurately detecting that I was elevated, and was reporting that I was not elevated even when running the tool in CMD prompt started with "Run as Administrator" so I needed a more reliable method.

I didn't want to try writing to an Admin protected area of the filesystem or registry because Windows has the ability to transparently virtualize those writes for standard users, which would have created a false positive.

I found this post on Reddit that recommended attempting to os.Open \\.\PHYSICALDRIVE0 which is not something that is virtualized, and this worked well for my purpose.
https://www.reddit.com/r/golang/comments/53dthc/way_to_detect_if_the_programs_running_with/

To relaunch the tool as Admin with a UAC prompt, I used the ShellExecute function in the golang.org/x/sys/windows package, using the "runas" verb that I learned about from here:
https://www.codeproject.com/Articles/320748/Haephrati-Elevating-during-runtime

The sample Go code is below. Other solutions talk about creating and embedding an application manifest with the requiresAdministrator attribute set, but this method does not require a manifest to be present.

package main
import (
"fmt"
"golang.org/x/sys/windows"
"os"
"syscall"
"time"
)
func main() {
// if not elevated, relaunch by shellexecute with runas verb set
if !amAdmin() {
runMeElevated()
}
time.Sleep(10*time.Second)
}
func runMeElevated() {
verb := "runas"
exe, _ := os.Executable()
cwd, _ := os.Getwd()
args := strings.Join(os.Args[1:], " ")
verbPtr, _ := syscall.UTF16PtrFromString(verb)
exePtr, _ := syscall.UTF16PtrFromString(exe)
cwdPtr, _ := syscall.UTF16PtrFromString(cwd)
argPtr, _ := syscall.UTF16PtrFromString(args)
var showCmd int32 = 1 //SW_NORMAL
err := windows.ShellExecute(0, verbPtr, exePtr, argPtr, cwdPtr, showCmd)
if err != nil {
fmt.Println(err)
}
}
func amAdmin() bool {
_, err := os.Open("\\\\.\\PHYSICALDRIVE0")
if err != nil {
fmt.Println("admin no")
return false
}
fmt.Println("admin yes")
return true
}
@kyparisisg
Copy link

Hello,

Thank you for sharing the code in the first place!

I would like to make a question that might prove to be a potential suggestion for improvement.

After line 36, I would suggest (always with a question-mark) that the following line of code should be executed:
os.Exit(0)
To terminate the current thread that tries to elevate the privileges on the process.

However, according to the usage of the code, the os.Exit(0) can take place after the function call for the NOT elevated process.�

Please let me know your thoughts :)

@lucasoares
Copy link

@kyparisisg The problem is when you run your code using go run.

Golang will create the build file using the temporary folder and when you call os.Exit() the application will try to delete those files and they will be in use by the elevated process (and it will throw an error). If you are ok with the displayed error, it's fine.

I don't know if you can wait for the elevated proccess termination before calling os.Exit.

@nineninesevenfour
Copy link

Hello @jerblack ,
thank you so much for sharing this! 👍

Checking if a user is running elevated can also be done with windows.GetCurrentProcessToken().IsElevated() from package "golang.org/x/sys/windows". So the function amAdmin could be written as:

func amAdmin() bool {
	elevated := windows.GetCurrentProcessToken().IsElevated()
	fmt.Printf("admin %v\n", elevated)
	return elevated
}

Checked with go1.20.3 windows/amd64, Windows 11.
Of course, maybe at the time of writing this gist, it might not have existed yet. 😉

@OneSeven
Copy link

@lucasoares @kyparisisg @jerblack @nineninesevenfour

Hi, everyone. When I executed the above code, a privilege-escalation window appeared successfully, but after I clicked OK, the command line pop-up window of the current program would continue to appear. Have you encountered or known the reason?
Executed by go run
The above situation still occurs after go install

@cybersecurityskills
Copy link

Thank you for this. It worked really well for me.

@majohn-r
Copy link

I have made a slight change in my copy of runMeElevated: it returns a bool based on the return from the call to windows.ShellExecute: I return err == nil. That way, the code that calls runMeElevated can decide to, for example, knowingly run with reduced privileges. And, for my codebase, that is often quite acceptable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment