【问题标题】:Win32 - Launching a highestAvailable child process as a normal user processWin32 - 将最高可用的子进程作为普通用户进程启动
【发布时间】:2018-10-22 05:58:10
【问题描述】:

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

  • 如果 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 框,让您可以使用 Admin privs 启动或根本不启动。如果有任何方法可以实现它,UAC 框可能会提供第三个按钮以在没有权限的情况下启动。但话又说回来,这可能只是一个 UX 决定,第三个选项对平民来说太混乱了。)

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

【问题讨论】:

  • 合法的方式不存在。但是,您可以在调用 CreateProcess 之前将RtlQueryElevationFlags api 挂接到自身进程中(例如通过 DR 寄存器和 vex 处理程序)并将返回的标志设置为 0。在这种情况下,将启动子进程。
  • @RbMm - 谢谢,我会调查的。虽然如果它不是“合法方式”,它可能不是我想要的。我确实从所有有关从提升中启动非提升的相关问题中得到印象,你是对的,但它根本不可能“合法”。
  • 确实如此(使用 DR 和 vex 挂钩 api 调用,非常容易实现)但当然这在未来可能会被打破
  • @RbMm - 我读到了。这绝对是正确的,但你确定这是一个黑客攻击是正确的。
  • 如果想要我可以发布准备好的代码怎么做。当然破解。但照原样

标签: winapi createprocess elevation createprocessasuser


【解决方案1】:

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

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

【讨论】:

  • 谢谢 - 这看起来很简单!奇怪的是他们没有在任何地方记录它。
  • 刚刚测试了环境变量的方法。似乎工作得很好。 (出于我的目的,环境变量方法是我需要的方法,因为我不希望它是系统范围的或永久的——它只适用于一个应用程序。)
【解决方案2】:

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

对于钩子单次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;
}

【讨论】:

  • 谢谢!这很好用。而且你的方法肯定比我根据你的建议找到的其他类似黑客要好一些,以寻找 RtlQueryElevationFlags() 钩子。我发现的其他方法都想通过内存修补来劫持 DLL 入口点,这对我来说似乎真的很脆弱。您的断点挂钩似乎更安全、更可靠。我认为 Microsoft 可能仍会不赞成这种方法,因为它会搞乱人不应该知道的事情,但它似乎与你能得到所需的东西一样可靠。
  • @mjr - 还要注意这个钩子是线程安全的 - 只有当前线程会受到 DR 的影响,如果另一个线程同时调用钩子函数 - 他不会被破坏。与 detour 不同,它影响了所有线程
  • 太棒了!那时候我想过那个。它实际上与我的特定场景非常相关,因为有问题的应用程序正在从线程启动子进程。
  • @mjr 从线程启动 ?一切都在某个线程中执行。但是认为__COMPAT_LAYER 的解决方案还是更好
  • “从一个线程启动?全部在某个线程中执行”——我的意思是每次启动都有一个新线程,因此它们中的多个线程可能同时运行。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-11-01
  • 2016-06-24
  • 2019-01-14
  • 1970-01-01
  • 1970-01-01
  • 2015-10-28
  • 2011-01-04
相关资源
最近更新 更多