在 Windows 系统中,管理员权限和非管理员权限运行的程序之间不能使用 Windows 提供的通信机制进行通信。对于部分文件夹(ProgramData),管理员权限创建的文件是不能以非管理员权限修改和删除的。
然而,一个进程运行之后启动的子进程,会继承当前进程的 UAC 权限;于是有时我们会有降权运行的需要。本文将介绍 Windows 系统上降权运行的几种方法。
问题描述:
A进程(安装包进程)通过UAC弹窗提权为Administrators,A进程要启动B进程(界面进程),B进程却无法读取到网络驱动器。但是手动启动B进程,则可以读取到网络驱动器。(且前者中,B进程调用GetFileAttributes等函数查看网络驱动器文件路径,报错路径不正确,后者则没问题)
原因:
初步认为是权限不同导致的。使用accesschk工具查看A启动的B进程,与手动启动B进程的权限差别。发现第一种情况下,B进程是BUILTIN\Administrators用户组,表示本地管理员组,而第二种情况下,则是DELL用户组。由此可以进一步尝试探索是否是权限问题导致。(如下图)
解决方案:
在A进程启动B进程时,采取一定方式,降低B进程权限。
方法1:用 explorer.exe 代理运行程序(不能携带参数)
请特别注意,使用 explorer.exe 代理运行程序的时候,是不能带参数的,否则 explorer.exe 将不会启动你的程序。
因为绝大多数用户启动系统的时候,explorer.exe 进程都是处于运行状态,而如果启动一个新的 explorer.exe,都会自动激活当前正在运行的进程而不会启动新的。
于是我们可以委托默认以普通权限运行的 explorer.exe 来代理启动我们需要启动的子进程,这时启动的子进程便是与 explorer.exe 相同权限的。如果用户计算机上的 UAC 是打开的,那么 explorer.exe 默认就会以标准用户权限运行。
BOOL CLauncherApp::StartProcessWithExplorer(const std::wstring& appPath)
{std::wstring command = appPath; WriteInfo(_T("command = %s"), command.c_str());// 使用 ShellExecute 启动 explorer.exeHINSTANCE result = ShellExecuteW(NULL, L"open", L"explorer.exe", command.c_str(), // 进程完整路径NULL, SW_SHOWNORMAL // 不显示窗口);if ((int)result <= 32){WriteError(_T("ShellExecuteW failed with error code: %d(HINSTANCE)"), (int)result);return FALSE;}return TRUE;
}
方法2:使用cmd的runas 命令来运行程序(需要输入密码)
使用 runas 命令来运行,可以指定一个权限级别,或者指定用户。这里指定为当前用户,即可获得当前用户权限。
但请注意:可以手动先试一下cmd中的runas命令,结果发现指定用户时是需要输入密码的(且不支持空密码),因此这里改为代码,无法使用,除非密码已知。
// 使用 runas 命令启动一个进程
bool RunAsUser(const std::wstring& appPath, const std::wstring& username)
{
// 构造 runas 命令
std::wstring command = L"runas /user:" + username + L" \"" + appPath + L"\""; // runas /user:WIN-26554\DELL "要启动程序的完整路径"
// (WIN-26554\DELL是要指定用户的DOMAIN\USER名称) // 使用 ShellExecuteW 启动 runas 命令
HINSTANCE result = ShellExecuteW(
NULL, // 父窗口句柄
L"open", // 操作类型
L"cmd.exe", // 命令提示符
command.c_str(), // runas 命令
NULL, // 当前目录
SW_HIDE // 不显示窗口
); if ((int)result <= 32)
{
std::cerr << "ShellExecuteW failed with error code: " << (int)result << std::endl;
return false;
} return true;
}
方法3;使用 CreateProcessAsUser 启动低权限进程(仅限父进程必须在 LocalSystem 帐户 的上下文中运行,并具有 SE_TCB_NAME 特权)
可以通过 WTSQueryUserToken 获取当前会话的用户令牌,然后使用 CreateProcessAsUser 启动一个低权限的子进程
void CreateProcessWithLowerPrivileges(LPCWSTR szAppPath)
{DWORD sessionId = WTSGetActiveConsoleSessionId(); // 获取当前活动窗口的会话IDHANDLE hToken, hNewToken;STARTUPINFO si = { sizeof(STARTUPINFO) };PROCESS_INFORMATION pi;if (!WTSQueryUserToken(sessionId, &hToken)) // 查询对应的Token{std::cerr << "WTSQueryUserToken failed: " << GetLastError() << std::endl;return;}// 复制Tokenif (!DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityImpersonation, TokenPrimary, &hNewToken)){std::cerr << "DuplicateTokenEx failed: " << GetLastError() << std::endl;CloseHandle(hToken);return;}// 使用当前会话Token创建子进程if (!CreateProcessAsUser(hNewToken,NULL,(TCHAR*)szAppPath,NULL,NULL,FALSE,CREATE_UNICODE_ENVIRONMENT,NULL,NULL,&si,&pi)){std::cerr << "CreateProcessAsUser failed: " << GetLastError() << std::endl;CloseHandle(hNewToken);CloseHandle(hToken);return;}CloseHandle(pi.hProcess);CloseHandle(pi.hThread);CloseHandle(hNewToken);CloseHandle(hToken);
}
方法4:调用AdjustTokenPrivileges调整权限(不推荐,除非知道要调整什么权限)
可以在程序中调整访问令牌的权限,禁用一些高权限相关的权限,从而降低进程的权限。
BOOL DisablePrivilege(LPCWSTR lpPrivilegeName)
{HANDLE hToken;LUID luid;TOKEN_PRIVILEGES tp;if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)){std::cerr << "OpenProcessToken failed: " << GetLastError() << std::endl;return FALSE;}if (!LookupPrivilegeValue(NULL, lpPrivilegeName, &luid)){std::cerr << "LookupPrivilegeValue failed: " << GetLastError() << std::endl;CloseHandle(hToken);return FALSE;}tp.PrivilegeCount = 1;tp.Privileges[0].Luid = luid;tp.Privileges[0].Attributes = 0; // 禁用权限(在这里指定要禁用什么权限)if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL)){std::cerr << "AdjustTokenPrivileges failed: " << GetLastError() << std::endl;CloseHandle(hToken);return FALSE;}CloseHandle(hToken);return TRUE;
}
还有就是可以通过修改访问令牌的完整性级别,再启动子进程,不过这样只能修改完整性级别,修改不了别的权限
BOOL CLauncherApp::CreateProcessWithMediumIL(LPCWSTR lpCommandLine)
{HANDLE hToken = NULL;HANDLE hDupToken = NULL;STARTUPINFO si = { sizeof(STARTUPINFO) };PROCESS_INFORMATION pi = { 0 };BOOL bResult = FALSE;// 获取当前进程的访问令牌if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken)) {WriteError(_T("OpenProcessToken failed with error = %d"), GetLastError());return FALSE;}// 复制访问令牌if (!DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityImpersonation, TokenPrimary, &hDupToken)) {WriteError(_T("DuplicateTokenEx failed with error = %d"), GetLastError());CloseHandle(hToken);return FALSE;}// 设置复制的访问令牌的完整性级别为中等if (!SetTokenIntegrityLevel(hDupToken, SECURITY_MANDATORY_MEDIUM_RID)) {WriteError(_T("SetTokenIntegrityLevel failed"));CloseHandle(hToken);CloseHandle(hDupToken);return FALSE;}// 使用复制的访问令牌创建新的进程bResult = CreateProcessAsUser(hDupToken, NULL, (LPWSTR)lpCommandLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);if (!bResult) {WriteError(_T("CreateProcessAsUser failed with error = %d"), GetLastError());} else {CloseHandle(pi.hProcess);CloseHandle(pi.hThread);}// 关闭句柄CloseHandle(hToken);CloseHandle(hDupToken);return bResult;
}