go 编译的 windows 进程(exe)以管理员权限启动(UAC)

引言

windows 系统,在打开某些 exe 的时候,会弹出“用户账户控制(UAC)”的弹窗 “你要允许来自xx发布者的此应用对你的设备进行更改吗?”

UACUser Account Control,用户账户控制)是 Windows 操作系统中的一个安全组件,如果程序未通过管理员权限启动,在涉及一些敏感操作时可能会导致应用程序发生错误。

本篇简单介绍 2 种在 go 中以管理员权限启动程序的方式。

实现

这里介绍 2 种方式:

  1. manifest,使用 github.com/akavel/rsrc 库,一般都这么用。
  2. 运行时重新启动,动态提权。

一、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 程序

  1. 安装 rsrc 工具
go get github.com/akavel/rsrc
  1. 生成资源文件
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)
		}
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值