从OpenSSH会话在Windows用户会话启动进程遇权限拒绝问题
核心问题分析
你的场景和服务启动交互进程类似,但SSH会话有个关键差异:远程SSH登录的会话是无桌面的终端会话,即使账号相同,它的进程上下文和用户本地登录的交互会话(带winsta0桌面)完全隔离。出现CreateProcessAsUser错误5(拒绝访问),主要是令牌权限不足、桌面访问权限缺失、会话未正确关联这几个原因。
必须修正的几个点
1. 显式启用必要特权
你的administrator账号虽然配置了相关权限,但SSH进程默认不会主动启用这些特权。在复制令牌前,必须通过AdjustTokenPrivilegesAPI启用以下特权:
SeAssignPrimaryTokenPrivilege(分配主令牌权限)SeIncreaseQuotaPrivilege(增加配额权限)SeDebugPrivilege(调试权限,用于打开用户会话中的explorer进程)
2. 调整令牌复制参数
DuplicateTokenEx的SECURITY_IMPERSONATION_LEVEL应改为SecurityDelegation,而非SecurityImpersonation——因为需要跨会话使用复制后的令牌;同时,请求的令牌权限至少要包含TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY,避免权限过度请求的同时保证足够权限。
3. 确保访问目标桌面权限
SSH进程默认没有访问winsta0\default桌面的权限,需要先打开桌面对象并设置访问权限,确保新进程能正常关联到用户桌面。
4. 确认会话ID关联
复制的explorer令牌已包含目标会话ID,但需确保CreateProcessAsUser创建的进程继承该会话ID,无需额外修改,但需保证获取的explorer进程确实属于目标用户的交互会话。
修正后的代码示例
// 补充必要的Win32常量和结构体声明 const uint TOKEN_QUERY = 0x0008; const uint TOKEN_DUPLICATE = 0x0002; const uint TOKEN_ASSIGN_PRIMARY = 0x0001; const uint TOKEN_ADJUST_PRIVILEGES = 0x0020; const uint SE_PRIVILEGE_ENABLED = 0x00000002; const uint DESKTOP_CREATEWINDOW = 0x0002; const uint DESKTOP_WRITEOBJECTS = 0x0008; const uint DESKTOP_READOBJECTS = 0x0001; const int UOI_USER_SID = 4; const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400; enum SECURITY_IMPERSONATION_LEVEL { SecurityAnonymous, SecurityIdentification, SecurityImpersonation, SecurityDelegation } enum TOKEN_TYPE { TokenPrimary = 1, TokenImpersonation = 2 } [StructLayout(LayoutKind.Sequential)] struct LUID { public uint LowPart; public int HighPart; } [StructLayout(LayoutKind.Sequential, Pack = 1)] struct TOKEN_PRIVILEGES { public int PrivilegeCount; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] public LUID_AND_ATTRIBUTES[] Privileges; } [StructLayout(LayoutKind.Sequential)] struct LUID_AND_ATTRIBUTES { public LUID Luid; public uint Attributes; } // Win32 API导入(仅补充新增部分,其余API你已有) [DllImport("advapi32.dll", SetLastError = true)] static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle); [DllImport("advapi32.dll", SetLastError = true)] static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, ref LUID lpLuid); [DllImport("advapi32.dll", SetLastError = true)] static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, int BufferLength, IntPtr PreviousState, IntPtr ReturnLength); [DllImport("user32.dll", SetLastError = true)] static extern IntPtr OpenDesktop(string lpszDesktop, int dwFlags, bool fInherit, uint dwDesiredAccess); [DllImport("user32.dll", SetLastError = true)] static extern bool SetUserObjectInformation(IntPtr hObj, int nIndex, IntPtr pvInfo, uint dwInfoLen); [DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess(); [DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr hObject); // 启用特权的辅助方法 private static bool EnablePrivilege(string privilegeName) { if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out IntPtr tokenHandle)) return false; var luid = new LUID(); if (!LookupPrivilegeValue(null, privilegeName, ref luid)) { CloseHandle(tokenHandle); return false; } var tp = new TOKEN_PRIVILEGES(); tp.PrivilegeCount = 1; tp.Privileges = new LUID_AND_ATTRIBUTES[1]; tp.Privileges[0].Luid = luid; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; var result = AdjustTokenPrivileges(tokenHandle, false, ref tp, Marshal.SizeOf(tp), IntPtr.Zero, IntPtr.Zero); CloseHandle(tokenHandle); return result != false && Marshal.GetLastWin32Error() == 0; } // 主执行逻辑 public static void Main(string[] args) { // 替换为目标用户会话中explorer的PID int explorerPID = 1234; // 先启用必要特权 EnablePrivilege("SeAssignPrimaryTokenPrivilege"); EnablePrivilege("SeIncreaseQuotaPrivilege"); EnablePrivilege("SeDebugPrivilege"); // 打开explorer进程 var pHandle = OpenProcess(0x001F0FFF, false, explorerPID); if (pHandle == IntPtr.Zero) { Console.WriteLine($"OpenProcess failed: {Marshal.GetLastWin32Error()}"); return; } // 打开进程令牌(请求必要权限) if (!OpenProcessToken(pHandle, TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY, out IntPtr tHandle)) { Console.WriteLine($"OpenProcessToken failed: {Marshal.GetLastWin32Error()}"); CloseHandle(pHandle); return; } // 复制令牌(使用SecurityDelegation) var lpT = new SECURITY_ATTRIBUTES(); if (!DuplicateTokenEx( tHandle, TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY, ref lpT, SECURITY_IMPERSONATION_LEVEL.SecurityDelegation, TOKEN_TYPE.TokenPrimary, out IntPtr newtHandle)) { Console.WriteLine($"DuplicateTokenEx failed: {Marshal.GetLastWin32Error()}"); CloseHandle(tHandle); CloseHandle(pHandle); return; } // 打开并设置桌面权限 IntPtr hDesktop = OpenDesktop("default", 0, true, DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS); if (hDesktop != IntPtr.Zero) { SetUserObjectInformation(hDesktop, UOI_USER_SID, IntPtr.Zero, 0); CloseHandle(hDesktop); } // 启动notepad进程 var startInfo = new STARTUPINFO(); var lpProcAttr = new SECURITY_ATTRIBUTES(); var lpThreadAttr = new SECURITY_ATTRIBUTES(); startInfo.lpDesktop = "winsta0\\default"; startInfo.cb = Marshal.SizeOf(startInfo); if (!CreateProcessAsUser(newtHandle, "c:\\Windows\\notepad.exe", null, ref lpProcAttr, ref lpThreadAttr, false, CREATE_UNICODE_ENVIRONMENT, IntPtr.Zero, null, ref startInfo, out PROCESS_INFORMATION pi)) { Console.WriteLine($"CreateProcessAsUser failed: {Marshal.GetLastWin32Error()}"); } else { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); Console.WriteLine("Notepad started successfully"); } // 清理句柄 CloseHandle(newtHandle); CloseHandle(tHandle); CloseHandle(pHandle); }
额外配置注意事项
- OpenSSH服务器配置:确保
sshd_config中PermitTTY yes(默认开启),AllowUsers包含你的administrator账号,未禁用交互会话。 - 用户权限分配:在本地安全策略的「用户权限分配」中,确保administrator账号拥有「替换进程级令牌」和「调整进程的内存配额」权限。
- 会话校验:用
query user命令确认目标用户的会话ID,确保获取的explorer进程确实属于该会话。
可行性确认
这个需求完全可行,主流远程管理工具的跨会话交互进程启动功能,核心逻辑和你的思路一致,只要处理好令牌权限、桌面访问和会话关联这几个关键点即可正常运行。
内容的提问来源于stack exchange,提问作者cristopher

