引言
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 main
import (
"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.Second
result, 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 string
for 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)
}
}
}