Win32 - 将 highestAvailable child 进程作为普通用户进程启动

Win32 - Launching a highestAvailable child process as a normal user process

假设您的 Windows 用户帐户在 Admin 组中,启用了 UAC,并且您正在 运行 以普通用户权限运行某些程序 A。 A 从不要求提升,也从不接受。现在假设 A 想要启动程序 B,它的清单中有 highestAvailable。

我的问题是:有没有什么办法可以实现第三种选择,我们只需在没有提升的情况下启动 B?

这似乎在原则上应该是可能的,因为 "highestAvailable" 意味着 B 更喜欢 而不是 运行 并且完全有能力 运行在普通用户模式下。但我想不出任何方法来实现它。我已经用令牌和 CreateProcessAsUser() 尝试了各种事情,但这一切似乎都归结为:"highestAvailable" 似乎不可改变地指用户帐户中固有的潜在特权,而不是表达的实际特权任何显式构建的标记。

我希望实际上有一些方法可以使用 CreateProcessAsUser() 来执行此操作,而我只是错过了正确构建令牌的技巧。

更新 - 已解决: 下面的 __COMPAT_LAYER=RunAsInvoker 解决方案效果很好。不过有一个警告。这会无条件地将子进程强制为 运行 "as invoker":即使被调用的 exe 在其清单中指定了 "requireAdministrator",它也适用。我认为当 exe 指定 "requireAdministrator" 时,原始 "elevation required" 错误通常更可取。我想要标有 "highestAvailable" 的程序的 RunAsInvoker 行为的全部原因是此类程序明确表示 "I can function properly in either mode" - 所以让我们继续 运行 在正常用户模式下使用 Admin 不方便模式。但是 "requireAdministrator" 是另一回事:这样的程序说 "I can't function properly without elevated privileges"。对于这样的程序,预先失败似乎比强制它们 运行 un-elevated 更好,这可能会使它们遇到 privilege/access 错误,而它们没有正确编程来处理。所以我认为完整的 general-purpose 解决方案需要检查应用程序清单,并且仅在清单显示 "highestAvailable" 时才应用 RunAsInvoker 强制。一个更完整的解决方案是使用其他地方讨论的技术之一,让调用者可以选择在出现 "requireAdministrator" 程序时调用 UAC,并为用户提供启动它的机会。我可以想象一个 CreateProcessEx() 覆盖了 "treat process privileges as highest available privileges" 和 "invoke UAC if elevation is required" 的几个新标志。 (下面描述的另一种方法,挂钩 NTDLL!RtlQueryElevationFlags() 以告诉 CreateProcess() UAC 不可用,对于 requireAdministrator 程序具有完全相同的警告。)

(这可能表明 Windows shell 甚至没有提供执行此操作的方法...直接从 shell 启动 B 会给你 UAC 框这让您可以使用 Admin privs 启动或根本不启动。如果有任何方法可以实现它,UAC 框可能会提供第三个按钮以在没有特权的情况下启动。但话又说回来,这可能只是一个 UX 决定,第三个选项对平民来说太混乱了。)

(请注意,在 Whosebug 和 Microsoft 开发支持网站上有很多帖子询问非常 similar-seeming 的情况,不幸的是,这里不适用。那种情况是你有 parent 程序 运行ning 提升,它想要启动 non-elevated child 进程。典型的例子是安装程序,运行ning 提升作为安装程序倾向于做, 它想在退出之前以普通用户级别启动它刚刚安装的程序。有很多关于如何做到这一点的已发布代码,我已经基于其中一些技术进行了尝试,但这确实是一个不同的场景和解决方案在我的情况下不起作用。最大的区别是他们在这种情况下尝试启动的 child 程序 没有 标记为 highestAvailable - child 只是一个普通程序,在正常情况下无需任何 UAC 参与即可启动。还有另一个区别,即在那些情况下, parent 已经 运行ning 提升了,而在我的场景中 parent 是 运行ning 作为普通用户级别;这会稍微改变一些事情,因为在另一种情况下,parent 进程可以访问一些我无法使用的令牌上的特权操作,因为 A 本身并没有提升。但据我所知,那些特权令牌操作无论如何都无济于事;事实上 child 具有 highestAvailable 标志,这是我的场景的关键元素。)

可能的黑客解决方案调用 CreateProcess 来自未提升的管理员用户(受限管理员)用于清单中带有 highestAvailable 的 exe(或来自任何未提升的用户用于 requireAdministrator exe)-这是钩子 RtlQueryElevationFlags 调用并将返回的标志设置为 0。 这是目前的工作,但当然没有任何受让人将在 windows 的下一版本中工作,如果有什么改变的话。然而原样。

对于 hook 单次 api 调用 - 我们可以将硬件断点设置为函数地址和 VEX handler 。演示工作代码:

NTSTATUS NTAPI hookRtlQueryElevationFlags (DWORD* pFlags) 
{
    *pFlags = 0;
    return 0;
}

PVOID pvRtlQueryElevationFlags;

LONG NTAPI OnVex(::PEXCEPTION_POINTERS ExceptionInfo)
{
    if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP &&
        ExceptionInfo->ExceptionRecord->ExceptionAddress == pvRtlQueryElevationFlags)
    {
        ExceptionInfo->ContextRecord->
#if defined(_X86_)
        Eip
#elif defined (_AMD64_)
        Rip 
#else
#error not implemented
#endif
             = (ULONG_PTR)hookRtlQueryElevationFlags;

        return EXCEPTION_CONTINUE_EXECUTION;
    }

    return EXCEPTION_CONTINUE_SEARCH;
}

ULONG exec(PCWSTR lpApplicationName)
{
    ULONG dwError = NOERROR;

    if (pvRtlQueryElevationFlags = GetProcAddress(GetModuleHandle(L"ntdll"), "RtlQueryElevationFlags"))
    {
        if (PVOID pv = AddVectoredExceptionHandler(TRUE, OnVex))
        {
            ::CONTEXT ctx = {};
            ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
            ctx.Dr7 = 0x404;
            ctx.Dr1 = (ULONG_PTR)pvRtlQueryElevationFlags;

            if (SetThreadContext(GetCurrentThread(), &ctx))
            {
                STARTUPINFO si = {sizeof(si)};
                PROCESS_INFORMATION pi;
                if (CreateProcessW(lpApplicationName, 0, 0, 0, 0, 0, 0, 0, &si,&pi))
                {
                    CloseHandle(pi.hThread);
                    CloseHandle(pi.hProcess);
                }
                else
                {
                    dwError = GetLastError();
                }

                ctx.Dr7 = 0x400;
                ctx.Dr1 = 0;
                SetThreadContext(GetCurrentThread(), &ctx);
            }
            else
            {
                dwError = GetLastError();
            }
            RemoveVectoredExceptionHandler(pv);
        }
        else
        {
            dwError = GetLastError();
        }
    }
    else
    {
        dwError = GetLastError();
    }

    return dwError;
}

在您的进程中将 __COMPAT_LAYER 环境变量设置为 RunAsInvoker。我认为这在任何地方都没有正式记录,但它一直适用于 Vista。

您也可以通过在注册表中的 AppCompatFlags\Layers 项下设置它来使其永久化。