Post

From CTFs to Real-World Exploitation: Root Cause Analysis of CVE-2017-14493 in dnsmasq

From CTFs to Real-World Exploitation: Root Cause Analysis of CVE-2017-14493 in dnsmasq

[CVE-2017-14493 ] Root Cause Analysis on vulnerable dnsmasq software.

This is my first step of moving from CTFs to Real World Vulnerabilities. My goal is to move from exploiting CTFs to real world software such has dnsmasq and eventually both the linux and andriod kernel.

Lab Setup.

I will be running the vulnerable software on a kali virtual machine using virtualbox, and i will use my ubuntu machine as the attacker machine.

First lets download the vulnerable software version ( < 2.78)

1
 wget https://dnsmasq.org/dnsmasq-2.77.tar.xz

Then i first compiled it with out security mitigations.

1
make CFLAGS="-O0 -g -fno-stack-protector -z execstack -no-pie -Wno-error=incompatible-pointer-types -Wno-error=deprecated-non-prototype -fpermissive" LDFLAGS="-z execstack -no-pie"

Then I set up my network interfaces on both machines so that i can use a IPv6 address (since the vulnerabilities are in DHCPv6)

Kali:

1
sudo ip -6 addr add 2001:db8::1/64 dev eth0

Ubuntu:

1
sudo ip -6 addr add 2001:db8::1/64 dev wlp4s0

So To find the where the vulnerable code/code path is I will use a debugger (gdb with gef extension).

So lets start with CVE-2017-14493.

CVE-2017-14493

CVE Description: Stack-based buffer overflow in dnsmasq before 2.78 allows remote attackers to cause a denial of service (crash) or execute arbitrary code via a crafted DHCPv6 request.

To find where the vulnerability is lets run they program while its attached to a debugger and run the PoC and analyse it from there.

To make this easier I copied the necessary source code into the folder dnsmasq-2.77/src and then I used GDB script with the following commands.

directory dnsmasq-2.77/src/
set follow-exec-mode new
set breakpoint pending on
run -d -i wlp4s0 --dhcp-range=2001:db8::10,2001:db8::ae,64 --enable-ra

As we find interesting code paths we will use this gdb script to but breakpoints in the source code.

Then run gdb with this script.

1
sudo gdb ./dnsmasq -x ./gdb_cmds

GDB

In the attacker machine run the PoC

1
2
./poc.py 2001:db8::1 547
#./poc.py <ipv6 addr> <port>

segfault As we can see that we got a segfault in the dhcp6_reply().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
unsigned short 
dhcp6_reply(struct dhcp_context *context, int interface, char *iface_name,
        struct in6_addr *fallback, struct in6_addr *ll_addr, struct in6_addr *ula_addr,
        size_t sz, struct in6_addr *client_addr, time_t now)
{
        struct dhcp_vendor *vendor;
        int msg_type;
        struct state state;
        if (sz <= 4) return 0;
        msg_type = *((unsigned char *)daemon->dhcp_packet.iov_base);
        /* Mark these so we only match each at most once, to avoid tangled linked lists */
        for (vendor = daemon->dhcp_vendors; vendor; vendor = vendor->next)
                vendor->netid.next = &vendor->netid;
        
        reset_counter();
        state.context = context;
        state.interface = interface;
        state.iface_name = iface_name;
        state.fallback = fallback;
        state.ll_addr = ll_addr;
        state.ula_addr = ula_addr;
        state.mac_len = 0;
        state.tags = NULL;
        state.link_address = NULL;

        if (dhcp6_maybe_relay(&state, daemon->dhcp_packet.iov_base, sz, client_addr,
                N6_IS_ADDR_MULTICAST(client_addr), now))
                return msg_type == DHCP6RELAYFORW ? DHCPV6_SERVER_PORT : DHCPV6_CLIENT_PORT;
        return 0;
}

When looking at function, there is nothing really interesting, so my first thought was that there vulnerability is in another function but affects the stack of this function. The only function that has a argument that resides in the dhcp6_reply is dhcp6_maybe_relay and the variable name is state.

Here is the state struct:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  
struct state {
	unsigned char *clid;
	int clid_len, iaid, ia_type, interface, hostname_auth, lease_allocate;
	char *client_hostname, *hostname, *domain, *send_domain;
	struct dhcp_context *context;
	struct in6_addr *link_address, *fallback, *ll_addr, *ula_addr;
	unsigned int xid, fqdn_flags;
	char *iface_name;
	void *packet_options, *end;
	struct dhcp_netid *tags, *context_tags;
	unsigned char mac[DHCP_CHADDR_MAX];
	unsigned int mac_len, mac_type;
#ifdef OPTION6_PREFIX_CLASS
	struct prefix_class *send_prefix_class;
#endif
};

So now lets look at dhcp6_maybe_reply() and only focus on state. Here is the whole function :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
static int dhcp6_maybe_relay(struct state *state, void *inbuff, size_t sz, 
			     struct in6_addr *client_addr, int is_unicast, time_t now)
{
  void *end = inbuff + sz;
  void *opts = inbuff + 34;
  int msg_type = *((unsigned char *)inbuff);
  unsigned char *outmsgtypep;
  void *opt;
  struct dhcp_vendor *vendor;

  /* if not an encapsulated relayed message, just do the stuff */
  if (msg_type != DHCP6RELAYFORW)
    {
      /* if link_address != NULL if points to the link address field of the 
	 innermost nested RELAYFORW message, which is where we find the
	 address of the network on which we can allocate an address.
	 Recalculate the available contexts using that information. 

      link_address == NULL means there's no relay in use, so we try and find the client's 
      MAC address from the local ND cache. */
      
      if (!state->link_address)
	get_client_mac(client_addr, state->interface, state->mac, &state->mac_len, &state->mac_type, now);
      else
	{
	  struct dhcp_context *c;
	  state->context = NULL;
	   
	  if (!IN6_IS_ADDR_LOOPBACK(state->link_address) &&
	      !IN6_IS_ADDR_LINKLOCAL(state->link_address) &&
	      !IN6_IS_ADDR_MULTICAST(state->link_address))
	    for (c = daemon->dhcp6; c; c = c->next)
	      if ((c->flags & CONTEXT_DHCP) &&
		  !(c->flags & (CONTEXT_TEMPLATE | CONTEXT_OLD)) &&
		  is_same_net6(state->link_address, &c->start6, c->prefix) &&
		  is_same_net6(state->link_address, &c->end6, c->prefix))
		{
		  c->preferred = c->valid = 0xffffffff;
		  c->current = state->context;
		  state->context = c;
		}
	  
	  if (!state->context)
	    {
	      inet_ntop(AF_INET6, state->link_address, daemon->addrbuff, ADDRSTRLEN); 
	      my_syslog(MS_DHCP | LOG_WARNING, 
			_("no address range available for DHCPv6 request from relay at %s"),
			daemon->addrbuff);
	      return 0;
	    }
	}
	  
      if (!state->context)
	{
	  my_syslog(MS_DHCP | LOG_WARNING, 
		    _("no address range available for DHCPv6 request via %s"), state->iface_name);
	  return 0;
	}

      return dhcp6_no_relay(state, msg_type, inbuff, sz, is_unicast, now);
    }

  /* must have at least msg_type+hopcount+link_address+peer_address+minimal size option
     which is               1   +    1   +    16      +     16     + 2 + 2 = 38 */
  if (sz < 38)
    return 0;
  
  /* copy header stuff into reply message and set type to reply */
  if (!(outmsgtypep = put_opt6(inbuff, 34)))
    return 0;
  *outmsgtypep = DHCP6RELAYREPL;

  /* look for relay options and set tags if found. */
  for (vendor = daemon->dhcp_vendors; vendor; vendor = vendor->next)
    {
      int mopt;
      
      if (vendor->match_type == MATCH_SUBSCRIBER)
	mopt = OPTION6_SUBSCRIBER_ID;
      else if (vendor->match_type == MATCH_REMOTE)
	mopt = OPTION6_REMOTE_ID; 
      else
	continue;

      if ((opt = opt6_find(opts, end, mopt, 1)) &&
	  vendor->len == opt6_len(opt) &&
	  memcmp(vendor->data, opt6_ptr(opt, 0), vendor->len) == 0 &&
	  vendor->netid.next != &vendor->netid)
	{
	  vendor->netid.next = state->tags;
	  state->tags = &vendor->netid;
	  break;
	}
    }
  
  /* RFC-6939 */
  if ((opt = opt6_find(opts, end, OPTION6_CLIENT_MAC, 3)))
    {
      state->mac_type = opt6_uint(opt, 0, 2);
      state->mac_len = opt6_len(opt) - 2;
      memcpy(&state->mac[0], opt6_ptr(opt, 2), state->mac_len);
    }
  
  for (opt = opts; opt; opt = opt6_next(opt, end))
    {
      int o = new_opt6(opt6_type(opt));
      if (opt6_type(opt) == OPTION6_RELAY_MSG)
	{
	  struct in6_addr align;
	  /* the packet data is unaligned, copy to aligned storage */
	  memcpy(&align, inbuff + 2, IN6ADDRSZ); 
	  state->link_address = &align;
	  /* zero is_unicast since that is now known to refer to the 
	     relayed packet, not the original sent by the client */
	  if (!dhcp6_maybe_relay(state, opt6_ptr(opt, 0), opt6_len(opt), client_addr, 0, now))
	    return 0;
	}
      else if (opt6_type(opt) != OPTION6_CLIENT_MAC)
	put_opt6(opt6_ptr(opt, 0), opt6_len(opt));
      end_opt6(o);	    
    }
  
  return 1;
}

Here is where the vulnerability happens:

1
2
3
4
5
6
if ((opt = opt6_find(opts, end, OPTION6_CLIENT_MAC, 3)))
{
	state->mac_type = opt6_uint(opt, 0, 2);
			state->mac_len = opt6_len(opt) - 2;
	memcpy(&state->mac[0], opt6_ptr(opt, 2), state->mac_len);
}

User controlled data is copied to state->mac[0] using a user controlled size which is not validated by the program.

Lets check the size of state->mac. Has we have seen from the state struct.

1
unsigned char mac[DHCP_CHADDR_MAX];

state->mac is a character buffer, DHCP_CHADDR_MAX is defined with size 16.

To confirm this vulnerability lets use a debugger to check the size of state->mac_len when memcpy() is called.

This vulnerability happens in dhcp6_maybe_relay() but it corrupts stack memory in dhcp6_relay because state is stored in that function.

Here is the exact corruption layout:

1
2
3
4
5
6
7
8
9
10
11
12
13
nsigned char mac[DHCP_CHADDR_MAX];
	unsigned int mac_len, mac_type;
#ifdef OPTION6_PREFIX_CLASS
	struct prefix_class *send_prefix_class;
#endif

state.mac[16]
state.mac_len 
state.mac_type
...
Stack Canary : at offset 42
saved RBP
saved RIP

The Patch

Through source patch diffing I found out how they patched this vulnerability.

1
2
3
4
5
6
7
8
9
10
11
if ((opt = opt6_find(opts, end, OPTION6_CLIENT_MAC, 3)))
{
	/* Patch */
	if (opt6_len(opt) - 2 > DHCP_CHADDR_MAX) {
	return 0;
	}
	/* End of patch */
	state->mac_type = opt6_uint(opt, 0, 2);
	state->mac_len = opt6_len(opt) - 2;
	memcpy(&state->mac[0], opt6_ptr(opt, 2), state->mac_len);
}

I check was added to ensure that the data copied into the state->mac buffer was less than DHCP_CHADDR_MAX. That is simply the patch.

To exploit this vulnerability (Later blog post) I will need a CVE-2017-14494 which is a information leak vulnerability.

Research Status

The root cause analysis for CVE-2017-14493 has been completed, including patch diffing and vulnerable code path tracing.

Ongoing work focuses on:

  • Reliable heap layout control
  • Leveraging CVE-2017-14494 for information disclosure
  • Evaluating exploitability under modern glibc and compiler mitigations
  • Writing a fully reliable exploit

Updates will follow as the research progresses.

This post is licensed under CC BY 4.0 by the author.