概述

围绕 Windows Token 的两种相反操作: 提权 —— 启用进程特权(SeDebugPrivilege 等)+ 获取目标用户令牌; 降权 —— 在 SYSTEM 服务中以普通用户权限启动进程。

两者常组合使用:先在服务中提权取得 Explorer 令牌,再以降权方式启动安装程序,避免安装程序继承 SYSTEM 权限。

相关文章:


背景:为什么需要提权 + 降权

典型场景:服务程序(以 SYSTEM 权限运行)调用某个安装程序。由于权限继承,安装程序也继承了 SYSTEM 权限,导致行为与预期不符(如安装到错误位置、UAC 弹窗、写入了受保护目录)。

目标:在服务中以普通用户权限启动安装程序。

实现分两步:

  1. 提权:开启本进程的调试特权(SeDebugPrivilege),才能打开 explorer.exe 并获取其令牌(GetExplorerToken
  2. 降权:用 Explorer 的令牌(普通用户身份)通过 CreateProcessAsUser 启动安装程序
服务进程 (SYSTEM)
  │  ① 提权:EnablePrivilege(SE_DEBUG_NAME)
  │        → GetExplorerToken() 获取 Explorer 令牌
  ▼
Explorer 令牌 (普通用户)
  │  ② 降权:TokenSessionId + CreateProcessAsUser()
  ▼
安装程序 (普通用户权限,不再继承 SYSTEM)

一、提权:启用进程特权

要访问系统进程(如 explorer.exe)的令牌,需要先开启相应特权。下面提供三种写法,任选其一。

1.1 通用版(可指定任意特权)

C++cpp
#include <windows.h>
 
BOOL EnablePrivilege(LPCTSTR lpszPrivilegeName, BOOL bEnable)
{
    BOOL  bResult = FALSE;
    BOOL  bRetCode = FALSE;
    HANDLE hToken = NULL;
    TOKEN_PRIVILEGES tkp = { 0 };
 
    do
    {
        bRetCode = ::OpenProcessToken(GetCurrentProcess(),
            TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
        if (!bRetCode)
            break;
 
        bRetCode = ::LookupPrivilegeValue(NULL, lpszPrivilegeName, &tkp.Privileges[0].Luid);
        if (!bRetCode)
            break;
 
        tkp.PrivilegeCount = 1;
        tkp.Privileges[0].Attributes = bEnable ? SE_PRIVILEGE_ENABLED : 0;
        bRetCode = ::AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof(tkp), NULL, NULL);
        if (!bRetCode)
            break;
 
        bResult = TRUE;
    } while (FALSE);
 
    if (hToken != NULL)
        CloseHandle(hToken);
 
    return bResult;
}

使用:

C++cpp
// 开启调试特权(访问其他进程令牌的前提)
EnablePrivilege(SE_DEBUG_NAME, TRUE);

注:也可用 PromotePrivilege()(等价实现,见下文降权代码)开启 SE_DEBUG_NAME

1.2 调试特权专用版

C++cpp
BOOL EnableDebugPrivilege()
{
    HANDLE hToken;
    BOOL fOk = FALSE;
    if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
    {
        TOKEN_PRIVILEGES tp;
        tp.PrivilegeCount = 1;
        LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
 
        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
        AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
 
        fOk = (GetLastError() == ERROR_SUCCESS);
        CloseHandle(hToken);
    }
    return fOk;
}

1.3 入参形式(带错误输出)

C++cpp
BOOL EnableXXXPrivilege(LPCTSTR pszPrivilegeName)
{
    HANDLE hToken;
    LUID seXXXNameValue;
    TOKEN_PRIVILEGES tkp;
 
    if (!OpenProcessToken(GetCurrentProcess(),
        TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
    {
        MYTRACE(L"OpenProcessToken() failed, Error = %d  %s is not available.\n",
            GetLastError(), pszPrivilegeName);
        return FALSE;
    }
 
    if (!LookupPrivilegeValue(NULL, pszPrivilegeName, &seXXXNameValue))
    {
        MYTRACE(L"LookupPrivilegeValue() failed, Error = %d %s is not available.\n",
            GetLastError(), pszPrivilegeName);
        CloseHandle(hToken);
        return FALSE;
    }
 
    tkp.PrivilegeCount = 1;
    tkp.Privileges[0].Luid = seXXXNameValue;
    tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
 
    if (!AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof tkp, NULL, NULL))
    {
        MYTRACE(L"AdjustTokenPrivileges() failed, Error = %d %s is not available.\n",
            GetLastError(), pszPrivilegeName);
        CloseHandle(hToken);
        return FALSE;
    }
 
    CloseHandle(hToken);
    return TRUE;
}

提权后的典型使用:读取用户配置文件

C++cpp
HANDLE hExplorerToken = GetExplorerToken();
if (hExplorerToken == NULL)
    return;
 
char szUserProfilePath[MAX_PATH] = { 0 };
DWORD cchSize = MAX_PATH;
if (!GetUserProfileDirectoryA(hExplorerToken, szUserProfilePath, &cchSize))
{
    CloseHandle(hExplorerToken);
    return;
}
// 使用 szUserProfilePath...
CloseHandle(hExplorerToken);

二、获取 Explorer 令牌

GetExplorerToken 通过进程快照枚举 explorer.exe,打开其进程句柄并取出令牌。它是”提权”与”降权”共用的基础函数。

C++cpp
#include <windows.h>
#include <tlhelp32.h>
#include <shlwapi.h>
 
HANDLE GetExplorerToken()
{
    EnablePrivilege(SE_DEBUG_NAME, TRUE);   // 提权:否则无法 OpenProcess
 
    HANDLE hSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnapshot == INVALID_HANDLE_VALUE)
    {
        return NULL;
    }
 
    HANDLE hExplorerToken = NULL;
    PROCESSENTRY32 pe = { 0 };
    pe.dwSize = sizeof(pe);
 
    BOOL bMore = ::Process32First(hSnapshot, &pe);
    while (bMore)
    {
        if (_tcsicmp(_T("explorer.exe"), pe.szExeFile) == 0)
        {
            HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pe.th32ProcessID);
            if (hProcess == NULL)
                continue;
 
            if (OpenProcessToken(hProcess, TOKEN_QUERY, &hExplorerToken))
            {
                CloseHandle(hProcess);
                break;
            }
            CloseHandle(hProcess);
        }
        bMore = ::Process32Next(hSnapshot, &pe);
    }
    CloseHandle(hSnapshot);
 
    return hExplorerToken;
}

注意:多用户(RDP 多会话)环境下,任务管理器里可能出现多个 explorer.exe。此时应结合 WTSGetActiveConsoleSessionId() 筛选当前会话的进程,避免取到其他会话的令牌。完整筛选代码见 Service降权启动进程


三、降权:以普通用户权限启动进程

3.1 实现逻辑

1. GetExplorerToken()          获取 Explorer(普通用户)令牌
2. GetTokenInformation(TokenSessionId)  通过令牌获取用户会话 ID
3. CreateProcessAsUser()       通过令牌 + 会话 ID 启动进程

此逻辑也可用于解决 UAC 弹窗问题。

PlantUML diagram

3.2 接口封装

C++cpp
int ExecutePackage(LPCSTR pszPath, LPCSTR pszParams)
{
    if (nullptr == pszPath)
        return -1;
 
    wstring szPath = stringToWstring(pszPath);
 
    wstring szParams;
    if (pszParams)
    {
        szParams = stringToWstring(pszParams);
    }
 
    CreateProcessWithAdmin(szPath.c_str(), szParams.c_str());
    return 0;
}

3.3 主实现:CreateProcessWithAdmin

C++cpp
bool CreateProcessWithAdmin(LPCWSTR lpExePath, LPCWSTR lpParam)
{
    HANDLE hExplorerToken = GetExplorerToken();
    HANDLE hTokenDup = NULL;
    LPVOID pEnvironment = nullptr;
    bool res{ false };
 
    char szErr[256] = { 0 };
    int  iErrCode = 0;
    do
    {
        if (hExplorerToken == NULL)
        {
            iErrCode = GetLastError();
            break;
        }
 
        DWORD dwReturnBytes = 0;
        DWORD dwReturnLen = 0;
        DWORD dwTokenSessionId = 0;
 
        // 通过 token 获取 sessionId
        if (::GetTokenInformation(hExplorerToken, TokenSessionId,
                &dwTokenSessionId, sizeof(DWORD), &dwReturnLen) == FALSE)
        {
            break;
        }
 
        // 判断当前令牌是否已提升:
        //   已提升 → 直接用提升后的令牌启动
        //   未提升 → 走 CreateProcessAsSystemBySession(SYSTEM → 指定会话)
        HANDLE hNewToken = NULL;
        if (GetElevatedToken(hExplorerToken, &hNewToken) && hNewToken)
        {
            res = CreateProcessByToken(hNewToken, hExplorerToken,
                lpExePath, lpParam, NULL, FALSE);
            CloseHandle(hNewToken);
        }
        else
        {
            res = CreateProcessAsSystemBySession(lpExePath, lpParam,
                NULL, dwTokenSessionId, hExplorerToken);
        }
        CloseHandle(hExplorerToken);
 
    } while (false);
 
    return res;
}

3.4 判断 / 获取提升后的令牌

C++cpp
BOOL IsElevatedToken(HANDLE hToken, PBOOL pbElevated)
{
    DWORD dwReturnBytes = 0;
    DWORD dwElevationType = 0;
    BOOL bElevated = FALSE;
 
    if (hToken && pbElevated)
    {
        if (GetTokenInformation(hToken, TokenElevationType, &dwElevationType, sizeof(dwElevationType), &dwReturnBytes))
        {
            if (dwElevationType == TokenElevationTypeFull)
                bElevated = TRUE;
            else if (dwElevationType == TokenElevationTypeDefault)
            {
                TOKEN_ELEVATION te;
                ZeroMemory(&te, sizeof(te));
 
                if (GetTokenInformation(hToken, TokenElevation, &te, sizeof(te), &dwReturnBytes))
                {
                    if (te.TokenIsElevated)
                        bElevated = TRUE;
                }
            }
        }
 
        if (pbElevated)
            *pbElevated = bElevated;
 
        return TRUE;
    }
 
    return FALSE;
}
 
BOOL GetElevatedToken(HANDLE hToken, PHANDLE phNewToken)
{
    BOOL bElevated = FALSE;
 
    IsElevatedToken(hToken, &bElevated);
 
    if (bElevated)
    {
        // 已提升:直接复制为主令牌
        return DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, NULL,
            SecurityImpersonation, TokenPrimary, phNewToken);
    }
    else
    {
        // 未提升(filtered token):获取 linked token(完整管理员令牌)
        DWORD dwReturnBytes = 0;
        return GetTokenInformation(hToken, TokenLinkedToken, phNewToken,
            sizeof(HANDLE), &dwReturnBytes);
    }
}

3.5 通过会话 ID 以 SYSTEM 创建进程

C++cpp
bool CreateProcessAsSystemBySession(LPCTSTR pszAppName, LPCTSTR pszCmd,
    LPCTSTR pszCwd, DWORD dwSession, HANDLE hEnvToken)
{
    if (pszCmd == NULL)
        return false;
 
    bool bRet = false;
 
    HANDLE hTokenThis = NULL;
    bRet = OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE | TOKEN_QUERY, &hTokenThis);
    if (!bRet || hTokenThis == NULL)
        return false;
 
    HANDLE hTokenDup = NULL;
    bRet = DuplicateTokenEx(hTokenThis, MAXIMUM_ALLOWED, NULL,
        SecurityIdentification, TokenPrimary, &hTokenDup);
    CloseHandle(hTokenThis);
    if (!bRet || hTokenDup == NULL)
        return false;
 
    // 把 SYSTEM 令牌放入指定会话
    if (!SetTokenInformation(hTokenDup, TokenSessionId, &dwSession, sizeof(DWORD)))
    {
        CloseHandle(hTokenDup);
        return false;
    }
 
    bRet = CreateProcessByToken(hTokenDup, hEnvToken, pszAppName, pszCmd, pszCwd, TRUE);
    CloseHandle(hTokenDup);
 
    return bRet;
}

3.6 最终调用:CreateProcessByToken

C++cpp
typedef BOOL(STDMETHODCALLTYPE FAR* LPFNCREATEENVIRONMENTBLOCK)(LPVOID* lpEnvironment, HANDLE hToken, BOOL bInherit);
typedef BOOL(STDMETHODCALLTYPE FAR* LPFNDESTROYENVIRONMENTBLOCK)(LPVOID lpEnvironment);
 
bool CreateProcessByToken(HANDLE hToken, HANDLE hEnvToken, LPCTSTR pszAppName,
    LPCTSTR pszCmd, LPCTSTR pszCwd, BOOL bWndHide /* = FALSE */)
{
    STARTUPINFO si = { sizeof(si) };
    PROCESS_INFORMATION pi = { 0 };
 
    si.lpDesktop = (LPWSTR)L"Winsta0\\Default";
 
    if (bWndHide)
    {
        si.dwFlags = STARTF_USESHOWWINDOW;
        si.wShowWindow = SW_HIDE;
    }
    DWORD dwCreationFlag = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;
    LPVOID pEnvironment = NULL;
    LPFNCREATEENVIRONMENTBLOCK  lpfnCreateEnvironmentBlock = NULL;
    LPFNDESTROYENVIRONMENTBLOCK lpfnDestroyEnvironmentBlock = NULL;
    HMODULE hUserEnvLib = NULL;
    hUserEnvLib = LoadLibrary(L"userenv.dll");
    if (NULL != hUserEnvLib)
    {
        lpfnCreateEnvironmentBlock = (LPFNCREATEENVIRONMENTBLOCK)GetProcAddress(hUserEnvLib, "CreateEnvironmentBlock");
        lpfnDestroyEnvironmentBlock = (LPFNDESTROYENVIRONMENTBLOCK)GetProcAddress(hUserEnvLib, "DestroyEnvironmentBlock");
    }
 
    if (NULL != lpfnCreateEnvironmentBlock)
    {
        if (lpfnCreateEnvironmentBlock(&pEnvironment, hEnvToken, FALSE))
        {
            dwCreationFlag |= CREATE_UNICODE_ENVIRONMENT; // must specify
        }
        else
            pEnvironment = NULL;
    }
 
    bool bRet = false;
 
    if (CreateProcessAsUser(hToken, pszAppName, (LPTSTR)pszCmd, NULL, NULL,
            FALSE, dwCreationFlag, pEnvironment, pszCwd, &si, &pi))
    {
        CloseHandle(pi.hThread);
 
        DWORD dwRet = WaitForSingleObject(pi.hProcess, 2 * 60 * 60 * 1000);
        if (WAIT_TIMEOUT == dwRet)
        {
            // 超时,可做相应处理
        }
        else if (WAIT_OBJECT_0 == dwRet)
        {
            bRet = true;
        }
        CloseHandle(pi.hProcess);
    }
 
    if (NULL != lpfnDestroyEnvironmentBlock)
        lpfnDestroyEnvironmentBlock(pEnvironment);
    if (NULL != hUserEnvLib)
        FreeLibrary(hUserEnvLib);
 
    return bRet;
}

3.7 给本进程提权(PromotePrivilege)

降权前需先为本进程开启调试特权,才能访问 explorer.exe。此函数是 EnablePrivilege(SE_DEBUG_NAME, TRUE) 的等价实现。

C++cpp
/*
* @brief    调整进程权限,将进程权限提升为具有调试权限的进程
* @detail   前提:启动该进程的账户必须是管理员,否则无法提升
*/
BOOL PromotePrivilege()
{
    HANDLE hToken;
    // 打开进程的访问令牌
    if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
    {
        // 取得特权名称为 SE_DEBUG_NAME 的 LUID
        LUID uID;
        if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &uID))
        {
            CloseHandle(hToken);
            return FALSE;
        }
        TOKEN_PRIVILEGES tp;
        tp.PrivilegeCount = 1;              // 只开启调试权限
        tp.Privileges[0].Luid = uID;
        // Attributes = SE_PRIVILEGE_ENABLED 激活权限;= 0 关闭权限
        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
 
        if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL))
        {
            CloseHandle(hToken);
            return FALSE;
        }
        CloseHandle(hToken);
        return TRUE;
    }
    return FALSE;
}

四、提权 vs 降权 对比

方向目标核心 API典型场景
提权开启特权、获取高权限令牌OpenProcessTokenLookupPrivilegeValueAdjustTokenPrivileges访问系统进程令牌、Token窃取与利用
降权以低权限(普通用户)启动进程GetExplorerTokenDuplicateTokenExCreateProcessAsUser服务中启动安装程序,避免继承 SYSTEM

两者常串联使用:

提权(EnablePrivilege → GetExplorerToken)
   → 降权(CreateProcessAsUser)
   → 目标进程以普通用户身份运行

相关推荐