那这个时钟信号对 CPU 有什么作用呢?我们知道,CPU 内部有上亿个半导体组成非常复杂的电路。CPU 每执行一个简单的指令,其实都要被细分成好几个步骤——指令解析、数据访问、数据计算、数据缓存、数据写入等等。
每个步骤都要经过成千上万个半导体的逻辑电路,电子在 CPU 中就像在跑马拉松一样,虽然电子的速度非常快,但它仍然是有速度的,从一头跑到另一头是要花时间的。不同的指令、不同的环节,电子要跑过的赛道是不一样长的,所需花费的时间也就不一样。再加上几乎所有的这些环节都是并行运算的,如果让电子们各跑各的,那不全乱套了。
因此,时钟信号就像一个发号施令的,在时钟信号还没变化的时候,电子们都乖乖站在起跑线上。一旦 CPU 看到一个时钟信号的上行边沿,马上就打发令枪,电子们就开始冲刺。那么关键的地方来了。有些电子跑的是 110 米跨栏,一眨眼就到终点了,有些电子跑的是马拉松,一时半会连汗都还没出来,所以要找出所有这些执行的任务中耗时最久的,作为时钟周期的下限,从而保证在下一个上行边沿发生之前,所有的电子都已到达各自的终点,在下一回合的起跑线上就位了。
每款 CPU 的数据手册中都会给出一个安全时钟频率范围。一般来说理论下限 0 Hz 都无所谓,因为 CPU 没看到边沿信号就 hold 住不继续跑而已,并不会造成什么数据完整性上的问题。但是频率上限就不一样了,一旦严重超出频率上限,有些电子都还没跑完呢就看到前面的电子已经开始跑下一回合了。那有些需要等待之前数据的操作,还没等到数据呢就开始处理了,这就出现了数据出现完整性问题了。
有些玩过超频的朋友应该就遇到过,如果超频太离谱,不但你的 CPU 可以烤肉,还有可能各种蓝屏、花屏,这些其实都是数据损坏造成的。
超频可能造成的后果非常多,除了数据损坏,还可以让一个指令直接失效,相当于跳过了这个指令。
那毛刺攻击究竟长什么样?我这里给出它理论上的模样:
可以看到所谓的毛刺其实就是在正常的时钟信号上增加了一些「锯齿」。这些在我们看来是锯齿开口的形状,换在 CPU 眼里其实关注的只有上行边沿。在锯齿产生的部分,两个上行边沿挨得非常近。根据我们前面的理论,如果这个距离小于安全时钟周期,则很有可能导致 CPU 运行异常。这下就好玩了,我们来实验测试一下看看它到底能不能让 CPU 跳过指令或者损坏关键数据。
大部分的 SoC 芯片都可以配置成使用外部输入的时钟信号。这在设计上提供了使用更高质量、更高精度的晶振时钟源的选择,还可以让多个 SoC 共享同一个时钟信号等等。我们做的就是把目标 CPU 配置成外部时钟信号驱动的,然后我们的主控板上的 FPGA 攻击模块生成时钟信号,控制目标芯片的心跳。
我们需要让目标 CPU 保持正常运行到我们想要攻击的那条指令,也就是说目标芯片开始运行后,我们要给他提供符合标准的正常时钟频率。然后在非常精准的时间点,当 CPU 运行到我们想跳过的关键指令的时候,开始让 FPGA 在时钟信号中加入毛刺,持续一段时间的毛刺攻击,然后看看关键指令是否被成功跳过了。
为了找到这个精准的开始攻击的时间点,我们需要一个触发信号(Trigger)作为参考时间点,从触发信号的时间点开始计时,等待一段时间后马上开始攻击。这个从触发信号开时间点到开始攻击的等待时间我们记为 ext_offset,是我们可以调整的变量之一。触发信号在时间上距离我们的攻击目标指令执行的时间越接近越好,因为离得越远,变数越大,精度就损失了。在现实中,我们一般可以选择 RESET,也就是 CPU 启动的时间点作为触发信号,这个信号所有 CPU 都有,但是它也是距离最远的一个信号,容易损失精度。如果可能的话在真实攻击中我们都会选择更加靠近目标指令的触发信号,比如说 eMMC 上的数据传输、各种总线上的指令等等。要想采集这些信号往往都需要对 PCB 进行一定程度的改动或者焊接,但是对于一些复杂的毛刺攻击来说,这些辛苦都是值得的。不过因为我们现在是学习阶段,我们用不着这么高端的触发信号采集方式,我们可以直接在我们给目标 CPU 写的测试程序里面人为地安置一个触发信号,就挨着放在我们目标指令的前面,这个触发信号简单地把一个 GPIO 拉低即可。我们实施攻击的时候就观察这个 GPIO 输出作为触发信号,一看到它拉低了就开始计时,一过 ext_offset 需要的时间就开始发起毛刺攻击。