也谈 “Spectre” CPU 漏洞

元旦回来 P0 就放出了 2018 年的第一个大新闻,多款处理器被曝出存在安全漏洞可以泄露某些敏感信息,P0 将其分别命名为 “Spectre” 和 “MeltDown”。其中 “Spectre” 漏洞是由于 CPU 的分支预测进而导致的缓存信息泄露,其效果可以越界读取某些内容。

根据原理上来说 “Spectre” 是可以并且比较容易在浏览器中通过 js 相关技术实现攻击的,在 P0 的论文后也附加了一个 C 语言实现的 demo。出于对这种攻击理论上的认可我们开始尝试通过 js 来实现 “Spectre” 的漏洞利用。经过四天的研究我们终于 “全球首发” “Spectre” 漏洞在线检测工具,虽然依然很不完善但是效果却出乎我们所有人的意料。检测工具地址

实现的代码全是 JS 因此可以直接通过前端获取,经过 Exp-sky 师傅的润色代码也大体上可读(ps:如果知道要放出来的话变量命名写的时候就不会那么随意了~ orz)。这里记录一下代码编写过程中的几个问题,以备以后查询。

手撸 ASM.JS

在编写 vul_call() 时考虑到要尽可能的与 C 程序相似以减少其他情况的干扰,最先想到的是使用 WebAssembly 来实现这一功能,但是由于这个函数需要访问一些外部的数组 ProbTable 使用 WebAssembly 在调试上可能并不方便,同时考虑到在 Chakra 中 WebAssembly 最终都是调用 ASMJS 的接口来实现的。因此转而直接使用 ASMJS 来编写代码。

ASMJS 与常规的 js 相比多了很多限制,一旦有违反这个限制的情况出现那么浏览器将会直接把 ASMJS 的代码当作常规 JS 来处理。在编写过程中遇到的坑总结起来有如下:

  • ASMJS 中只能使用 TypedArray 与 Math 的相关函数
  • ASMJS 中所有的 TypedArray 都必须以 ASMJS 整体所在的 heap 作为 buffer,内存的偏移需要自己处理
  • ASMJS 中只支持 uint,double 等基本数据类型,以 a|0 表示 uint,以 +a 表示 double
  • ASMJS 函数的参数一定要在函数开头按参数顺序声明其类型
  • ASMJS 中所有对于变量的赋值取值操作都需要指定类型
  • ASMJS 中使用到的变量需要提前声明

最保险的办法就是实时在浏览器的控制台中检查编写过程有无发生错误。最终这个函数的编写结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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); // shift the index to the high mem
index = (index & ((0x2000000-1)|0))|0;
junk = (junk ^ (probeTable[index]|0))|0;
}
}

高精度计时器的实现

“Spectre” 漏洞利用的一个关键就是通过访问时间的差异确定一个数据是否存在于 Cache 之中,在 performance.now 的精度已经被浏览器降低的情况下,我们可以通过最新的 SharedArrayBuffer 技术来实现一定意义上的计时。各大浏览器厂商对于这个漏洞的修补就是取消了默认对 SharedArrayBuffer 的支持。在这种前提下我们需要找到另外一种可以实现高精度计时的方法。

在 2017 年马耳他会议上的一篇文章恰好提到了这一点,Fatastic Time是一篇介绍如何在现代浏览器中实现高精度计时器的。

文章中一共介绍了十一种方法,9种是借助 js 中的其他功能模块比如 SAB、worker 等来实现,两种是通过数据逻辑方法根据 performance.now 方法的结果进行判断。文章中给的统计数据显示,使用 SAB 和一种数学方法实现的计时器的精确度最高,可以达到纳秒级别。

这里针对这种数学方法进行研究。这种方法在文章中被称为 Edge-thresholding

Edge thresholding

翻译过来就是 边缘阈值。这种方法的思想是使用 padding 来使得所要区分的时间整体增大,从而使其正好可以处于 performance.now 的两个不同时钟周期中,从而可以看出区别。

详细说来,我们假设需要用来区分的两个时间分别为 fast_tslow_t ,而 performance.now 的一个最小时间片为 per_clock 。由于 performance.now 的时间精度很小,因此这个时间片就会相对来说很大以至于比需要检测的时间都大,从而看不出差别,即 fast_t < per_clock && slow_t < per_clock。如下所示:

1
2
3
---------clock------------
_______fast_______
____slow____

那么如果我们可以在 fast_tslow_t 后面分别加上一段等长的时间 padding,使得 fast_t 刚好比 slow_t 多跨越一个时间片的边界,那么就可以通过 performance.now 的结果对二者进行区分。如下所示

1
2
3
---------clock------------
_______fast_______***********
____slow____***********

在同时从一个时间片开始的情况下 fast 刚好比 slow 多跨越一个 per_clock 边界,那么通过 performance.now 返回值的结果就可以区分出 fastslow

我们所做的的工作就是找到这个 padding 值,使得访问不在 cache 中的数据时间刚好大于访问在 cache 中的数据时间。

测试实现

测试环境为 win7 chrome v63.0.3239.132

首先需要将操作开始的时间与时间片的边界相重合,接着需要找到可以在一个时间片内多次执行的操作。这些操作可以通过以下方式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function wait_for_tick() {
var junk = 0;
var count = 0;
var then = performance.now();
var now;
while ((now = performance.now()) == then)
;
// now is at clock edge
for (;;) {
for(var i =0 ;i<0xd18; i++) // 0xd18 may be the number of constant padiing per clock_time in chrome
junk++;
var after = window.performance.now();
if (after != now){
return count;
}
count++;
}
}

经过测试在 performance.now 的一个时间片内可以循环执行 0xd18 次累加操作。
在当前测试环境下,通过 performance.now 检测的有无 cache 的访问时间都是 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var then = performance.now();
var now;
while ((now = performance.now()) == then)
;
junk = ab[i];
then = window.performance.now();
fast = then - now;
var then = performance.now();
var now;
while ((now = performance.now()) == then)
;
junk = ab[i*0x10000];
then = window.performance.now();
slow = then - now;

于是在操作后面逐渐增加 padding ,直至 slow 大于 fast 为止。

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
for(i=0;i<repeat_time;i++){
var then = performance.now();
var now;
while ((now = performance.now()) == then)
;
junk = ab[i];
for(var l =0 ;l<padding_size; l++)
junk++;
then = window.performance.now();
arr[i] = then-now//[t2-t1,t1,t2];
}
for(i=0;i<repeat_time;i++){
var then = performance.now();
var now;
while ((now = performance.now()) == then)
;
junk = ab[i*0x10000]
for(var l =0 ;l<padding_size; l++)
junk++;
then = window.performance.now();
arr2[i] = then-now//[t2-t1,t1,t2];
}
function find_padding(){
var max = 0.0
var max_padding = 0.0;
for (padding_size = 0x600;padding_size<0x900;padding_size+=4){
var succ = 0.0
succ += test(false);
if(succ>max){
max = succ;
max_padding = padding_size;
}
console.log(succ + ' ' + padding_size)
}
console.log(max + ' ' + max_padding)
}

但是测试的结果与论文中描述差异很大,并不能找到一个 padding 值使得每次访问操作有大概率可以识别出时间差异。

逻辑分析

查看 Chrome 的源代码,其中与高精度计时器相关的代码如下

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
class HighResolutionTickClock final : public TickClock {
public:
explicit HighResolutionTickClock(int64_t ticks_per_second)
: ticks_per_second_(ticks_per_second) {
DCHECK_LT(0, ticks_per_second);
}
virtual ~HighResolutionTickClock() {}
int64_t Now() override {
uint64_t now = QPCNowRaw();
// Intentionally calculate microseconds in a round about manner to avoid
// overflow and precision issues. Think twice before simplifying!
int64_t whole_seconds = now / ticks_per_second_;
int64_t leftover_ticks = now % ticks_per_second_;
int64_t ticks = (whole_seconds * Time::kMicrosecondsPerSecond) +
((leftover_ticks * Time::kMicrosecondsPerSecond) / ticks_per_second_);
// Make sure we never return 0 here, so that TimeTicks::HighResolutionNow()
// will never return 0.
return ticks + 1;
}
bool IsHighResolution() override { return true; }
private:
int64_t ticks_per_second_;
};
// .....
double PerformanceBase::now() const
{
double nowSeconds = monotonicallyIncreasingTime() - m_timeOrigin;
const double resolutionSeconds = 0.000005;
return 1000.0 * floor(nowSeconds / resolutionSeconds) * resolutionSeconds;
}

函数直接调用 QueryPerformanceCounter 接口返回值,接着使用 floor 函数减小精度,这个精度最小值变为 0.005 秒左右。

经过测试发现,一次访问操作的时间相当于 0x298 次累加操作操作(这个值与 CPU 相关),访问有缓存的数据和访问无缓存的数据他们之间的时间差值极小,约等于一次或两次循环累加操作所需的时间。

显然这个精度无法满足我们对于缓存访问的需求。

进一步分析。我们直接用 C 程序调用 QueryPerformanceCounter 函数查看 Cache 前后的调用时间差

1
2
3
4
5
6
7
8
9
10
11
12
13
addr = (uint64_t*)&time[1];
result = QueryPerformanceCounter(&now);
junk = *addr;
result = QueryPerformanceCounter(&then);
uint64_t time1 = then.QuadPart - now.QuadPart;
printf("Start Time : %d, end time : %d \n", now, then);
result = QueryPerformanceCounter(&now);
junk = *addr;
result = QueryPerformanceCounter(&then);
uint64_t time2 = then.QuadPart - now.QuadPart;
printf("Start Time : %d, end time : %d \n", now, then);

测试发现即使是 QueryPerformanceCounter 的结果都无法满足 Cahce 的精度要求,自然通过 floor 减小精度之后的值更加无法满足

但是出于对 padding 理论上的认可,我们通过 C 程序对 padding 算法进行了验证,发现在 c 程序上这种方式是可行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
addr = (uint64_t*)&time[1];
result = QueryPerformanceCounter(&then);
do{
result = QueryPerformanceCounter(&now);
} while (now.QuadPart == then.QuadPart);
junk = *addr;
for (t = 0; t < 100; t++)
junk++;
result = QueryPerformanceCounter(&then);
uint64_t time1 = then.QuadPart - now.QuadPart;
printf("Start Time : %d, end time : %d \n", now, then);
result = QueryPerformanceCounter(&then);
do {
result = QueryPerformanceCounter(&now);
} while (now.QuadPart == then.QuadPart);
junk = *addr;
for (t = 0; t < 100; t++)
junk++;
result = QueryPerformanceCounter(&then);
uint64_t time2 = then.QuadPart - now.QuadPart;
printf("Start Time : %d, end time : %d \n", now, then);

然而在 js 上调用函数会多出许多奇怪的干扰,我们将 QueryPerformanceCounter 使用函数包裹起来,再次调用,发现这种 padding 算法就不再稳定了,而 performance.now 的调用经调试一共包裹了三层函数,并且有许多浮点数的计算操作,从而将引入更大的误差。而其误差已经大于 Cache 的差值。因此理论上无法通过 performance.now 区分缓存与非缓存的时间。

逆向分析

在实际发行版的 Chrome 浏览器中逆向分析 performance.now 的结果如下,大体流程与源代码相符合。performance.now 操作会调用函数 blink::PerformanceBase::now:,这个函数调用 WTF::MonotonicallyIncreasingTime 来实现功能。

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
chrome_child!blink::PerformanceBase::now:
00007ffc`a900db18 4053 push rbx
00007ffc`a900db1a 4883ec20 sub rsp,20h
00007ffc`a900db1e 488bd9 mov rbx,rcx
00007ffc`a900db21 e8d6180300 call chrome_child!WTF::MonotonicallyIncreasingTime (00007ffc`a903f3fc)
00007ffc`a900db26 0f28c8 movaps xmm1,xmm0
00007ffc`a900db29 4533c0 xor r8d,r8d
00007ffc`a900db2c f20f108398000000 movsd xmm0,mmword ptr [rbx+98h]
00007ffc`a900db34 4883c420 add rsp,20h
00007ffc`a900db38 5b pop rbx
00007ffc`a900db39 e962b7ebff jmp chrome_child!blink::PerformanceBase::MonotonicTimeToDOMHighResTimeStamp (00007ffc`a8ec92a0)
00007ffc`a8ec92a0:
00007ffc`a8ec92a0 4883ec28 sub rsp,28h
00007ffc`a8ec92a4 0f28d0 movaps xmm2,xmm0
00007ffc`a8ec92a7 0f57c0 xorps xmm0,xmm0
00007ffc`a8ec92aa 660f2ec8 ucomisd xmm1,xmm0
00007ffc`a8ec92ae 7a02 jp chrome_child!blink::PerformanceBase::MonotonicTimeToDOMHighResTimeStamp+0x12 (00007ffc`a8ec92b2)
00007ffc`a8ec92b0 7432 je chrome_child!blink::PerformanceBase::MonotonicTimeToDOMHighResTimeStamp+0x44 (00007ffc`a8ec92e4)
00007ffc`a8ec92b2 660f2ed0 ucomisd xmm2,xmm0
00007ffc`a8ec92b6 7a02 jp chrome_child!blink::PerformanceBase::MonotonicTimeToDOMHighResTimeStamp+0x1a (00007ffc`a8ec92ba)
00007ffc`a8ec92b8 742a je chrome_child!blink::PerformanceBase::MonotonicTimeToDOMHighResTimeStamp+0x44 (00007ffc`a8ec92e4)
00007ffc`a8ec92ba f20f5cca subsd xmm1,xmm2
00007ffc`a8ec92be 660f2fc1 comisd xmm0,xmm1
00007ffc`a8ec92c2 7725 ja chrome_child!blink::PerformanceBase::MonotonicTimeToDOMHighResTimeStamp+0x49 (00007ffc`a8ec92e9)
00007ffc`a8ec92c4 f20f5e0de43df802 divsd xmm1,mmword ptr [chrome_child!_real (00007ffc`abe4d0b0)]
00007ffc`a8ec92cc 0f28c1 movaps xmm0,xmm1
00007ffc`a8ec92cf e844299400 call chrome_child!floor (00007ffc`a980bc18)
00007ffc`a8ec92d4 f20f5905d43df802 mulsd xmm0,mmword ptr [chrome_child!_real (00007ffc`abe4d0b0)]
00007ffc`a8ec92dc f20f59053c98f802 mulsd xmm0,mmword ptr [chrome_child!_real (00007ffc`abe52b20)]
00007ffc`a8ec92e4 4883c428 add rsp,28h
00007ffc`a8ec92e8 c3 ret
------------------------------------------------------------------
chrome_child!WTF::MonotonicallyIncreasingTime:
00007ffc`a903f3fc 4883ec38 sub rsp,38h
00007ffc`a903f400 488b05b1ab9303 mov rax,qword ptr [chrome_child!WTF::g_mock_time_function_for_testing (00007ffc`ac979fb8)]
00007ffc`a903f407 4885c0 test rax,rax
00007ffc`a903f40a 754f jne chrome_child!WTF::MonotonicallyIncreasingTime+0x5f (00007ffc`a903f45b)
00007ffc`a903f40c 488d4c2440 lea rcx,[rsp+40h]
00007ffc`a903f411 ff15a1c27303 call qword ptr [chrome_child!g_now_function (00007ffc`ac77b6b8)]
00007ffc`a903f417 488d4c2440 lea rcx,[rsp+40h]
00007ffc`a903f41c c644242001 mov byte ptr [rsp+20h],1
00007ffc`a903f421 488b10 mov rdx,qword ptr [rax]
00007ffc`a903f424 4889542440 mov qword ptr [rsp+40h],rdx
00007ffc`a903f429 4889542440 mov qword ptr [rsp+40h],rdx
00007ffc`a903f42e 668364244000 and word ptr [rsp+40h],0
00007ffc`a903f434 e8375de4ff call chrome_child!base::internal::RangeCheck::IsValid (00007ffc`a8e85170)
00007ffc`a903f439 84c0 test al,al
00007ffc`a903f43b 7425 je chrome_child!WTF::MonotonicallyIncreasingTime+0x66 (00007ffc`a903f462)
00007ffc`a903f43d 0f57c0 xorps xmm0,xmm0
00007ffc`a903f440 b840420f00 mov eax,0F4240h
00007ffc`a903f445 0f57c9 xorps xmm1,xmm1
00007ffc`a903f448 f2480f2ac2 cvtsi2sd xmm0,rdx
00007ffc`a903f44d f2480f2ac8 cvtsi2sd xmm1,rax
00007ffc`a903f452 f20f5ec1 divsd xmm0,xmm1
00007ffc`a903f456 4883c438 add rsp,38h
00007ffc`a903f45a c3 ret
----------------------------------------------------------------------
chrome_child!`anonymous namespace'::QPCNow:
00007ffc`a8e85804 4053 push rbx
00007ffc`a8e85806 4883ec20 sub rsp,20h
00007ffc`a8e8580a 488bd9 mov rbx,rcx
00007ffc`a8e8580d 33c0 xor eax,eax
00007ffc`a8e8580f 488d4c2430 lea rcx,[rsp+30h]
00007ffc`a8e85814 4889442430 mov qword ptr [rsp+30h],rax
00007ffc`a8e85819 ff15a10fcb02 call qword ptr [chrome_child!_imp_QueryPerformanceCounter (00007ffc`abb367c0)] ds:00007ffc`abb367c0={KERNEL32!QueryPerformanceCounterStub (00007ffc`fd516180)}
00007ffc`a8e8581f 4c8b542430 mov r10,qword ptr [rsp+30h] // LARGE_INYGER.qword
00007ffc`a8e85824 48b8f75ad07b63080000 mov rax,8637BD05AF7h
00007ffc`a8e8582e 4c3bd0 cmp r10,rax
00007ffc`a8e85831 0f8dcd849e00 jge chrome_child!`anonymous namespace'::QPCNow+0x9e8500 (00007ffc`a986dd04)
00007ffc`a8e85837 4969c240420f00 imul rax,r10,0F4240h
00007ffc`a8e8583e 4899 cqo
00007ffc`a8e85840 48f73db123aa03 idiv rax,qword ptr [chrome_child!g_qpc_ticks_per_second (00007ffc`ac927bf8)]
00007ffc`a8e85847 488903 mov qword ptr [rbx],rax
00007ffc`a8e8584a 488bc3 mov rax,rbx
00007ffc`a8e8584d 4883c420 add rsp,20h
00007ffc`a8e85851 5b pop rbx
00007ffc`a8e85852 c3 ret
00007ffc`a8e85853 cc int 3
00007ffc`a986dd04:
00007ffc`a986dd04 4c8b05ed9e0b03 mov r8,qword ptr [chrome_child!g_qpc_ticks_per_second (00007ffc`ac927bf8)]
00007ffc`a986dd0b 498bc2 mov rax,r10
00007ffc`a986dd0e 4899 cqo
00007ffc`a986dd10 498bc8 mov rcx,r8
00007ffc`a986dd13 49f7f8 idiv rax,r8
00007ffc`a986dd16 480fafc8 imul rcx,rax
00007ffc`a986dd1a 4c8bc8 mov r9,rax
00007ffc`a986dd1d 4c2bd1 sub r10,rcx
00007ffc`a986dd20 4969c240420f00 imul rax,r10,0F4240h
00007ffc`a986dd27 4969c940420f00 imul rcx,r9,0F4240h
00007ffc`a986dd2e 4899 cqo
00007ffc`a986dd30 49f7f8 idiv rax,r8
00007ffc`a986dd33 4803c1 add rax,rcx
00007ffc`a986dd36 e90c7b61ff jmp chrome_child!`anonymous namespace'::QPCNow+0x43 (00007ffc`a8e85847)
0:000> dq 7ffc`ac927bf8 L1
00007ffc`ac927bf8 00000000`001ac2e5 // g_qpc_ticks_per_second

总结

综上所属,论文给出的数据可能在某种程度上无法复现。

Refenrence