Postpwnium Writeup

The following post gives an overview of the bugs that I was cobbling together for the Pwnium 3 competition to achieve remote code execution on the device.

While inspecting the browser plugins installed by default on my Chromebook, I noticed that the “Google Talk” plugin is running in an ‘unsandboxed’ mode. Inspecting the entry more closely, one sees that not one MIME type, but two are registered, application/googletalk and application/vnd.o3d.auto. Since the Google Talk extension is closed source and O3D is an open source project, I decided to set my sights on O3D. Moreover, since O3D seems to be a dead-end technology, that looked like a safe bet to find bugs in.

By inspecting the Chromium source code and using gdb I reassured myself that the O3D plugin really was running in unsandboxed mode:

[from chrome/common/chrome_content_client.cc in Chromium r185835,
 function ComputeBuiltInPlugins(std::vector<content::PepperPluginInfo>* plugins]:
  static bool skip_o3d_file_check = false;
  if (PathService::Get(chrome::FILE_O3D_PLUGIN, &path)) {
    if (skip_o3d_file_check || file_util::PathExists(path)) {
      content::PepperPluginInfo o3d;
      o3d.path = path;
      o3d.name = kO3DPluginName;
      o3d.is_out_of_process = true;
      o3d.is_sandboxed = false;
      o3d.permissions = kO3DPluginPermissions;
      webkit::WebPluginMimeType o3d_mime_type(kO3DPluginMimeType,
                                              kO3DPluginExtension,
                                              kO3DPluginDescription);
      o3d.mime_types.push_back(o3d_mime_type);
      plugins->push_back(o3d);

      skip_o3d_file_check = true;
    }
  }

A plan was formed: Pop the Chromebook through the O3D plugin. Let’s see what steps are necessary to get from here to there*.

* where here is a URL I give you to visit on your Chromebook and there is a connect-back shell being spawned on your shiny toy.

Getting a O3D plugin object instantiated

This was initially perceived as “an easy feat”, but just like most easy tasks, it turned out to be much harder than expected. “Why?”, I hear you ask. The plugin is configured with a list of whitelisted domains:

[from build/branding.gypi]:
'plugin_domain_whitelist': ('".corp.google.com", '
                            '".prod.google.com", '
                            '".googleplex.com", '
                            '"hostedtalkgadget.google.com", '
                            '"mail.google.com", '
                            '"plus.google.com", '
                            '"plus.sandbox.google.com", '
                            '"talk.google.com", '
                            '"talkgadget.google.com"')

The factory install of my Chromebook was ChromeOS 23.x, which didn’t actually implement this whitelisting feature for reasons unknown.

However, on ChromeOS 25.x, unless the HTML document embedding the o3d plugin is served over a HTTPS connection from one of these domains, the plugin will be blocked. This restriction can be overcome with an XSS on one of the above Google properties, by being able to spoof window.location.href or by making use of Chromium Issue #64229 (which has been marked as WontFix since January 2011). Have a look at the function IsDomainAuthorized():

[from plugin/cross/whitelist.cc]
bool IsDomainAuthorized(NPP instance) {
#ifdef O3D_PLUGIN_DOMAIN_WHITELIST
  std::string url(GetURL(instance));
  if (url.empty()) {
    // This can happen in Chrome due to a bug with cross-origin security checks,
    // including on legitimate pages. Until it's fixed we'll just allow any
    // domain when this happens.
    // http://code.google.com/p/chromium/issues/detail?id=64229
    LOG(WARNING) <<
        "Allowing use despite inability to determine the hosting page";
    return true;
  }

Since that bug had been rotting in the bugtracker for over two years and also was marked as WontFix, I assumed it had gained the the status of a “feature”. Albeit, after being unable to get the situation reproduced, I went trawling through the WebKit repository and after a while made myself believe that the problem was fixed by this or a related commit:

[from http://trac.webkit.org/changeset/124693]
2012-08-04  Adam Barth  <abarth@webkit.org>

    [V8] Re-wire "target" half of the same-origin security check through Document rather than DOMWindow
    https://bugs.webkit.org/show_bug.cgi?id=93079

    Reviewed by Eric Seidel.

    Before this patch, we were traversing from Nodes to Frames to
    DOMWindows to SecurityOrigins when determing the "target" of an
    operation for the same-origin policy security check. Rather than
    detouring through DOMWindow, these security checks should operate in
    terms of ScriptExecutionContexts (aka Documents) because that's the
    canonical place we store SecurityOrigin objects.

    A future patch will re-wire the "active" part of the security check to
    use ScriptExecutionContexts as well and we'll be able to remove the
    extra copy of SecurityOrigin that we keep in DOMWindow.

More specifically, this change caught my eye:

static v8::Handle<v8::Context> activeContext()
{
    v8::Handle<v8::Context> context = v8::Context::GetCalling();
    if (!context.IsEmpty())
        return context;
    // Unfortunately, when processing script from a plug-in, we might not
    // have a calling context. In those cases, we fall back to the
    // entered context.
    return v8::Context::GetEntered();
}

[activeContext() now uses GetEntered() instead of GetCurrent() which I thought might mitigate the problem described in the above discussion. BUT I WAS WRONG! IT DOES NOT MITIGATE THE PROBLEM. SEE BELOW.]

I then wasted endless hours trying to find an XSS in one of the above Google sites. Apparently I suck at these things; I heard that this is supposed to be rather easy…

Revisiting the situation later — using much hair-pulling and gdb — I was able to discern a scenario that triggered the href property becoming 0 due to a failed cross-origin check. A Google Groups discussion among Google developers proved to be very helpful for that. The trigger is racy, but with a forced reload on the outer frame it works reliably. The trigger can be reduced to something as simple as embedding an iframe containing the following:

<html>
<body onload="document.defaultView.getComputedStyle(e).getPropertyValue('width');">
<div id="e"></div>
</body>
</html>

Quick explanation: we can force a CSS style computation to happen in an inner iframe with the o3d plugin embedded on the outer frame. This in turn will trigger a layout of the page, which happens in the V8 context of the inner iframe. During this layout (actually, in the post layout stage) a plugin instantiation will be attempted for the o3d object on the outer frame. This in turn will lead to NPN_GetProperty calling V8 bindings (V8Location) for the window.location.href property. Since this is in the V8 context of the inner iframe, the access will fail due to a cross-origin violation.

An exploitable memory corruption in the O3D plugin

To any arrogant memory corruption practioner, the O3D code base looks sufficiently large to find at least a couple of decent use-after free or type confusion vulnerabilities in. And I only need one. The reference-counting patterns used throughout the code base together with the weak_ptrs were extremely annoying (due to me hitting a number of false positives), but finally I found a simple way to UAF: Setting the owner property on a DrawElement object and subsequently destroying that owner object will cause a dangling pointer. To wit, the DrawElement class contains a bare Element pointer:

[from /core/cross/draw_element.h]:
Element* owner_;  // our current owner.

Let’s have a look at the available Javascript bindings of DrawElement:

[from plugin/idl/draw_element.idl]:
[getter, setter, userglue_setter] Element? owner_;

[verbatim=cpp_glue] %{
  void userglue_setter_owner_(
      o3d::DrawElement* _this,
      o3d::Element* owner) {
    _this->SetOwner(owner);
  }
-- snap --

and more specifically at the setter function:

[from /core/cross/draw_element.cc]:
void DrawElement::SetOwner(Element* new_owner) {
  // Hold a ref to ourselves so we make sure we don't get deleted while
  // as we remove ourself from our current owner.
  DrawElement::Ref temp(this);

  if (owner_ != NULL) {
    bool removed = owner_->RemoveDrawElement(this);
    DCHECK(removed);
  }

  owner_ = new_owner;

  if (new_owner) {
    new_owner->AddDrawElement(this);
  }
}

The o3d::Element class follows the following inheritance: Element –> ParamObject –> NamedObject –> ObjectBase –> RefCounted

More importantly, there is a virtual class inheriting from Element that we can instantiate through Javascript bindings, namely the Primitive class. This means the standard dangling pointer vtable overwrite, using the JS binding to pull the UAF trigger, is possible. pop goes the glock

Please note that this find may look somewhat easier than it was – there are tons of other cases (see above) in which various design patterns, for instance the Object Manager pattern are very successful at preventing UAF by making the objects unavailable through Javascript bindings.

A memory leak to defeat ASLR

While many a memory leak can be procured out of a UAF usually, I was not so lucky with the one that I had. But that just meant I need to find a dedicated one…. Contrary to more security-conscious operating systems such as OpenBSD [sorry, can’t help the trolling here :)], free()d memory is not zero-filled on Linux. This means that being able to allocate memory that can be read through Javascript bindings and which is left uninitialized after the allocation can give us the juicy bits we want, especially if we can choose the allocation size as well! I found this behaviour in o3d::Buffer, in the processing of RawData objects into fields with an input that intentionally is too short. In this case, the fields are not overwritten and can be used to peek into previously freed memory – just be careful to not get any floating point conversions into your way – this is what happened to me initially and made pointers only approximately correct ;) Look at the function Buffer::Set(o3d::RawData *, size_t, size_t) in file core/cross/buffer.c as well as Buffer::AllocateElements(unsigned) and VertexBufferGLES2::ConcreteAllocate(size_t) to see what I’m talking about [GLES2 and not GL is used on the Chromebook, also I’ve arbitrarily chosen VertexBuffer over IndexBuffer here].

This now allowed me to leak useful objects like the base::PendingTask object of Chrome which gives us the offset of the chrome binary to reliably predict addresses in memory. This also gives a great way to allocate memory and set its content for a use-after free, as long as the chunk is a multiple of 4 bytes in size.