Win32 - 将 highestAvailable child 进程作为普通用户进程启动
Win32 - Launching a highestAvailable child process as a normal user process
假设您的 Windows 用户帐户在 Admin 组中,启用了 UAC,并且您正在 运行 以普通用户权限运行某些程序 A。 A 从不要求提升,也从不接受。现在假设 A 想要启动程序 B,它的清单中有 highestAvailable。
如果 A 调用 CreateProcess(B),这将失败并出现错误 740 ("elevation required")
如果 A 调用 ShellExecuteEx(B),Windows 将显示一个 UAC 框,要求 运行 B 提升。用户可以说是,在这种情况下 B 将 运行 提升,或者说否,在这种情况下启动将失败。
我的问题是:有没有什么办法可以实现第三种选择,我们只需在没有提升的情况下启动 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
项下设置它来使其永久化。
假设您的 Windows 用户帐户在 Admin 组中,启用了 UAC,并且您正在 运行 以普通用户权限运行某些程序 A。 A 从不要求提升,也从不接受。现在假设 A 想要启动程序 B,它的清单中有 highestAvailable。
如果 A 调用 CreateProcess(B),这将失败并出现错误 740 ("elevation required")
如果 A 调用 ShellExecuteEx(B),Windows 将显示一个 UAC 框,要求 运行 B 提升。用户可以说是,在这种情况下 B 将 运行 提升,或者说否,在这种情况下启动将失败。
我的问题是:有没有什么办法可以实现第三种选择,我们只需在没有提升的情况下启动 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
项下设置它来使其永久化。