搭建 FortiGate 调试环境 (一)

Catalpa 网络安全爱好者

关于如何获取 FortiGate shell 权限以及如何完成 License 授权验证。(一)

处理判断逻辑

本博客某历史文章中提到过一篇参考资料,不过这篇资料是针对旧版本 FortiGate 的,Fortinet 曾在 2019 年 11 月 14 日发布了一个影响 FortiGate 的 CVE,在这个漏洞的描述中,官方认为 VM 应用缺少对文件系统的检查,可能导致攻击者向系统中注入恶意程序。为此在启动流程中添加了文件系统校验,按照参考资料的步骤修改系统文件之后可能会导致无法启动或者无限重启。

对于这个问题,我们首先想到的是定位检查逻辑,看看能否根据算法打包出正确的文件或者直接将检查逻辑 patch 掉。先看一下系统启动时的输出信息:

考虑文件系统的校验可能是在内核或者用户态实现,我们首先在文件系统中尝试搜索 System is starting 字符串

在二进制文件 bin/init 中找到了匹配,逆向该文件看看这个字符串是何时打印的:

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
  t_printf("\nSystem is starting...\n", v11, v8, v12, v5, v6, requested_time.tv_sec);// 打印字符串
fflush(stdout);
sub_206FB40();
reboot(0);
close(0);
close(1);
close(2);
sub_44DE80(0);
chdir("/");
setsid();
v14 = sub_44DDF0("/dev/null");
v15 = v14;
if ( v14 >= 0 )
{
dup2(v14, 0);
dup2(v15, 1);
dup2(v15, 2);
}
if ( sub_290E8D0(1024LL, 1LL) < 0 )
{
t_printf("could not setup epoll in init.\n", 1, v16, v17, v18, v19, requested_time.tv_sec);
}
else if ( sub_452C00() >= 0 )
{
sub_450A80();
sub_1F6CD90(16, "%s()-%d: %s: run_initlevel(SYSINIT)\n\n", "main", 2649, "main", v20, requested_time.tv_sec);
sub_44E2D0(1LL);
sub_451670();
sub_450BC0();
if ( sub_44F240() )
do_halt();
if ( !sub_44F1A0() )
do_halt();
if ( sub_2745D50() )
{
sub_2825E00();
if ( sub_44DFC0("/bin/fips_self_test") )
do_halt();
}
else
{
if ( sub_44F1F0() )
do_halt();
sub_2781560();
}
// ...
}
// ...

我们看到在 init 程序的 main 函数中第 82 行位置打印了此字符串,而向下分析发现有几处判断,其中比较明显的位置引用了 /bin/fips_self_test 字符串。do_halt 函数的内容:

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
unsigned __int64 do_halt()
{
FILE *v0; // rax
FILE *v1; // r12
int v2; // eax
int v3; // er12
struct timespec requested_time; // [rsp+0h] [rbp-30h] BYREF
unsigned __int64 v6; // [rsp+18h] [rbp-18h]

v6 = __readfsqword(0x28u);
sub_451FA0("do_halt", 1388);
v0 = fopen("/etc/shutdown.dat", "w");
if ( v0 )
{
v1 = v0;
fprintf(v0, "%d", 1LL);
fclose(v1);
sync();
sleep(1u);
}
sub_44EF50();
v2 = open("/dev/console", 2305);
v3 = v2;
if ( v2 >= 0 )
{
dprintf(v2, "\r\nThe system is halted.\r\n");
fsync(v3);
close(v3);
}
requested_time.tv_sec = 2LL;
requested_time.tv_nsec = 0LL;
while ( nanosleep(&requested_time, &requested_time) == -1 && *__errno_location() == 4 )
;
if ( !fork() )
reboot(1126301404);
while ( pause() )
;
return v6 - __readfsqword(0x28u);
}

这里会输出信息 The system is halted,表示系统已停止运行。

第一个判断函数内部执行了 ioctl 和 socket 等函数,向内核发送或接收某些信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __fastcall sub_2907930(int a1, __int64 a2)
{
int v2; // eax
int v3; // er13
int v4; // er12

if ( dword_468D350 >= 0 )
return ioctl(dword_468D350, a1, a2);
v2 = socket(2, 2, 0);
v3 = v2;
if ( v2 < 0 )
return -1;
v4 = ioctl(v2, a1, a2);
close(v3);
return v4;
}

第二个函数 fork 出一个子进程,主要处理逻辑:

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
_BOOL8 sub_447D40()
{
char v0; // cl
char v1; // dl
__int64 i; // rax
char v3; // cl
char v4; // dl
__int64 v5; // rax
FILE *v6; // rax
FILE *v7; // r12
int v8; // ebx
FILE *v9; // r13
_BOOL8 v10; // r12
__int64 v11; // rax
__int64 v12; // r15
__int64 v13; // rax
__int64 v14; // r14
void *v15; // rsp
void *v16; // rsp
size_t v17; // rax
__int64 v19; // [rsp+0h] [rbp-F0h] BYREF
size_t n; // [rsp+8h] [rbp-E8h]
void *v21; // [rsp+10h] [rbp-E0h]
__int64 *v22; // [rsp+18h] [rbp-D8h]
void *v23; // [rsp+20h] [rbp-D0h] BYREF
__int64 v24; // [rsp+28h] [rbp-C8h]
char v25[32]; // [rsp+30h] [rbp-C0h] BYREF
char filename[8]; // [rsp+50h] [rbp-A0h] BYREF
__int16 v27; // [rsp+58h] [rbp-98h]
__int64 ptr[16]; // [rsp+70h] [rbp-80h] BYREF

v0 = 97;
v1 = 78;
ptr[9] = __readfsqword(0x28u);
*filename = 0x42F1C441217474ELL;
strcpy(ptr, "aiqu0oZi");
for ( i = 0LL; ; v0 = *(ptr + i) )
{
v25[i++] = v0 ^ v1;
if ( i == 8 )
break;
v1 = filename[i];
}
v3 = 97;
v4 = 78;
v24 = 0x42F1C441217474ELL;
strcpy(ptr, "aiqu0oZi");
v5 = 0LL;
v25[8] = 0;
while ( 1 )
{
filename[v5++] = v3 ^ v4;
if ( v5 == 8 )
break;
v4 = *(&v24 + v5);
v3 = *(ptr + v5);
}
v27 = 50;
v6 = fopen(filename, "r");
v7 = v6;
if ( v6 && (v8 = fread(ptr, 1uLL, 0x40uLL, v6), fclose(v7), v8 > 0) && (v9 = fopen(v25, "r")) != 0LL )
{
LODWORD(v10) = 0;
v23 = &unk_3FAC280;
v11 = d2i_PUBKEY(0LL, &v23, 294LL);
v12 = v11;
if ( v11 )
{
v13 = EVP_PKEY_get1_RSA(v11);
v14 = v13;
if ( v13 )
{
v22 = &v19;
n = RSA_size(v13);
v15 = alloca(n);
v21 = &v19;
v16 = alloca(RSA_size(v14));
v17 = fread(v21, 1uLL, n, v9);
if ( RSA_public_decrypt(v17, v21, &v19, v14, 1LL) == v8 )
v10 = memcmp(ptr, &v19, v8) == 0;
RSA_free(v14);
}
EVP_PKEY_free(v12);
}
fclose(v9);
}
else
{
LODWORD(v10) = 0;
}
return v10;
}

函数开头通过异或运算处理了一个字符串,解密之后得到结果 /.fgtsum

在根目录能找到这个文件,那么显然此函数就是利用该文件实现了某些系统校验。

第三个函数(sub_2745D50)似乎和 FIPS 模式相关,FIPS 简单来说是美国政府针对计算机系统定义的一种标准化信息处理方式,旨在提升信息的安全性。第四个函数(sub_44F1F0)同样 fork 出了子进程,相关逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
_BOOL8 __fastcall sub_2780BD0(unsigned int a1)
{
__int64 v1; // rax
__int64 v2; // r12
_BOOL8 result; // rax
char *v4; // [rsp+8h] [rbp-138h] BYREF
char v5[268]; // [rsp+10h] [rbp-130h] BYREF
__int16 v6; // [rsp+11Ch] [rbp-24h]
char v7; // [rsp+11Eh] [rbp-22h]
unsigned __int64 v8; // [rsp+128h] [rbp-18h]

v8 = __readfsqword(0x28u);
qmemcpy(v5, &off_35CFDC0, sizeof(v5));
v6 = 256;
v7 = 0;
v4 = v5;
v1 = d2i_RSAPublicKey(0LL, &v4, 270LL);
if ( v1 && (v2 = v1, !sub_2746F20("/data/rootfs.gz", "/data/rootfs.gz.chk", a1, v1)) )
result = sub_2746F20("/data/flatkc", "/data/flatkc.chk", a1, v2) == 0;
else
result = 0LL;
return result;
}

此函数实现对 rootfs.gz 的判断。

到这里可以大体推断文件系统的校验是在用户态程序 /bin/init 中实现的。相关逻辑并不复杂,我们可以深入研究以便于打包一份合适的 rootfs.gz 通过校验,或者采用更简单的方法即 patch 掉校验逻辑。

经过验证,只需要 patch fgtsum 和 rootfs 两个检验即可,将对应跳转指令取反,保存并替换原始文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if ( sub_44F240() )
do_halt();
if ( sub_44F1A0(1LL, "%s()-%d: %s: run_initlevel(SYSINIT)\n\n") )
do_halt();
if ( sub_2745D50() )
{
sub_2825E00();
if ( sub_44DFC0("/bin/fips_self_test") )
do_halt();
}
else
{
if ( !sub_44F1F0(1LL, "%s()-%d: %s: run_initlevel(SYSINIT)\n\n") )
do_halt();
sub_2781560();
}

或者,我们使用更简单的补丁,直接将 do_halt 函数的第一条指令改为 ret,这样就算无法通过验证,也不会执行重启操作。

植入后门并启动

后续的操作和参考文章提到的类似,先用 msf 生成一个后门程序,替换 /bin/smartctl 文件,然后将 patch 的 init 程序替换 /bin/init,同时在系统中放置一个 busybox 并将 /bin/sh 指向 busybox 方便后续使用。

根据前面的文章我们知道系统启动时会首先执行 /sbin/init,这个程序的逻辑很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char *argv[4]; // [rsp+0h] [rbp-20h] BYREF

argv[3] = (char *)__readfsqword(0x28u);
sub_4017D0(a1, a2, a3);
unlink("/sbin/init.chk");
if ( (int)sub_401AD0("bin") >= 0 && (int)sub_401AD0("migadmin") >= 0 && (int)sub_401AD0("node-scripts") >= 0 )
sub_401AD0("usr");
argv[0] = "/bin/init";
argv[1] = 0LL;
execve("/bin/init", argv, 0LL);
return 0LL;
}

它负责解压各个压缩包并唤起 /bin/init 继续执行,没有其他操作。方便起见我们跳过重新打包 xxx.tar.xz 的步骤,在系统启动过程中调试内核并修改内核参数,让它直接去执行 /bin/init 程序。

打包命令

1
2
find . | cpio -H newc -o > ../rootfs.raw
cat ./rootfs.raw | gzip > rootfs.gz

将新的 rootfs.gz 替换到磁盘中,挂载到原先系统上,并添加调试桥。

启动时用 gdb 附加,并定位到执行用户空间程序位置:

此时执行的目标程序为 /sbin/init

我们将其修改成 /bin/init,然后继续执行,系统将正常启动,执行命令 diagnose hardware smartctl 即可触发后门程序。由于设备存在防火墙,端口不能随便访问,我们将其自带的 sshd 服务 kill 掉并替换成 telnetd 后门

1
killall sshd && busybox telnetd -l /bin/sh -b 0.0.0.0 -p 22

这样就可以获取一个完整的 root 权限:

另外,也可以使用我编写的一个简单工具,使用方法请参考说明文档。

License 授权分析

第一次启动系统时,需要导入一个合适的 License 才能使用各项功能,在前面的文章中我们提到可以注册 FortiCloud 账户并使用评估版本 License,不过这样操作比较复杂,而且每有一个新版本就要重新注册一个账户,有没有一种方法可以绕过或者破解 License 验证呢?本部分将会介绍一种构造 License 的方法。

首先定位校验 License 的逻辑,通过查询 FortiGate 手册,得知存在一个叫做 vm-print-license 的调试命令,使用方法为 diagnose debug vm-print-license,在未激活状态下执行会看到以下输出,提示我们当前处于试用状态。

系统大部分业务逻辑都在 /bin/init 中,我们直接在 /bin/init 程序中搜索 Licensevm-print-license 等字符串,在结果中找到比较有趣的几条数据:

看起来像 License 文件的定位符,交叉引用最终可定位到以下代码

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
unsigned __int64 maybe_check_license()
{
__int64 v0; // rbx
int v1; // eax
const char *v2; // r13
time_t v3; // rax
void *lic_file_content; // [rsp+0h] [rbp-70h] BYREF
__int64 lic_length; // [rsp+8h] [rbp-68h] BYREF
struct tm tp; // [rsp+10h] [rbp-60h] BYREF
unsigned __int64 v8; // [rsp+48h] [rbp-28h]

v8 = __readfsqword(0x28u);
lic_file_content = 0LL;
lic_length = 0LL;
read_file("/data/etc/vm.lic", &lic_file_content, &lic_length);// 读取 vm.lic 文件
if ( lic_length && lic_file_content )
{
v0 = 0LL;
init_lic_struct(&lic_struct);
v1 = sub_29200D0(lic_file_content, lic_length, &lic_struct);
v2 = lic_struct;
if ( !v1 )
{
while ( strncmp(v2, lincese_types[v0].name, 6uLL) )
{
if ( ++v0 == 27 )
goto LABEL_8;
}
license_tag_val = lincese_types[v0].tag_val;// 0x16
LABEL_8:
if ( qword_F5E2DC8 )
{
strptime(qword_F5E2DC8, "%a %b %e %H:%M:%S %Y", &tp);
v3 = timegm(&tp);
qword_F5E2DF8 = v3;
if ( nCfg_debug_zone )
*(nCfg_debug_zone + 3559) = v3;
}
}
free(lic_file_content);
}
return v8 - __readfsqword(0x28u);
}

该函数尝试读取 /data/etc/vm.lic 文件,并使用 sub_29200D0 对其解析。由于已经获取到了 shell 权限,我们可以看到未激活时这个文件的内容:

1
2
3
4
5
-----BEGIN FGT VM LICENSE-----
IAAAAJ89UQoa5fpHDP95to/FvROLPIKeChd2EMS1QHS43LMGUAAAAGULEt3EN96B
mKhmST8dR9f5RV2Yfp6EojX+Rx92RqAf+vBkeklRAh+GglCJzHRgGCqhgedueWbt
YQ/dmgF7MWbFqOkYRXjiaLPznTCafNb8
-----END FGT VM LICENSE-----

这进一步印证了我们的猜想,即这部分代码逻辑和 License 有较大关系。

外层函数 maybe_check_license 的逻辑比较简单,首先从 /data/etc/vm.lic 中读取授权信息,接着调用 init_lic_struct 函数初始化一个全局变量结构体 lic_struct,然后使用 sub_29200D0 函数解析授权文件

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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
__int64 __fastcall sub_29200D0(char *lic_file_content, __int64 lic_length, lic_struct *lic_struct)
{
char *lic_start; // r12
char *lic_end; // rax
char *lic_end_1; // rbx
char *lic_start_t; // r12
const unsigned __int16 *v8; // rax
char *decode_content; // rax
unsigned int *decode_content_2; // r14
int v11; // eax
__int64 v12; // rdi
unsigned int *v13; // rsi
__int64 v14; // r14
unsigned int *v15; // r12
size_t enc_data_length; // rbx
__int64 v17; // rax
__int64 v18; // r12
int v19; // eax
__m128i *v20; // rax
int v21; // er13
unsigned int v22; // er14
char *aes_decrypted_1; // rax
__int64 v24; // rsi
char *data_start; // r12
char *v26; // rbx
unsigned __int64 key_name_length; // rcx
_DWORD *v28; // rsi
char *v29; // r9
size_t key_value_length; // r8
unsigned __int32 *key_value; // r10
char key_flag; // dl
bool v34; // zf
void *aes_enc_data; // [rsp+8h] [rbp-198h]
__m128i *ptr; // [rsp+10h] [rbp-190h]
__int64 v37; // [rsp+20h] [rbp-180h]
char *decode_content_1; // [rsp+28h] [rbp-178h]
unsigned __int32 v39; // [rsp+3Ch] [rbp-164h] BYREF
__m128i aes_key; // [rsp+40h] [rbp-160h] BYREF
__m128i v41; // [rsp+50h] [rbp-150h]
char key_name[264]; // [rsp+60h] [rbp-140h] BYREF
unsigned __int64 v43; // [rsp+168h] [rbp-38h]

v43 = __readfsqword(0x28u);
lic_struct->SERIALNO = 0LL;
lic_struct->unknow9 = 0LL;
memset(
(&lic_struct->unknow2 & 0xFFFFFFFFFFFFFFF8LL),
0,
8LL * ((lic_struct - ((lic_struct + 8) & 0xFFFFFFF8) + 168) >> 3));
if ( lic_file_content )
{
lic_start = strstr(lic_file_content, "-----BEGIN FGT VM LICENSE-----");// 判断是否包含这两个 banner
lic_end = strstr(lic_file_content, "-----END FGT VM LICENSE-----");
lic_end_1 = lic_end;
if ( lic_start )
{
if ( lic_end )
{
lic_start_t = lic_start + 30; // 跳过开头
if ( lic_end > lic_start_t )
{
v8 = *__ctype_b_loc();
while ( (v8[*lic_start_t] & 0x2000) != 0 )// _ISspace
{
if ( lic_end_1 == ++lic_start_t )
return -1;
}
if ( lic_end_1 > lic_start_t )
{
while ( (v8[*lic_end_1] & 0x2000) != 0 )// _ISspace
{
if ( --lic_end_1 == lic_start_t ) // 跳过开头和结尾多余的空白字符
return -1;
}
if ( lic_end_1 > lic_start_t )
{
*lic_end_1 = 0;
decode_content = malloc(3 * ((lic_end_1 - lic_start_t + 3) >> 2));// 分配空间,用于存放 Base64 解码后的内容
decode_content_1 = decode_content;
decode_content_2 = decode_content;
if ( decode_content )
{
v11 = maybe_base64_decode(lic_start_t, decode_content, 3 * ((lic_end_1 - lic_start_t + 3) >> 2));// base64 解码
*lic_end_1 = '-';
if ( v11 <= 15
|| (v12 = *decode_content_2, // AES key 长度
v13 = decode_content_2,
v14 = (decode_content_2 + 1),
v15 = (v14 + v12),
v14 + v12 > v13 + v11)
|| (enc_data_length = *v15, aes_enc_data = v15 + 1, (v37 = decode_pub_key()) == 0) )
{
v22 = -1;
free(decode_content_1);
return v22;
}
v17 = EVP_PKEY_get1_RSA(v37);
v18 = v17;
if ( v17 )
{
v19 = RSA_size(v17);
v20 = calloc(v19, 1uLL);
if ( !v20 )
{
v22 = -1;
RSA_free(v18);
goto LABEL_47;
}
ptr = v20;
v21 = RSA_public_decrypt(v12, v14, v20, v18, 1LL);// RSA 公钥解密
RSA_free(v18);
if ( v21 > 31 )
{
v22 = 0;
aes_key = _mm_loadu_si128(ptr);
v41 = _mm_loadu_si128(ptr + 1);
free(ptr);
LABEL_22:
aes_decrypted_1 = AES_DEC(aes_enc_data, enc_data_length, &aes_key, &aes_key);// AES 解密部分
if ( *(aes_decrypted_1 + 1) == 0x13A38693 )// 解密之后 check magic
{
v24 = aes_decrypted_1[enc_data_length - 1];
data_start = aes_decrypted_1 + 12;// 越过 前 12 字节
if ( v24 < 0x10u )
enc_data_length -= v24;
v26 = &aes_decrypted_1[enc_data_length];
if ( data_start >= v26 )
{
final_check:
if ( v22 )
{
LOBYTE(lic_struct->unknow9) = 1;
v34 = memcmp(lic_struct->SERIALNO, "FGVMEV", 6uLL) == 0;
v22 = !v34;
if ( !v34 )
{
v34 = memcmp(lic_struct->SERIALNO, "FGVM--UNLICENSED", 0x10uLL) == 0;
v22 = !v34;
if ( !v34 )
v22 = -(memcmp(lic_struct->SERIALNO, "FGVMPG", 6uLL) != 0);
}
}
else
{
LOBYTE(lic_struct->unknow9) = 0;
}
goto LABEL_47;
}
while ( 1 ) // 主要解析逻辑
{
key_name_length = *data_start;
v28 = data_start + 1;
v29 = &data_start[key_name_length + 1];
key_value_length = *(v29 + 1);
key_value = (v29 + 3);
key_flag = *v29;
v39 = 0;
data_start = &v29[key_value_length + 3];// 分段数据
if ( key_name_length < 8 )
{
if ( (key_name_length & 4) != 0 )
{
*key_name = *v28;
*&key_name[key_name_length - 4] = *(v28 + key_name_length - 4);
}
else if ( key_name_length )
{
key_name[0] = *v28;
if ( (key_name_length & 2) != 0 )
*&key_name[key_name_length - 2] = *(v28 + key_name_length - 2);
}
key_name[key_name_length] = 0;
if ( key_flag == 'n' )
{
LABEL_37:
if ( key_value_length != 4 )
goto LABEL_34;
key_value = &v39;
v39 = _byteswap_ulong(*(v29 + 3));
fillin_struct:
fillin_struct_func(lic_struct, key_name, key_flag, key_value, key_value_length);// 填充 lic_struct 结构体
goto LABEL_34;
}
}
else
{
*&key_name[key_name_length - 8] = *(v28 + key_name_length - 8);
qmemcpy(key_name, v28, 8 * ((key_name_length - 1) >> 3));
key_name[key_name_length] = 0;
if ( key_flag == 'n' )
goto LABEL_37;
}
if ( key_flag == 's' )
goto fillin_struct;
LABEL_34:
if ( v26 <= data_start )
goto final_check;
}
}
goto failed;
}
free(ptr);
if ( v12 == 32 ) // key_length == 32
{
v22 = 1;
aes_key = _mm_loadu_si128((decode_content_1 + 4));// RSA 解密失败的话走这里
v41 = _mm_loadu_si128((decode_content_1 + 20));
goto LABEL_22;
}
}
failed:
v22 = -1;
LABEL_47:
free(decode_content_1);
EVP_PKEY_free(v37);
return v22;
}
*lic_end_1 = 45;
}
}
}
}
}
}
return -1;
}

我重命名了一些变量并添加了一部分注释,这个函数逻辑稍稍复杂,简要描述如下:

首先检查授权文件是否包含定位符,根据定位符确定主要内容的起止位置,对内容进行 Base64 解码,然后从解码的数据中读取头部结构。从内存中解压一个 RSA 公钥,先尝试使用这个公钥解密余下请求,如果解密失败,再尝试使用 AES-128-CBC 解密内容,使用的 key 和 iv 是从 Base64 解码的数据头部获得的。

之后对解密的数据进行判断,首先是 magic 部分需要等于 0x13A38693,之后跟随数个 Block 结构,Block 结构类似 TLV,由数据长度、数据名、数据值组成,每个 Block 对应 License 中的一个信息字段。

解析好各个结构之后将这些信息填充到 lic_struct 结构体中,并在后续功能继续使用。

根据以上代码逻辑,我们可以整理出 Licnese 的大致组成,以 Python 来表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class License:
aes_key_iv_length = 32 # 4 bytes
aes_key = b"\x61" * 32 # 32 bytes, contains iv(16 bytes) and key(16 bytes)
enc_data_length = None # 4 bytes
enc_data = None # length = enc_data_length
license_data = None

def __init__(self, licensedata):
pass

class LicenseDataBlock:
key_name_length = None # 1 byte
key_name = None
key_flag = None # 1 byte, 's' for str or 'n' for num
key_value_length = None # 2 bytes
key_value = None

def __init__(self, keyname, keyvalue):
pass

程序中还存在一个关键结构体,定义了 Block 基本信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct lic_block
{
char *key_name;
__int64 key_flag;
__int64 offset_in_lic_struct;
};

lic_block lic_block_array[12] =
{
{ "SERIALNO", 115LL, 0LL },
{ "CERT", 115LL, 8LL },
{ "KEY", 115LL, 16LL },
{ "CERT2", 115LL, 24LL },
{ "KEY2", 115LL, 32LL },
{ "CREATEDATE", 115LL, 40LL },
{ "UUID", 115LL, 48LL },
{ "CONTRACT", 115LL, 56LL },
{ "USGFACTORY", 110LL, 64LL },
{ "LENCFACTORY", 110LL, 68LL },
{ "CARRIERFACTORY", 110LL, 72LL },
{ "EXPIRY", 110LL, 76LL }
};

至此我们已经得知了 License 的结构和组成成分,接下来需要分析 block 中各项都起什么作用,以及 lic_struct 结构体是如何参与 License 验证的。不过这里有比较简单的方法,Block 具有比较清晰的名称,根据名称可大致猜测其作用,例如 SERIALNO 应该表示设备序列号,CREATEDATE 表示 license 导入日期,EXPIRY 表示过期时间等。

通过调试并结合代码分析,Block 中其关键作用的应该是 SERIALNO、CREATEDATE、USGFACTORY、LENCFACTORY、CARRIERFACTORY、EXPIRY 几个字段,其余字段可以省略。

根据分析结果,编写出对应的注册程序,你可以在 Github 上获取。

导入生成的 License 文件后再次执行 vm-print-license 得到 License 合法的结果,而且后台功能也可以正常使用了。

本部分介绍了 FortiGate License 授权验证逻辑,分析了 License 组成结构和各字段大致含义,并编写了一个注册程序可以生成合适的 License。

  • Title: 搭建 FortiGate 调试环境 (一)
  • Author: Catalpa
  • Created at : 2023-03-02 00:00:00
  • Updated at : 2024-10-17 08:48:34
  • Link: https://wzt.ac.cn/2023/03/02/fortigate_debug_env1/
  • License: This work is licensed under CC BY-SA 4.0.
On this page
搭建 FortiGate 调试环境 (一)