Windows下Go程序以受限用户启动进程并关联Job Object问题
我在Windows Server 2022上运行一个以Administrator身份启动的Go应用,需求是以权限更受限的用户账户启动另一个应用,并将该进程分配到Job Object。
尝试了两种方案但都遇到问题:
方案1:通过
LogonUser获取令牌并传入SysProcAttr.Token
调用LogonUser获取用户令牌后启动应用,报错:Error running app: fork/exec ./main8.exe: A required privilege is not held by the client.猜测缺少
SE_INCREASE_QUOTA_NAME和SE_ASSIGNPRIMARYTOKEN_NAME权限,尝试用DuplicateTokenEx设置访问权限但无效。方案2:调用
CreateProcessWithLogonW
成功以受限用户启动进程,但无法将进程分配到Job Object,报错Access is denied,原因是该进程已被Secondary Logon Service分配到一个默认Job中。
请问我遗漏了什么?还有哪些解决方法可以尝试?
方案1的问题修复:正确处理令牌与权限
你猜测的权限方向是对的,但操作流程有误,需按以下步骤调整:
先启用当前Admin进程的目标权限
SE_INCREASE_QUOTA_NAME和SE_ASSIGNPRIMARYTOKEN_NAME需要在当前进程中先启用,而非通过DuplicateTokenEx设置到用户令牌上。要调用AdjustTokenPrivileges将这两个权限从禁用状态改为启用。将模拟令牌转换为主令牌
LogonUser返回的是模拟令牌(Impersonation Token),而SysProcAttr.Token要求传入主令牌(Primary Token)。必须用DuplicateTokenEx指定TokenPrimary参数完成转换。确保令牌拥有足够权限
复制令牌时需指定TOKEN_ALL_ACCESS或至少包含PROCESS_CREATE_PROCESS的权限掩码。
关键Go代码片段:
import ( "syscall" "os/exec" "golang.org/x/sys/windows" ) // 启用指定进程权限 func enablePrivilege(privName string) error { hToken, err := windows.OpenCurrentProcessToken(windows.TOKEN_ADJUST_PRIVILEGES | windows.TOKEN_QUERY) if err != nil { return err } defer windows.CloseHandle(hToken) var luid windows.LUID if err := windows.LookupPrivilegeValue(nil, windows.StringToUTF16Ptr(privName), &luid); err != nil { return err } tp := windows.TokenPrivileges{ PrivilegeCount: 1, Privileges: [1]windows.LUIDAndAttributes{ {Luid: luid, Attributes: windows.SE_PRIVILEGE_ENABLED}, }, } return windows.AdjustTokenPrivileges(hToken, false, &tp, 0, nil, nil) } func startProcessWithRestrictedUser() error { // 启用必要权限 if err := enablePrivilege(windows.SE_INCREASE_QUOTA_NAME); err != nil { return err } if err := enablePrivilege(windows.SE_ASSIGNPRIMARYTOKEN_NAME); err != nil { return err } // 调用LogonUser获取模拟令牌(此处需替换为实际的用户名、密码、域) var logonToken windows.Token err := windows.LogonUser( windows.StringToUTF16Ptr("RestrictedUser"), windows.StringToUTF16Ptr("."), windows.StringToUTF16Ptr("UserPassword"), windows.LOGON32_LOGON_INTERACTIVE, windows.LOGON32_PROVIDER_DEFAULT, &logonToken, ) if err != nil { return err } defer windows.CloseHandle(logonToken) // 转换为主令牌 var primaryToken windows.Token err = windows.DuplicateTokenEx( logonToken, windows.TOKEN_ALL_ACCESS, nil, windows.SecurityIdentification, windows.TokenPrimary, &primaryToken, ) if err != nil { return err } defer windows.CloseHandle(primaryToken) // 启动进程 cmd := exec.Command("./main8.exe") cmd.SysProcAttr = &syscall.SysProcAttr{ Token: syscall.Token(primaryToken), } return cmd.Start() }
方案2的问题修复:避免系统Job冲突
CreateProcessWithLogonW会自动将进程放入Secondary Logon Service的系统Job,Windows不允许进程同时属于多个Job,因此会触发Access is denied。解决思路有两个:
改用
CreateProcessAsUserW替代
该API不会自动绑定系统Job,只要你能正确获取受限用户的主令牌(参考方案1的令牌处理流程),就可以直接启动进程并分配到自定义Job。允许进程脱离系统Job
若必须使用CreateProcessWithLogonW,可在创建自定义Job时设置JOB_OBJECT_LIMIT_BREAKAWAY_OK限制,允许进程从系统Job中脱离,再加入你的Job。注意需要确保系统Job的权限允许此操作。
关键代码片段(创建支持脱离的Job):
import "unsafe" import "golang.org/x/sys/windows" func createBreakawayJob() (windows.Handle, error) { hJob, err := windows.CreateJobObject(nil, nil) if err != nil { return 0, err } info := windows.JobObjectExtendedLimitInformation{ BasicLimitInformation: windows.JobObjectBasicLimitInformation{ LimitFlags: windows.JOB_OBJECT_LIMIT_BREAKAWAY_OK, }, } err = windows.SetInformationJobObject( hJob, windows.JobObjectExtendedLimitInformation, &info, uint32(unsafe.Sizeof(info)), ) if err != nil { windows.CloseHandle(hJob) return 0, err } return hJob, nil } // 在启动进程后调用AssignProcessToJobObject // 需确保进程句柄拥有PROCESS_SET_QUOTA和PROCESS_TERMINATE权限
其他可选解决方法
- 检查进程句柄权限:分配进程到Job时,必须持有进程句柄的
PROCESS_SET_QUOTA和PROCESS_TERMINATE权限,否则会触发权限拒绝错误。启动进程时需指定对应的权限掩码。 - 预缓存主令牌:如果需要多次启动受限进程,可提前获取并缓存主令牌,避免重复执行
LogonUser和权限调整的开销。 - 指定桌面会话:启动进程时在
STARTUPINFO中设置lpDesktop为winsta0\default,避免因桌面会话隔离导致的权限问题。
内容的提问来源于stack exchange,提问作者Lucas Lopes

