SonicOS 固件解密 (2)

Catalpa 网络安全爱好者

本文介绍一些 SonicWall SonicOS 固件解密的思路。

本 Blog 曾发布过一篇关于如何解密 SonicWall NSv 系统的文章,漫长的时间过去了,厂商也对该系统进行了数次升级迭代,现在我们来看看新版本中是否实现了更多安全措施。

本文提及的环境可以在网盘获取,相关工具可以在 Github 获取。

SonicOS 7.0

前篇文章分析的系统基于 SonicOS 6.0,SonicWall 于 2023 年发布了新的 GEN 7 系统,根据发布说明,该系统在多个方面进行了改进。我们以 7.0.1_5161 为例来看看新的系统是否修改了磁盘加密逻辑。

导入虚拟镜像后启动,等待系统完成安装过程,之后将磁盘挂载到 Linux 中分析,通过 lsblk 命令可以看到磁盘具有 7 个分区

1
2
3
4
5
6
7
8
sdb                                                                                     
├─sdb1 vfat FAT16 EFI-SYSTEM 3115-E9D4
├─sdb2
├─sdb3 crypto_LUKS 1 a75000be-2dc9-11e8-bddb-67945b040d06
├─sdb4
├─sdb6 crypto_LUKS 1 664f41bc-3bdb-11e8-a9f1-37e7a8e911e8
├─sdb7 crypto_LUKS 1 77f38900-3d70-11e8-9e8c-8b99c51126e1
└─sdb9 crypto_LUKS 1 9943fac6-37f1-11e8-bbd4-4b1d323a5df2

其中 3、6、7、9 这 4 个分区依然是 LUKS 加密的,可以推测在 7.0 版本的系统中,还没有修改磁盘加密逻辑。

前篇文章是通过调试 luks 模块从内存中 dump 密钥实现磁盘解密,这种方法比较麻烦且容易出现错误,这次我们来分析一下具体的密钥生成算法是什么。

解密逻辑位于 luks.mod 中,关键函数 sub_0。该模块应该属于 grub 项目的一部分,通过搜索找到源代码位于 luks.c 的 luks_recover_key 函数。对比源码发现,SonicWall 的开发者修改了 LUKS 相关逻辑,在 luks_recover_key 函数中会尝试从本地文件或者磁盘头部读取密钥信息,并自动对磁盘进行解密。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
__int64 __fastcall luks_recover_key(_QWORD *a1, __int64 a2, __int64 a3, __int64 *a4, __int64 a5)
{
v5 = &v53;
v7 = 62LL;
v52 = 0LL;
while ( v7 )
{
*v5 = 0;
v5 += 4;
--v7;
}
if ( a3 )
{
grub_file_seek(a3, 0LL);
v9 = grub_file_read(a3, disk_data, 592LL);
result = 14LL;
v11 = v9 == 592;
}
else
{
result = grub_disk_read(a1, 0LL, 0LL, 592LL, disk_data); // [1]
v11 = result == 0;
}
if ( !v11 )
return result;
grub_puts_("Loading..."); // [2]
v12 = _byteswap_ulong(disk_data[27]);
v38 = v12;
if ( v12 > 0x80 )
{
v16 = "key is too long";
v17 = 9LL;
return grub_error(v17, v16);
}
v13 = 0LL;
v14 = 1LL;
v15 = &disk_data[63]; // [3]
do
{
if ( disk_data[v13 + 52] == 0xF371AC00 )
{
v18 = _byteswap_ulong(disk_data[v13 + 63]);
if ( v14 < v18 )
v14 = v18;
}
v13 += 12LL;
}
while ( v13 != 96 );
v19 = grub_malloc(v38 * v14, &disk_data[63], 384LL, &disk_data[52]);
v20 = 0LL;
if ( !v19 )
return grub_errno;
do
{
v21 = *(&disk_data[28] + v20++);
v47[v20 + 19] = v21;
}
while ( v20 != 20 );
for ( i = 0LL; i != 32; ++i )
{
LOBYTE(v15) = *(&disk_data[33] + i);
v47[i + 40] = v15;
}
v23 = &v48;
for ( j = 0LL; j != 52; ++j )
{
v25 = &v49[-j - 1];
LOBYTE(v25) = v47[j + 20] ^ v49[-j - 1];
if ( v25 <= 0x1Fu )
v25 = (v25 | 0x20);
v49[j] = v25;
}
v45 = 1;
v26 = 2 - (a4 == 0LL);
while ( 1 )
{
v43 = a5;
if ( !a4 )
break;
LABEL_38:
v31 = &disk_data[54];
for ( k = 0; k != 8; ++k )
{
if ( *(v31 - 2) == -210654208 )
{
grub_real_dprintf("disk/luks.c", 246LL, "luks", "Trying keyslot %d\n", k);
v32 = grub_crypto_pbkdf2(*(a2 + 88), a4, v43, v31, 32LL, _byteswap_ulong(*(v31 - 1)), v51, v38); // [5]
if ( v32 )
goto LABEL_51;
grub_real_dprintf("disk/luks.c", 263LL, "luks", "PBKDF2 done\n");
v33 = grub_cryptodisk_setkey(a2, v51, v38);
if ( v33 )
goto LABEL_55;
v34 = _byteswap_ulong(v31[8]);
v35 = v38 * _byteswap_ulong(v31[9]);
if ( a3 )
{
grub_file_seek(a3, v34 << 9);
if ( grub_file_read(a3, v19, v35) != v35 )
{
v36 = 14;
LABEL_46:
v39 = v36;
grub_free(v19);
return v39;
}
}
else
{
v36 = grub_disk_read(a1, v34, 0LL, v35, v19);
if ( v36 )
goto LABEL_46;
}
v33 = grub_cryptodisk_decrypt(a2, v19, v35, 0LL);
if ( v33 )
goto LABEL_55;
v33 = AF_merge(*(a2 + 88), v19, v50, v38, _byteswap_ulong(v31[9]));
if ( v33 )
goto LABEL_55;
grub_real_dprintf("disk/luks.c", 306LL, "luks", "candidate key recovered\n");
v32 = grub_crypto_pbkdf2(
*(a2 + 88),
v50,
_byteswap_ulong(disk_data[27]),
&disk_data[33],
32LL,
_byteswap_ulong(disk_data[41]),
v47,
20LL);
if ( v32 )
{
LABEL_51:
v40 = v32;
grub_free(v19);
v37 = v40;
return grub_crypto_gcry_error(v37);
}
if ( !grub_memcmp(v47, &disk_data[28], 20LL) )
{
v33 = grub_cryptodisk_setkey(a2, v50, v38);
if ( v33 )
{
LABEL_55:
grub_free(v19);
v37 = v33;
return grub_crypto_gcry_error(v37);
}
grub_free(v19);
return 0LL;
}
v15 = &loc_148;
grub_real_dprintf("disk/luks.c", 328LL, "luks", "bad digest\n");
}
v31 += 12;
}
if ( v26 == 1 )
{
v26 = 0;
}
else
{
v15 = (sub_0 + 1);
v26 = 1;
grub_printf_("%u attempt%s remaining.\n", 1, &unk_B64);
}
a4 = 0LL;
if ( !v26 )
{
grub_free(v19);
v16 = "access denied";
v17 = 30LL;
return grub_error(v17, v16);
}
}
if ( v45 )
{
a4 = v49; // [4]
v45 = 0;
v43 = 52LL;
goto LABEL_38;
}
name = 0LL;
v28 = a1[5];
if ( v28 )
name = grub_partition_get_name(v28, v15, v25, v23);
v29 = &unk_B64;
v30 = &unk_B64;
if ( name )
v30 = name;
if ( a1[5] )
v29 = &unk_B65;
grub_printf_("Enter passphrase for %s%s%s (%s): ", *a1, v29, v30, a2 + 140);
grub_free(name);
v15 = (&loc_FE + 2);
if ( grub_password_get(&v52, 256LL) )
{
a4 = &v52;
v43 = grub_strlen(&v52);
goto LABEL_38;
}
grub_free(v19);
v16 = "Passphrase not supplied";
v17 = 18LL;
return grub_error(v17, v16);
}

简要分析上面的代码,首先在 [1] 处,代码会调用 grub_disk_read 尝试从磁盘读取 592 字节的数据,在 [2] 处,调用 grub_puts 函数输出了 Loading... 字符串,在开机时可以看到这个字符串。

[3] 处开始,代码使用从磁盘读取的 592 字节数据进行一些复杂运算,最终会将计算结果保存在变量 v49 中。在 [5] 处会调用 grub_crypto_pbkdf2 解密磁盘,我们知道它的第二个参数应该是密钥,但在调用该函数之前都没有操作变量 a4 的逻辑。

继续向下分析代码发现在 [4] 处会将 a4 赋值为 v49,说明密钥确实和磁盘头部数据有关。那么将生成密钥的逻辑梳理出来形成程序,在本地就可以直接解密硬盘,无需复杂的调试过程。

具体实现请参考仓库代码。

SonicOS 7.1

目前 SonicOS 的最新版本为 7.1.2,和前面相同的思路挂载硬盘后发现分区信息有变化

1
2
3
4
5
6
7
sdb                                                                                   
├─sdb1 vfat FAT16 BOOT EBD9-9F71
├─sdb2 vfat FAT16 BOOT_A CE80-C973
├─sdb3 vfat FAT16 BOOT_B 8752-5DF4
├─sdb4 vfat FAT16 SYSTEM_BOOT 31CF-394A
├─sdb5 ext4 1.0 INSTALL-CACHE 829fb2a8-48d1-4416-8d60-a63cc4bcea77
└─sdb6

前面 4 个似乎都是启动分区,sdb5 可以直接挂载,但其中只保存了系统当前正在运行的 .bin.sig 固件文件,且该文件是加密的,sdb6 的格式无法识别。

挂载 BOOT 分区,文件内容也和老版本不同,现在 GRUB 被打包成了 EFI/BOOT/bootx64.efi 文件,此外还有 SYSTEM.LIC 和 SYSTEM.SYS 两个文件,SYSTEM.LIC 是一个压缩包,解压可以得到 DATA:FW-crypt-release.key-877cebb9-f923-4245-9952-18a00ce5f77d 等数个类似密钥的文档,但格式都无法识别。

使用 binwalk 分析并解压 bootx64.efi 文件,得到一个叫做 soniccorex.bin 的 Linux 内核文件,它应该就是虚拟机 Linux 系统的内核。

解密磁盘这个行为有可能发生在内核或者 initramfs 中,按照以往的思路先定位到 populate_rootfs,该函数负责解压 initramfs 镜像。通过调试内核的方式在 populate_rootfs 下断点,从内存中可以提取 initramfs 压缩包。

解压后得到一个标准的 Linux 文件系统,其中包含一些关键文件,例如 onetime.key.encsunup.cpio.gz.enc 等。分析 /init 这个初始化脚本,里面包含大量和解密相关的代码,这里列举部分关键代码,简要分析这个脚本的逻辑。

脚本主要执行以下几个操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
early_setup

test "$VERBOSE" = "0" && exec 2>/tmp/preload.log

[ -z "$CONSOLE" ] && CONSOLE="/dev/console"

# read and process command line args (for dev pass verbose debugshell)
#read_args

# decrypt stage2 initramfs (sunup)
setup_stage2

# jump to stage2
mount_and_boot

从代码可以看到应该还存在一个叫做 “sunup” 的 initramfs,说明内核加载的 initramfs 中可能没有直接解密硬盘。

early_setup 函数执行一些初始化操作,setup_stage2 为关键函数,它会调用 unpack_stage2 函数。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
unpack_stage2() {
echo
if test -n "${SPECIAL_BUILD_MESSAGE// /}"
then echo "**${SPECIAL_BUILD_MESSAGE//?/*}**" &&
echo "* $SPECIAL_BUILD_MESSAGE *" &&
echo "**${SPECIAL_BUILD_MESSAGE//?/*}**"
fi

sanity_check

log "Setting up stage2 initramfs (sunup) ..."

local use_sunup_key_from="${SUNUP_USE_KEY_FROM}"
local kek
kek=$(mktemp)

if test "$use_sunup_key_from" = "FS"
then
if is_key_in_fs
then
log "Found key in FS..."
# key=/onetime.key is present in FS
else
halt "Unable to setup stage2 (key not in fs) ..."
fi
fi

if test "$use_sunup_key_from" = "UEFI"
then
if is_kek_in_uefi
then
log "Found key in UEFI ..."
do_get_kek_from_uefi > "$kek"
openssl rsautl -verify -in "${ENC_KEY}" -pubin -inkey "$kek" -out ${KEY}
else
halt "BIOS not properly provisioned, please contact technical support"
fi
fi

if test "$use_sunup_key_from" = "SSSS"
then if get_key_from_ssss > "$KEY"
then log "Found key in SSSS ..."
else halt "Corrupt/missing secure store, please contact technical support"
fi
fi

if test "$use_sunup_key_from" = "OVMF"
then
if is_kek_in_ovmf
then
log "Found key in OVMF ..."
do_get_kek_from_ovmf > "$kek"
openssl rsautl -verify -decrypt -in "${ENC_KEY}" -inkey "$kek" -out ${KEY}
else
halt "Please use SonicWall supplied UEFI OVMF_CODE. Please contact technical support "
fi
fi

if test "$use_sunup_key_from" = "ACPI_BGRT_XFRM_UTIL"
then
if is_kek_in_acpi_bgrt
then
log "Found key in XFRM ..."
do_get_kek_from_acpi_bgrt > "$kek"
openssl enc -a -aes-256-cbc -d -pbkdf2 -in ${ENC_KEY} -salt -out ${KEY} -pass file:"$kek"
else
halt "Please use SonicWall supplied OVMF, please contact technical support"
fi
fi

# decrypt stage2 (sunup)
openssl enc -a -d -aes-256-cbc -pbkdf2 \
-in $ROOT_IMAGE_ENC -salt \
-out $ROOT_IMAGE \
-pass file:${KEY}

rc=$?

if test $rc -ne 0
then
halt "Unable to setup stage2 initramfs (sunup) ..."
fi

log "Finished setting up stage2 initramfs (sunup) ... "
}

这个函数包含几种不同的密钥加载方法,虚拟机环境中默认使用 SSSS 方法,因此代码又会调用 get_key_from_ssss 函数。

部分关键逻辑列举如下:

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
efi_get_var() {
if test -e "/sys/firmware/efi/efivars/$1" # check if file exists
then dd bs=1 skip=4 2>/dev/null < "/sys/firmware/efi/efivars/$1"
elif test -n "${SECURE_STORE_BACKUP_SYS}" # look on backup
then with-mount-sys-partition "${SECURE_STORE_BACKUP_SYS}" "${SECURE_STORE_BACKUP_PARTITION}" tar -xzf "${SECURE_STORE_BACKUP_FILE}" -O "$1" | dd bs=1 skip=4 2>/dev/null
fi && pipe-result
}

# non TINY data
efi_get_data() {
local pass
# 2-step to avoid contention on tpm0 device as sonictpm doesn't retry
pass=$(efi_get_var "KEY${SCX_SECURE_STORE_SEPARATOR}$1-${SSSS_UEFI_ID}" | /bin/gunzip | dessss | base64 && pipe-result ) &&
efi_get_var "DATA${SCX_SECURE_STORE_SEPARATOR}$1-${SSSS_UEFI_ID}" | openssl enc -des-ecb -iter 512 -d -pass file:<( base64 -d <<<"$pass" ) && pipe-result
}

get_kek_from_ssss() {
efi_get_data "${SSSS_UEFI_KEY}"
}

get_key_from_ssss() {
local kek
kek=$(mktemp) && get_kek_from_ssss > "$kek" && openssl rsautl -decrypt -inkey "$kek" -in "${ENC_KEY}"
just rm -f "$kek"
}

简要总结解密 sunup 的逻辑

1
2
3
4
5
6
1. 从 /sys/firmware/efi/efivars 目录下读取 KEY:SUNUP 密钥
2. 根据 CryptoEnging 设置解密方法 (默认为 crypto-engine-openssl)
3. 读取 LastBootMachineId 作为密码对密钥进行解密
4. 使用得到的 KEY 去解密 DATA:SUNUP 文件
5. 用得到的 SUNUP RSA 私钥解密 onetime.key.enc 文件
6. 用 onetime.key 再去解密 sunup.cpio.gz.enc 文件

从逻辑上看系统应该是实现了一套 SecureBoot 流程,猜测在硬件设备上具有 TPM 等安全芯片,密钥信息被保存到芯片中。但在虚拟机环境下不具备 TPM 条件,所以只能将密钥保存在 BOOT 分区中。

按照以上逻辑解密 SUNUP,得到第二个 initramfs,在这个阶段代码就会解密磁盘。还是分析 /init 程序,它会拉起 /init.d 中的初始化脚本,其中有一个叫做 14-keys 的脚本负责初始化密钥。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
keys_install_key() {
if type -p secure-store &>/dev/null #&& test "${SECURE_STORE_RSA_SUPPORT:-0}" != "0"
then # info "Importing $2=$1"
local md5sum current
if ! md5sum=$(md5sum "$1") || ! read -r md5sum _ <<<"$md5sum"
then debug "Can't read $1"
return 1
elif current=$(secure-store "${SECURE_STORE_DEFAULT}" get "md5sum:$2") && read -r current _ <<<"$current" && test "$md5sum" = "$current"
then debug "Already imported $2"
elif secure-store "${SECURE_STORE_DEFAULT}" import "$1" "$2" && secure-store "${SECURE_STORE_DEFAULT}" put "md5sum:$2" <<<"$md5sum"
then debug "Imported $2=$1"
else warn "Importing $2=$1 failed"
return 1
fi

# copy key to extra store without re-importing
if test -n "${SECURE_STORE_EXTRA}"
then if current=$(secure-store "${SECURE_STORE_DEFAULT}" get "$2") && read -r current _ <<<"$current" &&
md5sum=$(secure-store "${SECURE_STORE_EXTRA}" get "$2") && read -r md5sum _ <<<"$md5sum" &&
test "$md5sum" = "$current"
then debug "Already imported extra $2"
else # get key imported somehow
if secure-store "${SECURE_STORE_DEFAULT}" get "$2" | secure-store "${SECURE_STORE_EXTRA}" put "$2" &&
secure-store "${SECURE_STORE_EXTRA}" put "md5sum:$2" <<<"$md5sum"
then debug "Imported extra $2=$1"
else # Can't import from SECURE_STORE_DEFAULT as it doesn't work
warn "Importing extra $2=$1 difficult"
if ! md5sum=$(md5sum "$1") || ! read -r md5sum _ <<<"$md5sum"
then debug "Can't read $1"
return 1
elif current=$(secure-store "${SECURE_STORE_EXTRA}" get "md5sum:$2") && read -r current _ <<<"$current" && test "$md5sum" = "$current"
then debug "Already imported extra $2"
elif secure-store "${SECURE_STORE_EXTRA}" import "$1" "$2" && secure-store "${SECURE_STORE_EXTRA}" put "md5sum:$2" <<<"$md5sum"
then debug "Imported extra $2=$1"
else info "Importing extra $2=$1 failed"
return 1
fi
fi
fi
fi
fi
}

keys_init_platform_keys() {
# import install keys into keyring
local name
for name in $INSTALLER_DECRYPT_KEY $FW_DECRYPT_KEY $SUNUP_DECRYPT_KEY
do keys_install_key "${SCX_KEY_PATH}$name${SCX_KEY_SUFFIX}" "$name"
done

for name in $SSDH_VERIFY_PUB $INSTALLER_VERIFY_PUB
do keys_install_pub "${SCX_KEY_PATH}$name${SCX_KEY_SUFFIX}" "$name"
done
}

这个脚本程序会将 SYSTEM.LIC 中的密钥按照相同的解密逻辑解密,然后加载到内核密钥链中,供后面的程序使用。

最后在 opt/sonicwall/soniccore/scripts/disks 脚本中,会执行真正的解密磁盘操作。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#! /bin/cryptic_foobar
# shellcheck shell=bash
# shellcheck disable=SC1091

# Copyright 2019 SonicWALL Corporation.
# All rights reserved
# Initial Author: sliddicott@sonicwall.com

PLATFORM_CONFIG_DIR=/usr/lib/config/platform
. $PLATFORM_CONFIG_DIR/system.sh
. $PLATFORM_CONFIG_DIR/image.sh

. sissy "disks" || return 0
sissy_load "sunup" || sissy_fail "Could not load required module sunup." || sissy_calledAsScript || return $?

: "${LUKS:=luks}"
: "${SYSTEM_PARTITION_NAME:=SYSTEM}"
: "${LUKS_HEADER:=${SYSTEM_PARTITION_NAME}.SYS}"

decrypt_luks_header() {
# Separate the declaration from the initialization else 'local' eats the exit code (https://stackoverflow.com/a/4421282/14627257)
local r1
local r2
# this looks like hell because it runs under busybox sh as well, so no <( ... )
exec 3<&0
{ printf "Salted__" ; cat "$1" ; } | openssl enc -d -pbkdf2 -aes-256-cbc ${3} -pass fd:3 -out "$2"
# Ensure printf and openssl return 0
test "${PIPESTATUS[*]}" = "0 0"
r1=$?
# Test the resulting file
cryptsetup luksDump $2 >/dev/null 2>&1
r2=$?

# Implicitly exit with 0 if both tests pass
test $r1 -eq 0 && test $r2 -eq 0
}

recover_luks_header() {
test -z "$LUKS_HEADER_KEYID" && test -n "$LUKS_HEADER_KEYNAME" && LUKS_HEADER_KEYID=$(keyctl search "${LUKS_KEYRING}" user "$LUKS_HEADER_KEYNAME")
#shellcheck disable=SC2015
# Try password "as is" with -nopad, then try "as is" without nopad.
# Then try removing newlines with -nopad, lastly try removing newlines and nopad.
# Using braces to allow || to work as intended - otherwise it is associated with the test-command exit code rather than the decrypt_luks_header exit code.
# Our pattern is "echo 0 && echo 1 || echo 2 && echo 3" where we only want to see "0 1" or "0 1 2 3".
# Without braces we'll ALWAYS see "0 1 3"...
{ test -n "$LUKS_HEADER_KEYID" && keyctl pipe "$LUKS_HEADER_KEYID" | decrypt_luks_header "${LUKS_HEADER}" "$1" "-nopad"; } ||
{ test -n "$LUKS_HEADER_KEYID" && keyctl pipe "$LUKS_HEADER_KEYID" | decrypt_luks_header "${LUKS_HEADER}" "$1"; } ||
{ test -n "$LUKS_HEADER_KEYID" && keyctl pipe "$LUKS_HEADER_KEYID" | tr -d '\n' | decrypt_luks_header "${LUKS_HEADER}" "$1" "-nopad"; } ||
{ test -n "$LUKS_HEADER_KEYID" && keyctl pipe "$LUKS_HEADER_KEYID" | tr -d '\n' | decrypt_luks_header "${LUKS_HEADER}" "$1"; } ||
{ sunup_readLuksHeaderKey | decrypt_luks_header "${LUKS_HEADER}" "$1"; }
}

just() {
set -- $? "$@"
"${@:2}"
return "$1"
}

# mount device node $1, cd, and run "${@:2}"
# return false if mount fails, or exit code of command
with_mount() {
set -- "$PWD" "$(mktemp -d -p /mnt "${1##*/}-XXXXXX")" "$@"
if mount "$3" "$2"
then cd "$2" && "${@:4}"
just cd "$1"
just umount "$2"
else false
fi
just rmdir "$2"
}

disks_with_mount() {
with_mount "$@"
}

disks_unlockHardware_ssdUnlockDeviceProfile() {
sissy_debug "${FUNCNAME[0]} ${1@Q} ${2@Q}"
local dev="$1" profile="$2" realdev=''
[[ -n "$dev" ]] || sissy_warn "${FUNCNAME[0]: No device specified}" || return 1
[[ -n "$profile" ]] || sissy_warn "${FUNCNAME[0]: No profile specified}" || return 1

local realdev=''
realdev="$(realpath "/dev/disk/by-id/block-storage-$dev")"
sissy_info "${FUNCNAME[0]}: unlocking ${realdev@Q}"
if [[ "$realdev" = /dev/* ]]
then
realdev="${realdev/\/dev\//}"
else
sissy_warn "${FUNCNAME[0]}: Invalid realdev ${realdev@Q}"
return 1
fi
sissy_debug "${FUNCNAME[0]}: ssdunlock -t ${profile@Q} ${realdev@Q}"
ssdunlock -t "$profile" "$realdev"
sissy_debug "${FUNCNAME[0]} ${1@Q} ${2@Q}: Status $?"
return $?
}

disks_unlockHardwareAll() {
local dev
for dev in ${DISKS_DEVICES_SSDUNLOCK:-}
do
disks_unlockHardware "$dev"
done
}
disks_unlockHardware() {
local dev="$1" profile=''
case "$dev" in
EXP_M0) profile="${SSDUNLOCK_DEVICE_EXP_M0_PROFILE:-$SSDUNLOCK_DEVICE_DEFAULT_PROFILE}" ;;
EXP_M1) profile="${SSDUNLOCK_DEVICE_EXP_M1_PROFILE:-$SSDUNLOCK_DEVICE_DEFAULT_PROFILE}" ;;
esac
if [[ -n "$profile" ]]
then
disks_unlockHardware_ssdUnlockDeviceProfile "$dev" "$profile"
else
sissy_warn "${FUNCNAME[0]}: Unsupported device $dev profile $profile"
fi
}

disks_startEncryptedDisks() {
# TODO: Should we insist --type "$LUKS" ?
# What if upgrade from LUKS1 ?
sissy_debug "Disks commences, discover volume groups"
if test -n "$LUKS_HEADER" # look on /dev/disk/by-partlabel/BOOT
then if with_mount "/dev/disk/by-partlabel/BOOT" recover_luks_header "/tmp/$LUKS_HEADER"
then if test -n "$LUKS_SYSTEM_KEYID" || test -n "$LUKS_SYSTEM_KEYNAME" && LUKS_SYSTEM_KEYID=$(keyctl search "${LUKS_KEYRING}" user "$LUKS_SYSTEM_KEYNAME") && test -n "$LUKS_SYSTEM_KEYID"
then # optimise for correct key or adding newline
{ keyctl pipe "$LUKS_SYSTEM_KEYID" ; printf '\n' ; } | cryptsetup open ${LUKS_HEADER:+--header "/tmp/$LUKS_HEADER"} --key-file /dev/stdin "/dev/disk/by-partlabel/${SYSTEM_PARTITION_NAME}" "${SYSTEM_PARTITION_NAME}" >/dev/null 2>&1 ||
{ keyctl pipe "$LUKS_SYSTEM_KEYID" | tr -d '\n' ; } | cryptsetup open ${LUKS_HEADER:+--header "/tmp/$LUKS_HEADER"} --key-file /dev/stdin "/dev/disk/by-partlabel/${SYSTEM_PARTITION_NAME}" "${SYSTEM_PARTITION_NAME}" >/dev/null 2>&1 ||
sunup_readDiskKey | cryptsetup open ${LUKS_HEADER:+--header "/tmp/$LUKS_HEADER"} --key-file /dev/stdin "/dev/disk/by-partlabel/${SYSTEM_PARTITION_NAME}" "${SYSTEM_PARTITION_NAME}" >/dev/null 2>&1
else # don't correct keyring cos we can't
sunup_readDiskKey | cryptsetup open ${LUKS_HEADER:+--header "/tmp/$LUKS_HEADER"} --key-file /dev/stdin "/dev/disk/by-partlabel/${SYSTEM_PARTITION_NAME}" "${SYSTEM_PARTITION_NAME}" >/dev/null 2>&1
fi
just dmsetup mknodes >/dev/null 2>&1 # make sure they are present ready for vgchange
fi
fi

just rm -f "/tmp/$LUKS_HEADER"

local dev

for dev in $DEVICES_LUKS
do if test -b "$dev"
then sunup_readDiskKey | cryptsetup open --key-file /dev/stdin "$dev" "${dev##*/}"
else sissy_warn "Device $dev not found"
fi
done
just dmsetup mknodes >/dev/null 2>&1 # make sure they are present ready for vgchange

just vgchange -ay >/dev/null 2>&1
}

# WARNING: you probably shouldn't be using disks_stopEncryptedDisks
# in most cases, as "dmsetup remove_all" is way too violent.
disks_stopEncryptedDisks() {
sissy_debug "Stopping encrypted disks"
dmsetup remove_all >/dev/null 2>&1
}

disks_main() {
[ "$#" -eq 0 ] && {
sissy_msg "Usage: disks [start|stop]"
return 1
}
while [ "$#" -gt 0 ] ; do
case "$1" in
"start") disks_startEncryptedDisks ;;
"stop") disks_stopEncryptedDisks ;;
"unlock-hardware-all") disks_unlockHardwareAll "${@:2}" ; return $? ;;
"unlock-hardware") disks_unlockHardware "${@:2}" ; return $? ;;
*) sissy_warn "unknown action $1" ; return 1 ;;
esac
shift
done
}

sissy_moduleLoaded
sissy_calledAsScript || return 0
disks_main "$@"

解密逻辑大致为

1
2
1. 从内核密钥链加载 TINY:SYSTEM-HEADER 密钥,使用这个密钥解密 SYSTEM.SYS 文件
2. 从内核密钥链加载 TINY:SYSTEM 密钥,结合 SYSTEM.SYS 对磁盘进行解密

SYSTEM.SYS 实际上是一个 LUKS header 文件,LUKS 支持将 header 和加密数据分离,在需要时可以通过指定 –header 参数设置 header 文件解密硬盘。

所以到这里我们就理清了 SonicOS 7.1 版本的磁盘解密思路,按照该思路可以编写出本地解密脚本。

具体实现请参考仓库代码。

.sig 固件

官方为硬件设备提供的升级包都是 .sig 格式,熵值分析显示固件都是加密的。

虚拟机的升级包和 .sig 文件格式不同,想要分析格式可能需要硬件设备来调试。不过在查找系统镜像时,我发现 SonicWall 的另一款设备 SMA100 的升级包也是 .sig 格式,并且这款设备提供虚拟机镜像。

SMA100 已有前人详细研究过,这里不再赘述,解包得到系统文件之后,通过搜索和升级相关的信息可以定位到 upgradefirmware 这个程序,它负责接收用户上传的固件并检查是否合法。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
int __cdecl sub_804A58F(int a1, int a2, char *data, int size)
{
unsigned __int8 v5; // al
int v6[5]; // [esp+24h] [ebp-A4h] BYREF
int sha_obj[25]; // [esp+38h] [ebp-90h] BYREF
const char *v8; // [esp+9Ch] [ebp-2Ch] BYREF
char *filename; // [esp+A0h] [ebp-28h] BYREF
int v10; // [esp+A4h] [ebp-24h]
size_t header_length; // [esp+A8h] [ebp-20h]
int v12; // [esp+ACh] [ebp-1Ch]
char *data_1; // [esp+B0h] [ebp-18h]
int v14; // [esp+B4h] [ebp-14h]
uint32_t dataval_length; // [esp+B8h] [ebp-10h]
int v16; // [esp+BCh] [ebp-Ch]

v14 = 0;
if ( !data || size <= 0 )
return 19;
v14 = sub_804B6A0(a1, a2, &v8, &filename, 0, 0, 0, 0);// 初始化几个路径
if ( v14 )
return v14;
data_1 = data;
v5 = get_opcode(data); // 获取一个字节
v12 = check_format(data_1, v5); // 文件格式检查
if ( v12 )
return 10;
sub_804D1A1(sha_obj, data_1, size);
sha1_wrap(sha_obj, v6); // 计算 SHA1 值
v12 = verify_file(data, v6);
if ( v12 )
return 12;
header_length = get_header_len(data_1);
dataval_length = get_data_len(data_1);
v10 = sub_804D3AC(data_1, size);
if ( (header_length + dataval_length) > size )
return 1;
if ( a1 == 1 && (get_opcode(data_1) == 6 || get_opcode(data_1) == 5) )
return 1;
if ( get_opcode(data_1) != 10 && get_opcode(data_1) != 9 && get_opcode(data_1) != 8 )
return 1;
if ( filename ) // /cf/firmware/new/FFWHDR.INF
{
v16 = header_length > 1023 ? 0 : 1024 - header_length;
remove(filename);
v14 = split_header(data, header_length, v16, filename);// 把 header 写入本地文件
if ( v14 )
return v14;
}
if ( get_opcode(data_1) == 10 || get_opcode(data_1) == 8 )
{
dataval_length = aes_decrypt(v10, dataval_length, dataval_length, 0);
if ( dataval_length == -1 )
return 7;
}
if ( sub_804C450(&data[header_length], dataval_length, "/cf/firmware/new/CRCHDR.INF") )
return 10;
return 0;
}

简要概括流程:

1
2
3
4
5
1. 获取用户上传的文件,保存到本地路径
2. 固件分为 header 和 body 两个部分,header 中包括 DSA 签名数据
3. 代码计算 body 部分的 SHA1 值,并使用内置公钥对文件完整性进行 DSA 验证
4. 分离 header 和 body
5. 对 body 进行 AES 解密

实际上是使用固定的 AES KEY 和 IV 对固件进行解密,参照流程可实现解密程序,具体请参考仓库代码。

参考链接

本文分析了 SonicOS 的磁盘和固件加密方法,实现了相应的解密脚本,希望能对感兴趣的研究者有一些帮助。

https://badmonkey.site/archives/sonicwall-sma-research

https://www.gnu.org/software/grub/grub-download.html

  • Title: SonicOS 固件解密 (2)
  • Author: Catalpa
  • Created at : 2024-09-05 00:00:00
  • Updated at : 2024-10-17 08:55:15
  • Link: https://wzt.ac.cn/2024/09/05/sonicwall_dec2/
  • License: This work is licensed under CC BY-SA 4.0.
On this page
SonicOS 固件解密 (2)