Introduction

This blog is created with Insu Yun and Wen Xu.

In the Real World CTF 2019 final, we designed a guest Safari exploitation (w/ sandbox escape) challenge based on two full-chain Safari exploits we built previously. We leveraged the new JSC structureID entropy mitigation bypass techniques to get RCE in the content process. To escape the sandbox, we formulated some flexible techniques for exploiting UAF bugs in Webkit’s broker IPC.

We would like to thank Meng Xu for helpful ideas, and all teams in the final for making efforts to solve our challenges.

Attacking JavaScriptCore

To simulate an attack against the webcontent process, we deliberately introduce a bug into the JSC. Even it is a runtime bug, we can exploit it by introducing unintended JIT side effects.

Analyzing the FastStructureCache

We introduce a vulnerable FastPath to enable RegExp's allocation cache, namely, FastStructureCache. The full patch could be found here.

Here is the code snippet related to the bug:

class FastStructureCache final : public JSNonFinalObject {
public:
    using Base = JSNonFinalObject;
    static Structure** fastCacheStructure;
    static uint64_t fastCacheSizeMax;
    static uint64_t fastCacheSizeUsed;

    static Structure* createStructureFastPath(VM& vm, 
        SGlobalObject* globalObject, JSValue prototype, 
        const TypeInfo& typeInfo, const ClassInfo* classInfo)
    {
        if (fastCacheStructure == NULL) {
            fastCacheStructure = new Structure*[fastCacheSizeMax];
            uint64_t idx = 0;
            while (idx < fastCacheSizeMax) {
                // * [1]
                // Later, we will set the correct globalObject and prototype
                fastCacheStructure[idx] = Structure::create(
                    vm, globalObject, prototype, typeInfo, classInfo);
                idx++;
            }
        }
        if (fastCacheSizeUsed < fastCacheSizeMax) {
            Structure* return_value = fastCacheStructure[fastCacheSizeUsed];
            // * [2]
            // set the correct global object and prototype
            return_value->setPrototypeWithoutTransition(vm, prototype);
            return_value->setGlobalObject(vm, globalObject);
            fastCacheSizeUsed += 1;
            return return_value;
        }
        return Structure::create(vm, globalObject, prototype, typeInfo, classInfo);
    }

protected:
    FastStructureCache(VM&, Structure* structure);
};

At [1], we create a cache for the allocation of structures related to RegExp. Although It does not have proper prototypes, we will set it with the corresponding prototype later at [2]. It at least introduces two bugs. First, the type info and class info are never changed. The objects created later will hold incorrect information in its structure. However, I haven't tried to find a way to exploit it. Second, as Figure 1 illustrates, comparing with the normal process in Structure::create, it fails to mark that the object has become a prototype with didBecomePrototype().


Figure 1. Wrongly create the strucures


Prelimitary exploitation primitives

According to lokihardt's comments, JSC doesn't allow native arrays to have Proxy objects as prototypes. However, with this bug, we can break the assumption holding by the JSC.

As Figure 2 delineates, suppose there is a RegExp has appeared in an native array's protochain. While putting a Proxy object into RegExp's protochain, it will not trigger any conversion to swithToSlowPutArrayStorage. Thus, we follow the same tricks described by lokihardt. i.e., introducing unintended side effects by abusing the HasIndexedProperty IR, causing type confusion in the JSC.


Figure 2. JavaScriptCore doesn't allow native arrays to have Proxy objects as prototypes

We give a code snippert to demonstrate the addrOf and fakeObj primitives. After executing the poc, we can notice the segment fault.

➜  ./jsc pwn.js
0x00007fa42ebdc240
[1]    27955 segmentation fault  ./jsc pwn.js

Leaking valid structureID

Initially, we formulated an approach to leak the strctureID while writing the exploit chains(Actually, there is a team that used the same technique with us). However, there is a talk at BlackHat-EU proposed a public method against structureID randomization. Another team also used it during the competition.

The bypass trick based on a simple but effective fact that not all builtin functions and "mechanisms" rely on valid structureIDs.

One way to utilize this weakness is that abusing Function.prototype.toString.call() with a special fake object. Let us walk through the entire process.

Figure 3 illustrates how to leverage it to leak the strcutureID. We need to fake three objects (An object w/o valid structureID, a fake FunctionExecutable object, and a fake UnlinkedFunctionExecutable object), bypassing the isBuiltinFunction check. In this way, we can leak a valid structureID and a butterfly pointer.


Figure 3. Leaking the valid structureID. (The blue boxes represent fake object, and the green boxes represent normal objects.)

We give a snippet of example code to show how to leak the structureID:

➜  ./jsc pwn.js
Structure ID: 8230700009a5e

Bypassing the gigacage isolation and get code execution

Two approaches have been widely used in the community to bypass Webkit's gigacage (an isolated heap mechanism in JSC), i.e., abusing WASM's Memory buffer or Object's butterfly, because the gigacage does not isolate them.

By using the OOB read described in the previous section, we can also leak the butterfly of the target object. So it could be much easier if we use the butterfly approach.

Finally, we overwrite a jitted function's memory into shellcodes to get code execution.

Furthermore, you can find a copy of the binary to do your examination from here.


Figure 4. Bypassing gigacage isolation using a fake object on butterfly. (The blue boxes represent fake objects, and the green boxes represent real objects/memory.)

Reviving the bug: Escaping the sandbox through Core-IPC

Webkit broker IPC

Because of the modern browsers' multithreading model and sandbox model, the broker IPC has become one of the most powerful attack surfaces for sandbox escape. e.g., the successful demonstration against Chrome Desktop in 2018.

As Figure 5 indicates, Webkit's broker IPC server consists of several message proxies. The WebProcesses and the UI-Process can communicate through IPC.


Figure 5. The communications between web process and other processes. They have two sides, and can be syncoronous or asyncronous.

An intro of a Use-After-Free bug in the UI-Process

We introduce a deliberate bug by reverting a patch for a bug exploited by us a few months ago. You can check the patch at here.

We eliminate the contextID check in the VideoFullScreenMessageProxy::setHasVideo to make the bug could be easily triggered by sending this message multiple times. As Figure 6 indicates, every contextID is associated with a series of objects. The references are maintained by the WTF::HashMap. According to its implementation, the index "0" has a special meaning that the bucket is empty. However, we eliminate the check, and we can pass a zero contextID into it. While putting objects into the hashmap continuously, we could trigger vulnerable rehash behavior. It tries to copy the objects in the old hashmap to the new hashmap. Once the old objects are deallocated, a dangling pointer will appear.


Figure 6. Raw pointers considered harmful

Due to there is a raw pointer in the PlaybackSessionInterface that points to a PlaybackSessionModel object, the raw pointer will become a dangling pointer when triggering the rehash. Once the VideoFullscreenInterfaceMac object is deconstructed, the use-after-free will be triggered.

The UAF could be triggering by sending the following messages to the UI-process.

send(Messages::VideoFullscreenManagerProxy::SetHasVideo(2, true));
send(Messages::VideoFullscreenManagerProxy::SetHasVideo(-3, true));
send(Messages::VideoFullscreenManagerProxy::SetHasVideo(255, true));
send(Messages::VideoFullscreenManagerProxy::SetHasVideo(7, true));
send(Messages::VideoFullscreenManagerProxy::SetHasVideo(4095, true));
send(Messages::VideoFullscreenManagerProxy::SetHasVideo(0, true));
send(Messages::VideoFullscreenManagerProxy::SetHasVideo(18, true));
send(Messages::VideoFullscreenManagerProxy::SetHasVideo(800, true));

If you recompile the browser with ASAN, you can find some crash reports like this one.

A flexible approach to spray the heap in the UI process

To exploit UAF bugs in CPP, we usually need to figure out where to put our fake vtable and how to get the address of the fake vtable.

Instead of sending some messages multiple times, we abuse the shared memory between the web process and UI process to achieve the goal.

Here is a pseudo-code for this message

WebProcess::singleton().parentProcessConnection()->sendSync(
    Messages::WebPasteboardProxy::
        SetPasteboardBufferForType("name", "type", handle, 0x10000000), 
    Messages::WebPasteboardProxy::
        SetPasteboardBufferForType::Reply(newChangeCount), 0);

Refilling the freed object stably

The WebAuthenticatorCoordinatorProxy has a hash parameter, whose type is Vector<uint8_t>, we can abuse it to refill the memory hole.

void WebAuthenticatorCoordinatorProxy::makeCredential(FrameIdentifier frameId, 
    SecurityOriginData&& origin, 
    Vector<uint8_t>&& hash, 
    PublicKeyCredentialCreationOptions&& options, 
    RequestCompletionHandler&& handler) {
    handleRequest({ WTFMove(hash), 
        WTFMove(options), 
        makeWeakPtr(m_webPageProxy), 
        WebAuthenticationPanelResult::Unavailable, 
        nullptr, 
        GlobalFrameIdentifier 
        { 
            m_webPageProxy.webPageID(), 
            frameId 
        }, 
        WTFMove(origin) 
        }, 
        WTFMove(handler));
}

The reason why it is very flexible is that the uint8 vector can help us to refill the memory in byte granularity without any impurities.

Here is a snippet pseudo-code of this message:

sendWithAsyncReply(Messages::WebAuthenticatorCoordinatorProxy::MakeCredential(
        m_mainFrame->frameID(), 
        SecurityOriginData("http", "hqzhao.me", 8080), hash, options), 
        callback
);

Hijicking the control flow through vtable

By utilizing the tricks described above, as Figure 7 illustrates, we can hijack the control flow through the virtual function table and do some ROP to spawn a calculator.


Figure 7. From UAF to sandbox escape

Reflections and conclusion

Although the road to break the macOS's Safari is tedious, I think the leading edge of Safari security research is on iOS. There are still several open challenges for security researches to overcome. e.g., how to get code execution after getting arbitrary address read/write on a PAC enabled iPhone. To the sandbox, AAPL has enhanced the sandbox profile; even some BSD syscalls can not be invoked in the container sandbox. Furthermore, if you want the original challenges in Real World CTF, please check slipper's repo.