本章介绍Windows桌面开发中,服务程序如何启动有管理员权限的界面进程。
在这种情况下,以下几点需要弄清楚:
Windows的服务是什么
Microsoft Windows 服务(过去称为 NT 服务)允许用户创建可在其自身的 Windows 会话中长时间运行的可执行应用程序。 这些服务可在计算机启动时自动启动,可以暂停和重启,并且不显示任何用户界面。 这些功能使服务非常适合在服务器上使用,或者需要长时间运行的功能(不会影响在同一台计算机上工作的其他用户)的情况。 还可以在与登录用户或默认计算机帐户不同的特定用户帐户的安全性上下文中运行服务。
服务的特殊性有如下几点:
要在当前登录用户下启动界面程序
服务程序是在system用户下的,如果用普通方式启动进程,那启动的进程也在system用户下,也是不能展示界面的。需要特殊操作,将进程以当前登录用户启动进程,启动程序在当前用户下可展示界面。
服务程序启动有管理员权限的界面进程的过程:
GetCurrentUserToken获取到当前登录用户的受限token。
GetTokenInformation可以将受限token提升为未受限token。
需要注意,在未开启UAC的情况下,该函数会失败。GetLastError()返回1312,它在winerror.h中定义为ERROR_NO_SUCH_LOGON_SESSION,并描述为“指定的登录会话不存在。它可能已经被终止了。”。这种情况下直接使用受限token启动即可。
CreateProcessAsUser以当前用户启动进程。
完整代码如下:
#ifndef UITIL_H
#define UITIL_H
#ifdef WIN32
#include
#include
#include
#include
#include
#include
#endif
#include
class Uitil
{
public:
Uitil();
static std::wstring stringToWString(const std::string &string);
static HANDLE getCurrentUserToken();
static bool runProgAsCurUser(HANDLE token, const std::string &progPath, const std::string &progArgs);
static bool RunProgAsCurUserAdminPrivilege(const std::string &progPath, const std::string &progArgs);
};
#endif // UITIL_H
#include "uitil.h"
#include
#include
#include
Uitil::Uitil()
{
}
std::wstring Uitil::stringToWString(const std::string &string)
{
std::wstring_convert> cv;
return cv.from_bytes(string);
}
HANDLE Uitil::getCurrentUserToken()
{
// 查询sessionID
PWTS_SESSION_INFO pSessionInfo = 0;
DWORD dwCount = 0;
::WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSessionInfo, &dwCount);
int session_id = 0;
for (DWORD i = 0; i < dwCount; ++i)
{
WTS_SESSION_INFO si = pSessionInfo[i];
if (WTSActive == si.State)
{
session_id = si.SessionId;
break;
}
}
::WTSFreeMemory(pSessionInfo);
// 查询token
HANDLE current_token = 0;
BOOL bRet = ::WTSQueryUserToken(session_id, ¤t_token);
if (bRet == FALSE)
{
std::cout << "WTSQueryUserToken error, code:" << GetLastError() << std::endl;
return nullptr;
}
HANDLE primaryToken = 0;
bRet = ::DuplicateTokenEx(current_token, TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS, 0, SecurityImpersonation, TokenPrimary, &primaryToken);
::CloseHandle(current_token);
if (bRet == FALSE)
{
std::cout << "DuplicateTokenEx error, code:" << GetLastError() << std::endl;
return nullptr;
}
return primaryToken;
}
bool Uitil::runProgAsCurUser(HANDLE token, const std::string &progPath, const std::string &progArgs)
{
STARTUPINFO StartupInfo = {0};
PROCESS_INFORMATION processInfo;
StartupInfo.cb = sizeof(STARTUPINFO);
auto command = std::string("\"") + progPath + "\"";
if (!progArgs.empty())
{
command += " " + progArgs;
}
void* lpEnvironment = NULL;
BOOL resultEnv = ::CreateEnvironmentBlock(&lpEnvironment, token, FALSE);
if (!resultEnv)
{
std::cout << "CreateEnvironmentBlock error, code:" << GetLastError() << std::endl;
return false;
}
std::cout << "runProgAsCurUser, command:" << command << std::endl;
// 获取到的hUnfilteredToken就是不受限的token,以token作为CreateProcessAsUser的第一个参数,就可以创建出具有管理员权限,并且属于当前用户的界面程序了,并且这种情况下,不需要加入窗口站。
BOOL result = ::CreateProcessAsUser(token, 0, Uitil::stringToWString(command).data(), NULL, NULL, FALSE, CREATE_NEW_CONSOLE | NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT, lpEnvironment, 0, &StartupInfo, &processInfo);
if(!result)
{
std::cout << "CreateProcessAsUser error, code:" << GetLastError() << std::endl;
return false;
}
if(lpEnvironment != NULL)
{
::DestroyEnvironmentBlock(lpEnvironment);
}
return true;
}
bool Uitil::RunProgAsCurUserAdminPrivilege(const std::string &progPath, const std::string &progArgs)
{
// UAC开启时,当前用户拥有两个token,分别是受限的token和不受限的token。explorer.exe进程的token就属于受限的token。
// 在服务程序中,可以用下面代码获取到受限的token。
HANDLE primaryToken = getCurrentUserToken();
if (primaryToken == 0)
{
std::cout << "GetCurrentUserToken error." << std::endl;
return false;
}
// 由此token可以得到不受限的token
bool isOpenOk = false;
HANDLE hUnfilteredToken = NULL;
DWORD dwSize = 0;
BOOL bRet = ::GetTokenInformation(primaryToken, TokenLinkedToken, (VOID*)&hUnfilteredToken, sizeof(HANDLE), &dwSize);
if (bRet)
{
isOpenOk = runProgAsCurUser(hUnfilteredToken, progPath, progArgs);
::CloseHandle(hUnfilteredToken);
}
else
{
// UAC未开时,继续使用原来的token打开
std::cout << "GetTokenInformation error and continue open width primary token. code:" << GetLastError() << std::endl;
isOpenOk = runProgAsCurUser(primaryToken, progPath, progArgs);
}
::CloseHandle(primaryToken);
return isOpenOk;
}
# 链接库:user32.lib Userenv.lib Wtsapi32.lib Advapi32.lib
Uitil::RunProgAsCurUserAdminPrivilege("C:\\Program Files\\Notepad++\\notepad++.exe","");
构造测试环境,使notepad++写入c:/windows目录下的test.txt文件失败,但是通过服务程序拉起notepad++能够显示界面并且能写入该目录下的文件。具体流程如下:
构造使系统在windows目录下写入text.txt需要特定权限。
将UAC打开
测试
在服务程序中加入以上调用代码即可测试。
无服务程序可将调用写成一个命令行程序。然后使用PsExec启动命令行程序在system用户下启动测试。
PsExec下载:PsExec - Windows Sysinternals | Microsoft Learn
PSExec启动调试程序
# PsExec64换成自己的对应目录
# processtest 缓存自己的程序对应目录
# -i 运行程序
# -s 在系统帐户中运行远程进程
D:\PSTools\PsExec64.exe -i -s D:\code\alltest\codetest\processtest\release\processtest.exe