β

NtApphelpCacheControl漏洞分析

奇虎360技术博客 169 阅读

起因:

Google Project Zero团队的新晋成员James Forshaw在9月30日向微软提交了名为“Windows: Elevation of Privilege in ahcache.sys/NtApphelpCacheControl”的安全问题,并且在Google的漏洞公开期限(90天)后,也就是2014年12月29日(北京时间的12月30日)公开了此问题的细节。

在这里可以看到他针对该问题的一个简单描述和攻击验证代码:https://code.google.com/p/google-security-research/issues/detail?id=118

针对这个漏洞的争论很多,很多观众在争论微软和Google对待安全漏洞的做法,90天公开策略是否合情合理等等,也有很多技术人员在争论这个安全问题是否是严格意义上的权限提升漏洞。后者的一个主要原因是James Forshaw在演示这个漏洞时,是通过此漏洞来劫持UAC默认级别下自动提权的ComputerDefaults程序,从而实现在默认UAC设置下静默从中完整性级别启动高完整性级别的程序。

通常来说,微软安全响应中心(MSRC)不认为UAC默认级别下中完整性级别到高完整性级别的提示绕过属于安全漏洞的(参考MSRC的”如何定义安全漏洞“ http://technet.microsoft.com/library/cc751383.aspx)。

但是,最近MSRC也将一些可以穿透Internet Explorer保护模式(PM)或增强保护模式(EPM)沙箱的安全问题(实际上是从低完整性级别穿透至中完整性级别的问题)作为安全漏洞来修补(例如CVE-2014-6349)。,所以我们先搁置是否安全漏洞的这个争议,深入分析下这个漏洞涉及的原理和问题。

漏洞分析、成因与利用

这个漏洞的基本原因,Google的这篇简单说明里已经讲得比较准确了,简单来说,就是NtApphelpCacheControl这个系统调用,在调用者模拟System权限时,没有正确识别调用者的Token,导致其本来仅提供给管理员和系统程序调用的接口,可以被低权限程序误用,并借助该调用的相关机制,劫持高权限程序,实现权限提升。

看完这个原因后,你可能会有疑问,这个系统调用是做什么的?什么是Apphelp?Token的判断具体哪里有问题?Apphelp的哪项机制如何能被利用、以及是如何劫持高权限进程的?接下来笔者就将深入地介绍这些内容,详细地解答这些问题。

1. Apphelp与NtApphelpCacheControl

在Windows 8.1 系统上,NtApphelpCacheControl这个系统调用,顾名思义,可以控制系统中Apphelp规则的快速缓存数据。

Apphelp是微软从WindowsXP操作系统开始引入的一项兼容性解决方案,官方的名字是”Application Compatibility Database”(应用程序兼容性数据库)。这套解决方案的目的是减少操作系统的向下兼容成本,不是在操作系统层面,而是通过这套兼容数据库引擎提供包括Shim(Hook)在内的大量控制功能,来做到实时地识别和修改特定存在兼容性问题的流行软件程序,使其能够兼容新的操作系统。

关于这套机制,微软官方从Windows7开始, 有一些功能性的介绍文档(http://technet.microsoft.com/en-us/library/ee461265(v=ws.10).aspx),也可以通过官方的”Microsoft Application Compatibility Toolkit”(ACT),在Windows7以上的操作系统上方便里查看、修改和添加本地的兼容性数据库。在此之前,Alex ionescu和一些其他的国内外研究者也深入地研究过兼容性数据库和Shim机制(http://www.alex-ionescu.com/?p=39) 。

微软操作系统的各个版本积累了针对应用程序的大量兼容性修改的方法和基础数据,使这些应用程序可以流畅地运行在新的操作系统中,这为微软的操作系统在世界范围内的复杂环境中广泛运行和推广起到了重要的作用。在Windows Vista发布后,它带来的海量革新的同时,也导致大量应用程序出现兼容性问题,成为该系统被人诟病的原因之一。为此,在其后的Windows7操作系统开发过程中,微软加大了对应用程序兼容性修复方面的重视和投入,不仅更广泛地收集和编制应用程序兼容性修复规则,也对这套兼容性机制做了升级换代。

本次出现问题的NtApphelpCacheControl也是支持这套机制的一个新的接口。这已经不是该函数第一次出现安全漏洞, 在j00ru Syscan2013上的关于Bochspwn的议题上,就公开了一个涉及NtApphelpCacheControl处理缓存代码中存在的竞争条件漏洞(CVE-2013-1278)。阅读下面的内容的同时,建议读者能看一看这个slides中NtApphelpCacheControl相关的内容(75~94页)(http://vexillium.org/dl.php?syscan_slides.pdf) 以及James的POC源码。

为什么Apphelp需要这个接口呢?这还需要从XP系统说起,应用程序兼容性的主数据库实际存储是在Windows安装目录下的AppPatch\sysmain.sdb文件中的,但是每次进程启动、模块加载时都去检查这个数据文件显然开销太大。为了能快速处理针对这些可执行程序的兼容性规则,就需要在内存中缓存这个数据库的机制。在Windows XP时代, Apphelp使用一块名为ShimSharedMemory的全局共享Section来在不同进程之间共享识别、修改可执行程序的兼容性数据内存。

从Windows 2003开始,NtApphelpCacheControl函数被引入系统调用中,作为apphelp的内核入口,在内核中提供了整套新的应用程序兼容性数据库检索、缓存的功能,使得应用程序可以跨Session、跨隔离,并且快速查询、应用兼容性数据库中的规则和处理方案,其缓存功能也实现了在尽量减少性能消耗的同时,快速地针对系统内已知的存在兼容问题的程序进行更快速地修复。

在Windows2003,Windows Vista,Windows7和Windows8操作系统上,NtApphelpCacheControl是实现在系统内核ntoskrnl内部的,从Windows8.1操作系统开始,为了能够减少升级成本,内核将该系统调用的绝大部分最终实现在了一个新的内核模式驱动程序ahcache.sys内部,系统内核通过发送设备控制命令给该驱动来实现该接口的绝大部分功能,在Windows10技术预览版上,我们看到这个驱动又进行了一次比较大的更新。

NtApphelpCacheControl的原型为:

NTSTATUS NtApphelpCacheControl(APPHELPCOMMAND Command , PVOID CommandData);

 

这个Command提供了针对应用程序兼容性数据库缓存的查看、添加、删除等等一系列功能。其中Command的枚举功能和CommandData的数据结构,J00ru的议题和James的源码中都提供了一些,但都不全面也有不少错误,通过IDA查看实现代码很容易就可以看清楚,这里我将这些功能的Command id、对应的控制码(适用于 Windows8.1)和对应实现的功能整理汇总后列出:

enum APPHELPCOMMAND
{
AppHelpCahceLookup,                //  IoControlCode: 0x220003*

AppHelpCahceRemove,                //  IoControlCode:  0x220007*

AppHelpCahceUpdate,                //  IoControlCode:  0x22000B*

AppHelpCacheFlush                  //  IoControlCode: 0x22000F* 

AppHelpCacheDump,                  //  IoControlCode: 0x220013 

AppHelpCacheNotifyStart ,          //  IoControlCode: 0x220017 

AppHelpCacheNotifyStop,            //  IoControlCode: 0x22001B

AppHelpCahceForward,               //  IoControlCode:  0x22001F

AppHelpCacheQuery,                 //  IoControlCode: 0x220023

AppHelpQueryModule,                //  IoControlCode: 0x220027

AppHelpRefresh,                    //  IoControlCode: 0x22002B

AppHelpCheckForChange,             //  IoControlCode: 0x22002F

AppHelpQueryHwId,                  
};

*:James的POC源码中,对这四个控制码少了一个0

AppHelpCacheLookup: 在Apphelp cache中寻找匹配的、需要处理的记录。

AppHelpCacheRemove:   删除匹配的Apphelp Cache记录

AppHelpCahceUpdate: 插入记录到Apphelp Cache

AppHelpCacheFlush:Flush AppHelp cache,将AppHelpCache的缓存数据清空(根据Flush的标志不同,不一定真的删除内存中的数据,只是去掉某些标记),并刷新到磁盘(注册表)上。

James源码中认为这个命令是没用处的AppHelpEnum。这是错误的 ,apphelp!ShimFlushAppcompatCache->kernelbase!BaseFlushAppcompatCache->kernel32!BaseFlushAppcompatCacheWorker  还在使用这个来清空shim的兼容数据数据。他应该是将AppHelCacheFlush看漏了,源码里对于NotifyStart\Enum等后面的几个都差了一个数。

AppHelpCacheDump:这才是一个“没用”的功能, AppHelp会枚举缓存中的数据,针对枚举的项目并不做操作。笔者猜测这个命令之所以叫“Dump”,可能是一个调试功能。于是笔者从WDK中找了个checked build的内核来看了下。果然在checked build内核中,会枚举缓存中的元素,并逐个打印出记录要去匹配的文件名记录。在Free build中,对应的代码被去掉了,所以成了一个看似无用的命令,实际在checked build,微软的开发人员可能是有对应的工具去通过这个接口方便地观察缓存中都有那些记录。

AppHelpCacheNotifyStart/AppHelpCacheNotifyStop:这两个命令主要用于同AppHelp的服务通讯,AppHelp内核会通过\AhcPort这个ALPC Port同Application Experience服务通讯。这两个命令用于重连/停止 同服务的LPC通讯。

AppHelpCacheForward: 用于将缓存可执行程序的信息排队,然后转发给Application Experience服务处理

AppHelpCacheQuery:  用于获取Shim Cache中的数据内容、ShimCache的相关统计和队列。

AppHelpQueryModule/AppHelpCacheRefresh/AppHelpCheckForChange/AppHelpQueryHwId:这些是Windows8.1新增的命令,和这里就不详细介绍了。

下面介绍这个调用中重要的数据CommandBuffer的结构,这个结构的数据驱动了Lookup/Remove/Update/Forward等大部分基本命令。

James在源码中定义了相关结构,不过看上去对这个结构的了解也不是很深入。

这个CommandBuffer的结构(我这里称为APPHELP_COMMANDDATA),是由三个结构体组成的:

其中第一个结构体用于存储整个shim缓存和统计内容,被AppHelpCacheQuery命令使用;

第二个结构体用于在AppHelpCacheLookup/AppHelpCacheRemove/AppHelpCacheUpdate这三个命令时来告诉接口检索、删除和添加的项目内容;

第三部分则用于AppHelpCacheForward命令,提供forward,用于提供转发给服务的相关数据。

无论是哪个命令,CommandBuffer都同时包含着三个结构体,这也是为什么J00ru的议题和James的源码里都提到提交的缓存结构前面有大量无用的0数据的原因。

下面给出这个结构的定义:

typedef struct APPHELP_COMMANDDATA{

APPHELP_CACHE_QUERY QueryData;                  //Query full data

APPHELP_CACHE_ENTRY EntryData ;                 //Lookup/Remove/Update entry

APPHELP_CACHE_FORWARD ForwardData;             //Data for forward

} APPHELP_COMMANDDATA, *PAPPHELP_COMMANDDATA;

其中APPHELP_CACHE_QUERY/APPHELP_CACHE_FORWARD的结构和本次漏洞无关,就留给感兴趣的读者去研究了。

需要注意的是,QueryData的数据长度会影响后面的EntryData(这个长度在Win7\Win8上不相同),Win7上是0x90,Win8.1上James已经提供了是0x98(这也是为什么Win7上这个POC无法工作的原因之一)。这点看一看ApphelpCacheControlValidateParameters或AhcValidateAndGetParameters就可以了解了。

这里提供一下APPHELP_CACHE_ENTRY的数据结构:

typedef struct APPHELP_CACHE_ENTRY{

DWORD Flags ;

ULONG CacheReturnFlags ;

HANDLE FileHandle;

HANDLE ProcessHandle ; //Only in Win8/Win8.1


UNICODE_STRING FileName;

UNICODE_STRING PackageFullName;    //only in Win8/Win8.1

DWORD  DataBufferSize;

PVOID DataBuffer;

} APPHELP_CACHE_ENTRY, *PAPPHELP_CACHE_ENTRY;

其中 Flags/CacheReturnFlags分别指明了这条Cache的功能和作用,尤其是对于DataBufferSize/DataBuffer是否有效的控制,是否在关机时保存到注册表中等。

FileHandle/FileName是对应要进行处理的可执行程序(包括模块)的对应句柄和文件名,之所以要提供文件句柄,是因为内核使用一个AVL Table来存储Entry数据,使用文件句柄的FileTime来做其中平衡树的索引。

DataBufferSize和DataBuffer描述了Entry相关的规则数据的内容。

ProcessHandle和PackageFullName仅在Windows8及以后的操作系统上使用。

DataBuffer可以保存由RING3存储的数据结构,一般来说由APPHELP_CACHEUPDATE更新到内核缓存中,APPHELP_CACHELOOKUP获取后使用,Shim引擎使用的数据结构,我称为APPHELP_LOOKUP_RESULT(即James源码中的APPHELP_QUERY结构),如下:

#define MAX_EXE_TAGS 16

#define MAX_LAYER_TAGS 8

typedef struct APPHELP_LOOKUP_RESULT

{

DWORD ExeTags[MAX_EXE_TAGS];

DWORD ExeFlags[MAX_EXE_TAGS];

DWORD LayerTags[MAX_LAYER_TAGS];

DWORD LayerFlags;

DWORD AppHelpTag;

DWORD ExeTagsCount ;

DWORD LayerTagsCount;

GUID ExeGuid;

DWORD Flags2;

DWORD Unknown;

DWORD Unknown2;

GUID Guid2[16];

};

如James源码中所使用的,  其中ExeTags指明了在sdb数据中的修复方案的tag id,ExeTagsCount是tag的数量,最多16个, LayerTags是Layer规则的数据。

通过这些命令和数据结构,我们大概了解了NtAppHelpCacheControl提供的能力,那么RING3是如何使用它的呢?

简单地说,Ring3的Module Loader等模块在模块加载、进程启动等事件触发时,会调用apphelp.dll中的相关接口, apphelp.dll再调用kernel32/kernelbase内BasepShim*相关的API,通过NtAppHelpCacheControl中的CacheLookup功能查询模块的可执行文件是否在缓存的数据中,如果存在就按照规则数据库中对应的规则处理,相关的数据通过CacheForward发送到服务进程后,会调用CacheUpdate功能缓存被使用的数据,加快二次查询的速度。

Token相关的问题

了解了AppHelpCache的接口和基本工作原理后,我们很容易可以明白,如果可以低权限的程序可以调用AppHelpCacheUpdate命令添加缓存,那么可以劫持任意高权限程序,利用shim规则对其进行修改,实现权限提升。

那么我们回来看看这个漏洞的成因,如James在说明里提到的,这个漏洞的原因是因为用于验证AppHelpCacheUpdate是否运行被调用的AhcVerifyAdminContext函数,验证Token存在问题,这个函数很简单,我们使用Hex-rays decomplier得到伪代码:

NTSTATUS AhcVerifyAdminContext()
{
  retstatus = STATUS_ACCESS_DENIED;
  CurrentThread = KeGetCurrentThread();
  CurrentProcess = PsGetCurrentProcess();
  TokenType = 0;
  TokenObj = PsReferenceImpersonationToken(CurrentThread, &CopyOnOpen, &EffectiveOnly, &ImpersonationLevel);
  if ( TokenObj || (TokenObj = PsReferencePrimaryToken(CurrentProcess), TokenType = 1, TokenObj) )
  {
    if ( SeQueryInformationToken(TokenObj, 1, &TokenUserInformation) >= 0 )
    {
      if ( RtlEqualSid(_SeExports->SeLocalSystemSid, TokenUserInformation->User.Sid) || SeTokenIsAdmin(TokenObj) )
      {
        retstatus = STATUS_SUCCESS;
      }
      ExFreePoolWithTag(TokenUserInformation, 0);
    }
    else
    {
      AhcTracePrintf(0, "AhcVerifyAdminContext", 937, "Failed to query token information.\n", v5);

    }
    if ( TokenObj )
    {
      if ( TokenType == 1 )
      {
        PsDereferencePrimaryToken(TokenObj);
      
      }
      else
      {
        PsDereferenceImpersonationToken(TokenObj);
      }
    }
  }
  else
  {
    AhcTracePrintf(0, "AhcVerifyAdminContext", 929, "Failed to get effective token", v5);

  }
  return retstatus;
}

函数的功能很简单,首先试图获取线程模拟的token,如果线程的模拟token不存在,就获取当前进程的主token对象,获取token对象后, 通过SeQueryInformationToken(TokenUser)获得Token的用户信息。

接着,对比如果下面token对象符合下面两种情况的任一种,就返回STATUS_SUCCESS允许AppHelpCacheUpdate操作,否则就返回STATUS_ACCESS_DEIND拒绝操作:

1. Token的用户SID匹配LocalSystem的SID

2.Token通过SeTokenIsAdmin的验证,也就是token用户组内有启用的Adminsitrator组。

那么这个对比就是James所说的验证Token有问题的部分,有问题的原因就如James所说, 是因为没有去判断ImpersonationLevel。

这是因为被模拟的Token的SID并不能决定Token就真的拥有对应SID的权利,可以参考微软关于SECURITY_IMPERSONATION_LEVEL的MSDN解释(http://msdn.microsoft.com/en-us/library/windows/hardware/ff556631(v=vs.85).aspx )。

低于SecurityImpersonation的模拟token其实并不是以被模拟的Client的安全上下文运行的,所以仅仅通过SID来判断调用者是否具备LocalSystem的权限是不够,类似的问题其实是Windows的内核、内核模式驱动程序中还有不少地方存在,感兴趣的读者可以再进行一些挖掘。

以James源码的方法为例,通过Bits服务的BackgroundCopyManager接口创建一个下载任务后,代码将自己的Notify对象设置为任务的通知接口。

此时服务在下载通知时会调用对应的对象,接着combase通过LPC回调试图调用对象的接口函数,而在调用前,就会使用RpcImpersonateClient->NtAlpcImpersonateClientOfPort,由AlpcpImpersonateMessage来为执行回调接口准备Alpc port指定的安全上下文。

由于Bits的OLE Port指定的允许的SecurityQos->ImpersonationLevel级别的模拟,模拟完成后调用到James的处理代码,他的代码打开并保存了线程的token句柄,由此获得了一个SecurityIdentification级别的Token句柄 ,Token是来自Bits服务的安全上下文,因此Token的SID自然也就是NT AUTHORITY\SYSTEM(LocalSystem)

这里Bits服务本身是没有安全问题的,因为ALPC获得和模拟的token只是SecurityIdentification级别,即使模拟这个token(就如James的代码后面所做的),也无法以System权限上下文工作,在访问对象ACL时,会被拒绝访问(可以参考SeAccessCheck中的实现和判断),也不会被识别为admin/system(可以参考SeTokenIsAdmin在Windows7以上操作系统的实现),但是NtAppHelpCacheControl这里只判断Token SID的方式,就导致了安全检查被绕过,也是这个漏洞的根本原因。

漏洞的利用

James提供的POC针对这个漏洞利用的方法是劫持一个UAC默认级别下会不区分命令行,无提示自动提权的程序ComputerDefaults.exe,找到regsv32修复的的sdb tag(通过替换模块镜像为regsvr32来进行兼容修复),并将其设置到apphelp缓存中,这样,在启动ComputerDefaults.exe的时候,实际启动的可执行程序的镜像就被Apphelp替换成了regsvr32.exe ,而启动的时候命令行上加上想要注入高权限进程的dll文件,就会使得regsvr32.exe在高完整性级别上加载我们想要加载的DLL,James的DLL代码里实现的是启动一个计算器程序。

这个利用方法引来了一些争议,很多人认为这样只证明可以绕过UAC默认级别下中完整性级别到高完整性级别的弹框,通常来说不能说是安全漏洞,而且实际之前也有很多公开的技巧可以绕过UAC提示。

但就如James自己说的,UAC的绕过仅仅只是个方便的演示方式,想一想测一测就能发现,这个漏洞还有更多的利用方式。

以IE11 的沙箱(PM)为例,这个漏洞需要的相关功能(包括BITS Token的获取、NtAppHelpCacheControl的调用、sdb Tag的获取等等)都是可以在IE11沙箱的低完整性级别下执行的,那么恶意代码一旦进入沙箱,就可以利用这个漏洞劫持一个常用的中完整性级别程序(甚至系统中经常被启动的高完整性、系统完整性级别程序),就可能通过这个漏洞穿透沙箱,在沙箱外执行恶意代码。使用icacls.exe /setintegritylevel 给测试程序设置Low完整性级别后,可以很容易证实这点。

即使是在通过Users用户组的账户登录的情况下,如果同时或之后有管理员账户登录,由于这个AppHelp Cache是全局跨Session的,因此管理员运行的程序也会可能受到劫持的影响,发生权限提升。

这些都是通过James的测试程序就能完成的攻击,但是这个攻击的一个缺点是必须要有高权限的程序去启动, 那么还有更好的利用方式么?

肯定是有的,大家看到刚才我们介绍的AppHelpCacheFlush功能了吧,这个接口的调用者验证也存在和 AppHelpCacheUpdate同样的问题,那么通过这个接口就可以直接将内存中已经添加的AppHelpCache刷入注册表中(HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\AppCompatCache下AppCompatCache),这样下次开机时就会加载并应用添加的规则,这样重新启动后就可以更方便的劫持想要劫持的程序。

同时,通过调用低完整性级别/低权限账户或AppContainer完整性级别下允许调用的某些服务LPC/COM接口,也可以使其启动特定的高权限程序,并可能进行劫持。

这些技术含量不高,已经提供了这么多Tips,再具体的方式就需要大家发挥想象力了,最后再提点细节:AppHelp规则不仅能应用于EXE进程创建,也可以应用在指定的任意模块加载时。

Windows7的问题

James在说明里提到了Windows7的问题,在Windows7上, AppHelpCacheUpdate这条指令专门被一个特别的ApphelpCacheVerifyContext检查所保护(对于其他被保护接口使用的是ApphelpCacheVerifyAdminContext,和Windows8/8.1一样存在这个安全漏洞),这个检查更严格,仅在主TOKEN具备TCB特权时才可以通过,代码如下:

NTSTATUS ApphelpCacheVerifyContext()
{

  status = STATUS_SUCCESS;
  if ( PsGetCurrentThreadPreviousMode() && SeSinglePrivilegeCheck(SeTcbPrivilege, UserMode)  == FALSE)
    status = STATUS_ACCESS_DENIED;
  return status;
}

看上去这个检查函数是无法绕过了,James同时提到,这个检查是在CommandBuffer中的某些Flags匹配时,才会进行,也许可以绕过,那么实际如何呢?

我们逆向NtAppHelpCacheControl在Win7上的相关实现可以得知,在CommandBuffer->Flags的Bit 2,3为1时,是不进行这个检查的,这样的Entry是可以通过AppHelpCacheUpdate的检查,加入缓存中的。

但是,我们再来看AppHelpCacheLookup的相关实现就不难发现, 仅当entry->Flags的bit 0为1时,Lookup才为调用者返回CommandBuffer->DataBuffer和DataBufferSize,而就像在第一节里提到的,这两个域描述了APPHELP_LOOKUP_RESULT数据结构,没有这个数据我们无法指定要应用的规则。

所以至少通过目前的分析来看,通过修改Flags的方法,是无法将达成我们的劫持数据加入缓存并生效的目的的,暂时可以认为这个漏洞难以在Windows7上实现利用。

你也许会喜欢:

作者:奇虎360技术博客
分享奇虎360公司的技术,与安全的互联网共同成长。
原文地址:NtApphelpCacheControl漏洞分析, 感谢原作者分享。

发表评论