首页
社区
课程
招聘
[翻译]Windows利用技巧:利用任意文件写实现本地提权
2018-5-9 21:27 6178

[翻译]Windows利用技巧:利用任意文件写实现本地提权

2018-5-9 21:27
6178

翻译:https://googleprojectzero.blogspot.com/2018/04/windows-exploitation-tricks-exploiting.html

引言

之前我发表了一个技术(https://googleprojectzero.blogspot.com/2017/08/windows-exploitation-tricks-arbitrary.html)利用Windows上的任意目录创建脆弱性给你任意文件读权限。在即将到来的春天,在新的更新中已经将之前博客中提到的滥用挂载点来链接到文件的方法修补了。

和之前的博客保持一样的精神,我将一个新的技术在Win10下进行更常用的任意文件写功能。或许微软也会加固操作系统使得利用这类脆弱性更困难。我将详细讲述ProjectZero最近报告给微软的问题。

任意文件写脆弱性使用户可以创建或修改它无法访问目录的文件。这可能是因为一个特权服务不正确地审查用户传给它的信息,或者因为符号链接移植攻击(用户写一个链接到一个被特权服务使用的位置)。理想的脆弱性是攻击者不仅可以控制被写文件的路径,还可以控制完整内容。这个博客讲的就是这样一个脆弱性。

常见的任意文件写利用方法是进行DLL劫持。当一个Windows可执行程序执行到最初的加载阶段,NTDLL将尝试寻找所有导入的DLL。检测导入DLL的位置顺序很复杂,可以概括为:

  • 检查KnuwnDll,这是个OS控制的预缓存的DLL列表。找到列表后,将DLL从预加载节(pre-loaded section)映射到内存。
  • 检查应用程序目录
  • 检查系统目录
  • 检查环境变量PATH

DLL劫持的目的是找到一个在高特权运行的可执行程序,它会从从一个脆弱性准许写的目录加载一个DLL,并且在搜索顺序排在前面的位置没有找到这个DLL。

有两个问题会使DLL劫持变得恼人:

  • 基本上需要创建一个特权进程的新实例,大多数导入的DLL在进程首次执行时被定位。
  • 大多数特权exe、dll都在system32目录

第二个问题意味着DLL搜索位置2和3也是在system32中。在重写DLL不是问题的前提下(如果DLL已经被加载到一个进程,将无法重写),这个问题使得找到合适的劫持DLL非常困难。这个问题的通常解决办法是,找到一个不在system32的特权程序,并可以被容易地启动(如通过加载COM服务或运行任务调度)。

就算你找到了一个合适的目标exe,进行DLL劫持也很困难。有时你需要实现原DLL的全部导出函数stub,不然加载DLL将会失败。此外,运行自己代码的最好位置是DLLMain,这又会遇见其它问题(如运行代码内部的加载锁https://msdn.microsoft.com/en-us/library/windows/desktop/dn633971%28v=vs.85%29.aspx)。最好的情况是,一个特权服务只加载一个DLL,无需让特权进程正常运行起来。问题是:哪里去找这样的服务?

某人回答有,并且这个服务在之前至少被利用过两次。一次是Lokihardt的沙箱逃逸,一个是我(James Forshaw)的user to system Eop(https://bugs.chromium.org/p/project-zero/issues/detail?id=887)。这个服务名字是Microsoft (R) Diagnostics Hub Standard Collector Service,在后面我们称它DiagHub。

DiagHub服务在Win10引进系统,在Win7/8.1有一个执行类似功能的服务叫IE ETW Collector。服务的任务是使用ETW搜集沙箱应用的诊断信息,特别是Edge和IE。它有个有趣的特征,可以配置来加载一个system32目录下的任意DLL,这就是Lokihardt和我用来进行提权的特征。这个服务的所有功能都通过一个注册的DCOM对象暴露出,所以为了加载我们的DLL,我们需要找到调用DCOM对象的方法。如果不关注寻找过程,只关注利用方法,可以直接跳到最后。

逆向一个DCOM对象

我们一起来走一遍整个步骤,寻找未知DCOM对象支持哪些接口并且找到接口的实现,进而取逆向某个接口。通常有两个思路:直接在IDA逆向;先做些系统内的检查定位我们关注的区域再逆向。这里我们选择第二种思路。

我们需要一些工具:OleViewDotNetv1.4+(OVDN)(https://github.com/tyranid/oleviewdotnet/releases);安装windbg。

首先要找到DCOM对象的注册信息并发现什么接口可访问。我们知道DCOM对象居于服务中,所以一旦你加载OVDN,点击菜单的Registry->Local Services,工具会加载一个导出COM对象的系统服务列表。如果此时发现 “Microsoft (R) Diagnostics Hub Standard Collector Service”服务(在这里可以进行一个过滤),会在列表中看到入口。如果打开服务树节点会看到一个子节点,子节点名字是“Diagnostics Hub Standard Collector Service”,DCOM对象就在这里面。当你打开树上的节点时,工具会创建这个对象,并请求所有远程可访问的COM接口来向你提供一个对象支持的接口列表。如下图:

在这里取查看一下访问DCOM对象需要的安全需要很重要。右键树节点,选择ViewAccessPermissions或者ViewLaunchPermissions,会弹出一个窗口显示需要的权限。本例中,DCOM对象需要在IE保护模式、Edge的APP容器沙箱和LPAC中访问。

接口列表显示,我们只需关注标准接口。有时,在会有感兴趣的工厂接口。标准接口中我们关心两个:IStandardCollectorAuthorizationService和IStandardCollectorService。假设我们已经知道IStandardCollectorService是我们关心的,随着后续分析进行我们先选择哪一个没有关系。右键接口的树节点选择属性,可以看到注册的接口的部分信息。

这里没有许多有用信息,但可以看到这个接口有8个方法。在诸多COM注册信息中,这个值可能不正确,但在本例中,我们假设它是正确的。要理解方法到底是做什么的,我们需要追踪到COM服务中IStandardCollectorService的实现里。现有知识使得我们可以有目标地进行逆向正确的方法。对一个进程内的COM对象做这些工作相对容易,因为我们可以通过解析一些指针直接请求对象的VTable指针。然而,对于进程外的COM对象就复杂一些。因为我们在进程中调用的真正对象其实是一个远程对象的代理,如下图:

我们任然可以通过提取存储在服务进程中的对象信息找到OOP对象的VTable。右键Diagnostics Hub Standard Collector Service对象树节点选择Create Instance。将建立一个新的COM对象实例,如下图:

创建的实例会提供给你基本信息,如对象的CLSID和支持的接口。现在我们要和接口建立起一个连接。在下面的列表选择IStandardCollectorService,在底部的Operation菜单选择Marshal->View Properties。如果成功,你将看到下图:

这里面有许多信息,我们感兴趣的是两个:宿主服务的PID和结构指针标识符(IPID)。本例中,PID是服务运行在其中的进程,但不总是如此。有时你创建了一个COM对象,无法知道它居于哪个进程中,此时这个信息没有价值。IPID是宿主进程中唯一的,我们可以使用PID和IPID找到找到服务,并在其中找到实现了COM方法的VTable。IPID中的PID最大尺寸是16bit,现在Windows版本大多有更大的PID,所以你需要手动找到进程或者重启服务多次指导得到一个合适的PID。

现在我们要使用OVDN的一个功能,到达服务进程的内存并找到IPID信息。你可以通过主菜单Object->Processes得到所有进程的信息,但是我们可以通过PID旁的View按钮直接定位到服务进程。点击后,OVND会请求配置符号支持。符号配置类似下图:(需要以管理器启动)

如果一切正确,你会看到IPID更详细地信息,如下图:

两个最有用的信息是接口指针(分配对象的堆位置)和接口的VTable指针。VTable地址告诉我们COM服务的实际实现加载位置。在这里我们看到,VTable在主可执行文件(DiagnosticsHub.StandardCollector.Server)外的一个不同的模块(DiagnosticsHub.StandardCollector.Runtime)中。我们可以通过使用windbg附加到服务进程并转储VTable地址处的符号来验证。在之前我们还知道了有8个方法,所以我们可以将这个也考虑在内:

dqs DiagnosticsHub_StandardCollector_Runtime+0x36C78 L8

注意,windbg将模块名加下划线。如果成功,结果如下图:

通过提取这些信息,我们获得了方法的名字和在二进制中的地址。我们可以设置断点查看在正常操作时哪些被调用,或者开始逆向过程。

ATL::CComObject<StandardCollectorService>::QueryInterface

ATL::CComObjectCached<StandardCollectorService>::AddRef

ATL::CComObjectCached<StandardCollectorService>::Release

StandardCollectorService::CreateSession

StandardCollectorService::GetSession

StandardCollectorService::DestroySession

StandardCollectorService::DestroySessionAsync

StandardCollectorService::AddLifetimeMonitorProcessIdForSession

方法列表看上去是正确的:他们开始于3个COM对象的标准方法,本例中他们由ATL库实现。后面的5个由StandardCollectorService类实现。根据公开的符号,我们无法知道我们要传给COM服务什么参数。根据C++名字包含一些类型信息,IDA可能提取那些信息,但并不是必须知道所有传给函数的结构的格式。幸运的是,根据COM代理使用NDR(Network Data Representation)解析器执行编组的实现代码,可以逆向NDR字节码到我们可以理解的形式。在本例中,回到最初的服务信息,右键IStandardCollectorService树节点选择View Proxy Definition。将会让OVDN取解析NDR代理信息并显示在一个新视图,如下图:

观察代理的定义会得到代理库实现的其它接口。这对于之后的逆向很有用。反编译的代理定义以像C#的伪代码形式显示,在需要时能方便地转化为C#或C++。注意代理定义不包含方法的名字,还好我们之前已经提取得到了。进行简单整理后结合方法名我们获得如下形式定义:

[uuid("0d8af6b7-efd5-4f6d-a834-314740ab8caa")]

struct IStandardCollectorService : IUnknown {

    HRESULT CreateSession(_In_ struct Struct_24* p0,

                          _In_ IStandardCollectorClientDelegate* p1,

                          _Out_ ICollectionSession** p2);

    HRESULT GetSession(_In_ GUID* p0, _Out_ ICollectionSession** p1);

    HRESULT DestroySession(_In_ GUID* p0);

    HRESULT DestroySessionAsync(_In_ GUID* p0);

    HRESULT AddLifetimeMonitorProcessIdForSession(_In_ GUID* p0, [In] int p1);

}

还差最后一个东西,我们不知道Struct_24结构的定义。这可以通过逆向过程提取出来,幸运地是,本例中我们不需要通过逆向来提取。NDR字节码必须知道如何整理这个结构,所以OVDN自动为我们提取出结构定义。选择Structures标签页找到Struct_24。

随着你进行逆向过程,你可以在需要的时候重复这个过程直到你理解所有事情如何工作。现在来实际利用DiagHub服务。

利用样例

通过逆向工程,我们发现为了加载system32里的DLL,我们需要进行以下步骤:

  • 使用IStandardCollectorService::CreateSession建立一个新的Diagnostics会话
  • 在新会话中调用ICollectionSession::AddAgent方法,传输要加载DLL的名字(只是名字,不包含路径)

ICollectionSession::AddAgent的简化加载代码如下:

void EtwCollectionSession::AddAgent(LPWCSTR dll_path,

                                    REFGUID guid) {

  WCHAR valid_path[MAX_PATH];

  if ( !GetValidAgentPath(dll_path, valid_path)) {

    return E_INVALID_AGENT_PATH;

  HMODULE mod = LoadLibraryExW(valid_path,

        nullptr, LOAD_WITH_ALTERED_SEARCH_PATH);

  dll_get_class_obj = GetProcAddress(hModule, "DllGetClassObject");

  return dll_get_class_obj(guid);

}

它首先检查代理路径是有效的,并返回完整路径(这是之前Eop bug存在的地方,不完整的检测)。这个路径被使用LoadLibraryEx加载,然后请求DLL的导出方法DllGetClassObject。所以要让代码执行只需要实现这个方法并将DLL放入system32目录。实现的DllGetClassObject方法将在加载锁外被调用,所以我们可以为所欲为。下面的代码满足加载一个叫dummy.dll的DLL。

IStandardCollectorService* service;

CoCreateInstance(CLSID_CollectorService, nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&service));


SessionConfiguration config = {};

config.version = 1;

config.monitor_pid = ::GetCurrentProcessId();

CoCreateGuid(&config.guid);

config.path = ::SysAllocString(L"C:\Dummy");

ICollectionSession* session;

service->CreateSession(&config, nullptr, &session);


GUID agent_guid;

CoCreateGuid(&agent_guid);

session->AddAgent(L"dummy.dll", agent_guid);

接下来需要做的就是将一个DLL放入system32,加载它并提权。这部分功能,我举一个我在系统存储服务的SvcMoveFileInheritSecurity RPC方法发现的脆弱性作为例子。这个函数引起我的注意因为它被用在一个ALPC的脆弱性利用样本里,并被 Clément Rouault & Thomas Imbert发表在PACSEC 2017。这个方法只是这个脆弱性的一个主要利用方法,我意识到它原理上说不是一个而是两个脆弱性。SvcMoveFileInheritSecurity代码看起来如:

void SvcMoveFileInheritSecurity(LPCWSTR lpExistingFileName,

                                LPCWSTR lpNewFileName,

                                DWORD dwFlags) {

  PACL pAcl;

  if (!RpcImpersonateClient()) {

    // Move file while impersonating.

    if (MoveFileEx(lpExistingFileName, lpNewFileName, dwFlags)) {

      RpcRevertToSelf();

      // Copy inherited DACL while not.

      InitializeAcl(&pAcl, 8, ACL_REVISION);

      DWORD status = SetNamedSecurityInfo(lpNewFileName, SE_FILE_OBJECT,

          UNPROTECTED_DACL_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,

          nullptr, nullptr, &pAcl, nullptr);

        if (status != ERROR_SUCCESS)

          MoveFileEx(lpNewFileName, lpExistingFileName, dwFlags);

    }

    else {

      // Copy file instead...

      RpcRevertToSelf();

    }

  }

}

这个方法的目的是移动一个文件,并应用新目录位置继承来的ACE到DACL。当一个文件移动到相同卷的另一个目录时这很重要,因为会直接将就文件链接到新位置。此时,新文件会保持在之前位置的安全属性。在目录里创建新文件会上述情况下需要继承ACE,通过SetNamedSecurityInfo函数明确知道ACE。

为了确保这个方法在以服务用户运行时不准许移动任何文件,主要指Local System,此时RPC调用者是被模拟的。脆弱性存在的原因是,在调用MoveFileEx之后立刻被触发,模拟被还原并且调用SetNamedSecurityInfo。如果调用失败,代码再次调用MoveFileEx尝试原来的移动操作。这是第一个脆弱性。最初的文件名现在指向其它位置,例如遇到符号链接。这将使SetNamedSecurityInfo调用失败,并增加一个Local System的拒绝ACL到文件的ACE并返回一个错误,这将造成恢复并使你实现任意文件创建。这个问题在https://bugs.chromium.org/p/project-zero/issues/detail?id=1427中介绍过。

但这不是我们这里要使用的脆弱性,那太简单了。相反我们将利用相同代码中的第二个脆弱性:我们可以在以Local System运行时,让服务对任务文件调用SetNamedSecurityInfo。这个通过在调用MoveFileEx时滥用模拟设备映射来重定向本地驱动器盘符实现,这导致lpNewFileName指向一个任意位置,或者直接滥用硬盘链接(https://bugs.chromium.org/p/project-zero/issues/detail?id=1428)。利用硬盘链接的过程如下图:

  1. 创建一个硬盘链接到system32目录下我们想重写的文件。我们可以在没有写权限的情况下建立硬盘链接,只要在沙箱外。
  2. 创建一个新的目录位置,它继承Everyone或Authenricated Users组的ACE,保证准许修改任何新文件。你不是必须这样按部就班修改ACE,例如,任何在C:根目录创建的新目录继承Authenticated Users的ACE。之后向RPC服务发起一个请求移动硬链接文件到新目录位置。只要对原位置有FILE_DELETE_CHILD权限,对新位置有FILE_ADD_FILE权限,这个操作就会成功。
  3. 服务将在被移动的硬链接上调用SetNamedSecurityInfo。SetNamedSecurityInfo将从新目录位置获得ACE将它们应用于硬链接文件。将这些ACE应用于硬链接文件的原因是从SetNamedSecurityInfo的视角看,硬链接文件在新位置,尽管被我们链接的原文件在system32下。

通过利用这个脆弱性,我们可以修改任何Local System拥有WRITE_DAC权限的文件的安全特性。所以我们可以修改system32目录下文件的安全特性,并使用DiagHub加载它。还有个明显的问题,system32下的大多数文件都归TrustedInstaller group所有,并且Local System也不能修改。我们需要找到一个不归TrustedInstaller group所有的可以写的文件。并且修改这个文件还不会造成系统启动崩溃。我们不在乎文件的扩展名,因为AddAgent只是检查它存在并用LoadLibraryEx加载它。有许多找到合适文件的方法,例如使用SysInternals的AccessChk,但为了100%确保存储服务的token可以修改文件,我们使用我的NtObjectManager PowerShell(https://www.powershellgallery.com/packages/NtObjectManager)模块。这个模块被用来检测沙箱中可以访问哪些文件,同样可以检测特权服务可以访问哪些文件。如果你在安装模块后以管理员权限运行下面脚本,$file变量将包含存储服务有 WRITE_DAC权限的文件列表:

Import-Module NtObjectManager


Start-Service -Name "StorSvc"

Set-NtTokenPrivilege SeDebugPrivilege | Out-Null

$files = Use-NtObject($p = Get-NtProcess -ServiceName "StorSvc") {

    Get-AccessibleFile -Win32Path C:\Windows\system32 -Recurse `

     -MaxDepth 1 -FormatWin32Path -AccessRights WriteDac -CheckMode FilesOnly

}

观察文件列表我决定使用license.rtf,这文件包含一个短的Windows证书说明。利用这个文件看上去对系统危害很小,也不会造成系统崩溃。

现在将它们结合:

使用存储服务脆弱性来改变system32目录下license.rtf的安全属性

复制一个DLL成为license.rtf,DLL要实现DllGetClassObject接口

使用DiagHub服务价值我们修改后的license文件作为一个DLL,使代码以Local System权限被执行为所欲为。

如果想看完整的实例,我上传可一个完整实例到https://bugs.chromium.org/p/project-zero/issues/detail?id=1428#c9

( 我测试结果表明这个脆弱性在Win10 1709可以复现,Win10 1703不行。 )

总结

这个博客中我讨论了一个有用的Win10下提权方法。也可以在Edge LPAC等沙箱环境使用。你能在类似DCOM实现中发现类似问题么?


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
点赞1
打赏
分享
最新回复 (2)
雪    币: 8863
活跃值: (2369)
能力值: ( LV12,RANK:760 )
在线值:
发帖
回帖
粉丝
cvcvxk 10 2018-5-9 21:30
2
0
这个提权法子,真的只能说这么大槽点。
HKEY_CURRENT_USER\Volatile  Environment\SYSTEMROOT了解下,DCOM?不需要的。

最后于 2018-5-9 21:31 被cvcvxk编辑 ,原因:
雪    币: 965
活跃值: (310)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
0sWalker 2018-5-9 21:58
3
0
cvcvxk 这个提权法子,真的只能说这么大槽点。HKEY_CURRENT_USER\Volatile&nbsp; Environment\SYSTEMROOT了解下,DCOM?不需要的。
这个方法使用起来是有点麻烦,但也算个新的脆弱点。而且逆向DCOM那块个人觉得还是挺精彩,学到些东西,所以详细翻译了。
游客
登录 | 注册 方可回帖
返回