ChipWhisperer:对岩浆的能量攻击

发表者rakf



在Digital Security的Hack of Summer 2019的一部分中,我处理了电源攻击并与ChipWhisperer合作。


这是什么


能量分析是通过第三方渠道进行的一种攻击。 也就是说,攻击使用的信息是由于系统的物理实施而出现的。


哪些信息可能对攻击者有用:


  • 密码转换时间;
  • 功耗;
  • 电磁场
  • 噪音等

能量攻击被认为是最普遍的。


为什么行得通?


微处理器,微控制器,RAM和许多其他逻辑电路均基于CMOS技术。 CMOS电路的功耗包括两个部分:静态和动态。 静态功耗非常小,这是技术垄断的原因之一。 动态功率归因于晶体管的开关,并取决于处理后的数据和执行的操作。 由于静态分量主要是恒定的,因此总功率的变化完全是由动态功率引起的,因此总能耗可用于分析。


工具包


ChipWhisperer ,我使用了两部分版本:


ChipWhisperer 2部分版本


ChipWhisperer是用于研究嵌入式设备安全性的开源工具包。 它允许进行能量分析和基于错误的攻击。


该费用为250美元。 开发人员将其定位为革命性的解决方案,因为根据创建者的说法,此类复杂结构的成本从3万美元起。 该设备包括2个板:目标板和捕获板。


还有其他版本,它们具有优点(但价格昂贵),您也可以单独购买各种目标板,扩展卡,探针以对设备进行全面分析,并仅使用ChipWhisperer进行拆卸。


ChipWhisperer拥有出色的Wiki和小型开发实验室 ,到了第5版,他们甚至编写了不错的API 文档 。 使用曲目的软件将曲目从连接的设备中删除,并记录为NumPy阵列。


首先,您需要为目标设备编写固件。 作为实验的一部分,考虑了密码AES,DES,TEA。 对于他们来说,已经有现成的固件和删除迹线的设置(要采集的样本数,偏移量,ADC频率等)。 对于独立研究,必须通过实验选择设置。


如上所述,您可能会引起目标板故障:引起时钟信号故障,跳过指令并提取机密信息。


在大型综合体中,使用示波器跟踪轨迹。


分析方法


有几种基本分析方法:


  • 简单功率分析(SPA);
  • 差分功率分析(DPA);
  • 功率相关分析(CPA)。

简单的功率分析包括功率图的可视化分析。 例如,当输入正确的字符并与错误的字符进行比较时,通过观察电源配置文件,可以一次获得一个字符的密码。


密码能力分析


或者,您可以在轨道上看到加密回合。 没有足够的数据来获取密钥,但是可以假定执行了哪种算法。 该图清楚地显示了10轮AES。


AES SPA


差分分析(DPA)比简单分析有效得多,DPA基于统计分析方法来检测功率曲线的差异。 但是,一种非常有效的工具来“打开”钥匙将需要大量的路线。 我没有使用这种方法进行分析,但是最后,我将提供一些链接,这些链接已被很好地描述。


相关性分析的基础是用于确定秘密加密密钥的统计设备。 有时,它以单独的类型被隔离,有时称为DPA。 与DPA相比,此方法所需的迹线更少,我将其用于功耗分析。 我会告诉你更多。


主要任务是建立所研究设备的能耗的准确模型。 如果建立了准确的模型,则预测值和实际值之间会存在明显的相关性。


可以使用的功率模型之一是汉明重量模型。 Hamming的权重是二进制表示形式中非零值的数量。 该模型的假设是寄存器中设置的位数将与设备消耗的能量相关。 汉明的体重自此被用作常规的能量单位。 还使用汉明距离模型-2个值中不同位数的数量。


为了比较汉明重量模型和实际功耗,使用了线性皮尔逊系数。 它显示了一个量对另一个量的线性依赖性。 使用正确构建的模型,该系数将趋于1。


通用CPA算法包括以下主要步骤:


  • 消除转换未知密钥上的消息时的功耗;
  • 当在关键子块的所有可能变体上转换相同的消息时,我们建立一个芯片能耗模型(每个字节256个选项);
  • 我们找到了模拟功耗和实数的线性相关系数。 如果是真密钥,系数将趋于1;否则为0。
  • 对其余关键子块重复该算法。

结果,密钥被一小部分依次恢复。 如果我们一次攻击密钥的一个字节,则每个密钥要进行2 8次尝试。 例如,如果选择AES-128,则密钥的16个字节的总尝试次数将为2 12 ,这比命中2 128更好。


岩浆密码分析


岩浆是GOST R 34.12-2015中定义的代码,实际上是GOST 28147-89中定义的代码,仅在配置文件中。 加密的块经过32轮,其中发生一些转换。 密钥包含256位,每个回合密钥都是原始密钥的一部分。


Feistel功能GOST


我们将分析使用CPA方法获得的数据。


首先,您需要选择算法的中间值,该中间值取决于已知数据和密钥的一小部分,并且可以通过简单的转换来计算。 通常,此值是第一个(已知开放文本)或最后一个回合(已知密文)的S-box输出(岩浆现在有1个替换表,因此更容易进行此类攻击)。


我用著名的开放文本研究了一种密钥,因此将进一步考虑该选项。 在Magma算法中,与DES,AES不同,添加了带有回合密钥的32位块以2 32为模,因此最好从S-box的最后一个输出开始分析,因为添加最大值不会影响较年轻的值。 我们考虑每个S-box的输出:首先是8,然后是8、7,依此类推,直到第一个。 轨道去除是在8位微控制器上进行的,我们可以假定它与双S盒配合使用。 因此,我将立即攻击1个字节。


最后一个关键字节的能量模型的计算:


for kguess in range(0, 256): #Initialize arrays & variables to zero sumnum = np.zeros(numpoint) sumden1 = np.zeros(numpoint) sumden2 = np.zeros(numpoint) hyp = np.zeros(numtraces) for tnum in range(numtraces): hyp[tnum] = HW[leak("Gost28147_tc26_ParamZ", kguess, block2ns(text[tnum])[0], 0)] 

其中泄漏函数仅返回最后一个字节的S-box输出。


我们根据以下公式计算模拟值和实际值的线性皮尔逊系数:


相关性


  cpaoutput = [0]*256 maxcpa = [0]*256 #Mean of hypothesis meanh = np.mean(hyp, dtype=np.float64) #Mean of all points in trace meant = np.mean(traces, axis=0, dtype=np.float64) #For each trace, do the following for tnum in range(0, numtraces): hdiff = (hyp[tnum] - meanh) tdiff = traces[tnum,:] - meant sumnum = sumnum + (hdiff*tdiff) sumden1 = sumden1 + hdiff*hdiff sumden2 = sumden2 + tdiff*tdiff cpaoutput[kguess] = sumnum / np.sqrt( sumden1 * sumden2 ) maxcpa[kguess] = max(abs(cpaoutput[kguess])) 

当选择一个真正的子项时,相关系数将显着增加:


相关1


因此,分析了回合密钥的每个字节。


  for bnum in range(0, 4): cpaoutput = [0]*256 maxcpa = [0]*256 for kguess in range(0, 256): best_round_key = kguess << (bnum * 8) | best_round ... 

给定第一个回合密钥,我们可以以相同的方式计算2个和随后的回合密钥。 完整的岩浆钥匙可以通过取出8个圆形钥匙来获得。


在解决这个问题的过程中,出现了一些细微差别。 与AES,DES,Grasshopper密码不同,添加明文轮回密钥的模数为2 32 。 与XORa不同,低位的添加会影响高位。 在计算每个后续子项时,必须考虑过去的结果。 圆形键也是如此。 数据非常敏感。 如果其中一个值的计算不正确,则整个密钥的进一步计算将不正确。


还值得考虑的是,现在很难找到具有四位架构的芯片:大多数芯片具有八位。 当然,有专门用于特定安全转换算法(或几种算法)的专用密码芯片,它们具有最有效的体系结构。


为了执行DES密码,对于Magma,这种密码处理器可以具有六位体系结构-四位体系结构,它允许分别处理每个S-box。 我的目标设备是8位,在“岩浆”的情况下,将以一种方法对8位进行转换,即 替换将在2个S-box上进行,将考虑2个S-box的功耗。 如果S-box(高级或初级)中的一个与真实密钥匹配,而另一个与之不匹配,则可能会发生高相关性突发。


鉴于以上所有内容,在输出中,我们具有以下脚本,用于分析岩浆密码的能耗路径:


Python脚本
  import numpy as np path = r'C:\Users\user\chipwhisperer\projects\gost_10000_2_data\traces\2019.08.11-19.53.25_' traces = np.load(path + 'traces.npy') text = np.load(path + 'textin.npy') keys = np.load(path + 'keylist.npy') HW = [bin(n).count("1") for n in range(0,256)] SBOXES = {"Gost28147_tc26_ParamZ": ( (12, 4, 6, 2, 10, 5, 11, 9, 14, 8, 13, 7, 0, 3, 15, 1), (6, 8, 2, 3, 9, 10, 5, 12, 1, 14, 4, 7, 11, 13, 0, 15), (11, 3, 5, 8, 2, 15, 10, 13, 14, 1, 7, 4, 12, 9, 6, 0), (12, 8, 2, 1, 13, 4, 15, 6, 7, 0, 10, 5, 3, 14, 9, 11), (7, 15, 5, 10, 8, 1, 6, 13, 0, 9, 3, 14, 11, 4, 2, 12), (5, 13, 15, 6, 9, 2, 12, 10, 11, 7, 8, 1, 4, 3, 14, 0), (8, 14, 2, 5, 6, 9, 1, 12, 15, 4, 11, 0, 13, 10, 3, 7), (1, 7, 14, 13, 0, 5, 8, 3, 4, 15, 10, 6, 9, 12, 11, 2), )} def _K(s, _in): """ S-box substitution :param s: S-box :param _in: 32-bit word :returns: substituted 32-bit word """ return ( (s[0][(_in >> 0) & 0x0F] << 0) + (s[1][(_in >> 4) & 0x0F] << 4) + (s[2][(_in >> 8) & 0x0F] << 8) + (s[3][(_in >> 12) & 0x0F] << 12) + (s[4][(_in >> 16) & 0x0F] << 16) + (s[5][(_in >> 20) & 0x0F] << 20) + (s[6][(_in >> 24) & 0x0F] << 24) + (s[7][(_in >> 28) & 0x0F] << 28) ) def block2ns(data): """ Convert block to N1 and N2 integers """ data = bytearray(data) return ( data[7] | data[6] << 8 | data[5] << 16 | data[4] << 24, data[3] | data[2] << 8 | data[1] << 16 | data[0] << 24, ) def addmod(x, y, mod=2 ** 32): """ Modulo adding of two integers """ r = x + int(y) return r if r < mod else r - mod def _shift11(x): """ 11-bit cyclic shift """ return ((x << 11) & (2 ** 32 - 1)) | ((x >> (32 - 11)) & (2 ** 32 - 1)) def round(sbox, key, data, byte): s = SBOXES[sbox] _in = addmod(data, key) sbox_leak = _K(s, _in); return (sbox_leak >> (8 * byte)) & 0xFF def Feistel(sbox, key, data, nround): s = SBOXES[sbox] w = bytearray(key) x = [ w[3 + i * 4] | w[2 + i * 4] << 8 | w[1 + i * 4] << 16 | w[0 + i * 4] << 24 for i in range(8) ] n1, n2 = block2ns(data) for i in range(nround): n1, n2 = _shift11(_K(s, addmod(n1, x[i]))) ^ n2, n1 return n1 numtraces = len(traces) numpoint = np.shape(traces)[1] bestguess = [0]*32 round_data = [0] * numtraces for i in range(numtraces): round_data[i] = [0] * 8 for rnum in range(0, 8): best_round = 0 for tnum_r in range(numtraces): round_data[tnum_r][rnum] = Feistel("Gost28147_tc26_ParamZ", bestguess, text[tnum_r], rnum) for bnum in range(0, 4): cpaoutput = [0]*256 maxcpa = [0]*256 for kguess in range(0, 256): #Initialize arrays & variables to zero best_round_key = kguess << (bnum * 8) | best_round sumnum = np.zeros(numpoint) sumden1 = np.zeros(numpoint) sumden2 = np.zeros(numpoint) hyp = np.zeros(numtraces) for tnum in range(numtraces): hyp[tnum] = HW[round("Gost28147_tc26_ParamZ", best_round_key, round_data[tnum][rnum], bnum)] #Mean of hypothesis meanh = np.mean(hyp, dtype=np.float64) #Mean of all points in trace meant = np.mean(traces, axis=0, dtype=np.float64) #For each trace, do the following for tnum in range(0, numtraces): hdiff = (hyp[tnum] - meanh) tdiff = traces[tnum,:] - meant sumnum = sumnum + (hdiff*tdiff) sumden1 = sumden1 + hdiff*hdiff sumden2 = sumden2 + tdiff*tdiff cpaoutput[kguess] = sumnum / np.sqrt( sumden1 * sumden2 ) maxcpa[kguess] = max(abs(cpaoutput[kguess])) best_round = best_round | (np.argmax(maxcpa) << (bnum * 8)) bestguess[((rnum + 1) * 4)-bnum - 1] = np.argmax(maxcpa) print "Best Key Guess: " for b in bestguess: print "%02x "%b, 

GitHub结果存储库


结论


作为研究的一部分,我与ChipWhisperer合作。 尽管我没有尝试所有工具(例如,故障),但我确实发现ChipWhisperer很有用,尤其是在您不想购买昂贵的专用硬件的情况下。


关于我们的功耗密码分析,它比上述密码更能抵抗这种攻击,但是有了足够的数据,就可以毫无问题地获得密钥。


有趣的材料:


Source: https://habr.com/ru/post/zh-CN477730/


All Articles