以下是在JuiceBox 40充电桩上进行的研究总结。我们发现了一个漏洞,在比赛中,我们成功利用了CVE-2024-23938,在充电桩上执行了任意代码。
硬件
就硬件而言,JuiceBox 40相对简单。有几个主要组件:
Silicon Labs WGM160PX22KGA3 应用处理器 + WiFi模块
Silicon Labs MGM13S12A 蓝牙/低功耗蓝牙(BLE)处理器
Atmel ATMega328P 微控制单元(MCU) - 我们推测这是负责充电和安全控制的单元
Atmel M90E36A 电能计量集成电路(IC)
我们专注于由WGM160P处理器托管的软件作为比赛的主要目标,并没有深入研究MGM13S12A前端。
Reconnaissance
在对JuiceBox进行目标评估期间,我们发现了Reddit上的一些帖子,讨论了充电器的问题以及如何通过同一网络下的设备可访问的远程终端来修复这些问题。此外,我们还发现了YouTube上的一些视频,车主们记录了他们对设备的修复和修改,以及其他包含同一供应商其他型号内部结构照片的帖子。例如,在iFixit上有一篇关于JuiceBox EVSE的文章,解释了如何更换设备中的继电器,这为我们提供了JuiceBox系列可能的主要硬件组件的线索。文章中的这张照片展示了我们寻找的有用信息——一个标有“ZENTRI”和“AMW106”的大型屏蔽模块:
简而言之,我们发现了很多有用的信息。最有趣的是,显然存在一个开放的管理员界面,网络上的任何其他设备都可以与之交互。
在我们初步探索结束时,我们得出了以下主要结论,这些结论可以用于进一步调查:
JuiceBox 40的旧版本基于一个名为AMW106的WiFi模块,由Zentri制造。这些版本运行ZentriOS。
JuiceBox 40的新版本可能基于一个后续的WiFi模块,即WGM160P。这些版本运行ZentriOS的后续版本,称为Gecko OS。
AMW106和WGM160P平台的开发模块和套件可以从许多经销商那里轻易获得。
ZentriOS和Gecko OS都有许多功能(例如远程终端功能),这些功能似乎可以在JuiceBox 40的生产环境中访问。
Getting closer to the firmware
我们开始仔细研究ZentriOS和Gecko OS的文档,重点是更新过程是如何实现的。
我们还希望找到一种方法,如果可能的话,下载固件。我们从文档中学到了很多,主要要点如下:
这两个操作系统都包括一个核心内核和“内置插件”部分,此外还有一个可选的应用程序,开发者可以将其与特定构建捆绑在一起。
两个系统都有一个完全管理的设备认证和更新机制,由Zentri设备管理服务(DMS)支持。
两个操作系统都被设计为既可以作为开发平台,也可以作为附加的即插即用解决方案。后者意味着该平台可以作为一种“连接性”附加组件,与一个独立的应用程序处理器结合使用。
user@birdie 4.2.7-11064 % find . -name '*.a'
./tools/toolchains/gcc/arm/osx/libgcc.a
./hardware/platforms/gecko_os/wgm160p/libs/gcc/gecko_os_libraries/plugins_core/lib_network_protocols_websocket.a
./hardware/platforms/gecko_os/wgm160p/libs/gcc/gecko_os_libraries/plugins_core/lib_network_util_arp_lock.a
...
./hardware/platforms/gecko_os/wgm160p/libs/gcc/gecko_os_libraries/kernel_shared/gecko_os_kernel_chips_efx32_peripherals_crc.a
./hardware/platforms/gecko_os/wgm160p/libs/gcc/gecko_os_libraries/kernel_shared/gecko_os_kernel_core_dump.a
...
Gecko OS,如同许多嵌入式操作系统一样,提供了操作系统的基本预期功能集,如硬件/软件接口、调度和资源管理。然而,不同于许多嵌入式操作系统,由于其作为IoT应用的交钥匙解决方案的设计理念,Gecko OS提供了许多高级功能。例如,它有一个配置Web界面,位于一个用于将设备连接到用户WiFi网络的引导WiFi接入点设置组件之上。它还提供了各种应用级功能的高级API,如HTTP、TLS,以及TCP客户端和服务器实现等等,这些都被以某种形式内置到了操作系统(或其子组件)中。
处理这些“高级”动作涉及许多活动部件,在这些交互过程中自然会有一些时候需要提供状态更新。这些更新可能以多种形式有用,例如,对于用户来说是以人类可读的格式,或者是对于其他解析文本格式的辅助应用处理器。为了这个目的,Gecko OS包含了一个系统消息记录设施,当某些事件发生时,该设施会将可定制的消息写入所使用的任何日志接口(串行、网络等)。根据Gecko OS系统.msg变量的文档,有各种消息类型(及其相应的事件)会被记录下来:
Message Name | Default Value |
---|---|
initialized | [@tReady] |
stream_closed | [@tClosed: @c] |
stream_failed | [@tOpen failed] |
stream_opened | [@tOpened: @c] |
stream_opening | [@tOpening: @h] |
sleep | [@tSleep] |
wlan_failed | [@tJoin failed] |
wlan_joined | [@tAssociated] |
wlan_joining | [@tAssociating to @s] |
wlan_leave | [@tDisassociated] |
softap_joined | [@t@m associated] |
softap_leave | [@t@m disassociated] |
ethernet_starting | [@tEthernet Starting] |
ethernet_started | [@tEthernet Started] |
[@tEthernet Stopped] | |
ethernet_failed | [@tEthernet Failed] |
这些消息可以通过设置Gecko OS的system.msg变量并指定所需的消息名称和消息文本来定义。例如,要将初始化消息的日志文本改为“Rise and shine”而不是默认消息,可以发出如下命令:
set system.msg initialized "Rise and shine"
此外,系统消息支持一小部分标签,以向记录的消息中添加动态信息。可以将这些系统消息视为模板,其中消息的一部分是字符串字面量,另一部分是占位符变量。某些标签被所有消息类型支持,比如@t标签,每次出现都会被替换成时间戳。与此同时,有些标签只被少数几种消息类型支持。例如,@c标签可用于打印流句柄,这仅对stream_closed和stream_opened消息有意义。以下是根据Gecko OS文档列出的各标签可用性:
Tag | Description | Tag is available for … |
---|---|---|
Timestamp | Can be set for all messages, but displays a value only for ethernet messages. | |
@s | SSID | WLAN messages |
@c | Stream handle | stream_closed, stream_opened |
@h | Connection host/port | stream_failed, stream_opening |
@m | Client MAC Address | softap_joined, softap_leave |
基于上述内容,假设我们想要记录一条消息,指示正在为HTTP请求打开流时的连接主机/端口对、一个静态消息和时间戳。为此,我们可以发出如下命令:
set system.msg stream_opening "[@t] Connecting to @h"
以此类推。主要限制是系统消息模板的最大长度为32字节(包括终止的NUL字节)。然而,由于标签表示的动态数据可能比标签本身占用的空间要长,因此实际打印的消息可能会远超32字节。
Out of bounds write
最初,我们是在寻找“经典”的漏洞,涉及通常的嫌疑函数,如带有攻击者控制的数据和计数的memcpy调用;或是snprintf调用中返回值被误用的情况。后者让我们进入了一个看似负责将动态数据应用于系统消息模板并记录结果的例程:
void print_system_msg(int msg_id, system_msg_arg *args)
{
char cVar1;
char **ppcVar2;
int iVar3;
byte *snrpintf_generic_arg;
char *dst;
char *template_;
size_t n;
uint snprintf_uint_arg;
byte local_190;
undefined scratch_buf_2 [35];
byte scratch_buf_1 [132];
char formatted_message_buffer [192];
undefined message_buffer_end [4];
system_message_entry *message_list;
char *fmt;
byte format_tag;
message_list = (system_message_entry *)system_message_list;
template_ = message_list[msg_id].message_format;
if (message_list[msg_id].message_format[0] != '\0') {
dst = formatted_message_buffer;
while (cVar1 = *template_, cVar1 != '\0') {
if (cVar1 == '@') {
format_tag = template_[1];
if (format_tag == 'm') {
ppcVar2 = &args->value2;
args = (system_msg_arg *)&args->value1;
FUN_0002a018(*ppcVar2,dst);
dst = dst + 0x11;
}
else if (format_tag < 'n') {
if (format_tag == 'c') {
snrpintf_generic_arg = (byte *)args->value2;
args = (system_msg_arg *)&args->value1;
fmt = "Running : %u";
snprintf_into_dst:
iVar3 = snprintf_signed_checked
(dst,(int)(message_buffer_end + -(int)dst),fmt + 0xe,
snrpintf_generic_arg);
dst = dst + iVar3;
}
else if (format_tag == 'h') {
snprintf_uint_arg = args->value1;
iVar3 = snprintf_signed_checked
(dst,(int)(message_buffer_end + -(int)dst),"%s",args->value2);
args = args + 1;
dst = dst + iVar3;
if (snprintf_uint_arg != 0) {
fmt = dst + 1;
*dst = ':';
iVar3 = snprintf_signed_checked
(fmt,(int)(message_buffer_end + -(int)fmt),"%u",snprintf_uint_arg);
dst = fmt + iVar3;
}
}
}
else {
if (format_tag == 's') {
n = args->value1;
memcpy(scratch_buf_2,args->value2,n);
local_190 = (byte)n;
args = args + 1;
escape_hex(scratch_buf_1,&local_190);
snrpintf_generic_arg = scratch_buf_1;
fmt = "Web browser : %s";
goto snprintf_into_dst;
}
if ((format_tag == 't') &&
(iVar3 = print_timestamp_to_string(scratch_buf_1,1), iVar3 == 0)) {
memcpy(dst,scratch_buf_1,10);
dst[0xb] = '|';
dst[10] = ' ';
dst[0xc] = ' ';
memcpy(dst + 0xd,scratch_buf_1 + 0xb,8);
dst[0x15] = ':';
dst[0x16] = ' ';
dst = dst + 0x17;
*dst = '\0';
}
}
template_ = template_ + 2;
}
else {
*dst = cVar1;
template_ = template_ + 1;
dst = dst + 1;
}
}
log_message(formatted_message_buffer,(int)dst - (int)formatted_message_buffer);
}
return;
}
该函数通过消息ID(msg_id)和一系列参数调用,这些参数根据系统消息的类型可能与消息相关。msg_id是对内部闪存中配置段存储的默认Gecko OS系统消息模板列表的索引。
函数为生成的消息分配了192字节的栈缓冲区。然后,它遍历模板中的字符,对于每个@字符,检查它是否是已知的模板标签,并相应填充。其他字符则直接复制到结果缓冲区。该循环在到达模板末尾时结束,而不检查结果缓冲区已写入的字符数是否超出界限。
在处理不同标签时,有几个问题值得关注,我们将逐一深入分析。首先,让我们看看在处理时间戳的@t标签时最突出的问题:
if ((format_tag == 't') && (iVar3 = print_timestamp_to_string(scratch_buf_1,1), iVar3 == 0)) {
memcpy(dst,scratch_buf_1,10);
dst[0xb] = '|';
dst[10] = ' ';
dst[0xc] = ' ';
memcpy(dst + 0xd,scratch_buf_1 + 0xb,8);
dst[0x15] = ':';
dst[0x16] = ' ';
dst = dst + 0x17;
*dst = '\0';
}
else if (format_tag == 'h') {
snprintf_uint_arg = args->value1;
iVar3 = snprintf_signed_checked
(dst,(int)(message_buffer_end + -(int)dst),"%s",args->value2);
args = args + 1;
dst = dst + iVar3;
if (snprintf_uint_arg != 0) {
fmt = dst + 1;
*dst = ':';
iVar3 = snprintf_signed_checked
(fmt,(int)(message_buffer_end + -(int)fmt),"%u",snprintf_uint_arg);
dst = fmt + iVar3;
}
}
回想一下,dst是指向输出字符串当前位置的指针,而message_buffer_end则是在输出字符串缓冲区末尾的下一个位置。代码非常直接,snprintf被调用以将字符串值写入dst,最大长度为message_buffer_end - dst。之后,snprintf的返回值(对于POSIX兼容的实现,这是如果缓冲区足够大它将写入的字节数)被无差别地加到了dst上。单凭这一点听起来就像是一个漏洞,如果提供的字符串args->value2比缓冲区的长度大得多怎么办?更重要的是,如果我们遇到dst > message_buffer_end的情况,就像我们在处理@t参数时滥用漏洞那样,又会怎样?
实际上,这个snprintf的实现有点不同:
int snprintf_signed_checked(char *dst,int maxn,char *fmt,...)
{
undefined4 *puVar1;
int res;
// ...
puVar1 = (undefined4 *)DAT_20000984;
if (maxn < 0) {
res = -1;
*puVar1 = 0x8b;
}
else {
// ...
}
return res;
}
如果提供的maxn参数是一个负数,那么它会返回-1并设置一些错误代码而不执行任何其他操作。在print_system_msg中并没有对此条件进行错误处理!事实上,在处理@h标签的情况下,如果dst已经超出缓冲区范围,则最终结果是dst被递减!此外,如果dst仍在范围内,但是提供的替换值大于缓冲区大小,那么这个snprintf实现将以预期的方式行为,并返回它将要写的字节数,这意味着dst将被递增到超出范围。后一种情况特别有趣,因为它意味着即使启用了栈保护(canary),利用仍然是可能的。
set system.msg stream_opening @t@t@t@t@h@t@h@hAB
http_head aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Exploitation
带着漏洞和利用路径,我们着手开发我们的利用链。事实证明,拥有WGM160P开发者套件是非常有用的,原因有几个。首先,我们能够轻松地部署和测试不同的场景,使用Gecko OS Studio连接到我们的测试模块。其次,也许最重要的是,我们能够通过测试模块上的SWD接口简单地检查副作用来调试我们的利用代码。后者是无法(轻易)在生产模块上做到的,比如JuiceBox使用的模块,因为那些模块是被锁定的。
平台的主要卖点之一是整个更新周期和设备认证参数是由厂商管理的,反过来,应用设计师可以通过Zentri DMS来管理这些参数。然而,这意味着生产单元实际上不需要任何形式的调试能力,使其成为一个安全(且更安全的选择)从一开始就禁用所有不必要的接口。
除此之外,没有什么缓解措施阻碍了利用开发,因此获得控制流控制是一项简单的工作。使开发过程稍微复杂一点的是我们被迫使用的多阶段以及设备本身的测试限制。
Testing on production
不管生产模块上缺乏JTAG/SWD接口,我们需要在事件前对设备进行利用链测试,以确保它确实能工作。自然而然,由于设备上部署的应用代码与操作系统之间的交互作用,在我们的开发设置中并未体现出来,因此我们遇到了一些问题。然而,我们找到了一种方法来了解发生了什么,以便我们可以根据需要进行调整。
Gecko OS提供了一种报告操作故障的机制,比如硬故障。如果触发了故障处理器,它会将几个关键寄存器的值存储到闪存中,然后重启到一个安全的状态。这些故障可以通过使用faults_print命令来列出:
0: Hard Fault Exception, PC:0x42424242 LR:0x0003F8AB HFSR:0x40000000 CFSR:0x00000001 MMFAR:0x00000000 BFAR:0x00000000
1: Hard Fault Exception, PC:0x000123A0 LR:0x00077C6B HFSR:0x40000000 CFSR:0x00008200 MMFAR:0x00000000 BFAR:0x400AE2C2
2: Hard Fault Exception, PC:0x000123A0 LR:0x00077C6B HFSR:0x40000000 CFSR:0x00008200 MMFAR:0x00000000 BFAR:0x400AE2C2
3: Hard Fault Exception, PC:0x000123A0 LR:0x00077C6B HFSR:0x40000000 CFSR:0x00008200 MMFAR:0x00000000 BFAR:0x400AE2C2
4: Hard Fault Exception, PC:0x000123A0 LR:0x00077C6B HFSR:0x40000000 CFSR:0x00008200 MMFAR:0x00000000 BFAR:0x400AE2C2
5: Hard Fault Exception, PC:0x000123A0 LR:0x00077C6B HFSR:0x40000000 CFSR:0x00008200 MMFAR:0x00000000 BFAR:0x400AE2C2
6: Hard Fault Exception, PC:0x000123A0 LR:0x00077C6B HFSR:0x40000000 CFSR:0x00008200 MMFAR:0x00000000 BFAR:0x400AE2C2
7: Hard Fault Exception, PC:0x000123A0 LR:0x00077C6B HFSR:0x40000000 CFSR:0x00008200 MMFAR:0x00000000 BFAR:0x400AE2C2
这些列表提供了足够多的信息,使我们能够在JuiceBox上继续我们的利用开发。嗯,直到我们遇到了安全模式。
重置JuiceBox
简而言之,安全模式是Gecko OS为应对系统持续异常行为而实施的一种恢复机制。默认情况下,当发生8次故障时,会触发安全模式。当系统处于安全模式时,大多数特性都是禁用的,包括WiFi连接,并且只有有限数量的命令可以通过串行接口使用。
当然,在开发我们的利用过程中,我们无意间进入了安全模式。如果我们知道安全模式的存在并在达到8次故障限制之前使用faults_reset命令,这种情况是可以避免的。
无论如何,我们不得不找到一种方式退出安全模式,回到正常的(非安全^W)操作模式。一个问题在于,默认情况下,用于与安全模式交互的串行口被连接到了Atmel ATMega328P充电控制器作为两个处理器之间的通信通道。这意味着只要Atmel控制器在运行,我们就无法发出必要的命令来退出WGM160P的安全模式。
因此,我们必须禁用Atmel芯片,为此我们选择了简单的解决方案,即拉低其nRESET引脚。这样就把它置于编程模式,并保持串行口的畅通,以便我们可以发出必要的命令:
Staging the exploit
我们的最终利用共使用了三个阶段:
初始的16字节shellcode启动器,存储在配置段中作为system.security_key,负责触发下一阶段。
较大的第二阶段shellcode块,存储在系统消息中,除了我们需要的那个!这些存储在内核数据内存区域,并由第一阶段可以到达的静态位置引用。此外,这一阶段的shellcode唯一限制是不能包含NUL字节或@符号,因为它们可能不符合给定消息的允许标签。此阶段负责读取并执行最终的有效载荷。
最后——有效载荷,作为一个文件保存在设备的闪存文件系统中。这只需使用远程终端下载一个文件并在设置阶段将其存储到闪存即可。
在我们前往比赛的所有准备好的利用中,这是迄今为止最复杂的,并且有最多的移动部件。
影响
就设计而言,JuiceBox的充电控制器与连通性处理器是分离的。因此,很可能对充电过程的损害是微乎其微的。然而,我们尚未充分研究连通性处理器与充电控制器之间的串行协议,以排除物理/电气损坏的可能性。
此外,蓝牙模块暴露的BLE服务提供了访问Gecko OS命令行的第二个接口,这使得获取设备连接的WiFi网络的凭证成为可能。这可以通过发动deauth攻击来实现,这将导致JuiceBox重新启动并进入配置模式,重新启用BLE服务。由于BLE连接不需要认证,攻击者可以使用它来检索WiFi网络的凭证,并让设备重新连接。之后,他们有可能利用充电桩在网络中获得隐蔽立足点或对同一网络中的其他设备发动进一步攻击。