翻译: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的位置顺序很复杂,可以概括为:
DLL劫持的目的是找到一个在高特权运行的可执行程序,它会从从一个脆弱性准许写的目录加载一个DLL,并且在搜索顺序排在前面的位置没有找到这个DLL。
有两个问题会使DLL劫持变得恼人:
第二个问题意味着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对象支持哪些接口并且找到接口的实现,进而取逆向某个接口。通常有两个思路:直接在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,我们需要进行以下步骤:
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。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课