初探侧信道攻击:时钟毛刺攻击绕过密码校验
2020-07-05 00:58:34 Author: bbs.pediy.com(查看原文) 阅读量:533 收藏

本文接 上一篇,这次我们开始探索有源侧信道攻击——毛刺攻击(Glitching)。

在开始之前,我想先解释一下有源和无源攻击的区别:

  • 有源(Active)指的是攻击过程会主动地对攻击的目标产生实质的干扰,比如扰乱目标的时钟信号、电源电压、改变周围的温度、光照、磁场等等,从而影响目标的正常运行,人为地制造运行时的错误,从而绕过各种安全机制。通常我们称这种人为植入错误的攻击手段为 Fault Injection Attack。
  • 无源(Passive)指的是攻击过程不会对攻击的目标产生实质的影响,不造成人为的干扰,比如仅仅观测输入输出的电压、电流、功耗、目标释放出来的温度、磁场、声波等等,以非侵入试的数据采集和分析计算出想要破解的数据。

我们之前一直在探讨如何通过采集目标的功耗数据进行分析计算,破解出我们想要的密钥。这种方法不影响程序的正常运行——如果要通过验证,我们仍然需要向程序提供正确的密码。

这次我们探索的有源攻击,可以让 CPU 直接跳过各种校验,完全忽略各种安全机制——也就是说我们可以一定程度上改变 CPU 的执行流程,即使输入错误的密码也可以通过验证。

本文先介绍时钟毛刺攻击,下次再讲电压毛刺攻击。

就像人有心率一样,CPU 也有它的频率。人的心率来自心脏,CPU 的频率来自时钟信号(Clock Signal)。我们在教科书上学到的时钟信号是长这样的:

十分规律、棱角分明的方波。然而现实中的时钟信号大多是长这样的:

并不是完全规整的方波。CPU 其实并不要求这个信号长得多规整,CPU 关注的其实只有两个部分:从低到高和从高到低的两个边沿(Edge),也就是图中直上直下的部分。CPU 会有一个电压阈值来判断当前的信号电压属于低(0)还是高(1),一旦越过这个阈值,CPU 就会人为它看到了一个边沿信号。一个上行边沿到下一个上行边沿所用的时间就是这个时钟信号的周期,而每秒钟的周期数就是这个时钟信号的频率。

那这个时钟信号对 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 需要的时间就开始发起毛刺攻击。

我绘制了一个简化过的时钟毛刺生成电路原理图:

Input Clock 是一个符合标准的正常时钟信号。我们把这个信号进行两次相位偏移(Phase Shift)得到 Shifted #1 和 Shifted #2 两个频率跟输入信号一样,但是相位各不相同的信号。然后我们用逻辑门把两个相位之间的上行边沿组成一个新的信号 Glitch。这个信号的频率跟输入时钟一样,但是相位不同、宽度不同。我们把这个信号的上行边沿跟输入时钟的上行边沿的偏移量记作 offset,一般表达为跟输入时钟周期的相对百分比,而不是使用绝对的时间。然后我们把 Glitch 信号的宽度记作 width,也使用百分比表示。

Trigger 是一个简单的触发信号,是从攻击目标板上观测到的。当 Trigger 被拉低之后,我们开始计时 ext_offset,时间一到就马上拉高 Glitch Enable,根据我们的电路原理图,这个线路拉高后,Glitch 信号就开始输出,然后会跟输入时钟信号进行异或运算,得到 Clock XOR Glitch 这个带锯齿的时钟信号,这个信号将最终传给攻击目标 CPU (DUT / Device Under Test)作为它的外部时钟信号。

还有一个可控的参数就是 repeat,就是 Glitch 毛刺的个数。我们通常只是想在非常短的时间内发送一些毛刺信号来影响单一指令的执行,我们不希望影响太多指令的执行,因此这个 repeat 可以控制我们发起攻击的持续时间。

由此,我们可以精确掌控何时开始加入、何时终止毛刺时钟信号、毛刺的相位偏移量、宽度等等。但是毛刺攻击仍然是有一定机率失败的,我们需要反复尝试不同的参数组合,每种组合都多次尝试,从而找到一个合适的参数范围,最大化我们的成功率。

这个实验将使用 jupyter/Fault_1-Introduction_to_Clock_Glitch_Attacks.ipynb 这个文件中的功能。跟之前一样,我们需要设置 PLATFORM = 'CW303' 表示我们的目标板子是 CW303 AVR CPU。

然后我们来看看我们给目标写的测试程序:

void glitch1(void)
{
    volatile uint8_t a = 0;
    trigger_high();
    trigger_low();
    while(a != 2){}
    uart_puts("1234");  
}

这里有一个明显的死循环,只有让 CPU 跳出这个循环,我们才能在串口读到 1234 的输出。如果我们把这个程序编译成汇编,可以看到死循环部分是这样实现的:

 movzbl -0x1(%rbp),%eax
 cmp $0x2,%al
 jne 40110b <glitch1+0x9>

第一行从栈上加载数据,第二行跟 2 进行对比,第三行 jne 表示如果前面的比较操作结果是不等于,则继续循环。也就是说如果我们能跳过 jne 这条指令,我们就可以让 CPU 跳出这个死循环,继续执行之后的指令。或者前面两行指令如果执行错误加载或写入了错误的数值,也有可能让 CPU 跳出这个死循环。

我们在这个循环前面人为设置一个 Trigger 信号,trigger_low() 会拉低一个 GPIO 的输出,我们的主控板会监听这个输出信号,一旦看到下行边沿就会开始倒计时 ext_offset 然后开始发送毛刺时钟信号。

因为我们这个程序设计得非常简单,我们可以使用默认的 ext_offset 和 repeat,只尝试不同的 offset 和 width 参数的组合。我们使用 ChipWhisperer 提供的脚本遍历这两个参数,记录每次攻击的结果是否成功。然后我们把所有的结果绘制成点阵图:

可以看到,当 wdith 处于 -3% 到 -10%,以及 offset 处于 3% 到 8% 范围的时候我们可以观测到一些成功的毛刺攻击。这些尝试都成功地让 CPU 跳出了死循环,成功从串口读到了输出值。

为了更好地让大家理解这个攻击的物理原理,我用电子示波器对 Trigger 信号和毛刺时钟信号进行了采样。下图是 Trigger 被拉低前后的采样结果:

黄色的 1 号通道是时钟信号,青色的 2 号通道是 Trigger 信号。可以看到 Trigger 被拉低之前,时钟信号都是正常的波形。我们放大来看看:

但是从 Trigger 被拉低后没过多久我们就看到时钟信号开始出现了毛刺,我们把毛刺信号放大来看看:

可以看到这个毛刺的形状跟我们上面理论分析的几乎一样。至此我们再次用实践证明了一个理论。我们这就来看看如何利用这个理论来设计真实的攻击。

还记得我们 第一篇 爆破的那个密码验证程序吗?我们这次试试用时钟毛刺攻击直接绕过验证,完全忽略密码的存在。这次我们使用如下代码:

void glitch3(void)
{
    char inp[16];
    char c = 'A';
    unsigned char cnt = 0;
    uart_puts("Password:");

    while((c != '\n') & (cnt < 16)){
        c = getch();
        inp[cnt] = c;
        cnt++;
    }

    char passwd[] = "touch";
    char passok = 1;

    trigger_high();
    trigger_low();

    //Simple test - doesn't check for too-long password!
    for(cnt = 0; cnt < 5; cnt++){
        if (inp[cnt] != passwd[cnt]){
            passok = 0;
        }
    }

    if (!passok){
        uart_puts("Denied\n");
    } else {
        uart_puts("Welcome\n");
    }
}

编译成汇编之后,if (!passok) 是这样实现的:

cmpb $0x0,-0x2(%rbp)
jne 401160 <glitch3+0x5e>

只要我们让 CPU 在这两行出错,就有可能让 CPU 进入正确密码的分支。这个攻击的脚本也在 CW 的文件中了,大家可以试着跑一下,基本上什么都不用做,不一会就成功试出正确的参数组合,成功绕过验证了。

除了绕过简单的密码校验,同样的时钟毛刺攻击还适用于绕过各种签名校验、无效化各种安全机制相关的寄存器写入操作等等,只要发挥想象力,运用范围十分广大。下一篇我将介绍另外一种毛刺攻击手段——电压毛刺攻击,敬请期待。

[公告]看雪论坛2020激励机制上线了!发帖不减雪币了!如何获得积分快速升级?

最后于 17小时前 被Invert编辑 ,原因: 错别字


文章来源: https://bbs.pediy.com/thread-260481.htm
如有侵权请联系:admin#unsafe.sh