Anti-DoS-Maßnahmen sind im Kontext von eingebetteten Systemen eng mit Speicher- und Verarbeitungsressourcen verknüpft. Da solche Systeme meist auf begrenzte Hardware ausgelegt sind, müssen Schutzmechanismen implementiert werden, um den regulären Betrieb nicht zu beeinträchtigen.
Bei einem DoS-Angriff wird das Zielsystem durch eine Flut von Anfragen überlastet, sodass legitime Anfragen nicht mehr verarbeitet werden können. Eingebettete Systeme, insbesondere solche mit geringem Speicher und CPU-Leistung, sind hier besonders anfällig. Anti-DoS-Mechanismen wie der Einsatz von Traffic-Filterungen und Ratenbegrenzungen erfordern eine effektive Speicherverwaltung und optimierte Algorithmen, um das System trotz hoher Last stabil zu halten
Im einfachen Beispiel eines CAN-Bus-Systems kann ein DoS-Angriff in Form eines Flooding-Angriffs verheerende Auswirkungen haben, da der CAN-Bus ein prioritätsbasiertes Protokoll nutzt. Jede Nachricht auf dem CAN-Bus hat eine spezifische Identifier (ID), die ihre Priorität festlegt. Nachrichten mit niedrigen numerischen Werten haben höhere Priorität und können andere Nachrichten mit höherem numerischen Wert blockieren, indem sie ständig Zugriff auf den Bus erhalten. Bei einem gezielten Flooding-Angriff könnte ein Angreifer das System so manipulieren, dass ständig Nachrichten mit hohen Prioritäts-IDs auf den Bus gesendet werden. Diese Nachrichten würden den gesamten Busverkehr dominieren und sicherstellen, dass andere, wichtige Nachrichten mit niedrigeren Prioritäts-IDs nicht mehr gesendet oder empfangen werden können. Solch ein Angriff führt dazu, dass kritische Steuergeräte und Sensoren im Netzwerk nicht mehr korrekt kommunizieren können, was erhebliche Sicherheitsrisiken für das Gesamtsystem mit sich bringt.
Ein konkretes Beispiel wäre ein Steuergerät, das kontinuierlich Nachrichten mit einer sehr niedrigen ID (und damit hoher Priorität) sendet. Diese Nachrichten könnten so häufig und in schneller Folge gesendet werden, dass der Bus dauerhaft blockiert ist. Wichtige Nachrichten, wie etwa von Sicherheitskomponenten (z. B. Bremssteuerungen oder Airbag-Systeme), werden dadurch verdrängt und erreichen ihre Empfänger nicht mehr rechtzeitig oder gar nicht. Im schlimmsten Fall könnte dies dazu führen, dass sicherheitskritische Funktionen deaktiviert werden oder nicht ordnungsgemäß funktionieren, was erhebliche Gefahren für die Sicherheit von Personen und das System selbst darstellt.
Ein weiteres potenzielles Problem bei einem Flooding-Angriff auf den CAN-Bus sind die Software-Timeouts, die viele Steuergeräte standardmäßig programmiert haben. Diese Timeout-Funktion sorgt dafür, dass ein Steuergerät nach einer bestimmten Zeit ohne empfangene Nachricht automatisch in einen Wait-Zustand übergeht, um auf neue Kommunikation zu warten.
Bei einem Flooding-Angriff auf den CAN-Bus könnte es jedoch dazu kommen, dass wichtige Nachrichten nicht mehr rechtzeitig oder überhaupt nicht an die entsprechenden Steuergeräte gelangen. Stattdessen wird der Bus von wiederholten Nachrichten mit hoher Priorität blockiert, die andere Kommunikation verdrängen. Die betroffenen Steuergeräte, die keine regulären Nachrichten mehr empfangen, interpretieren das Fehlen von erwarteten Signalen möglicherweise als Kommunikationsausfall und schalten nach Ablauf des Timeouts in einen Wait-Zustand.
Beim polling-basierten Empfang prüft die CPU kontinuierlich das UART-Register, um festzustellen, ob ein neues Byte eingetroffen ist. Sobald ein Byte empfangen wird, muss die CPU es sofort aus dem Empfangsregister lesen und verarbeiten. Dieser Modus erfordert keine zusätzliche Hardware oder komplexe Konfiguration, was ihn in der Implementierung einfach macht. Allerdings ist der polling-Modus für hohe Baudraten ungeeignet, da die CPU stark belastet wird und ständig auf neue Daten wartet. Besonders bei schnellen Datenströmen oder Daten-Bursts besteht ein erhöhtes Risiko von Datenverlusten, da die CPU möglicherweise nicht schnell genug reagiert. Der polling-Modus eignet sich daher vor allem für niedrige Baudraten und seltene Übertragungen.
/* Buffer */
uint8_t textRx[8] = {};
/* Infinite loop */
while (1) {
/* Receive using STM32 LowLevel library */
LL_USART_Receive(USART1, textRx, sizeof(textRx));
LL_mDelay(2000);
}
Im Interrupt-Modus wird bei jedem empfangenen Byte ein Interrupt ausgelöst, der die CPU dazu veranlasst, in eine spezielle Service-Routine zu springen, um das Byte sofort zu verarbeiten und es in einen Puffer zu schreiben. Dies ermöglicht es der Applikation, direkt und flexibel auf jedes empfangene Byte zu reagieren. Die Applikation kann dadurch Daten in Echtzeit verarbeiten und sofort auf eintreffende Signale reagieren, was den Interrupt-Modus ideal für Anwendungen macht, bei denen schnelle Reaktionen erforderlich sind.
Ein Vorteil dieses Ansatzes ist seine Flexibilität: Da die CPU sofort auf jedes Byte reagieren kann, eignet sich der Interrupt-Modus besonders gut für Anwendungen mit Standard-Baudraten und für Szenarien, in denen die Datenverarbeitung kontinuierlich und zeitkritisch ist. Im Vergleich zum Polling-Modus ist die Belastung der CPU durch das Warten auf Daten deutlich geringer.
Allerdings hat der Interrupt-Modus auch Nachteile, insbesondere bei schnellen Datenströmen. Eine hohe Interrupt-Frequenz, die durch schnelle Baudraten oder große Datenmengen verursacht wird, kann die CPU stark belasten. Die CPU wird ständig aus anderen Prozessen herausgerissen, um die eintreffenden Daten zu verarbeiten, was die Effizienz des Gesamtsystems beeinträchtigen und andere Prozesse verlangsamen kann. Bei sehr hohen Baudraten kann der Interrupt-Modus daher den Systemdurchsatz gefährden und zur Überlastung führen.
In Bezug auf die Abwehr von DoS-Angriffen ist der Interrupt-Modus verhältnismäßig robust. Durch die Implementierung von „Leaky-Bucket“-Checks können eingehende Daten überwacht und übermäßiger Datenverkehr blockiert werden. Bei verdächtigem Verhalten, etwa einer Flut an Daten, kann das System so konfiguriert werden, dass es den Datenfluss drosselt oder das Gerät vorübergehend vom Bus trennt, um die Stabilität des Systems zu sichern.
uint8_t textRx[8] = {};
uint32_t Rx_count = 0;
void USART2_IRQHandler(void) { // if receive buffer ready to read
if (USART2->ISR & USART_ISR_RXNE) { // move byte from buffer to message if (Rx_count < MAX) { // reading RDR clears RXNE flag
textRx[Rx_count] = USART2->RDR & 0x0FF; Rx_count++;
} } }
Im DMA-Modus (Direct Memory Access) erfolgt die Datenübertragung direkt vom UART in den Speicher, ohne dass die CPU aktiv eingreifen muss. Beim UART-Controller schiebt die empfangenen Daten autonom in einen definierten Speicherbereich. Erst nach Abschluss des Transfers oder wenn eine bestimmte Menge an Daten empfangen wurde (häufig die Hälfte des vordefinierten Puffers), wird die CPU durch einen Interrupt benachrichtigt.
Dieser Modus ist besonders effizient für hohe Baudraten und große Datenblöcke, da die CPU entlastet wird und sich anderen Aufgaben widmen kann, ohne ständig Daten manuell verarbeiten zu müssen. Der DMA-Modus eignet sich somit ideal für Anwendungen, die kontinuierlich große Datenmengen empfangen und verarbeiten müssen. Das folgende Code-Beispiel zeigt die Interrupt-Routinen für einen DMA-Channel.
void DMA1_Channel1_IRQHandler(void) {
if ((DMA1->ISR & DMA_ISR_TCIF1_Msk) == DMA_ISR_TCIF1_Msk) {
//receive code after transfer complete
LL_DMA_ClearFlag_TC1(DMA1);
}
if ((DMA1->ISR & DMA_ISR_HTIF1_Msk) == DMA_ISR_HTIF1_Msk) {
//receive code after half-transfer complete
LL_DMA_ClearFlag_HT1(DMA1);
}
}
Ein wesentlicher Nachteil des DMA-Modus ist jedoch, dass die Empfangsgröße im Voraus bekannt sein muss. Da die Daten ohne aktive CPU-Beteiligung direkt in den Speicher gelangen, „huschen“ sie am Prozessor vorbei, ohne dass dieser eine unmittelbare Kontrolle über die eingehenden Daten hat. Dies bedeutet, dass die Plausibilitätsprüfung der Daten erst nach dem Empfang durch die Applikation erfolgen kann. Die Anwendung muss also sicherstellen, dass keine fehlerhaften oder ungültigen („Jibberish“) Daten verarbeitet werden, was eine spezielle Art der Fehlerkontrolle und -handhabung erfordert. Der Schutz vor übermäßigen Datenströmen ist im DMA-Modus eingeschränkt, da die Daten kontinuierlich in den Puffer geschrieben werden. Statt eines klassischen Anti-DoS-Mechanismus ist hier eher ein „Anti-Jibberish“-Ansatz nötig, der sicherstellt, dass die Applikation nur valide Daten verarbeitet und ungültige Datenpakete ignoriert. DMA-generierte Interrupts treten meist auf, wenn der Puffer entweder zur Hälfte oder vollständig gefüllt ist, was der CPU eine gewisse Struktur bei der Datenverarbeitung erlaubt. Der DMA-Modus bietet damit eine effiziente Lösung für den Empfang großer Datenmengen, erfordert aber robuste Mechanismen auf Applikationsebene, um sicherzustellen, dass nur gültige und plausible Daten weiterverarbeitet werden.
Der DMA-Modus, bringt auch eine erhöhte Anfälligkeit für Memory Leaks mit sich. So kann es leicht passieren, dass Speicherpuffer nicht ordnungsgemäß freigegeben oder überschrieben werden, was im Laufe der Zeit zu einem Verlust von verfügbarem Speicher führt. Im Gegensatz zu anderen Modi, in denen die CPU die Datenverarbeitung eng kontrolliert, hat sie im DMA-Modus nur eingeschränkte Kontrolle über den Datenfluss und die Speicherverwaltung. Wenn beispielsweise eine Anwendung nicht ordnungsgemäß auf den Abschluss eines DMA-Transfers reagiert oder Daten nicht rechtzeitig verarbeitet, kann der Puffer erneut mit neuen Daten überschrieben werden, ohne dass der alte Speicherbereich freigegeben wurde. Über die Zeit kann dies zu einem „Aufblähen“ des Speichers führen, da immer mehr Speicherbereiche reserviert bleiben, die eigentlich freigegeben werden müssten.
Die Wahl des richtigen Empfangsmodus für UART-gestützte Datenübertragung – ob Polling, Interrupt oder DMA – hat entscheidende Auswirkungen auf die Performance und Resilienz eingebetteter Systeme. Jeder Modus bringt spezifische Stärken und Schwächen mit sich, die je nach Anwendungsfall und Hardwareanforderungen berücksichtigt werden müssen. Die Kombination aus einem angepassten Empfangsmodus, effektiven Schutzmechanismen in der Applikation und einer soliden Speicherverwaltung bildet das Fundament für performante Mikrocontroller-Software.