This post is a quick vulnerability report summary for a vulnerability we found while fuzzing the TCP/IP stack CycloneTCP.
Summary
CycloneTCP is a fully-featured open source IPv4/IPv6 stack for embedded systems developed by Oryx Embedded. The stack is developed in ANSI C and distributed under GPLv2 or with a commercial license. The source code is available on Oryx's website (or a Github mirror). It supports various microprocessors, operating systems and a whole variety of protocols including HTTP, SNMP, Icecast, CoAP, MQTT, etc.
While working on TCP/IP stacks for embedded devices we ended up experimenting various software testing techniques on CycloneTCP like fuzzing with Honggfuzz [1] and concolic execution with Triton [2]. The fuzzing harness we wrote only targeted a few components (IPv4/TCP without other protocols). While running a campaign, a hanging input sample was generated, that ultimately led the CycloneTCP in an infinite loop. After a quick investigation, it appeared to be a remotely triggerable Denial-of-Service vulnerability in the handling of TCP options.
Affected Versions : From CycloneTCP 1.7.6 to 2.0.0 (we have not been able to check versions earlier than 1.7.6).
Fixed Versions : CycloneTCP 2.0.2 and above.
Impact : Remote Denial of Service.
Scope : The bug happens in the default configuration, and is triggerable by any attacker that can communicate with a device using CycloneTCP as long as IPv4 and TCP are activated.
Potential mitigations : If it is not possible to update the device with a fixed version of CycloneTCP, filtering inbound TCP packets with a zero length TCP option will prevent exploitation of the bug.
Technical details
A malformed packet can trigger an infinite loop in the function tcpGetOption() of tcp_misc.c.
504 TcpOption *tcpGetOption(TcpHeader *segment, uint8_t kind)
505 {
506 size_t length;
507 uint_t i;
508 TcpOption *option;
509
510 //Make sure the TCP header is valid
511 if(segment->dataOffset < 5)
512 return NULL;
513
514 //Compute the length of the options field
515 length = segment->dataOffset * 4 - sizeof(TcpHeader);
516
517 //Point to the very first option
518 i = 0;
519
520 //Parse TCP options
521 while(i < length)
522 {
523 //Point to the current option
524 option = (TcpOption *) (segment->options + i);
525 //NOP option detected?
526 if(option->kind == TCP_OPTION_NOP)
527 {
528 i++;
529 continue;
530 }
531 //END option detected?
532 if(option->kind == TCP_OPTION_END)
533 break;
534 //Check option length
535 if((i + 1) >= length || (i + option->length) > length)
536 break;
537 //Current option kind match the specified one?
538 if(option->kind == kind)
539 return option;
540 //Jump to next the next option
541 i += option->length;
542 }
543
544 //Specified option code not found
545 return NULL;
547 }
At line 541, the statement i += option->length; increments i with a value that is user-controlled. If the value option->length is set to 0, then the tcpGetOption() function will never exit the while loop. The length check should be performed and exit the loop if option->length value is less than 2 (as it can be seen in linux TCP stack [3]).
This portion of code is executed by the main thread of the stack scheduling and processing all frames. It includes processing incoming frames from the driver up to transmitting the payload to applicative threads. This thread also performs all internal "ticks" and internal state updates. Thus, if it gets stuck, it can no longer receive incoming frames or handle data coming from applicative layers.
In conclusion, by sending a TCP segment with a TCP option with a length value of zero, an attacker can put the CycloneTCP stack in an infinite loop that prevents it from processing any other packet.
Patch
We proposed the following patch, which consists in a single check at the end of the while loop to properly exit it, and especially, if the option->length given in the TCP segment is an invalid one.
diff --git a/core/tcp_misc.c b/core/tcp_misc.c
index fdbec2b..e99bb77 100644
--- a/core/tcp_misc.c
+++ b/core/tcp_misc.c
@@ -535,6 +535,11 @@ TcpOption *tcpGetOption(TcpHeader *segment, uint8_t kind)
//Current option kind match the specified one?
if(option->kind == kind)
return option;
+
+ //If packet is malformed
+ if (option->length < 2 )
+ break;
+
//Jump to next the next option
i += option->length;
}
This patch fixes the infinite loop and normally has no deleterious effects on the processing of other options.
Wrap-up notes
MITRE assigned the identifier CVE-2021-26788 to the vulnerability [4].
Even though we believe the vulnerability cannot be used to gain further privileges on the device it can nonetheless temporarily DOS the device. To our knowledge, solely a reboot can restore the stack to a working state. We strongly encourage any user (of both the open-source and commercial version) to upgrade to the latest version (as the only other remediations are dedicated filtering rules).
While we only tested a narrow portion of the whole CycloneTCP code, the tests we performed have shown the stack to be robust, well designed and trustworthy in comparison to other existing stacks. We also want to highlight the reactiveness and commitment of the Oryx team in the disclosure process.
Disclosure timeline
2020-10-15: Discovery of the vulnerability.
2020-12-21: Email sent to Oryx Embedded.
2020-12-23: Vendor requested details and PoC.
2020-12-23: Technical details and PoC sent.
2021-01-11: CycloneTCP v2.0.2 release fixes the issue. Vendor sends release notes and security advisory.
2021-01-13: Email sent to coordinate disclosure.
2021-01-19: Reply from vendor suggesting a conference call in February.
2021-02-02: Email to vendor proposing dates & times for a conference call to coordinate disclosure.
2021-02-02: CVE ID requested via web form at https://cve.mitre.org, automated reply received.
2021-02-17: Email sent to MITRE asking for update on CVE ID request.
2021-03-04: Email sent to MITRE asking for update on CVE ID request.
2021-03-08: CVE-2021-26788 assigned by MITRE.
2021-03-09: Email sent to Oryx Embedded to coordinate disclosure date. Conference call scheduled for 2021-03-16.
2021-03-16: Conference call with the vendor. Publication date set to April 8th, 90 days after initial report.
2021-04-13: Vulnerability details disclosed.