Sky's blog

JavaScript侧信道时间测量

字数统计: 1,321阅读时长: 5 min
2019/01/22 Share
1
文章首发于安全客 https://www.anquanke.com/post/id/170268

前言

最近因为需求,需要测量JavaScript单个函数的执行时间,但由于精度问题,遇到了各种各样的问题,在此做简单记录。

JavaScript高精度时间函数

首先得明确,JavaScript单个函数执行时间应该是微秒级的,所以必须需要高精度。那么第一反应自然是查找是否有相关函数,以往的时间函数new Date().getTime()肯定是不够了,因为其精度为毫秒级,测量函数执行时间,得到的结果必然为0。
这里我们查阅手册,将目光定位到performance.now()
我们不妨做个测试

1
2
3
4
5
let n = 0;
while (n<10000){
console.log((new Date()).getTime());
n = n+1;
}

得到结果

而对于

1
2
3
4
5
let n = 0;
while (n<10000){
console.log(performance.now());
n = n+1;
}

得到结果

我们可以轻易的看出,这里进行一次循环大概是0.04ms,而精度在ms级的new Date().getTime()已经无能为力

精度失灵

既然确定了performance.now(),不妨在Chrome进行测试

我们可以轻松测量出crypto.getRandomValues(new Uint8Array(10))的运行时间大概为0.1ms
但由于可能存在误差,我尝试运行1000次,却出现了问题

竟然出现了大量的0
我又在我虚拟机里的Chrome运行

这是什么原因?对比之后发现
虚拟机

而物理机

查阅Chrome的Updates (2018)
由于高精度的时间可应用于重大漏洞speculative execution side-channel attack(https://spectreattack.com/)
所以各大主流浏览器做出了安全措施
例如FireFox、Safari,将performance.now()精度降低为1ms


而Chrome改为100微秒并加入了抖动

所以这也不难解释,为什么Chrome 71版本得到这么多0,相比FireFox、Safari,能得到数据,已经算是仁慈了

柳暗花明

那么怎么进行高精度测量呢?不能因为浏览器的不支持,我们就不完成需求吧~
这里查阅文章发现

1
https://link.springer.com/chapter/10.1007/978-3-319-70972-7_13

一文中进行了JavaScript侧信道测量时间的介绍
由于精度问题,例如

1
2
3
var start = performance.now();
func()
var end = performance.now();

会使得start = end,这样测量出来只能为0,而作者很巧妙的使用了wait_edge()

1
2
3
4
5
6
7
function wait_edge()
{
var next,last = performance.now();
while((next = performance.now()) == last)
{}
return next;
}

这样一来就可以到下一次performance.now()的时候再继续
那么问题又来了,中间空转的时间怎么办呢?
作者又使用了count_edge()进行了空转次数探测

1
2
3
4
5
6
function count_edge()
{
var last = performance.now(),count = 0;
while(performance.now() == last) count++;
return count;
}

那么怎么把空转次数的单次时间测量出来呢?这里作者又设计了calibrate()

1
2
3
4
5
6
7
8
9
10
11
function calibrate()
{
var counter = 0,next;
for(var i=0;i<10;i++)
{
next = wait_edge();
counter += count_edge();
}
next = wait_edge();
return (wait_edge() - next)/(counter/10.0);
}

假设我们要测量函数fuc(),即可如下编写即可

1
2
3
4
5
6
7
function measure()
{
var start = wait_edge();
fuc();
var count = count_edge();
return (performance.now()-start)-count*calibrate();
}

即结束减去开始的时间,再减去中间空转的时间。
我们再来用chrome 71测试一下

和之前的performance.now()对比

显然误差已经控制在了0.01ms,即10微秒内,这是我们能接受的
当然,在FireFox这种ms级的更有成就感,因为之前的结果都是0,但是用这样的方法,可以测量了
FireFox:

测试与结论

我以crypto.getRandomValues(new Uint8Array(n));为例测试
performance.now()的结果和measure()进行做差比较,不难发现

Chrome

在Chrome 57版本下,差异仅在10微秒以内。(注:结果由performance.now()经过进制转换输出)


而在Chrome 71版本下,差异却达到了50微秒以内(注:结果由performance.now()经过进制转换输出)


原因也很明显,因为71版本的performance.now()降低了精度,并且加入了抖动,导致许多end-start的值为0
那么我们在71版本下直接测试侧信道方式得到的时间

不难发现,其实在71版本下计算差是没有意义的,因为performance.now()的精度已经变为100微秒
所以做差得到的值基本是侧信道方式测得的结果。
所以我们基本可以确定,这样的方式在目前chrome版本可以得到比performance.now()更高精度的时间测量
但我们的目的肯定不局限于Chrome,我们再看看Firefox

Firefox

对于Firefox就更过分了,通过performance.now()测量高精度时间直接变成了不可能,因为精度被调整成了毫秒级,所以end-start的值都变为了0

而对于侧信道测量方式

我们依旧还是可以测量出许多微秒级的时间

后记

这样的方式可以有效突破浏览器的高精度函数毫秒级的限制,甚至对于一些特定攻击依旧奏效。
若有更好的方式还请大佬不吝赐教~

参考链接

https://zhuanlan.zhihu.com/p/32629875
https://link.springer.com/chapter/10.1007/978-3-319-70972-7_13

点击赞赏二维码,您的支持将鼓励我继续创作!
CATALOG
  1. 1. 前言
  2. 2. JavaScript高精度时间函数
  3. 3. 精度失灵
  4. 4. 柳暗花明
  5. 5. 测试与结论
    1. 5.1. Chrome
    2. 5.2. Firefox
  6. 6. 后记
  7. 7. 参考链接