Autel MaxiCharger研究报告

admin 2024年12月2日22:43:00评论3 views字数 10579阅读35分15秒阅读模式

没跑路没跑路!!!

最近更新慢了很多,有师傅来问我们是不是跑路了,没跑路,在忙各种事情

团队持续招新,欢迎对车联网安全感兴趣师傅联系(招新文章还没写出来,可以直接后台私聊推荐!!!)

2024 Pwn2Own Automotive充电桩相关文章合集:

  1. 目标介绍
    1. 详细了解Pwn2Own汽车EV充电器硬件
    2. 电动汽车充电桩攻击面解析 -- 2024 Pwn2Own Automotive目标 ChargePoint Home Flex
  2. 安全研究
    1. 电动汽车充电桩网络安全研究基础
    2. 破解ChargePoint充电桩
    3. Ubiquiti Connect EV Station的攻击面
    4. Autel Maxicharger 充电桩RCE
    5. Autel MaxiCharger(CVE-2024-23967 /CVE-2024-23957解析)
    6. JuiceBox 40充电桩探究

这篇文章应该是2024 Pwn2Own Automotive充电桩系列最后一篇,本篇和之前其他关于Autel MaxiCharger充电桩的文章不同,不止单纯漏洞分析,更偏向一个从头到尾的记录报告。(内容很值得一看Orz)

Autel MaxiCharger研究报告

这是关于Autel MaxiCharger的研究报告,包括Computest研究人员发现的漏洞(CVE-2024-23958、CVE-2024-23959和CVE-2024-23967)以及攻击手段。在比赛中,能够在没有任何其他先决条件的情况下,仅通过蓝牙连接就在该充电桩上执行任意代码。

背景

在研究的充电桩中,Autel MaxiCharger拥有迄今为止最为丰富的硬件功能集。从外观上看,我们就能注意到:

•  WiFi
•  Ethernet port
•  Bluetooth
•  4G LTE connection (with a SIM card slot)
•  RFID reader
•  LCD touch screen
•  RS485
•  Undocumented USB-C port next to the SIM card slot
还展示了许多我们在其他充电桩上未见到的功能。例如,用户可以设定设定充电桩连接到特定的OCPP URL。它甚至允许用户将充电桩设置为公共充电桩,这意味着充电桩可以接受任意的RFID充电卡,充电桩进行收费。这是一个非常值得注意的功能。
Autel MaxiCharger研究报告

在充电桩内部,我们发现了很多标记清晰的测试点,包括多个UART接口。我们甚至在PCB上找到了一些未使用的内部micro-USB接口,尽管我们没有尝试使用它们,因此不清楚它们可能携带什么数据。

我们识别出的硬件包括:
•  一个GigaDevices GD32F407作为充电控制模块(ECC)
•  一个运行AT固件的ESP32-WROOM-32D,仅用于WiFi和蓝牙
•  一个ST Micro STM32F407ZGT6作为电源控制模块(ECP)
•  一个Quectel EC25-AFX(4G LTE)
•  一个LCD控制器,我们有其固件(包括LCD控制模块、LCD信息单元、LCD资源单元和LCD语言单元),但不清楚具体的芯片类型

我们并不能完全确定所有这些都是正确的:充电桩有很多功能,并且这些功能分布在很多不同的组件上。其中许多对我们研究来说并不相关。例如,我们不确定Barrot BR8051A01芯片的作用,虽然ZDI将其标识为蓝牙无线电芯片,但根据我们所见,ESP32处理了WiFi和蓝牙的所有功能。

获取固件

获取固件是破解这款充电桩过程中最困难的部分。

第一次开机时,我们通过蓝牙配对了手机,并将充电桩连接到一个正在用tcpdump捕获所有流量的WiFi网络。我们启动了一个可用的固件更新,但后来发现捕获的数据包中没有任何相关内容。显然,手机下载了更新并通过蓝牙发送给了充电桩。

我们再次尝试,同时用Burp拦截手机的网络流量。我们确定更新过程如下:

•  应用程序通过BLE从充电桩主控制器请求所有固件组件的版本。

•  应用程序将此信息提交给Autel服务器。

•  在之后的任何时候,应用程序都可以询问服务器是否有针对该充电桩的任何更新。

•  如果有更新,服务器会为每个可更新的组件返回一个URL。

•  应用程序从这些URL下载文件,并通过BLE发送给充电桩。

•  充电桩的主控制器将更新发送给相应的组件。

我们试图复制这个过程,但得到的URL被混淆了。为了反混淆,我们尝试反编译或hook应用程序。这比我们预期的要难得多:移动应用的代码被混淆了,而且应用中包含了一些反调试技巧。

最终,我们放弃了通过应用程序的方法,转而更仔细地观察那些被混淆的URL,注意到它们看起来非常接近base64编码。如果我们对它们进行base64解码,虽然不能得到可读文本,但我们能看到某些字节与(预签名的)Amazon S3 URL对齐:

Autel MaxiCharger研究报告

通过猜测一些字符(比如开头的“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

•  y ➔ 7
弄清楚这一点后,我们就可以下载固件文件了。为了获取每个组件的固件URL,我们需要向服务器请求。由于我们的充电桩已经是最新版本,所以在下一次更新可用之前,服务器不会回复任何URL。
对于充电桩的大多数组件,我们发现只需将当前的小版本号减1提交,就能获得当前版本的URL。但对于其中一个组件,这种方法不起作用:它的版本号是0.00,我们无法提交更低的版本号。不过,我们已经获得了主控制器的固件,这正是我们最感兴趣的攻击目标。
解密固件
这些文件再次被混淆了,但混淆得并不高明。我们观察到每256字节就有一个重复的模式:
Autel MaxiCharger研究报告

这表明它可能是使用了一个256字节的密钥进行异或操作。我们对这个密钥进行了几种猜测:首先,我们在256字节块中的每个偏移位置寻找出现频率最高的字节值,然后查看整个256字节块中哪个是最常见的。固件文件中经常包含只填充了NUL字节的部分,这样我们希望确定NUL字节(在密钥中的每个偏移位置)的异或值。这两种方法得出的密钥几乎是相同的。但用这些密钥对整个文件进行异或操作并没有得到可读的文件。

接着,我们尝试使用减法代替异或操作,这也未能产生可读的文件。不过,有一些小片段是正确的,比如少量可读文本或者正确对齐到字符串末尾的行结束符。但当我们试图从这些片段开始,猜测其前后字符时,发现这样做会破坏文件中其他位置的一个小片段。因此,显然也不是简单的加法或减法操作。
Autel MaxiCharger研究报告

我们的下一个猜测是加法/减法与异或操作的组合。这意味着我们需要寻找两个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 = 0 ⇒ x ^ y = x + y
所以,若 plaintext & b == 0, 那么:
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调试日志,但这并不是一直有效。设备在高速率下记录了很多信息,有时会记录一段时间的垃圾数据。最终我们发现那些看起来接地的安装孔实际上并没有接地。(是的,发现这一点比找到这个漏洞花费了更多的时间。)Autel MaxiCharger研究报告

当我们正确地将UART接地后,我们现在可以通过向UART打印一条新的自定义消息来观察我们的shellcode是否工作。
缓冲区溢出 #2 (CVE-2024-23967)
如果我们选择BLE攻击路径,绕过认证似乎是不可避免的,但对于缓冲区溢出,我们可以使用比完全控制的memcpy调用更为微妙的方法,因此我们花了一些时间寻找其他漏洞。
Autel MaxiCharger建立了两个互联网连接:一个是用于OCPP的,另一个内部称为“ACMP”(我们假设它代表类似于“Autel云管理协议”的意思)。就像OCPP一样,ACMP连接也是通过HTTPS使用WebSocket中的JSON。尽管OCPP URL可以从应用程序配置,但ACMP URL不能。当我们直接发送BLE消息时,我们可以将其更改为任何我们想要的URL。
一旦这个连接建立起来,我们发现如果我们在ACMP连接上向充电桩发送如下结构的消息:
{    "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写入自定义消息的功能。

影响
正如我们查看的所有充电桩一样,连接功能与管理充电过程的控制器是分开的。但在这款充电桩中,可以通过固件更新来更新充电控制器。由于这些固件文件似乎没有签名,在被攻破的充电桩上,充电控制器可以被重新编程以超出其安全参数工作。这可能会导致损坏充电桩或汽车,甚至通过在同一区域黑进多个充电桩,可能对电网造成影响。这取决于多少安全检查仍然由硬件执行。
此外,通过蓝牙黑进充电桩可以让攻击者通过代理连接或窃取WiFi密码来转向配置好的WiFi或以太网网络。
特别对于这款充电桩来说,记得它可以被设为公共使用,允许其他人使用标准的RFID充电卡或标签为其汽车充电。在这种情况下,我们假设充电服务运营商会为使用的能源支付Autel费用,然后再转给所有者(保留一定比例作为服务费)。但是,充电卡的发行者并不知道实际充了多少电能:通常情况下,汽车和充电卡发行者之间并没有直接联系,因此汽车无法告知发行者增加了多少电能,只有充电桩会向其云服务器报告充了多少电。因此,通过黑进一个充电桩(甚至是自己的充电桩),可以使其报告任何电量。这可以用来欺诈在公共充电站充电的人,通过报告远高于实际充电量的电量来进行诈骗。
公共充电功能意味着充电桩实际上成为了接收付款的“可信”硬件。尽管ATM机和支付终端已经实施了许多安全措施来抵御攻击(即使是对那些能够物理接触硬件的人),但这款充电桩存在一些简单的漏洞、认证功能中的后门以及内部可访问的UART或其他调试端口。这是相当令人担忧的。

原文始发于微信公众号(安全脉脉):Autel MaxiCharger研究报告

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年12月2日22:43:00
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Autel MaxiCharger研究报告https://cn-sec.com/archives/3460195.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息