引言
windows
系統,在打開某些 exe
的時候,會彈出“用戶賬戶控制(UAC
)”的彈窗 “你要允許來自xx發布者的此應用對你的設備進行更改嗎?”
UAC
(User Account Control
,用戶賬戶控制)是 Windows
操作系統中的一個安全組件,如果程序未通過管理員權限啟動,在涉及一些敏感操作時可能會導致應用程序發生錯誤。
本篇簡單介紹 2 種在 go
中以管理員權限啟動程序的方式。
實現
這里介紹 2 種方式:
manifest
,使用github.com/akavel/rsrc
庫,一般都這么用。- 運行時重新啟動,動態提權。
一、manifest
標準的方式,通過嵌入清單文件觸發 UAC
提示。
1. 創建 app.manifest
文件,內容如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"><trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"><security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false"/></requestedPrivileges></security></trustInfo>
</assembly>
2. 嵌入清單到 go
程序
- 安裝
rsrc
工具
go get github.com/akavel/rsrc
- 生成資源文件
rsrc -manifest app.manifest -o app.syso# -arch 平臺
# -manifest manifest文件,可以添加管理員權限啟動
# -ico 圖標文件
# -o 目標文件(.syso)
# rsrc -arch amd64 -manifest app.manifest -o app.syso -ico favicon.ico
3. 編譯 go
程序
直接 go build
即可,**注意:上述通過 rsrc
生成的 syso
需要放到項目路徑進行 build
**。
二、動態提權
適用于應用程序并不是所有的操作都需要使用管理員權限,只在用戶需要的時候進行提示,然后重啟應用動態提權。
直接上代碼,主要函數解釋:
isAdmin()
:通過嘗試訪問系統設備檢測權限。runAsAdmin()
:使用ShellExecute
以管理員權限重新啟動程序。
package mainimport ("fmt""os""syscall""time""github.com/google/uuid""golang.org/x/sys/windows"
)// 判斷程序是否為管理員啟動,不是則需要重啟// 檢測當前是否以管理員權限運行
func isAdmin() bool {_, err := os.Open("\\\\.\\PHYSICALDRIVE0")return err == nil
}// 以管理員權限重新啟動進程,并傳遞事件名稱
func runAsAdmin(eventName string) error {exe, _ := os.Executable()args := fmt.Sprintf(`--event-name "%s"`, eventName) // 傳遞事件名稱verbPtr, _ := syscall.UTF16PtrFromString("runas")exePtr, _ := syscall.UTF16PtrFromString(exe)argsPtr, _ := syscall.UTF16PtrFromString(args)cwd, _ := os.Getwd()cwdPtr, _ := syscall.UTF16PtrFromString(cwd)showCmd := int32(windows.SW_NORMAL)return windows.ShellExecute(0, verbPtr, exePtr, argsPtr, cwdPtr, showCmd)
}// 父進程等待事件觸發
func waitForChildReady(eventName string) bool {// 創建事件對象(初始狀態為未觸發)event, err := windows.CreateEvent(nil, // 默認安全屬性0, // 手動重置(false表示自動重置)0, // 初始狀態未觸發windows.StringToUTF16Ptr(eventName),)if err != nil {fmt.Println("CreateEvent error:", err)return false}defer windows.CloseHandle(event)// 等待事件觸發(最多10秒)const timeout = 10 * time.Secondresult, err := windows.WaitForSingleObject(event, uint32(timeout.Milliseconds()))if err != nil {fmt.Println("WaitForSingleObject error:", err)return false}return result == windows.WAIT_OBJECT_0
}// 子進程觸發事件
func signalParent(eventName string) {// 打開事件對象(需要EVENT_MODIFY_STATE權限)event, err := windows.OpenEvent(windows.EVENT_MODIFY_STATE,false,windows.StringToUTF16Ptr(eventName),)if err != nil {fmt.Println("OpenEvent error:", err)os.Exit(1)}defer windows.CloseHandle(event)// 觸發事件if err := windows.SetEvent(event); err != nil {fmt.Println("SetEvent error:", err)os.Exit(1)}
}func main() {// 解析命令行參數中的事件名稱var eventName stringfor i, arg := range os.Args {if arg == "--event-name" && i+1 < len(os.Args) {eventName = os.Args[i+1]break}}if isAdmin() {// 管理員模式下,觸發事件并執行業務邏輯if eventName != "" {signalParent(eventName)}fmt.Println("以管理員模式啟動!")// TODO: 主程序邏輯fmt.Println("按任意鍵退出程序...")// 直接從標準輸入讀取一個字節,用于檢測按鍵操作buf := make([]byte, 1)os.Stdin.Read(buf)} else {// 非管理員模式,生成唯一事件名稱并重啟eventName := uuid.New().String()if err := runAsAdmin(eventName); err != nil {fmt.Println("Failed to restart as admin:", err)os.Exit(1)}// 等待子進程就緒if waitForChildReady(eventName) {fmt.Println("Child process started successfully.")os.Exit(0)} else {fmt.Println("Failed to start child process.")os.Exit(1)}}
}