假设您的 Windows 用户帐户位于管理员组中,已启用 UAC,并且您正在使用普通用户权限运行某个程序 A。 A 从不要求提升,也从不接受提升。 现在假设 A 想要启动程序 B,该程序 B 的清单中具有最高的可用。
-
如果 A 调用 CreateProcess(B),这将失败并显示错误 740("需要提升")
-
如果 A 调用 ShellExecuteEx(B),Windows 将显示一个 UAC 框,要求提升运行 B。 用户可以说"是",在这种情况下,B 将提升运行,或者说"否",在这种情况下,启动将失败。
我的问题是:有没有办法实现第三种选择,我们只是在不提升的情况下发射 B?
这在原则上似乎是可能的,因为"highestAvailable"意味着 B更喜欢以提升方式运行,但完全能够在普通用户模式下运行。 但是我想不出任何方法来完成它。 我已经用令牌和CreateProcessAsUser()尝试了各种各样的事情,但这一切似乎都归结为:"highestAvailable"似乎不可改变地指的是用户帐户中固有的潜在特权,而不是任何显式构造的令牌中表达的实际特权。
我希望实际上有某种方法可以使用 CreateProcessAsUser() 来做到这一点,而我只是错过了正确构造令牌的技巧。
更新 - 已解决:下面的 __COMPAT_LAYER=RunAsInvoker 解决方案运行良好。 不过,有一个警告。 这会强制子进程无条件地"作为调用者"运行:即使被调用的 exe 在其清单中指定了"requireAdministrator",它也适用。 我认为当exe指定"requireAdministrator"时,原始的"需要提升"错误通常更可取。 我希望标记为"highestAvailable"的程序的 RunAsInvoker 行为的全部原因是此类程序明确表示"我可以在任一模式下正常运行" - 所以让我们继续在正常用户模式下运行,当使用管理员模式不方便时。 但是"requireAdministrator"是另一回事:这样的程序说"如果没有提升的权限,我就无法正常运行"。 对于此类程序,预先失败似乎比强制它们以未提升的方式运行要好,这可能会使它们遇到未正确编程处理的权限/访问错误。 因此,我认为这里的完整通用解决方案需要检查应用程序清单,并且仅在清单显示"highestAvailable"时才应用 RunAsInvoker 强制。 一个更完整的解决方案是使用其他地方讨论的技术之一,为调用方提供在出现"requireAdministrator"程序时调用 UAC 的选项,并为用户提供提升启动它的机会。 我可以想象一个 CreateProcessEx() 封面,上面有几个新标志,用于"将进程特权视为最高可用特权"和"如果需要提升,则调用 UAC"。 (下面描述的另一种方法,钩住NTDLL!RtlQueryElevationFlags() 告诉 CreateProcess() UAC 不可用,对于 requireAdministrator 程序具有完全相同的警告。
(这可能说明Windows shell甚至没有提供一种方法来做到这一点......直接从 shell 启动 B 会给你一个 UAC 框,让你要么使用管理员权限启动,要么根本不启动。 如果有任何方法可以完成它,UAC 框可能会提供第三个按钮,无需权限即可启动。 但话又说回来,这可能只是一个用户体验决定,第三种选择对平民来说太混乱了。
(请注意,在StackOverflow和Microsoft开发支持站点上有很多帖子询问了一个非常相似的场景,不幸的是,这种情况不适用于这里。 在这种情况下,你有一个运行提升的父程序,并且它想要启动非提升的子进程。 规范示例是一个安装程序,它像安装程序倾向于的那样提升运行,它希望在退出之前在普通用户级别启动它刚刚安装的程序。 有很多关于如何做到这一点的已发布代码,我已经基于其中一些技术进行了尝试,但这实际上是一个不同的场景,解决方案在我的情况下不起作用。 最大的区别在于,在这种情况下,他们尝试启动的子程序没有标记为 highestAvailable - 子程序只是一个普通程序,在正常情况下无需任何 UAC 参与即可启动。 还有另一个区别,即在这些情况下,父级已经提升运行,而在我的方案中,父级作为普通用户级别运行;这稍微改变了事情,因为另一个方案中的父进程可以访问令牌上的一些特权操作,我无法使用这些操作,因为 A 本身没有提升。 但据我所知,这些特权令牌操作无论如何都无济于事;事实上,孩子拥有最高的可用标志,这是我方案的关键要素。
将__COMPAT_LAYER
环境变量设置为在流程中RunAsInvoker
。我不认为这在任何地方都有正式记录,但它一直可以追溯到 Vista。
您还可以通过在注册表中的AppCompatFlagsLayers
项下设置它来使其永久化。
可能的黑客解决方案调用CreateProcess
来自清单中具有highestAvailable
的 exe 的未提升管理员用户(受限管理员)(或来自requireAdministrator
exe 的任何未提升用户) - 这是钩子RtlQueryElevationFlags
调用并将返回的标志设置为 0。 这目前是有效的,但是如果发生更改,当然没有任何受助者可以在下一版本的Windows中工作。然而,照原样。
对于钩子单次 API 调用 - 我们可以将硬件断点设置为函数地址和 VEX 处理程序。 演示工作代码:
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;
}