没跑路没跑路!!!
最近更新慢了很多,有师傅来问我们是不是跑路了,没跑路,在忙各种事情
团队持续招新,欢迎对车联网安全感兴趣师傅联系(招新文章还没写出来,可以直接后台私聊推荐!!!)
2024 Pwn2Own Automotive充电桩相关文章合集:
- 目标介绍
- 安全研究
这篇文章应该是2024 Pwn2Own Automotive充电桩系列最后一篇,本篇和之前其他关于Autel MaxiCharger充电桩的文章不同,不止单纯漏洞分析,更偏向一个从头到尾的记录报告。(内容很值得一看Orz)
这是关于Autel MaxiCharger的研究报告,包括Computest研究人员发现的漏洞(CVE-2024-23958、CVE-2024-23959和CVE-2024-23967)以及攻击手段。在比赛中,能够在没有任何其他先决条件的情况下,仅通过蓝牙连接就在该充电桩上执行任意代码。
背景
在研究的充电桩中,Autel MaxiCharger拥有迄今为止最为丰富的硬件功能集。从外观上看,我们就能注意到:
在充电桩内部,我们发现了很多标记清晰的测试点,包括多个UART接口。我们甚至在PCB上找到了一些未使用的内部micro-USB接口,尽管我们没有尝试使用它们,因此不清楚它们可能携带什么数据。
我们并不能完全确定所有这些都是正确的:充电桩有很多功能,并且这些功能分布在很多不同的组件上。其中许多对我们研究来说并不相关。例如,我们不确定Barrot BR8051A01芯片的作用,虽然ZDI将其标识为蓝牙无线电芯片,但根据我们所见,ESP32处理了WiFi和蓝牙的所有功能。
获取固件
获取固件是破解这款充电桩过程中最困难的部分。
第一次开机时,我们通过蓝牙配对了手机,并将充电桩连接到一个正在用tcpdump捕获所有流量的WiFi网络。我们启动了一个可用的固件更新,但后来发现捕获的数据包中没有任何相关内容。显然,手机下载了更新并通过蓝牙发送给了充电桩。
我们再次尝试,同时用Burp拦截手机的网络流量。我们确定更新过程如下:
• 应用程序通过BLE从充电桩主控制器请求所有固件组件的版本。
• 应用程序将此信息提交给Autel服务器。
• 在之后的任何时候,应用程序都可以询问服务器是否有针对该充电桩的任何更新。
• 如果有更新,服务器会为每个可更新的组件返回一个URL。
• 应用程序从这些URL下载文件,并通过BLE发送给充电桩。
• 充电桩的主控制器将更新发送给相应的组件。
我们试图复制这个过程,但得到的URL被混淆了。为了反混淆,我们尝试反编译或hook应用程序。这比我们预期的要难得多:移动应用的代码被混淆了,而且应用中包含了一些反调试技巧。
最终,我们放弃了通过应用程序的方法,转而更仔细地观察那些被混淆的URL,注意到它们看起来非常接近base64编码。如果我们对它们进行base64解码,虽然不能得到可读文本,但我们能看到某些字节与(预签名的)Amazon S3 URL对齐:
通过猜测一些字符(比如开头的“https://”),并将我们猜测的URL进行base64编码后的版本与实际数据进行对比,我们逐步推断出他们所做的只是在进行base64解码之前应用了一个简单的字符替换:
• A ➔ a
• a ➔ A
• B ➔ b
• b ➔ B
• C ➔ c
• c ➔ C
• D ➔ d
• d ➔ D
• E ➔ e
• e ➔ E
• F ➔ f
• f ➔ F
• G ➔ g
• g ➔ G
• 7 ➔ y
这表明它可能是使用了一个256字节的密钥进行异或操作。我们对这个密钥进行了几种猜测:首先,我们在256字节块中的每个偏移位置寻找出现频率最高的字节值,然后查看整个256字节块中哪个是最常见的。固件文件中经常包含只填充了NUL字节的部分,这样我们希望确定NUL字节(在密钥中的每个偏移位置)的异或值。这两种方法得出的密钥几乎是相同的。但用这些密钥对整个文件进行异或操作并没有得到可读的文件。
我们的下一个猜测是加法/减法与异或操作的组合。这意味着我们需要寻找两个256字节的密钥。那些小片段能够正确解密实际上对于这种混淆方法来说是合理的。
数学背景
我们可以从数学上解释为什么在尝试减法时会出现正确解密的字节,但如果你对这部分不感兴趣,可以跳过这一节。
假设加密过程如下(所有操作都是针对每个字节进行的,因此是对256取模):
ciphertext = (plaintext ^ b) + a
那么对于一个零字节,其密文就是 (b ^ 0) + a = b + a。当我们减去这个值时,最终得到的是:
subtracted = (plaintext ^ b) + a - (b + a)
= (plaintext ^ b) - b
x & y = 0 ⇒ x ^ y = x + y
subtracted = (plaintext ^ b) - b
= (plaintext + b) - b = plaintext
因此,在256字节块中的每个偏移位置,所有不与相应的b值共享任何位的数值,如果你减去零值的“加密”值,就会得到原始值。对于那些确实共享某些位的数字来说,差值取决于加法/减法操作触发了多少进位,这也解释了为什么经常看到相差2(或不同的2的幂)的情况。例如,上面的截图显示了一些行结束符是0b 0a 或 0d 06,而不是正常的0d 0a。
解密
由于我们现在需要为每个索引寻找两个值,简单的频率分析不足以恢复这些值。我们可以尝试所有256 * 256 = 65536种组合,但由于我们不确定如何识别正确的解密结果(它可能不是一个ELF文件),我们也不确定如何从这65536个文件中找出正确的一个。
因此,我们转而基于零值的减法开始处理文件,并开始修复我们能够猜测出其他部分的字符串字面量,类似于我们填充S3 URL的方式。每次修复一些内容后,我们会运行一个脚本,该脚本会为那个偏移位置创建一个新的方程,直到最终我们有一个明确的解决方案。一旦有了明确的解决方案,我们就可以填充文件中的更多字符,揭示其他字符串,依此类推。我们继续这个过程,直到找到所有256个偏移位置的正确值。
许多字符串很容易被识别出来。例如,实现中包含了mbedTLS,因此与TLS密码套件和X.509证书相关的字符串可以很容易地完成。较长的句子也相对容易通过猜测消息的内容来填补,尽管有些可能比较棘手,比如不知道某个字符串是以小写还是大写字母开头,以及内部调试信息中存在不少拼写错误可能会干扰进程。
总的来说,我们花了大约两天时间来完成这个奇特的填字游戏。事后看来,如果能找到合适的启发式方法来确定65536种可能的解密中最有可能正确的那一个,可能会更快些。
如果你想自己研究Autel固件文件,我们已经在这里发布了我们的解密脚本:
https://gist.github.com/sector7-nl/3fc815cd2497817ad461bfbd393294cb
寻找漏洞
在解密了固件文件之后,我们可以开始寻找漏洞。我们已经确定充电桩没有开放的TCP端口,并且我们的数据包捕获只显示了使用TLS的出站连接。因此,我们决定专注于蓝牙攻击面,而非IP攻击面。
BLE认证 (CVE-2024-23958)
这款充电桩上的低功耗蓝牙(BLE)由运行ESP-AT固件的ESP32处理。此固件允许通过串行发送命令给ESP32来使用Wi-Fi和BLE。对于Wi-Fi,这处理了ESP32上所有与Wi-Fi和TCP/IP相关的事宜,允许另一侧发出诸如“通过TCP连接到主机:端口”的命令。对于BLE,可以使用命令来启动广播、断开设备等。主控制器重新组装BLE数据包,以允许发送大于BLE最大ATT有效载荷大小(约500字节)的数据。
为了配置充电桩,用户需要使用Autel应用程序扫描手册中的二维码,其中包含序列号和一个8位数的代码。然后,应用程序将此信息提交给Autel服务器,服务器返回一个认证令牌。这样也将充电桩链接到了用户的Autel账户。
当通过BLE连接到充电桩时,设备需要执行握手操作,其中充电桩和应用程序各自选择一些随机数,然后计算SHA256哈希值。应用程序用接收到的认证令牌和随机数进行哈希计算,但充电桩实际上是根据存储在充电桩上的6位令牌(这与手册中的8位令牌不同)及其序列号来计算这个认证令牌的。
充电桩将接收到的哈希值与其计算出的哈希值进行比较,如果它们相等,则用户就被认证。如果不匹配,它会再次进行相同的哈希计算,但这次不是使用其6位令牌来计算(特定于充电桩的)认证令牌,而是使用固件中硬编码的认证令牌。我们不太确定这个过程的目的,但是当这种情况发生时的日志消息是"authbd succ",所以这可能是有意留下的“后门”。通过从固件中提取该令牌,我们可以在不知道手册中的8位代码的情况下对任何充电桩进行认证。只需处于BLE范围内就足以获得认证连接。
void __fastcall authRequest(char *authMsgData, __int16 authMsgLen)
{
char *v4; // r4
int *object; // r0
void **v6; // r0
unsigned __int8 i; // r5
unsigned __int8 j; // r5
unsigned __int8 k; // r0
int v10; // r1
int v11; // r0
unsigned __int8 m; // r0
unsigned __int8 n; // r5
char v14; // r0
char randomNumbers[12]; // [sp+4h] [bp-DCh] BYREF
char reply[20]; // [sp+10h] [bp-D0h] BYREF
unsigned __int8 cpAuthData[32]; // [sp+24h] [bp-BCh] BYREF
unsigned __int8 appAuthData[32]; // [sp+44h] [bp-9Ch] BYREF
int v19[8]; // [sp+64h] [bp-7Ch] BYREF
char backdoorPasswordData[36]; // [sp+84h] [bp-5Ch] BYREF
char passwordHashData[36]; // [sp+A8h] [bp-38h] BYREF
qmemcpy(reply, (int *)"UxAAx11x00x00x00x00x00x00x00x01", sizeof(reply));
qmemcpy(passwordHashData, (int *)&defaultPasswordData, sizeof(passwordHashData));
qmemcpy(backdoorPasswordData, (int *)&defaultBackdoorPasswordData, sizeof(backdoorPasswordData));
memset(randomNumbers, 0, sizeof(randomNumbers));
bzero(appAuthData, 32);
bzero(cpAuthData, 32);
qmemcpy(v19, &dword_80ECCD0, sizeof(v19));
v4 = malloc(101);
memset(v4, 0, 101u);
trace("AppAuthenRequest:rn");
if ( authMsgData && authMsgLen == 32 )
{
log_msg("A_Ble_Bus", 2, 536, "auth msgrn");
object = (int *)allocate_object(288);
if ( object )
dword_2001C2CC = (int)sub_8081E40(object, (char *)v19, 256u);
else
dword_2001C2CC = 0;
memcpy(appAuthData, authMsgData, sizeof(appAuthData));
get_auth_token(passwordHashData);
sub_80822E6((unsigned __int8 *)v4, 100u);
memcpy(randomNumbers, &appRandomNum, 4u);
memcpy(&randomNumbers[4], &cpRandomNum, 4u);
retrieveCpAuthData(randomNumbers, passwordHashData, (int)cpAuthData);
if ( dword_2001C2CC )
{
v6 = sub_8081EFA((void **)dword_2001C2CC);
sub_8014502((unsigned int)v6);
}
trace("-------------------------------------------------rn");
trace("appAuthData and cpAuthData::rn");
for ( i = 0; i < 0x20u; ++i )
trace("0x%x ", appAuthData[i]);
trace("rn");
for ( j = 0; j < 0x20u; ++j )
trace("0x%x ", cpAuthData[j]);
trace("rn");
trace("-------------------------------------------------rn");
for ( k = 0; k < 0x20u; ++k )
{
if ( appAuthData[k] != cpAuthData[k] )
reply[12] = 1;
}
}
else
{
reply[12] = 2;
}
if ( reply[12] )
{
reply[12] = 0;
retrieveCpAuthData(randomNumbers, backdoorPasswordData, (int)cpAuthData);
for ( m = 0; m < 0x20u; ++m )
{
if ( appAuthData[m] != cpAuthData[m] )
reply[12] = 1;
}
for ( n = 0; n < 0x20u; ++n )
trace("0x%x ", cpAuthData[n]);
trace("rn");
if ( reply[12] )
{
set_ble_authenticated(0);
log_msg("A_Ble_Bus", 2, 639, "auth failed, %s.rn", v4);
}
else
{
set_ble_authenticated(1);
log_msg("A_Ble_Bus", 2, 634, "authbd succrn");
}
}
else
{
set_ble_authenticated(1);
v11 = sub_801726E(dword_2001C5E0, v10);
log_msg("A_Ble_Bus", 2, 605, "con:step4->authentication succ, %drn", v11);
dword_2001C5E0 = 0;
}
v14 = send_message(reply, 17u);
if ( !v14 )
v14 = ble_send_response(ble_connection, reply, 17u);
if ( v14 )
log_msg("A_Ble_Bus", 2, 654, "auth ret SD Succrn");
else
log_msg("A_Ble_Bus", 2, 658, "auth ret SD Failed.rn");
if ( !is_ble_authenticated() )
{
msleep(100);
ble_disconnect(ble_connection);
}
free(v4);
}
缓冲区溢出 #1 (CVE-2024-23959)
一旦我们建立了经过认证的BLE连接,就可以发送多种不同类型的消息。固件根据1字节的操作码(opcode)和1字节的子码(subcode)将消息传递给不同的函数。当操作码为3(基于可能与实际充电过程相关的参数设置有关的长消息)且子码为0时,调用该操作码会导致一个栈缓冲区溢出:
int __fastcall opcode_3(__int16 subcode, void *packet, unsigned __int16 packet_length)
{
int result; // r0
char v7; // r4
unsigned int i; // r5
char v9[4]; // [sp+8h] [bp-168h] BYREF
char v10[28]; // [sp+Ch] [bp-164h] BYREF
_DWORD v11[5]; // [sp+28h] [bp-148h] BYREF
int v12[5]; // [sp+3Ch] [bp-134h] BYREF
char to[60]; // [sp+50h] [bp-120h] BYREF
char v14[68]; // [sp+8Ch] [bp-E4h] BYREF
char value[140]; // [sp+D0h] [bp-A0h] BYREF
bzero(to, 60);
[...]
if ( subcode )
{
[...]
}
else
{
qmemcpy(v12, (int *)&byte_80F4234, sizeof(v12));
send_message(v12, 0x11u);
memcpy(to, packet, packet_length);
[...]
这段代码处理程序预留了一个60字节的栈缓冲区,同时将一个可能远大于这个大小的BLE数据包复制到这个栈分配的缓冲区中(理论上最大可达65536字节)。通过写入超过60+68+140字节的数据,我们能够覆盖栈上保存的寄存器,包括程序计数器(PC)。由于这里没有栈保护机制、ASLR或DEP需要处理,因此利用这个漏洞来获得任意代码执行权限仅花费了我们大约半天的时间。我们唯一需要处理的潜在随机性来源(尽管实际上我们并不清楚这到底有多随机)是固件运行了多个RTOS任务,这意味着如果我们当前任务的栈地址可能由于任务创建顺序因时间差异而不可预测,那么可能不会在一个可预测的地址。因此,我们没有硬编码地址,而是使用了一些ROP小工具来动态获取我们当前任务的栈地址,然后跳转到我们BLE数据包中的一段shellcode上栈执行。
编写正确的shellcode以便观察结果有点棘手。虽然我们可以观察设备的UART调试日志,但这并不是一直有效。设备在高速率下记录了很多信息,有时会记录一段时间的垃圾数据。最终我们发现那些看起来接地的安装孔实际上并没有接地。(是的,发现这一点比找到这个漏洞花费了更多的时间。)
{
"act": "",
"seq": "1234",
"PL": {
"msgId": "msgId",
"msgData": [
{
"msgH": "10:1:1:1:0",
"data": "<... data ...>"
}
]
}
}
对于data键的值,会未经长度验证就被base64解码到一个1024字节的栈缓冲区中。同样,这会导致溢出到栈上的其他数据,并允许我们覆盖保存的返回地址。
char *__fastcall sub_808B254(void *a1, char *a2)
{
[...]
string v26; // [sp+8h] [bp-4C0h] BYREF
string data; // [sp+24h] [bp-4A4h] BYREF
string msgId; // [sp+40h] [bp-488h] BYREF
string seq; // [sp+5Ch] [bp-46Ch] BYREF
string act; // [sp+78h] [bp-450h] BYREF
string v31; // [sp+94h] [bp-434h] BYREF
char decoded[1024]; // [sp+B0h] [bp-418h] BYREF
[...]
v11 = obtain_values_json((int)a1, a2, &act, &seq, &msgId, &data);
if ( string_compare(&act, "Reboot") )
{
[...]
}
if ( v11 >= 1 )
{
strData = to_cstring(&data);
trace("strData:%s", strData);
memset(decoded, 0, sizeof(decoded));
strData_1 = to_cstring(&data);
data_base64_decode(strData_1, decoded);
...
我们更喜欢这个漏洞,因为它更加隐蔽:它不调用memcpy函数,配置的ACMP URL可以被更改这一点也不是立即显而易见的,并且需要结合BLE和对外部互联网的连接才能触发。我们可以保持shellcode基本不变。由于shellcode中还有一些额外的空间,我们实现了向LCD写入自定义消息的功能。
原文始发于微信公众号(安全脉脉):Autel MaxiCharger研究报告
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论