VC++如何创建低权限的标准用户权限进程

Posted dvlinker

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了VC++如何创建低权限的标准用户权限进程相关的知识,希望对你有一定的参考价值。

目录

1、程序的运行权限

2、为啥需要创建一个低权限的标准用户权限进程

3、从Process Explorer工具入手,找到创建低权限进程的线索

4、Windows安全框架

4.1 安全对象

4.2 安全描述符

4.3 安全标识

4.4 访问令牌

4.5 访问控制列表及其访问控制项

4.6 线程和安全对象间的交互

4.7 完整性级别

5、创建低权限进程的代码实现


       Windows自Vista开始就引入了UAC用户账户控制的概念,对程序运行权限有着严格的控制,这给众多Windows软件开发人员带来一些负担和困扰。本文来讲述一类与进程运行权限有关的特殊问题,即如何创建低权限的标准用户权限进程。

1、程序的运行权限

       程序的运行权限主要有两种,一种是低权限的标准用户权限,一种是高权限的管理员权限。对于操作系统关键路径(比如C:\\Windows、C:\\Windows\\System32等)、操作HKEY_LOCAL_MACHINE路径下的注册表项、向系统注册控件等,都需要管理员权限才能完成,没有管理员权限会操作失败。一般情况下,安装包程序都需要管理员权限,因为安装包可能会操作系统关键路径、写注册表、向系统注册控件等。

       让程序以管理员权限运行,有多种方法:

1)右键以管理员身份运行;

2)在Visual Studio的工程属性中给进程设置requireAdministrator,指定程序以管理员权限运行;

3)可以给进程添加manifest文件,在manifest文件中设置requireAdministrator,指定程序以管理员权限运行;

4)在代码中调用API函数ShellExecuteEx,传入runas参数,以管理员权限启动目标进程。

上述4种方法的具体细节就不再赘述了,如需要了解,自行搜索一下即可。

       除上述要点之外,程序到底是以管理员权限运行,还是以标准用户运行,还和当前登录的用户类型是有关系的。用户类型主要有普通用户管理员用户超级管理员Administrator。用户类型对程序运行程序的影响如下:

1)普通用户(标准用户)登录时,程序默认是以标准用户权限运行。如果要运行设置了管理员权限运行的程序,则会UAC提权的提示框,需要输入管理员的用户和密码才能运行。

2)管理员用户登录时,要看UAC用户账户控制是否关闭:

i)UAC开关打开:如果程序没有指定以管理员权限运行,则默认是以标准用户权限运行;如果指定以管理员权限运行,则可以直接运行程序。

ii)UAC开关关闭:所有程序默认都以管理员权限运行。

3)超级管理员Administrator登录时,所有程序均以管理员权限运行。

2、为啥需要创建一个低权限的标准用户权限进程

       我们开发的软件在运行时遇到的一个问题,就和进程运行的权限有关系。我们的软件只允许运行一个进程实例,如果再次启动程序(比如通过双击桌面快捷方式去启动程序),则需要将已经运行的进程的主窗口弹出来或拉到最前面显示,不允许运行第二个进程。

       为了实现软件的单实例运行,我们会先给软件的主窗口,设置一个Property属性串,当启动本软件时,会检测软件是否已经运行,如果已经运行,则去遍历桌面上的所有窗口,通过窗口的Property属性串找到找到已运行进程的主窗口,调用ShowWindow将窗口show出来,并调用SetForegroundWindow将窗口拉到最前面显示,代码如下:

CString strLog;

BOOL bFind = FALSE;
HWND hPrevWnd = ::GetDesktopWindow();
hPrevWnd = ::GetWindow(hPrevWnd, GW_CHILD);
while (hPrevWnd)

    if (::GetProp(hPrevWnd, STRING_TARGET_PROGRAM_PROPERTY_NAME))
    
        bFind = TRUE;
        WriteLog(_T("[CheckShowExsitingWnd] 找到目标窗口."));

        // 窗口处于最小化或掩藏状态,则直接将其显示出来
        if (::IsIconic(hPrevWnd))
        
            WriteLog(_T("[CheckShowExsitingWnd] IsIconic返回TRUE,将窗口显示出来."));
            ::ShowWindow(hPrevWnd, SW_RESTORE);
        


        if (!::IsWindowVisible(hPrevWnd))
        
            SetLastError(0);
            ::ShowWindow(hPrevWnd, SW_RESTORE);

            // 可能会show失败,将lasterror打印出来
            strLog.Format(_T("[CheckShowExsitingWnd] IsWindowVisible返回FALSE,将窗口显示出来,LastError: %d."), GetLastError());
            WriteLog(strLog);
        

        // 再尝试show一次
        SetLastError(0);
        ShowWindow(hPrevWnd, SW_SHOWNORMAL);
        strLog.Format(_T("[CheckShowExsitingWnd] 再show一次,LastError: %d."), 
        GetLastError());
        WriteLog(strLog);

        SetForegroundWindow(hPrevWnd);
    

    hPrevWnd = ::GetWindow(hPrevWnd, GW_HWNDNEXT);

结果在部分用户的电脑上,并没有将已经运行的程序主窗口弹出来。 通过打印出来的日志发现,在调用ShowWindow时居然失败了,获取的lasterror值为5,即Access Denied访问被拒绝的错误。

       这个着实有点奇怪,这段代码很通用的,xp时代就有了,咋在win10系统上就出问题了呢。按讲是可以在一个进程中去操作另一个进程的窗口,可为啥会出现操作失败的情况呢?经过一番思考和推敲,怀疑可能是两个进程的权限不对等导致的。经研究找到了具体的原因,出问题的用户那边执行了软件最新的安装包更新了软件版本,在安装完成后会自动将软件启动起来,因为安装包是以管理员权限启动的,所以启动起来的软件也是有管理员权限的。而用户双击桌面快捷方式启动起来的新的进程是没有管理员权限的,是以标准用户权限运行的。我们的软件没有设置管理员权限,默认是以标准用户权限运行的。如果软件设置了以管理员权限运行,在系统UAC开关打开的情况下,每次启动时都会弹出UAC提示框,用户的体验会很差。

       所以导致该问题的根本原因在于,安装包启动的进程是有管理员权限的,而通过桌面快捷方式启动的新进程是以标准用户权限运行的,在低权限的进程中去操作高权限进程的窗口,会因为进程权限不对等导致操作失败,所以出现了Access Denied的问题了。

       我们对比了一款腾讯的主流软件,他们也实现了打开已运行程序的窗口的功能,他们软件并没有这样的问题,难道他们实现的机制和我们的不同?后来使用我们自己实现的一个判断某个进程是否有管理员权限的工具发现,腾讯这款软件的安装包启动起来的进程居然是以标准用户运行的,这样就不存在进程的权限不对等的问题了!在我们的认知中,以管理员权限运行的进程中启动的子进程都是有管理员权限的, 腾讯的软件怎么做到在以管理员权限运行的进程中创建一个以低权限(标准用户权限)运行的进程的呢?于是我们也想研究下这个问题,所以也就引出了本篇文章的主题。

3、从Process Explorer工具入手,找到创建低权限进程的线索

       提到Mark Russinovich,很多Windows程序员应该都比较了解。Mark Russinovich是一位世界顶级的软件开发大神,Windows系统内核大师,现任微软Azure云计算平台的首席技术主管。Mark Russinovich主持编写了最受瞩目的Windows技术权威参考图书《深入解析Windows操作系统(Windows Internals)》,深入剖析了Windows技术内幕及实现细节。Mark Russinovich还开发了Windows历史上鼎鼎大名的系统工具集Sysinternals,该工具集合中包含了多个实用工具,比如我们日常工作中常用的Process ExplorerProcess MonitorTCPView等。

       其中Process Explorer工具类似于Windows系统的任务管理器,但其功能比Windows的任务管理器要强大的多。使用Process Explorer可以查看到目标进程加载了哪些库文件以及这些库的路径,可以查看到进程中各个线程的CPU占用情况,还可以看到线程中的函数调用堆栈,可以看到进程对GPU的占用情况。正因为使用该工具可以查看到线程的CPU占用情况和线程的函数调用堆栈,我们经常使用该工具来辅助排查进程CPU占用高的问题。

      发散的有点多了,我们回到本文的主题上来。Process Explorer工具中提供了一个“Run  as Limited User”菜单项:(点击菜单栏File->Run  as Limited User...)

这个地方的Limited User,就是我们将的标准用户,所以Run  as Limited User就是以标准用户运行程序。于是想研究一下该工具是如何创建一个标准用户的进程的,经过一番搜索,找到了Mark Russinovich写过的一篇专题文章:

Running as Limited User - the Easy Wayhttps://docs.microsoft.com/en-us/archive/blogs/markrussinovich/running-as-limited-user-the-easy-way文章阐述了如何创建一个以标准用户权限运行的进程,其中一段话为:

use the CreateRestrictedToken API to create a security context, called a token, that’s a stripped-down version of its own, removing administrative privileges and group membership. After generating a token that looks like one that Windows assigns to standard users Process Explorer calls CreateProcessAsUser to launch the target process with the new token.

使用系统API函数CreateRestrictedToken来创建一个安全上下文,降低令牌(Token)的管理员权限及组成员资格,使其创建的令牌看起来像Windows赋予普通用户时一样,然后使用此令牌作为传入参数调用CreateProcessAsUser去创建新的子进程。

       此外还有一个细节:

Process Explorer queries the privileges assigned to the Users group and strips out all other privileges, including powerful ones like SeDebugPrivilege, SeLoadDriverPrivilege and SeRestorePrivilege.

Process Explorer去查询赋予用户组的特权并从当前进程权限中剔除这些权限,比如SeDebugPrivilege、SeLoadDriverPrivilege和SeRestorePrivilege等。

4、Windows安全框架

       Windows的安全架构主要是建立在Windows访问控制模型ACM(Access Control Model)基础上的,而Windows访问控制模型ACM是个复杂的概念,要搞清楚,必须了解Token访问令牌,ACL访问控制列表(Access Control List),DACL选择访问控制列表(Discretionary Access Control List),ACE访问控制列表项(Access Control Entries)等与访问控制模型相关的名词含义及之间的关系。 

       ACM访问控制模型中最重要的两部分是访问令牌(Access Token)和安全描述符表(Security Descriptor)。访问令牌存在于访问主体中,安全描述符表存在于访问客体中。比如我要去意大利,我就是访问主体,意大利就是访问客体,我持有的签证就是访问令牌。

       系统中访问主体是进程客体是一切系统对象。访问令牌中有当前用户的唯一标识SID,组唯一标识SID以及一些权限标志(Privilege)。安全描述符表(SD)存在于Windows系统中的任何对象中(文件,注册表,互斥量,信号量等等)。SD中包含对象所有者的SID,组SID以及两个非常重要的数据结构选择访问控制列表(DACL)和系统访问控制列表(SACL),其中SACL涉及系统日志用的很少可以先无视。DACL中包含一个个ACE访问控制入口也是权限访问判断的核心,当一个进程访问某一对象的时候,对象会将进程的Token与自身的ACE依次比对,直到被允许或被拒绝,前面的ACE优于后面的ACE。

       下面我们就来逐一介绍这些概念。

4.1 安全对象

       有资格拥有安全描述符的对象如文件、管道、进程、进程间同步对象等。所有已命名的Windows对象都是安全的,那些未被命名的对象比如线程或进程对象也可以拥有安全描述符。

       对于大多数的安全对象,当创建该对象时可以指定它的安全描述符。当一个安全对象被创建时,系统会对其赋予一个安全描述符,安全描述符包含由其创建者指定的安全信息,或者缺省的安全信息(如果没有特意进行指定的话)。

       应用程序可以使特定的函数对已有的对象进行操作以来获取和设置安全信息。每种类型的安全对象定义了它自身的访问权限和自身映射的通用访问权限。

      关于安全对象的详细说明,可以参见微软的页面:

Securable Objectshttps://docs.microsoft.com/en-us/windows/win32/secauthz/securable-objects?redirectedfrom=MSDN

4.2 安全描述符

       在 Windows 中,每个安全对象都有一个安全描述符(Security Descriptor,SD)。它有助于控制对对象的访问,它包含所有者的信息、要审核的内容以及以何种方式授予访问权限。它也包含了真正的用于设置安全权限的 ACL访问控制列表,ACL主要包括:

1)自由访问控制列表(DACL):指明特定用户或组对该对象的访问是允许或拒绝;

2)安全访问控制列表(SACL):控制系统审计如何访问对象。

4.3 安全标识

       安全标识SID(Security Identifiers)是用来标识信任方的唯一的数值,其长度可变。而信任方就是用户,组,会话。所以基本上可以把SID理解为是一个用户名,一个组名,会话名。它们是经过安全认证,且不会重复,也就是安全可靠的。

       安全标识主要运用于如下几个方面:

1)在安全描述符中定义对象的所有者和基本组;

2)在访问控制项中定义托管的权限是允许、拒绝或是审计;

3)在访问令牌中定义用户和用户所在的组。

4.4 访问令牌

       访问令牌Token包含登录用户的信息。用来描述一个进程或线程的安全上下文的对象,令牌的信息包含关联到进程或线程的账号的标识和特权。

       当一个用户登录时,系统对用户的账号和密码进行认证,如果登录成功,系统则创建一个访问令牌,每个进程运行时都有一个访问令牌代表当前的用户,访问令牌中的安全描述符指明当前用户所属的账号和所属的组账号,令牌也包含一系列由用户或用户所在组进行维护的权限,在一个进程试图进行访问安全对象或执行系统管理员任务过程中需要权限时,系统通过这个令牌来确认关联的用户。

4.5 访问控制列表及其访问控制项

       自由访问控制列表(DACL)包含若干个访问控制项(ACEs)。约定的执行规则如下:

1)如果对象没有自由访问控制列表(DACL),则任何用户对其均有完全的访问权限;

2)如果对象拥有DACL,那么系统仅允许那些在访问控制项(ACE)显式指明的访问权限;

3)如果在访问控制列表(DACL)中不存在访问控制项(ACE),那么系统不允许任何用户对其进行访问;

4) 如果访问控制列表中的访问控制项对准许访问的用户或组数目有限,那么系统会隐式拒绝那些不在访问控制项中的其他托管的访问

       需要注意的是访问控制项的排序很重要。因为系统按照队列的方式读取访问控制项,直到访问被拒绝或允许。用户的访问拒绝ACE必须放在访问允许ACE的前头,否则当系统读到对组的访问允许ACE时,它会给当前限制的用户赋予访问的权限。系统在检测到请求访问被允许或拒绝后就不再往下检查。

       你可以通过标识允许访问的ACE来控制对对象的访问,你无需显式地拒绝一个对象的访问。

4.6 线程和安全对象间的交互

       当一个线程想要使用一个安全对象时,系统在线程执行前会进行访问审核,在访问审核中,系统将线程访问令牌中的安全信息与对象安全描述符中的安全信息进行比对。

       访问令牌中包含的安全标识(SIDs)可以指明与线程关联的用户,系统查看线程访问令牌中用户或组的SID,同时检查对象的自由访问控制列表(DACL),自由访问控制列表(DACL)中包含存储有指明对指定的用户或组的访问权限是允许或拒绝信息的访问控制项(ACE),系统检查每个访问控制项(ACE)直至出现指明针对此线程(的用户或组的SID)的访问权限是允许还是拒绝的ACE,或者到最终都没有对应的ACEs可以检查。

       系统按照序列检查每个ACE,查询ACE中的托管与定义在线程中的托管(根据托管的SID)一致的ACE,直到如下的情况出现:

1)表明访问拒绝的ACE显式拒绝在线程的访问令牌中的一个托管的任何访问权限;

2)一个或多个表明访问允许的ACEs显式地为线程访问令牌中的托管提供所有访问权限;

3)所有ACEs已经比对审核完但至少有一个请求访问权限没有显式允许,这种情况下该访问权限则被隐式拒绝。

       一个访问控制列表(ACL)可以有多个的访问控制项(ACE)针对令牌的(同一个)SIDs,这种情况下每个ACE授予的访问权限是可以进行累积叠加,比如,如果一个ACE对一个组允许读的访问权限,而另一个ACE对该组内的一个用户允许写的访问权限,那么该用户对于当前对象就拥有了读和写的访问权限。

       如上图所示,对于线程A,尽管在ACE@3中允许写权限,但因为在ACE@1中已经显式拒绝“Andrew”用户的访问权限,所以该安全对象对于线程A是不可访问的;对于线程B,在ACE@2中显式指明A组用户可以有写的权限,并且在ACE@3中允许任何用户读和执行的权限,所以线程B对这个安全对象拥有读、写以及执行的权限。

       以上内容参考了微软的页面:

Interaction Between Threads and Securable Objectshttps://docs.microsoft.com/en-us/windows/win32/secauthz/interaction-between-threads-and-securable-objects?redirectedfrom=MSDN

4.7 完整性级别

       Windows完整性机制是对Windows安全架构的扩展,该完整性机制通过添加完整性等级到安全访问令牌和添加强制性标记访问控制项到安全描述符中的系统访问控制列表(SACL)。进程在安全访问令牌中定义完整性等级,IE在保护模式下的完整性等级为低,从开始菜单运行的应用程序的等级为中等,需要管理员权限并以管理员权限运行的应用程序的等级为高。

       保护模式能够有效地减少IE进程附带的攻击行为如篡改和摧毁数据、安装恶意程序;相比其他程序,连接网络的程序更容易遭受网络的攻击因为它们更可能从未知源地址下载未受信任的内容,通过降低完整性等级或限制对其的允许权限,可以减少篡改系统或污染用户数据文件的风险。

       在系统访问控制列表(SACL)中有一个称为强制标识的访问控制项(ACE),该控制项的安全描述符定义完整性等级或允许访问当前对象需要达到的等级,安全对象如果没有该控制项则默认拥有中等的完整性等级;即使用户在自由访问控制列表(DACL)中已经明确授予相应的写权限,低等级的进程也不能获取比其高等级的安全对象的写权限,完整性等级检验在用户访问权限审查之前完成。

        所有的文件和注册表键在缺省下的完整性等级为中,而由低等完整性进程创建的安全对象,系统会自动地赋予其低等完整性强制标志,同样,由低等完整性进程创建的子进程也是在低完整性等级下运行。

完整性访问等级(IL)

系统权限

安全标识

System

System

S-1-16-16384

High

Administrative

S-1-16-12288

可安装文件到Program Files文件夹、往敏感的注册表中如HKEY_LOCAL_MACHINE写数据

Medium

User

S-1-16-8192

创建和修改用户文档中的文件、往特定用户的注册表如HKEY_CURRENT_USER中写数据

Low

Untrusted

S-1-16-4096

仅能往低等级位置写数据如临时网络文件夹和注册表

HKEY_CURRENT_USER\\Software\\LowRegistry

       低完整性进程可以往用户存档文件下写文件,通常为%USER PROFILE%\\AppData\\LocalLow,可以通过调用SHGetKnownFolderPath 函数并传入FOLDERID_LocalAppDataLow参数来获取完整的路径名称:

SHGetKnownFolderPath(FOLDERID_LocalAppDataLow, 0, NULL, szPath, ARRAYSIZE(szPath));

      同样低完整性进程可以往指定的注册表下创建和修改子键,该注册表路径通常为HKEY_CURRENT_USER\\Software\\AppDataLow

       以上内容参考了微软的相关页面:

Windows Integrity Mechanism Designhttps://docs.microsoft.com/en-us/previous-versions/dotnet/articles/bb625963(v=msdn.10)?redirectedfrom=MSDN

5、创建低权限进程的代码实现

        创建低权限(标准用户权限)进程的步骤如下:

1)创建新的普通用户组和系统管理员的安全描述符标识;

2)获取当前进程的令牌,并根据令牌句柄获取当前进程所拥有的特权;

3)通过已创建的普通用户组的安全描述符标识获取普通用户组所拥有的特权;

4)给当前进程令牌中的管理员安全描述符添加Deny-only属性,以此达到避免新创建的进程以管理员作为其所有者;

5)从当前进程拥有的特权中剔除普通用户组所没有的特权;

6)从新的受限令牌中复制为模拟令牌;

7)将模拟令牌的完整性特权设为低级,以限制新创建的进程对普通文档、可执行程序的写、执行等访问权限;

        具体的代码如下:

void CreateRestrictedProcess()

    SECURITY_ATTRIBUTES sa;
    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
    sa.bInheritHandle = TRUE;
    sa.lpSecurityDescriptor = NULL;

    TCHAR szCmdLine[CMDLINE_SIZE] = 0;
    HANDLE hToken = NULL;
    HANDLE hNewToken = NULL;
    HANDLE hNewExToken = NULL;

    CHAR szIntegritySid[20] = "S-1-16-4096";
    PSID pIntegritySid = NULL;
    PSID pUserGroupSID = NULL;
    PSID pAdminSID = NULL;

    TOKEN_MANDATORY_LABEL tml = 0;

    PROCESS_INFORMATION pi;
    STARTUPINFO si;

    BOOL bSuc = FALSE;
    ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));

    ZeroMemory(&si, sizeof(STARTUPINFO));
    si.cb = sizeof(STARTUPINFO);
    GetStartupInfo(&si);
    DWORD fdwCreate = 0;

    __try
    

        if (!OpenProcessToken(GetCurrentProcess(),
            //MAXIMUM_ALLOWED,
            TOKEN_DUPLICATE |
            TOKEN_ADJUST_DEFAULT |
            TOKEN_QUERY |
            TOKEN_ASSIGN_PRIMARY,
            &hToken))
        
            char szMsg[DEFAULT_MSG_SIZE] = 0;
            Dbg("OpenProcessToken failed, GLE = %u.", GetLastError());
            __leave;
        

        Dbg("Using RestrictedTokens way !!!");
        DWORD dwSize = 0;
        DWORD dwTokenInfoLength = 0;
        SID_IDENTIFIER_AUTHORITY SIDAuth = SECURITY_NT_AUTHORITY;
        SID_IDENTIFIER_AUTHORITY SIDAuthNT = SECURITY_NT_AUTHORITY;
        if(!AllocateAndInitializeSid(
            &SIDAuthNT,
            0x2,
            SECURITY_BUILTIN_DOMAIN_RID/*0×20*/,
            DOMAIN_ALIAS_RID_USERS,
            0, 0, 0, 0, 0, 0,
            &pUserGroupSID))
        
            Dbg("AllocateAndInitializeSid for UserGroup Error %u", GetLastError());
            __leave;
        

        // Create a SID for the BUILTIN\\Administrators group.
        if(! AllocateAndInitializeSid( &SIDAuth, 2,
            SECURITY_BUILTIN_DOMAIN_RID,
            DOMAIN_ALIAS_RID_ADMINS,
            0, 0, 0, 0, 0, 0,
            &pAdminSID) )
        
            Dbg("AllocateAndInitializeSid for AdminGroup Error %u", GetLastError());
            __leave;
        

        SID_AND_ATTRIBUTES SidToDisable[1] = 0;
        SidToDisable[0].Sid = pAdminSID;
        SidToDisable[0].Attributes = 0;

        PTOKEN_PRIVILEGES pTokenPrivileges = NULL;
        PTOKEN_PRIVILEGES pTokenPrivilegesToDel = NULL;
        if(!GetTokenInformation(hToken, TokenPrivileges, NULL, 0, &dwSize))
        
            if(GetLastError() == ERROR_INSUFFICIENT_BUFFER)
            
                pTokenPrivileges = (PTOKEN_PRIVILEGES)LocalAlloc(0, dwSize);
                pTokenPrivilegesToDel = (PTOKEN_PRIVILEGES)LocalAlloc(0, dwSize);
                if(pTokenPrivileges != NULL && pTokenPrivilegesToDel != NULL)
                
                    if(!GetTokenInformation(hToken, TokenPrivileges, pTokenPrivileges, dwSize, &dwSize))
                    
                        Dbg("GetTokenInformation about TokenPrivileges failed GTE = %u.", GetLastError());
                        __leave;
                    
                
                else
                
                    char szMsg[DEFAULT_MSG_SIZE] = 0;
                    Dbg("LocalAlloc for pTokenPrivileges failed GTE = %u.", GetLastError());
                    __leave;
                
            
        

        LUID_AND_ATTRIBUTES *pTokenLUID = pTokenPrivileges->Privileges;
        Dbg("CurrentToken's TokenPrivileges Count: %u", pTokenPrivileges->PrivilegeCount);
        DWORD dwLuidCount = 0;
        PLUID pPrivilegeLuid = NULL;
        if(!CTWProcHelper::GetPrivilegeLUIDWithSID(pUserGroupSID, &pPrivilegeLuid, &dwLuidCount))
        
            Dbg("GetPrivilegeLUIDWithSID failed GTE = %u.", GetLastError());
            if(pPrivilegeLuid)
            
                //HeapFree(GetProcessHeap(), 0, pPrivilegeLuid);
                LocalFree(pPrivilegeLuid);
                pPrivilegeLuid = NULL;
            
            __leave;
        
        Dbg("UserGroup's TokenPrivileges Count: %u", dwLuidCount);

        DWORD dwDelPrivilegeCount = 0;
        for(DWORD dwIdx=0; dwIdx<(pTokenPrivileges->PrivilegeCount); dwIdx++)
        
            BOOL bFound = FALSE;
            DWORD dwJdx = 0;
            for(; dwJdx<dwLuidCount; dwJdx++)
            
                //if(memcmp(&(pTokenLUID[dwIdx].Luid), &(pPrivilegeLuid[dwJdx]), sizeof(LUID)) == 0)
                if((pTokenLUID[dwIdx].Luid.HighPart == pPrivilegeLuid[dwJdx].HighPart)
                    &&
                    (pTokenLUID[dwIdx].Luid.LowPart == pPrivilegeLuid[dwJdx].LowPart))
                
                    bFound = TRUE;
                    break;
                
            
            if(!bFound)
            
                char szPrivilegeName[MAX_PATH] = 0;
                DWORD dwNameSize = MAX_PATH;
                if(!LookupPrivilegeName(NULL, &(pTokenLUID[dwIdx].Luid), szPrivilegeName, &dwNameSize))
                
                    Dbg("LookupPrivilegeName failed GTE = %u.", GetLastError());
                    //Dbg("NoFound[%u]: i=%u, j=%u", dwDelPrivilegeCount, dwIdx, dwJdx);
                
                //else
                //
                //    Dbg("NoFound[%u]: i=%u, j=%u -> %s", dwDelPrivilegeCount, dwIdx, dwJdx, szPrivilegeName);
                //
                pTokenPrivilegesToDel->Privileges[dwDelPrivilegeCount++].Luid = pTokenLUID[dwIdx].Luid;
            
        
        pTokenPrivilegesToDel->PrivilegeCount = dwDelPrivilegeCount;
        Dbg("TokenPrivileges to delete Count: %u", dwDelPrivilegeCount);
        if(pPrivilegeLuid)
        
            //HeapFree(GetProcessHeap(), 0, pPrivilegeLuid);
            LocalFree(pPrivilegeLuid);
            pPrivilegeLuid = NULL;
        

        if(!CreateRestrictedToken(hToken,
            0,
            1, SidToDisable,
            //0, NULL,
            dwDelPrivilegeCount, pTokenPrivilegesToDel->Privileges,
            0, NULL,
            &hNewToken
            ))
        
            char szMsg[DEFAULT_MSG_SIZE] = 0;
            Dbg("CreateRestrictedToken failed GTE = %u.", GetLastError());
            __leave;
        

        // Duplicate the primary token of the current process.
        if (!DuplicateTokenEx(hNewToken, MAXIMUM_ALLOWED, NULL, SecurityImpersonation,
            TokenPrimary, &hNewExToken))
        
            Dbg("DuplicateTokenEx failed GTE = %u.", GetLastError());
            hNewExToken = NULL;
            //__leave;
        
        else
        
            if (ConvertStringSidToSid(szIntegritySid, &pIntegritySid))
            
                tml.Label.Attributes = SE_GROUP_INTEGRITY;
                tml.Label.Sid = pIntegritySid;

                // Set the process integrity level
                if (!SetTokenInformation(hNewExToken, TokenIntegrityLevel, &tml,
                    sizeof(TOKEN_MANDATORY_LABEL) + GetLengthSid(pIntegritySid)))
                
                    Dbg("SetTokenInformation failed GTE = %u.", GetLastError());
                    //__leave;
                
                else
                
                    CloseHandle(hNewToken);
                    hNewToken = hNewExToken;
                    hNewExToken = NULL;
                    Dbg("Assign Low Mandatory Level to New Token which used to CreateProcessAsUser.");
                
            

        

        if(!(bSuc = CreateProcessAsUser(hNewToken, NULL,
            szCmdLine,     // command line
            NULL,          // TODO: process security attributes
            NULL,          // TODO: primary thread security attributes
            TRUE,         // handles are inherited ??
            fdwCreate,     // creation flags
            NULL,          // use parent's environment
            NULL,          // use parent's current directory
            &si,           // STARTUPINFO pointer
            &pi)))         // receives PROCESS_INFORMATION
        
            Dbg("CreateProcessAsUser failed GTE = %u.", GetLastError());
            __leave;
        

        if(pTokenPrivileges)
        
            LocalFree(pTokenPrivileges);
        
        if(pTokenPrivilegesToDel)
        
            LocalFree(pTokenPrivilegesToDel);
        
    
    __finally
    
        if(pIntegritySid)
        
            LocalFree(pIntegritySid);
        
        if(pUserGroupSID)
        
            LocalFree(pUserGroupSID);
        
        if(pAdminSID)
        
            LocalFree(pAdminSID);
        
        //
        // Close the access token.
        //
        if (hToken)
        
            CloseHandle(hToken);
        
        if(hNewToken)
        
            CloseHandle(hNewToken);
        
        if(hNewExToken)
        
            CloseHandle(hNewExToken);
        
        if(!bSuc)
        
            Dbg("Retry to Create process in normal way.");
            //Create process.
            bSuc = CreateProcess(NULL,
                szCmdLine,     // command line
                NULL,          // TODO: process security attributes
                NULL,          // TODO: primary thread security attributes
                TRUE,          // handles are inherited ??
                fdwCreate,     // creation flags
                NULL,          // use parent's environment
                NULL,          // use parent's current directory
                &si,           // STARTUPINFO pointer
                &pi);          // receives PROCESS_INFORMATION
        
    

其中 GetPrivilegeLUIDWithSID 函数的实现如下:

BOOL GetPrivilegeLUIDWithSID(PSID pSID, PLUID *pLUID, PDWORD pDwCount)

    LSA_OBJECT_ATTRIBUTES ObjectAttributes;
    NTSTATUS ntsResult;
    LSA_HANDLE lsahPolicyHandle;

    // Object attributes are reserved, so initialize to zeros.
    ZeroMemory(&ObjectAttributes, sizeof(ObjectAttributes));

    // Get a handle to the Policy object.
    ntsResult = LsaOpenPolicy(
        NULL,    //Name of the target system.
        &ObjectAttributes, //Object attributes.
        POLICY_ALL_ACCESS, //Desired access permissions.
        &lsahPolicyHandle  //Receives the policy handle.
        );

    if (ntsResult != STATUS_SUCCESS)
    
        printf("OpenPolicy failed returned %lu", LsaNtStatusToWinError(ntsResult));
        return FALSE;
    

    PLSA_UNICODE_STRING UserRights = NULL;
    ULONG uRightCount;
    ntsResult = LsaEnumerateAccountRights(lsahPolicyHandle, pSID, &UserRights, &uRightCount);
    if (ntsResult != STATUS_SUCCESS)
    
        printf("LsaEnumerateAccountRights failed returned %lu", LsaNtStatusToWinError(ntsResult));
        LsaClose(lsahPolicyHandle);
        return FALSE;
    

    printf("LsaEnumerateAccountRights returned Right count: %lu", uRightCount);

    (*pDwCount) = 0;
    //pLUID = (PLUID)HeapAlloc(GetProcessHeap(), 0, uRightCount*sizeof(LUID));
    (*pLUID) = (PLUID)LocalAlloc(LPTR, uRightCount*sizeof(LUID));
    if((*pLUID) == NULL)
    
        printf("HeapAlloc for PLUID failed returned %u", GetLastError());
        LsaClose(lsahPolicyHandle);
        return FALSE;
    

    for(ULONG uIdx=0; UserRights != NULL && uIdx<uRightCount; uIdx++)
    
        int nLenOfMultiChars = WideCharToMultiByte(CP_ACP, 0, UserRights[uIdx].Buffer, UserRights[uIdx].Length,
            NULL, 0, NULL, NULL);
        PTSTR pMultiCharStr = (PTSTR)HeapAlloc(GetProcessHeap(), 0, nLenOfMultiChars*sizeof(char));
        if(pMultiCharStr != NULL)
        
            WideCharToMultiByte(CP_ACP, 0, UserRights[uIdx].Buffer, UserRights[uIdx].Length,
                pMultiCharStr, nLenOfMultiChars, NULL, NULL);
            LUID luid;
            if(!LookupPrivilegeValue(NULL, pMultiCharStr, &luid))
            
                printf("LookupPrivilegeValue about %s failed, GLE=%u.", pMultiCharStr, GetLastError());
                HeapFree(GetProcessHeap(), 0, pMultiCharStr);
                continue;
            
            (*pLUID)[(*pDwCount)++] = luid;
            HeapFree(GetProcessHeap(), 0, pMultiCharStr);
        
    
    if((ntsResult = LsaFreeMemory(UserRights)) != STATUS_SUCCESS)
    
        printf("LsaFreeMemory failed returned %lu", LsaNtStatusToWinError(ntsResult));
    
    LsaClose(lsahPolicyHandle);
    return TRUE;

 以上部分代码参考了微软的网页:

Designing Applications to Run at a Low Integrity Levelhttps://docs.microsoft.com/en-us/previous-versions/dotnet/articles/bb625960(v=msdn.10)?redirectedfrom=MSDN       新创建的进程的安全属性,可以在Process Explorer中查看,右键点击目标进程的属性,查看Security标签页,可以看到目标进程的安全信息:

       对于特权(Privilege),它是用于对一个对象或服务的访问控制,比自由访问控制更为严格,一个系统管理员通过使用特权控制那些用户可以操纵系统资源,一个应用程序在修改系统层级的资源需要使用到特权,比如修改系统时间和关闭系统。

以上是关于VC++如何创建低权限的标准用户权限进程的主要内容,如果未能解决你的问题,请参考以下文章

如何允许 SYNCHRONIZE 访问所有进程的权限

Windows下如何创建低权限进程

Vista/7 UAC:如何降低进程权限

如何创建只有具有管理员和管理频道权限的成员才能使用的“创建文本/vc 频道”命令?

标准用户(XP/Server 2003及以下)如何获取所有运行进程的镜像路径?

如何设置Windows7标准用户的权限?