查看原文
其他

CVE-2019-8603(macOS漏洞):利用一个越界读漏洞实现了Safari沙箱逃逸

lucywang 嘶吼专业版 2019-06-20

在今年Pwn2Own中,有研究人员发现利用一个越界读漏洞竟然实现了Safari沙箱逃逸,然后利用kextutil中存在的TOCTOU获得内核代码执行权限。

目前该漏洞已经被命名为CVE-2019-8603,简单来说,这是一个存在于Dock和苹果卸载网站(com.apple.uninstalld)中的堆越界读取漏洞,该漏洞将导致攻击者调用CFRelease并在macOS上实现Safari浏览器沙盒逃逸,最终获取到目标设备的root权限。

另外,在测试过程中,CVE-2019-8606允许研究者通过kextutil中的竞态条件(race condition),用root权限实现内核代码执行。如果再借助一个qwertyoruiopz和bkth开发的WebKit引擎漏洞(远程代码执行漏洞),则研究人员可以完全实现Safari沙箱逃逸。

在本文发布时,该漏洞已经在最新的macOS 10.14.5版本中被修复了,所以本文我们可以对它的来龙去脉进行详细复盘了。

详细复盘

本来研究人员是计划测试一个模糊测试工具的代码覆盖率,代码覆盖(Code coverage)是软件测试中的一种度量,描述程式中源代码被测试的比例和程度,所得比例称为代码覆盖率。但测试到AXUnserializeCFType函数时,却发现发现了CVE-2019-8603漏洞,这个函数是去年的Pwn2Own中发现的一个简单解析器,不过当时并没有发现其中有什么漏洞。不过在发现有漏洞后,研究人员对这个函数进行了仔细研究,发现它竟然是CoreFoundation对象序列化的另一种实现方式。Core Foundation框架(CoreFoundation.framework)是一组C语言接口,它们为iOS应用程序提供基本数据管理和服务功能。而AXUnserializeCFType则是HIServices框架的一部分,而且代码就存储在对应的dylib库中。

可以通过此函数对CFAttributedString对象进行反序列化,CFAttributedString是一个字符串,其中每个字符与CFDictionary相关联,CFDictionary包含了描述给定字符的任意属性。这些属性可以是颜色,字体或用户关心的任何其他内容。而在本文的示例中,CFDictionary的属性是代码执行。

为了帮助大家更直观地了解CFDictionary的属性,研究人员专门列出了数据结构,数据结构使用游程长度(run-length)压缩,而不是为每个单独的字符专门分配字典:

// from CFAttributedString.c

struct __CFAttributedString {

    CFRuntimeBase base;

    CFStringRef string;

    CFRunArrayRef attributeArray;  // <- CFRunArray of CFDictinaryRef's

};

// from CFRunArray.c

typedef struct {

    CFIndex length;

    CFTypeRef obj;

} CFRunArrayItem;

typedef struct _CFRunArrayGuts { /* Variable sized block. */

    CFIndex numRefs;                        /* For "copy on write" behavior */

    CFIndex length;                         /* Total count of values stored by the CFRunArrayItems in list */

    CFIndex numBlocks, maxBlocks;           /* These describe the number of CFRunArrayItems in list */

    CFIndex cachedBlock, cachedLocation;    /* Cache from last lookup */

    CFRunArrayItem list[0]; /* GCC */

} CFRunArrayGuts;

/* Definition of the CF struct for CFRunArray */

struct __CFRunArray {

    CFRuntimeBase base;

    CFRunArrayGuts *guts;

};

例如,字符串“attribuis hard”可以在内部表示为3个CFRunArrayItems:

1.从索引0开始,长度11,“粗体”属性标识;

2.从索引11开始,长度4,没有属性标识;

3.从索引15开始,长度4,“斜体”属性标识。

显然有一些不变量必须被维护,例如所有运行的union类型跨越整个字符串,并且没有任何运行重叠。

反序列化函数cfAttributedStringUnserialize有两个执行路径,第一个路径很简单,它只读取一个字符串,并为属性dict调用带有NULL的CFAttributedStringCreate。这意味着第二个路径必须解析一个字符串,以及一个包含了范围和关联字典的列表,然后调用内部函数_CFAttributedStringCreateWithRuns:

CFAttributedStringRef _CFAttributedStringCreateWithRuns(

        CFAllocatorRef alloc,

        CFStringRef str,

        const CFDictionaryRef *attrDictionaries,

        const CFRange *runRanges,

        CFIndex numRuns) { ...

解析器将会确保运行的次数和字典的数量匹配,但是它不会对实际的字符串范围信息执行任何验证。同样_CFAttributedStringCreateWithRuns也无法做到这一点。

    for (cnt = 0; cnt < numRuns; cnt++) {

CFMutableDictionaryRef attrs = __CFAttributedStringCreateAttributesDictionary(alloc, attrDictionaries[cnt]);

__CFAssertRangeIsWithinLength(len, runRanges[cnt].location, runRanges[cnt].length); // <- ouch

CFRunArrayReplace(newAttrStr->attributeArray, runRanges[cnt], attrs, runRanges[cnt].length);

CFRelease(attrs);

    }

因此,研究者将能够使用完全可控的range以及newLength值来调用CFRunArrayReplace。

void CFRunArrayReplace(CFRunArrayRef array, CFRange range, CFTypeRef newObject, CFIndex newLength) {

    CFRunArrayGuts *guts = array->guts;

    CFRange blockRange;

    CFIndex block, toBeDeleted, firstEmptyBlock, lastEmptyBlock;

    // [[ 1 ]]

    // ??? if (range.location + range.length > guts->length) BoundsError;

    if (range.length == 0) return;

    if (newLength == 0) newObject = NULL;

    // [...]

    /* This call also sets the cache to point to this block */

    // [[ 2 ]]

    block = blockForLocation(guts, range.location, &blockRange);

    guts->length -= range.length;

    /* Figure out how much to delete from this block */

    toBeDeleted = blockRange.length - (range.location - blockRange.location);

    if (toBeDeleted > range.length) toBeDeleted = range.length;

    /* Delete that count */

    // [[ 3 ]]

    if ((guts->list[block].length -= toBeDeleted) == 0) FREE(guts->list[block].obj);

    ...

仔细观察上图中的代码段[[ 1 ]],很明显,编写这段代码的人虽然对传入的参数有所怀疑,但却没有更改函数签名以返回错误信息。

而在代码段[[2 ]]中,错误执行就开始了:如果range.location太大,那么blockForLocation就会返回一个越界索引。这意味着,代码段[[ 3 ]]的FREE在调用CFRelease(使用越界索引获取的指针来实现调用)时,漏洞就被触发。

漏洞被触发后,又会导致对objc_release的调用,objc_release会开启vtable查询,为release选择器寻找需要的Objective-C函数。

id objc_msgSend(id obj, SEL sel, ...)

{

  __objc2_class *cls; // r10

  __int128 *v3; // r11

  __int64 i; // r11

  if ( !obj )

    return 0LL;

  if ( obj & 1 )

  {

    // [...]

  }

  else

  {

    cls = (*obj & 0x7FFFFFFFFFF8LL);

  }

  v3 = &cls->vtab[cls->mask & sel];

  if ( sel == *v3 )

    return (*(v3 + 1))();   // <- OUCH!!

请注意,如果研究人员完全控制传入的obj值,则可以轻松输入第一次检查的else情况并达到间接调用,前提是研究人员可以将伪造的对象放在已知位置,并且知道release选择器的地址。幸运的是,在本所讲的沙盒逃逸环境中,这几个前提条件都可以被满足,因为所有代码库都会映射到系统范围的相同地址,其中就包括选择器在内。

堆内存

研究人员为了将这个漏洞转换为对CFRelease的受控调用,就必须将某些值放在被越界访问的CFRunArray之后。为此,研究人员需要通过使用解析器本身自带的分配和释放原语来实现这一点。具体地说,解析器允许他们创建一个字典并重复设置解析对象,然后这些对象又在输入流中被解析出来。

通过向字典中添加一个新对象,研究人员可以分配一个对象。稍后通过覆盖该对象,对象将被释放。这个原语足以创建许多具有可预测性的数据序列了,其中就有CFRunArray和负责控制数据的CFString对象。

Dock中的漏洞测试

要在Dock中触发了这个安全漏洞,需要连续两次使用该漏洞,首先利用Dock托管的com.apple.dock.server服务,然后再在WebContent沙箱中进行访问,基于Mach的协议是通过MIG (Mach接口生成器)创建的。

研究人员测试时,攻击的是消息ID 96508的处理程序,研究人员并没有真正地将其进行什么处理。只要AXUnserializeCFType可以接收并解析某些数据,并将它们作为联接外部的内存描述符即可。

MIG还为测试提供了数千兆字节的数据,这些数据可以映射到接收器的地址空间中,说到这你可能明白了这就是大家都熟知的堆喷射技术,这样研究人员就可以将任意数据存放到他们想要的位置了。

接下来,就是要确保堆喷射对象的每一个页面都要有重复相同的数据(约800MiB),这些数据由下面这两个部分组成:

1.伪造的对象用来触发间接调用,并输入一个小型JOP stub来对堆结构进行pivot处理,PIVOT是通过将表达式某一列中的唯一值转换为输出中的多个列来旋转表值表达式,并在必要时对最终输出中所需的任何其余列值执行聚合;

2.ROP链可以完成所有自动化的过程。

请注意,此时的漏洞还明显无法完成信息的泄漏。

苹果卸载网站(com.apple.uninstalld)中的漏洞测试

由于研究者的真实目的是使用root权限实现内核代码的执行,所以在上面的分析快结束的时候,他们在谷歌中搜索了一下AXUnserializeCFType,并发现了Project Zero的1219漏洞,这是一个非常简单的越界漏洞,在2017年被发现。当时安全研究人员Ian Beer认为,该漏洞不会解析那些不受信任的数据。

但无论如何,Ian Beer当时已经提到,攻击者会以root身份运行的com.apple.uninstalld服务与Dock对话,并在Dock提供的数据上调用AXUnserializeCFType。所以,攻击者可能会冒充Dock并为为uninstalld提供有效载荷。

不过在实测时,研究人员遇到了以下问题:

1.要让uninstalld执行任何操作,就必须为其提供授权令牌,该令牌具有嵌入在Dock二进制文件中的特定权限。

2.研究人员实际上并没有在Dock中映射出他们自己的代码,这可能是由于在2018年的某个时候该漏洞被添加了某些代码签名机制。

3. 在Dock运行时,研究人员无法在com.apple.dock.server端口上注册,因为Dock占用了该端口。

在创建和转储授权令牌之后,为什么不能直接终止Dock并从另一个进程的端口上注册,这个问题目前研究人员也没有搞清楚。无论如何,研究人员最终还是在Dock内运行的ROP链中执行以下所有操作:

1.调用AuthorizationCreate和AuthorizationMakeExternalForm以生成具有uninstalld权限的令牌;

2.生成一个名为fakedock的二进制文件,用于注册一个mach服务;

3.查找fakedock服务;

4.将com.apple.dock.server服务的接收端以及授权令牌发送到fakedock;

5.潜伏下来。

之后,fakedock将等待接收权限和令牌,然后模拟com.apple.dock.server服务。然后它会与uninstalld进行会话,以使其开始卸载一个应用程序,卸载时,该应用程序将依次触发 “连接”并通过研究人员需要以适当方式处理和响应的特定MIG调用序列接收漏洞的有效载荷,uninstalld的ROP链只是使用研究人员的最终含有root权限的有效载荷来调用系统。

利用kextutil中存在的TOCTOU获得内核代码执行权限

TOCTOU是time-of-check-to-time-of-use的缩写,是竞争危害 (race hazard) 又名竞态条件 (race condition)的一种,它是指计算机系统的资料与权限等状态的检查与使用之间,因为某特定状态在这段时间已改变所产生的软件漏洞。有了上面的测试,kextutil允许研究人员以root身份加载内核扩展,但它会执行某些检查,例如代码是否签名以及是否得到用户的批准。要执行漏洞,显然需要绕过这些检查,在无需用户交互的条件下,来加载研究人员自己的未签名代码。研究人员绕过文件检查的首选方法是使用竞争条件,本文使用的是符号链接。

在进行了最近由逻辑错误专家CodeColorist详细描述的所有检查之后,kextutil会将所有的函数调用请求加载进IOKit!OSKextLoadWithOptions,并向内核发送一个加载请求。

但是,如果提供的kext路径是符号链接,就可以将它直接用来连接不同的操作了。

现在,需要满足几个条件,才能使完全实现漏洞的利用,其中一个就是交换符号链接目的地址的时机是否正确。为了实现这一点,研究人员运行了kextutil -verbose 6 -load / path / to / kext,该命令会输出大量调试信息,并提供一个完整的POSIX管道来作为STDOUT。研究人员可以在代码执行的过程中在特定的地址生成管道溢出,并挂起进程,这意味着研究人员可以替换掉符号链接并清除管道中的数据。

最终,研究人员就能够成功加载未签名的kext,进行任意代码执行,并最终实现Safari沙盒逃逸。

不过,在此漏洞利用完成后,安全研究人员Linus Henze则提出了一种更可靠的方法来触发竞争条件。由于kextutil实际上有一个标志-i,它会在安全检查后提示用户,但在加载kext之前,并没有出现“你想现在改变你的符号链接并继续加载其他东西吗?”的提示。

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存