Tuesday, 3 July 2018

ASUS DSL-AC3100 Router Firmware DHCP Bug

It's great that ASUS makes the GPL firmware source for their routers easy to download. I wish more vendors would do this.

Unfortunately, it didn't take more than a few minutes of auditing to come across the DHCPd code. Lets look at the original non ASUS code in wide-dhcp-server.

int
dhcp6_get_options(p, ep, optinfo)
        struct dhcp6opt *p, *ep;
        struct dhcp6_optinfo *optinfo;
{
        struct dhcp6opt *np, opth;
        int i, opt, optlen, reqopts, num;
        u_int16_t num16;
        char *bp, *cp, *val;
        u_int16_t val16;
        u_int32_t val32;
        struct dhcp6opt_ia optia;
        struct dhcp6_ia ia;
        struct dhcp6_list sublist;
        int authinfolen;

        bp = (char *)p;
        for (; p + 1 <= ep; p = np) {
                struct duid duid0;

                /*
                 * get the option header.  XXX: since there is no guarantee
                 * about the header alignment, we need to make a local copy.
                 */
                memcpy(&opth, p, sizeof(opth));
                optlen = ntohs(opth.dh6opt_len);

...
                case DH6OPT_STATUS_CODE:
                        if (optlen < sizeof(u_int16_t))
                                goto malformed;
                        memcpy(&val16, cp, sizeof(val16));
                        num16 = ntohs(val16);
                        debug_printf(LOG_DEBUG, "", "  status code: %s",
                            dhcp6_stcodestr(num16));

It's pretty clear that the options parsing code has to verify optlen. We also note that optlen is a signed 32-bit integer and that optlen casts ntohs() which returns a 16-bit unsigned int by default.

Lets look at the router firmware code which has added its own extensions:

      // brcm: get ACS URL from dhcp server option 17
      case DH6OPT_VENDOR_OPTS:
      {
          char *option_string;
          int option_len;
          u_int32_t enterprise_id;
          u_int16_t sub_option_num=1;
          int sub_option_offset=0;
          int sub_option_len=0;

          /* No guarentee on alignment, so copy to word variable */
          memcpy(&enterprise_id, cp, sizeof(enterprise_id));
          enterprise_id = ntohl(enterprise_id);

          // the first word in the data is the enterprise number.
          // I cannot find an Enterprise number for Broadband Forum in the
          // IANA database, so don't check for now.  See page 85 of RFC 3315.
          dprintf(LOG_DEBUG, FNAME, "    enterprise-number: %d (0x%x)\n",
                  enterprise_id, enterprise_id);

          // advance to point to the real data
          option_string = cp + 4;
          option_len = optlen - 4;

          // look for sub option 1: ManagementServer.URL
          if (findEncapVendorSpecificOption(option_string, option_len,
                 sub_option_num, &sub_option_offset, &sub_option_len))
          {
             int copyLen = sizeof(optinfo->acsURL) - 1;
             if (copyLen > sub_option_len) copyLen=sub_option_len;

             memcpy(optinfo->acsURL, &option_string[sub_option_offset], copyLen);
             optinfo->acsURL[copyLen] = '\0';
             // fprintf(stderr, "Found acsURL %s!!\n", optinfo->acsURL);
          }

Now we note that option_len is optlen - 4. There is no input validation on optlen. Because option_len is a signed int, if we make optlen < 4, we can get a negative value into option_len. Now _if_ we were able to enter the code that does:

             int copyLen = sizeof(optinfo->acsURL) - 1;
             if (copyLen > sub_option_len) copyLen=sub_option_len;

And sub_option_len was also negative, then we could get a buffer overflow, since copyLen and sub_option_len are both signed. Lets look at the function that triggers all of this: 

int findEncapVendorSpecificOption(const char *option, int len,
                                  u_int16_t sub_option_num,
                                  int *sub_option_offset, int *sub_option_len)
{
   struct dhcp6opt hdr;
   int i=0;
   u_int16_t curr_sub_option_num;
   int curr_sub_option_len;

   while (i < len)
   {
      /* no guarantee on alignment, so copy header */
      memcpy(&hdr, &option[i], sizeof(hdr));
      curr_sub_option_num = ntohs(hdr.dh6opt_type);
      curr_sub_option_len = ntohs(hdr.dh6opt_len);

      /* sanity check */
      if (i + 4 + curr_sub_option_len > len)
      {
         printf("sub-option exceeds len, %d %d %d",
                 i, curr_sub_option_len, len);
         return 0;
      }

      if (sub_option_num == curr_sub_option_num)
      {
         *sub_option_offset = i+4;
         *sub_option_len = curr_sub_option_len;
         return 1;
      }

      i += 4 + curr_sub_option_len;  /* advance i to the next sub-option */
   }

   return 0;
}

Hmm.. we get blocked. When len is negative, we can't enter the loop. Sure, we can still get out of bounds reads for option_len is positive since len is not validated. But it's unlikely that we can use this bug for anything interesting in terms of memory corruption.

So... tl;dr No input validation on length in vendor specific dhcp code. Out of bounds memory access. No memory corruption.

What other goodies exist in vendor supplied GPL source code?

Exploiting the Lorex 2K Indoor Wifi at Pwn2Own Ireland

Introduction In October InfoSect participated in Pwn2Own Ireland 2024 and successfully exploited the Sonos Era 300 smart speaker and Lor...