Windows开发:服务程序启动有管理员权限的界面程序

简介

本章介绍Windows桌面开发中,服务程序如何启动有管理员权限的界面进程。

在这种情况下,以下几点需要弄清楚:

  • Windows的服务是什么

    Microsoft Windows 服务(过去称为 NT 服务)允许用户创建可在其自身的 Windows 会话中长时间运行的可执行应用程序。 这些服务可在计算机启动时自动启动,可以暂停和重启,并且不显示任何用户界面。 这些功能使服务非常适合在服务器上使用,或者需要长时间运行的功能(不会影响在同一台计算机上工作的其他用户)的情况。 还可以在与登录用户或默认计算机帐户不同的特定用户帐户的安全性上下文中运行服务。

    服务的特殊性有如下几点:

    • 服务通常在系统启动时用户登录系统之前由SCM(Service Control manager)加载,并在系统启动时自动开启的。

    • 服务属于system用户,system用户是windows操作系统下的最高权限账户,类似于Linux下的root。

      如下图,everything有一个服务,其服务进程是属于system用户的

      Windows开发:服务程序启动有管理员权限的界面程序_第1张图片
      Windows开发:服务程序启动有管理员权限的界面程序_第2张图片

    • 服务不显示任何用户界面

    • 服务在不同的登录用户下都可运行

  • 要在当前登录用户下启动界面程序

    服务程序是在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++能够显示界面并且能写入该目录下的文件。具体流程如下:

  1. 构造使系统在windows目录下写入text.txt需要特定权限。

    • 新建管理员组的用户。

      在我的电脑-》右键“管理”打开如下界面添加新用户

      Windows开发:服务程序启动有管理员权限的界面程序_第3张图片

      删除用户的Users分组,并添加到Administrators分组下。在新建的用户上右键“属性”,打开如下界面:

    Windows开发:服务程序启动有管理员权限的界面程序_第4张图片

    • 将UAC打开

      1. 按“Windows + R”键,打开运行"gpedit.msc",打开组策略编辑器,在该界面上“计算机配置”-》“Windows设置”-》“安全设置”-》“本地策略”-》“安全选项”-》“用户账户控制:以管理员批准模式运行所有管理员”。设置“启动”,点击确定。
      2. 同时确认UAC的选择不是“从不通知”,如下图是开启了UAC的情况,选择为从不通知,就关闭UAC。UAC界面打开流程:按Windows+R键,打开“运行”,输入"msconfig",弹出系统设置页面,在该页面点击“工具”-》更改UAC设置-》启动后可弹出UAC修改页面。

    Windows开发:服务程序启动有管理员权限的界面程序_第5张图片

    • 重启后,以xiaobai用户登录,然后用notepade++将不能写入c:/windows/test.txt文件。
  2. 测试

    • 在服务程序中加入以上调用代码即可测试。

    • 无服务程序可将调用写成一个命令行程序。然后使用PsExec启动命令行程序在system用户下启动测试。

      1. PsExec下载:PsExec - Windows Sysinternals | Microsoft Learn

      2. PSExec启动调试程序

        # PsExec64换成自己的对应目录
        # processtest 缓存自己的程序对应目录
        # -i 运行程序
        # -s 在系统帐户中运行远程进程
        D:\PSTools\PsExec64.exe -i -s D:\code\alltest\codetest\processtest\release\processtest.exe
        

你可能感兴趣的:(windows开发,windows,microsoft,c++,个人开发)