Swimming Upstream: Uncovering Broadcom SDK Vulnerabilities from Bug Reports.

TL;DR;

We identified multiple security vulnerabilities affecting the UPnP implementation of Broadcom’s SDK when developing detection rules for Broadcom binaries.

While we found indications that Broadcom patched these vulnerabilities as early as 2011, we discovered them to still be affecting devices released years later by major vendors such as Cisco, DD-WRT, or Linksys.

This led to the disclosure of security vulnerabilities such as CVE-2021-34730 (an unauthenticated remote code execution as root affecting Cisco RV110/RV130/RV215, which turned a forever-day). We also linked one of these vulnerabilities to a security advisory released jointly by DD-WRT and SSD-Disclosure in early 2021.

This further demonstrates the crucial need for supply chain security validation, such as secure development lifecycles and source code reviews on the supplier’s end, and third-party source code review on the device vendor’s end. Using IoT Inspector, this and many other supply chain issues can be identified automatically. In an effort to making IoT secure, you can verify if you are affected by the issues reported in this blog post, free of charge.

Overview

In the continuous quest to extend our software composition analysis, we were exploring rules for Broadcom binaries that we frequently find in connected devices by a variety of manufacturers. The filenames of these binaries usually start with bcm (e.g., bcm_atmctlbcmcastctlbcm_flasher, …).

In the process, we also investigated a Broadcom binary implementing a UPnP service that we found in a 4G/LTE router manufactured by a Chinese vendor. This binary was named bcmupnp, and in the reversing process we discovered two obvious flaws:

  • a stack buffer overflow via SSDP M-SEARCH packets
  • a heap buffer overflow via UPnP PortmapAdd calls

When documenting these flaws, we noticed that some of these vulnerabilities were previously disclosed and documented already. However, none of the previous publications tied the vulnerabilities to the actual producer of that code: Broadcom.

With this new knowledge, we developed a vulnerability detection rule for IoT Inspector, and uncovered multiple affected vendors and manufacturers in our dataset.

Broadcom did not reveal the exact versions of their SDK that are affected by these flaws to us, so we can only provide some guesstimates based on diffing Broadcom source code leaked in GitHub public repositories. Furthermore, we are unable to provide generic CVE identifiers for these issues, as Broadcom never issued one and vendors such as DD-WRT or Cisco started issuing their own. However, we did try our best to untangle some pretty messy supply chains.

Affected vendor & product

Vendor Advisory

 

Broadcom SDK (https://www.broadcom.com)

N/A

Vulnerable version n/a – Broadcom did not disclose exact fixed versions to us (see below for our estimates).
Fixed version n/a – Broadcom did not disclose exact fixed versions to us (see below for our estimates).

Some vendors chose to fix it (DD-WRT) and others closed it as wontfix (Cisco)

CVE IDs CVE-2021-27137 – DD-WRT
CVE-2021-34730 – Cisco
Impact CVE-2021-27137 – DD-WRT – UNKNOWN
CVE-2021-34730 – Cisco – 9.8 (critical) – AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Credit Q. Kaiser, IoT Inspector Research Lab
Selim Enes Karaduman, working with SSD Disclosure

The only information on fix/non fix was obtained from Broadcom’s source code leaks on GitHub. First appearance of fix for stack overflow in upstream with a change towards strncpy:

* $Id: upnp_ssdp.c 305062 2011-12-26 10:48:21Z kenlo $

Latest appearance of heap buffer overflow in upstream, never saw a public fix. Not sure when it was fixed (if ever).

* $Id: InternetGatewayDevice.c 241192 2011-02-17 21:52:25Z gmo $

Ad Banner For BlogThe Vulnerabilities

Stack Buffer Overflow via ssdp_msearch

The UPnP discovery protocol relies on Simple Service Discovery Protocol (SSDP), a UDP protocol using the HTTP message format.

Two kinds of messages can be sent over SSDP:

  • M-SEARCH – sent by clients wishing to discover available services on a network.
  • NOTIFY – sent by UPnP servers to announce the establishment or withdrawal of service information to the multicast group.

Handling of SSDP requests is implemented by ssdp_msearch. The SSDP handling code defines a ssdp_methods array:

struct upnp_method ssdp_methods[] = 
{ 
        {"M-SEARCH ",   sizeof("M-SEARCH ")-1,  METHOD_MSEARCH,     ssdp_msearch}, 
        {0,             0,                      0,                  0} 
};

This array matches the HTTP verb from the incoming SSDP message (M-SEARCH) to a specific handler (ssdp_msearch).

In ssdp_msearch, headers HOST, MAN, and ST are parsed successively. On line 31, the code checks whether the ST header starts with uuid:. If it does, an insecure call to strcpy is made on line 35 to copy the rest of that header line into a fixed sized buffer (name):

/* Parse the M-SEARCH message */ 
int ssdp_msearch(UPNP_CONTEXT *context) 
{
        char name[128]; 
        int  type; 
        char *host; 
        char *man; 
        char *st; 

        /* check HOST:239.255.255.250:1900 */ 
        host = context->HOST; 
        if (!host || strcmp(host, "239.255.255.250:1900") != 0) 
                return -1; 
 
        /* check MAN:"ssdp:discover" */ 
        man = context->MAN; 
        if (!man || strcmp(man, "\"ssdp:discover\"") != 0) 
                return -1; 
 
        /* process search target */ 
        st = context->ST; 
        if (!st) 
                return -1; 
 
        if (strcmp(st, "ssdp:all") == 0) { 
                type = MSEARCH_ALL; 
        } 
        else if (strcmp(st, "upnp:rootdevice") == 0) { 
                type = MSEARCH_ROOTDEVICE; 
        } 
        else if (memcmp(st, "uuid:", 5) == 0) { 
                /* uuid */ 
                type = MSEARCH_UUID; 
                st += 5; 
                strcpy(name, st); 
        } 
}

To exploit this bug, one would simply need to send an SSDP M-SEARCH request resembling the one below: 

M-SEARCH * HTTP/1.1
HOST:239.255.255.250:1900
ST:uuid:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...
MX:2
MAN:"ssdp:discover"

This bug is fully exploitable given that we control the program counter. In the example below, we demonstrate that we control registers $r4 through $r11 on an ARM-based target.

stack overflow via M-SEARCH message, we proved control of the program counter by setting it to 0x41414141

 

We developed a quick proof-of-concept for that bug with Metasploit targeting Cisco RV130, based on our initial work targeting these brands of devices. You can see the exploit in action below.

RV130 remote command execution proof-of-concept

Heap Buffer Overflow via upnp_portmap_add

The UPnP protocol allows local devices to automatically set port forwarding rules for themselves, so that a port they expose internally can be reached from the WAN interface of the device exposing a UPnP service. This is usually done by sending a SOAP request to the UPnP control service. This request should contain the internal client, internal port, remote IP, remote port, protocol, and duration for that rule to live on.

This is what a legitimate port mapping request would look like:

POST /control?WANIPConnection HTTP/1.1 
Host: 192.168.100.1:1780 
SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping" 
Accept-Language: en-us;q=1, en;q=0.5 
Accept-Encoding: gzip 
Content-Type: text/xml; charset="utf-8" 
Connection: Keep-Alive 
Content-Length: 1105 

<?xml version="1.0"?> 
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> 
  <s:Body> 
    <u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1"> 
      <NewLeaseDuration>3600</NewLeaseDuration> 
      <NewPortMappingDescription>gaming console</NewPortMappingDescription> 
      <NewEnabled>0</NewEnabled> 
      <NewInternalClient>192.168.100.100</NewInternalClient> 
      <NewInternalPort>52942</NewInternalPort> 
      <NewProtocol>TCP</NewProtocol> 
      <NewExternalPort>52942</NewExternalPort> 
      <NewRemoteHost>192.168.1.42</NewRemoteHost> 
    </u:AddPortMapping> 
  </s:Body> 
</s:Envelope>

There’s a heap buffer overflow via the UPnP port mapping function, the overflow happens in a linked list of port mapping entries (map).

On line 3, a mapping between the SOAP action AddPortMapping and its expected argument and handler is defined (excerpt from ./device/InternetGatewayDevice/soap_x_wanipconnection.c):

UPNP_ACTION action_x_wanipconnection[] =
{
        {"AddPortMapping",              8,  arg_in_AddPortMapping,              0,  0,                                      &action_AddPortMapping},
        {"DeletePortMapping",           3,  arg_in_DeletePortMapping,           0,  0,                                      &action_DeletePortMapping},
        {"ForceTermination",            0,  0,                                  0,  0,                                      &action_ForceTermination},
        {"GetConnectionTypeInfo",       0,  0,                                  2,  arg_out_GetConnectionTypeInfo,          &action_GetConnectionTypeInfo},
        {"GetExternalIPAddress",        0,  0,                                  1,  arg_out_GetExternalIPAddress,           &action_GetExternalIPAddress},
        {"GetGenericPortMappingEntry",  1,  arg_in_GetGenericPortMappingEntry,  8,  arg_out_GetGenericPortMappingEntry,     &action_GetGenericPortMappingEntry},
        {"GetNATRSIPStatus",            0,  0,                                  2,  arg_out_GetNATRSIPStatus,               &action_GetNATRSIPStatus},
        {"GetSpecificPortMappingEntry", 3,  arg_in_GetSpecificPortMappingEntry, 5,  arg_out_GetSpecificPortMappingEntry,    &action_GetSpecificPortMappingEntry},
        {"GetStatusInfo",               0,  0,                                  3,  arg_out_GetStatusInfo,                  &action_GetStatusInfo},
        {"RequestConnection",           0,  0,                                  0,  0,                                      &action_RequestConnection},
        {"SetConnectionType",           1,  arg_in_SetConnectionType,           0,  0,                                      &action_SetConnectionType},
        {0,                             0,  0,                                  0,  0,                                      0}
};

On line 26, upnp_portmap_add is called with arguments parsed from the SOAP envelope:

/* << AUTO GENERATED FUNCTION: action_AddPortMapping() */
static int
action_AddPortMapping
(
        UPNP_CONTEXT * context,
        UPNP_SERVICE *service,
        IN_ARGUMENT *in_argument,
        OUT_ARGUMENT *out_argument
)
{
        /* --snip-- */

        /* << USER CODE START >> */
        IN_ARGUMENT *in_NewRemoteHost = UPNP_IN_ARG("NewRemoteHost");
        IN_ARGUMENT *in_NewExternalPort = UPNP_IN_ARG("NewExternalPort");
        IN_ARGUMENT *in_NewProtocol = UPNP_IN_ARG("NewProtocol");
        IN_ARGUMENT *in_NewInternalPort = UPNP_IN_ARG("NewInternalPort");
        IN_ARGUMENT *in_NewInternalClient = UPNP_IN_ARG("NewInternalClient");
        IN_ARGUMENT *in_NewEnabled = UPNP_IN_ARG("NewEnabled");
        IN_ARGUMENT *in_NewPortMappingDescription = UPNP_IN_ARG("NewPortMappingDescription");
        IN_ARGUMENT *in_NewLeaseDuration = UPNP_IN_ARG("NewLeaseDuration");

        UPNP_STATE_VAR *statevar;
        UPNP_VALUE value = {UPNP_TYPE_UI2, 2};

       int error = upnp_portmap_add(context,
                                                ARG_STR(in_NewRemoteHost),
                                                ARG_UI2(in_NewExternalPort),
                                                ARG_STR(in_NewProtocol),
                                                ARG_UI2(in_NewInternalPort),
                                                ARG_STR(in_NewInternalClient),
                                                ARG_BOOL(in_NewEnabled),
                                                ARG_STR(in_NewPortMappingDescription),
                                                ARG_UI4(in_NewLeaseDuration));
        if (error)
        {
            return error;
        }
        /* --snip-- */
}

upnp_portmap_add starts by requesting a portmap controller and validates the received protocol from line 23 to 27.

It then checks whether that port map is already set on line 30. If the port map is not yet present, a new port map structure is allocated on the port map controller’s port map list (lines 45-75). On lines 89 to 92, four insecure calls to strcpy are made, which is the core of this vulnerability:

/* Add a new port mapping entry */
int
upnp_portmap_add
(
        UPNP_CONTEXT    *context,
        char            *remote_host,
        unsigned short  external_port,
        char            *protocol,
        unsigned short  internal_port,
        char            *internal_client,
        unsigned int    enable,
        char            *description,
        unsigned long   duration
)
{
        UPNP_PORTMAP_CTRL *portmap_ctrl;
        UPNP_PORTMAP *map;

        /* Get control body */
        portmap_ctrl = (UPNP_PORTMAP_CTRL *)(context->focus_ifp->focus_devchain->devctrl);

        /* data validation */
        if (strcasecmp(protocol, "TCP") != 0 &&
                strcasecmp(protocol, "UDP") != 0) {
                upnp_syslog(LOG_ERR, "add_portmap:: Invalid protocol");
                return SOAP_ARGUMENT_VALUE_INVALID;
        }

        /* check duplication */
        map = upnp_portmap_find(context, remote_host, external_port, protocol);
        if (map) {
                if (strcmp(internal_client, map->internal_client) != 0)
                        return SOAP_CONFLICT_IN_MAPPING_ENTRY;

                /* Argus, make it looked like shutdown */
                if (enable != map->enable ||
                        internal_port != map->internal_port) {

                        if (map->enable) {
                                map->enable = 0;
                                upnp_osl_nat_config(map);
                        }
                }
        }
        else {
                if (portmap_ctrl->num == portmap_ctrl->limit) {

                        UPNP_PORTMAP_CTRL *new_portmap_ctrl;

                        int old_limit = portmap_ctrl->limit;
                        int old_size = UPNP_PORTMAP_CTRL_SIZE + old_limit * sizeof(UPNP_PORTMAP);

                        int new_limit = old_limit * 2;
                        int new_size = UPNP_PORTMAP_CTRL_SIZE + new_limit * sizeof(UPNP_PORTMAP);

                        /*
                         * malloc a new one for twice the size,
                         * the reason we don't use realloc is when realloc failed,
                         * the old memory will be gone!
                         */
                        new_portmap_ctrl = (UPNP_PORTMAP_CTRL *)malloc(new_size);
                        if (new_portmap_ctrl == 0)
                                return SOAP_OUT_OF_MEMORY;

                        /* Copy the old to the new one, and free it */
                        memcpy(new_portmap_ctrl, portmap_ctrl, old_size);

                        free(portmap_ctrl);

                        /* Assign the new one as the portmap_ctrl */
                        portmap_ctrl = new_portmap_ctrl;
                        context->focus_ifp->focus_devchain->devctrl = new_portmap_ctrl;

                        portmap_ctrl->limit = new_limit;
                }

                /* Locate the map and advance the total number */
                map = portmap_ctrl->pmlist + portmap_ctrl->num;
                portmap_ctrl->num++;
        }

        /* Update database */
        map->external_port  = external_port;
        map->internal_port  = internal_port;
        map->enable         = enable;
        map->duration           = duration;
        map->book_time          = time(0);

        strcpy(map->remote_host,        remote_host);
        strcpy(map->protocol,           protocol);
        strcpy(map->internal_client,    internal_client);
        strcpy(map->description,        description);
        
        /* Set to NAT kernel */
        if (map->enable)
                upnp_osl_nat_config(map);

        return 0;
}

Trigger:

POST /control?WANIPConnection HTTP/1.1
Host: 192.168.100.1:1780
SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping"
Accept-Language: en-us;q=1, en;q=0.5
Accept-Encoding: gzip
Content-Type: text/xml; charset="utf-8"
Connection: Keep-Alive
Content-Length: 1105

<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
      <NewLeaseDuration>0</NewLeaseDuration>
      <NewPortMappingDescription>AAAAAAAAA</NewPortMappingDescription>
      <NewEnabled>0</NewEnabled>
      <NewInternalClient>192.168.100.100</NewInternalClient>
      <NewInternalPort>8000</NewInternalPort>
      <NewProtocol>TCP</NewProtocol>
      <NewExternalPort>8000</NewExternalPort>
      <NewRemoteHost>192.168.1.42</NewRemoteHost>
    </u:AddPortMapping>
  </s:Body>
</s:Envelope>

Interestingly, this heap overflow can be turned into a stack buffer overflow under specific conditions. This bug is therefore directly exploitable, given that we control the program counter. In the example below, we demonstrate that we control registers $r4 through $r11 on an ARM-based target.

Heap buffer overflow via UPnP port mapping. We demonstrate control of the program counter by setting it to 0x41414141

Setting NewEnabled to 0 turns it into a stack buffer overflow, and we can bypass the NewRemoteHost validation (checking that it corresponds to the WAN IP) by setting a string value rather than a dotted decimal IP value.

Previous Discoveries

When googling for strings present in the binary, we stumbled upon Broadcom’s source code hosted in DD-WRT’s Subversion repository. This is when things got interesting.

We identified the stack buffer overflow in the code at https://svn.dd-wrt.com/browser/src/router/upnp/src/ssdp.c?rev=11589#L385.

We identified the heap buffer overflow in the code at https://svn.dd-wrt.com/browser/src/router/upnp/device/InternetGatewayDevice/InternetGatewayDevice.c?rev=11589#L215.

The vulnerability was present in older version but the strcpy call recently changed to strlcpy, which intrigued us.

We checked when this change was made:

svn blame ssdp.c | grep strlcpy
45724 brainslaye 		strlcpy(name, st, sizeof(name));

Looked at the commit itself:

svn diff -c 45724
Index: ssdp.c
===================================================================
--- ssdp.c	(revision 45723)
+++ ssdp.c	(revision 45724)
@@ -382,7 +382,7 @@
 		/* uuid */
 		type = MSEARCH_UUID;
 		st += 5;
-		strcpy(name, st);
+		strlcpy(name, st, sizeof(name));
 	}
 	else {
 		/* check advertise_table for specify name. */

And the commit message:

svn log -r 45724
------------------------------------------------------------------------
r45724 | brainslayer | 2021-02-09 09:48:07 +0100 (Di, 09 Feb 2021) | 1 line
fix potential stack overflow with modified upnp request
------------------------------------------------------------------------

Then by googling for “DD-WRT stack overflow”, we ended up finding this advisory from SSD Disclosure: https://ssd-disclosure.com/ssd-advisory-dd-wrt-upnp-buffer-overflow/. It only mentions DD-WRT, and it doesn’t mention the heap buffer overflow.

Even though it’s not mentioned in the advisory, DD-WRT maintainers also caught up on the potential heap overflow and fixed it in their branch:

svn blame ./device/InternetGatewayDevice/InternetGatewayDevice.c | grep strlcpy
45725 brainslaye 	strlcpy(map->remote_host,        remote_host, sizeof(map->remote_host));
45725 brainslaye 	strlcpy(map->protocol,           protocol, sizeof(map->protocol));
45725 brainslaye 	strlcpy(map->internal_client,    internal_client, sizeof(map->internal_client));
45725 brainslaye 	strlcpy(map->description,        description, sizeof(map->description));

Commit message:

svn log -r 45725
------------------------------------------------------------------------
r45725 | brainslayer | 2021-02-09 10:11:39 +0100 (Di, 09 Feb 2021) | 1 line

rework other potential insecure code
------------------------------------------------------------------------

Let the Hunt Begin

So, we have two serious vulnerabilities affecting the UPnP implementation that is part of Broadcom’s SDK, but no advisory was ever released about it. The only publication so far mentions DD-WRT.

We quickly wrote a vulnerability detection rule and went hunting into our firmware dataset. We identified the following affected devices:

  • Cisco RV110, RV215, RV130 – Affected by both stack and heap overflows.
  • Linksys devices (E900, E1200, E1500, E2500, E3200) – Affected by heap overflow.
  • Huawei device (B593) – A binary is affected by both issues but it is a leftover artifact.

Firmware images from Asus and NETGEAR contained a UPnP binary built from Broadcom’s SDK, but both stack and heap overflows were fixed.

Mapping the Supply Chain

The diagram below should help you get a better understanding of the complex supply chains. Items marked with a star on top are related to the stack overflow tracked as CVE-2021-27137.

Broadcom Upnp Timeline Final.drawio

In an ideal world, the initial discovery of these issues in 2012/2013 should have been reported to Broadcom, if not by the researchers, then at least by the affected manufacturers. Broadcom would have issued a patch, which in turn would have been applied by all manufacturers relying on Broadcom’s UPnP stack. Instead, the vulnerability still lives on in 2021 – but at least can be identified automatically by IoT Inspector.

Going Upstream

In order to pin down affected vendors, we rely on GitHub’s powerful search engine to identify repositories containing Broadcom’s UPnP code.

During the process, we uncovered uncanny similarities between two different SDKs: one from Ralink Tech Inc., released in 2002, and the one under investigation from Broadcom, released mid-2008.

While there may not be a hundred ways to write a UPnP service implementation in C, these two implementations make the exact same mistakes at the exact same locations in their codes. Each of them could have made use of insecure C functions at other locations, but they only made them in these two specific functions.

We can only speculate if Broadcom took inspiration from Ralink – but if that’s the case, these two UPnP bugs have been alive and well for almost 20 years.

Ralink / Broadcom code similarities

 

Ralink / Broadcom code similarities (2)

 

Conclusion

As awareness for supply chain transparency is on the rise among security experts, this is yet another example of the vast implications of an obscure IoT supply chain. Too often, critical vulnerabilities are the result of an  opaque supply chain for three simple reasons:

  1. On the supplier’s end, insufficient secure software development practices, in particular lack of security testing and code review, resulted in critical security issues to be introduced into Broadcom’s SDK. Insufficient communication and lack of public advisories led to downstream vendors to remain unaware of these vulnerabilities although Broadcom patched them.
  2. On the product vendor’s end, we see manufacturers with access to Broadcom’s source code (a requirement to build Broadcom SDK binaries for their own platform) who missed to sufficiently validate their supply chain, left the issues unspotted and distributed the vulnerabilities to hundreds of thousands of end customers – leaving them vulnerable to attacks.
  3. But supply chain issues can go upstream, too. During our research, we discovered that security researchers and pen-testers have previously identified issues in devices relying on the Broadcom SDK, but didn’t link these issues to Broadcom directly. This lead to these issues being rediscovered and fixed over a course of a decade, leaving many users vulnerable in the process.

PSIRTS of vendors along a supply chain must start working closer with each other to assure that security issues are reported up and down the supply chain in a timely manner. Then we stand a chance that critical vulnerabilities are addressed before they are exploited by cyber criminals.

Our firmware analysis platform IoT Inspector can support in detecting such crucial supply chain issues. It automatically detects whether a firmware is based on a vulnerable Broadcom SDK. In an effort to making IoT secure, you can verify if you are affected, free of charge.

Timeline

  • 2021-07-02 – We contact Broadcom’s security team to check if they’re aware that CVE-2021-27137 affects their SDK
  • 2021-07-02 – Broadcom confirms it’s not present in their current products and will check if older products are affected
  • 2021-07-03 – We start hunting for affected products and devices.
  • 2021-07-19 – We notify affected vendors (Cisco, Huawei, Linksys) via their respective PSIRTs, and get back to Broadcom asking if they could release some kind of advisory – given that recent models are still affected.
  • 2021-07-19 – Broadcom answers that “The code in question is not used in any current or remotely recent products. The last even remotely similar issues were corrected years ago and customers notified.”
  • 2021-07-23 – We confirm with Huawei analysts that the affected binary is not launched and is a leftover artifact from building the image with Broadcom’s SDK.
  • 2021-08-18 – We contact Linksys a second time, requesting they acknowledge reception of our vulnerability report.
  • 2021-08-18 – Cisco releases an advisory tracking the issues as CVE-2021-34730
  • 2021-10-05 – Releasing this post.