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));
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?