Bug 206551 - Heap overflow in iconv kernel module
Summary: Heap overflow in iconv kernel module
Status: Closed Not A Bug
Alias: None
Product: Base System
Classification: Unclassified
Component: kern (show other bugs)
Version: CURRENT
Hardware: Any Any
: --- Affects Only Me
Assignee: freebsd-bugs mailing list
URL:
Keywords: needs-patch, needs-qa, security
Depends on:
Blocks:
 
Reported: 2016-01-24 00:13 UTC by CTurt
Modified: 2016-01-24 22:57 UTC (History)
5 users (show)

See Also:
op: mfc-stable10?
koobs: mfc-stable9?


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description CTurt 2016-01-24 00:13:59 UTC
After loading the iconv kernel module, a new sysctl name, "kern.iconv.add" is accessible for `root` user.

This is handled with the following code:

    static int
    iconv_sysctl_add(SYSCTL_HANDLER_ARGS)
    {
        ...
        if (din.ia_datalen > ICONV_CSMAXDATALEN)
            return EINVAL;
        ...
        if (din.ia_datalen) {
            csp->cp_data = malloc(din.ia_datalen, M_ICONVDATA, M_WAITOK);
            error = copyin(din.ia_data, csp->cp_data, din.ia_datalen);
            if (error)
                goto bad;
        }
        ...
    }

Since the `ia_datalen` member of `struct iconv_add_in` is signed:

    struct iconv_add_in {
    	int	ia_version;
    	char	ia_converter[ICONV_CNVNMAXLEN];
    	char	ia_to[ICONV_CSNMAXLEN];
    	char	ia_from[ICONV_CSNMAXLEN];
    	int	ia_datalen;
    	const void *ia_data;
    };

The check on user supplied `din.ia_datalen` is insufficient. If a negative value is passed, it will bypass the check, and then wraparound to a huge value when converted to an `unsigned` type to be passed to `malloc` and `copyin`. For example, passing `-1` will bypass the check, and then attempt to allocate and copy to a buffer of size `0xffffffff`.

The impact of this is low, since it is only triggerable as `root`, and could at most result in DoS.

The solution is to replace this check:

    if (din.ia_datalen > ICONV_CSMAXDATALEN)

With this:

    if (din.ia_datalen > ICONV_CSMAXDATALEN || din.ia_datalen < 0)
Comment 1 CTurt 2016-01-24 00:39:01 UTC
Sorry, made a mistake in my report.
 
Supplying a size of `-1` actually passes `0xffffffffffffffff` to `malloc` and `copyin`.
 
However, since `malloc` truncates sizes to 32bit (at least version 9.0 did), this will result in an allocation of `0xffffffff`, but copy length of `0xffffffffffffffff`.

If there is enough memory for this allocation to succeed, a heap overflow will occur.
Comment 2 CTurt 2016-01-24 09:39:29 UTC
It's worth noting that the minimum size which can be passed for a signed 32bit integer is `-0x7fffffff`, which wraps around to `0xffffffff80000001`.

If on FreeBSD 9, when this size goes through `malloc` it will eventually be passed down to `uma_large_malloc`, which treats size as `vm_size_t`, a typedef for a 32bit unsigned integer, this means the size will truncate to `0x80000001` (just over 2GB).

An allocation of 2GB is much more likely to succeed. And once it has succeeded, `copyin` will attempt to copy `0xffffffff80000001` bytes from userland into this allocation, which will clearly result in a heap overflow.

The size of this heap overflow could be controlled by unmapping the page after the userland mapping, resulting in the function returning `EFAULT` once it has reached the end of the userland mapping. With a heap overflow of controllable size and contents, this bug shouldn't be difficult to exploit. I've demonstrated a similar exploit for PS4 kernel using `kevent` for heap layout manipulation primitives.

Fortunately, for later versions of FreeBSD, the inner calls of `malloc` correctly handle `size` as 64bit types, which means the worst that can happen is the thread locking up whilst trying to allocate `0xffffffff80000001` bytes (because `M_WAITOK` is passed).
Comment 3 CTurt 2016-01-24 10:34:02 UTC
In the disassembly of `libiconv.so`, the check is performed on an `unsigned int` for some reason:

    unsigned int v24;
    ...
    && v24 <= 0x41000

I'm not sure why this is, considering the type of `ia_data` is `int`, which should imply `signed` by default.

However, this means that it's not actually triggerable; `EINVAL` is returned for an `ia_data` of `-1`. I've tested on FreeBSD 9.0, and 10.2
Comment 4 Jilles Tjoelker freebsd_committer 2016-01-24 22:57:00 UTC
The explanation why there is no triggerable problem is that the ICONV_CSMAXDATALEN expression is of type size_t, an unsigned type. Then, comparing an int and a size_t, the int is converted to size_t, converting negative values to very large positive values.

This is fragile (changing ICONV_CSMAXDATALEN to a plain number like 266240 will make it vulnerable) and causes compiler warnings with -Wsign-compare, but is not an immediate bug.