POSTED BY: Dimitrios Glynos / 31.01.2020

Multiple NULL pointer dereference vulnerabilities in newlib

CENSUS ID:CENSUS-2020-0001
CVE IDs: CVE-2019-14871, CVE-2019-14872, CVE-2019-14873, CVE-2019-14874, CVE-2019-14875, CVE-2019-14876, CVE-2019-14877, CVE-2019-14878
Affected Products:newlib versions prior to 3.3.0, its derivatives (e.g. newlib-nano, picolibc and related Platform SDKs such as " GNU ARM Embedded Toolchain 64-bit Linux 8-2018-q4-major")
Class:NULL pointer dereference (CWE-476)
Discovered by:Dimitrios Glynos

During the security assessment of a firmware binary a number of NULL pointer dereference bugs were found caused by newlib-nano code. newlib-nano is a C library for use on 32-bit processors that have only a few kB of memory. It turns out that newlib-nano was part of the "GNU ARM Embedded Toolchain" that the chip manufacturer (Microchip/Atmel) delivered for application development purposes. newlib-nano inherits code from the newlib project, which is a C library intended for use on embedded systems. All NULL pointer dereference bugs identified in newlib-nano were inherited by newlib code and therefore CENSUS reported the respective vulnerabilities to the upstream project. Users of the newlib library are advised to update to version 3.3.0 and make sure to build the library sources with the newlib-reent-check-verify 'configure' option enabled.

There were three interesting points regarding these vulnerabilities. Firstly, NULL pointer dereference bugs will force the processor to read bytes or write bytes at an offset from the 0x0 memory address. It may also allow to execute code referenced through a function pointer found in these first few bytes of memory. It is very common for embedded processors to map the Device Vector Table at address 0x0 (or close to this address). Hence reading bytes from there may leak an interrupt handler address (or other setup configuration), writing bytes there may overwrite an interrupt handler, and executing a function based on a pointer found there may divert the execution of a program to an installed interrupt handler.

The second interesting point about these vulnerabilities had to do with the widespread use of newlib code. newlib is used on many "bare metal" devices that will never receive a security update. Hence, it may be the case that these devices will forever be vulnerable to these bugs. For the bugs we identified, the caller would usually have to somehow make the device enter a low memory condition and trigger the bugs (e.g. by performing a string formatting function that includes floating point numbers).

The third interesting point was that although access to the source code of the firmware and related library was available it was much easier to spot these bugs on the resulting binary, rather than say perform static analysis (or code review) to the codebase. This had to do with the fact that there were a number of preprocessor macros and function level indirections that hid the real culprits.

An example of an unsafe access pattern is shown below:

 000573d4 <__i2b>:
   573d4:       b510            push    {r4, lr}
   573d6:       460c            mov     r4, r1
   573d8:       2101            movs    r1, #1
   573da:       f7ff ff25       bl      57228 <_Balloc>
   573de:       2201            movs    r2, #1
   573e0:       6144            str     r4, [r0, #20] <-- see the unsafe access pattern?
   573e2:       6102            str     r2, [r0, #16] <-- this one is also unsafe!
   573e4:       bd10            pop     {r4, pc}

Which corresponds to:

Bigint *
i2b (struct _reent * ptr, int i)
{
  _Bigint *b;

  b = Balloc (ptr, 1);
  b->_x[0] = i;
  b->_wds = 1;
  return b;
}

Fortunately, some devices have the relevant memory pages (close to the 0x0 address) protected through memory protections so the implications of the above attacks are limited there.

Library authors are recommended to always check for NULL return values from memory allocators. When a NULL return value occurs, this must either be passed up to the caller to handle (when the API permits this), or execute a predefined "out of memory" handler which the programmer can setup.

Vulnerability Details

_REENT_CHECK macro null pointer dereference bug (CVE-2019-14871)

The REENT_CHECK macro (see newlib/libc/include/sys/reent.h) as used by REENT_CHECK_TM, REENT_CHECK_MISC, REENT_CHECK_MP and other newlib macros, does not check for memory allocation problems when the DEBUG flag is unset (as is the case in production firmware builds).

Specifically, we have:

/* Only add assert() calls if we are specified to debug.
#ifdef _REENT_CHECK_DEBUG
#include 
#define __reent_assert(x) assert(x)
#else
#define __reent_assert(x) ((void)0)
#endif
*/
#ifdef __CUSTOM_FILE_IO__
#error Custom FILE I/O and _REENT_SMALL not currently supported.
#endif
/* Generic _REENT check macro. */
#define _REENT_CHECK(var, what, type, size, init) do { \
    struct _reent *_r = (var); \
    if (_r->what == NULL) { \
        _r->what = (type)malloc(size); \
        __reent_assert(_r->what); \
        init; \
    } \
} while (0)

#define _REENT_CHECK_TM(var) \
    _REENT_CHECK(var, _localtime_buf, struct __tm *, sizeof *((var)->_localtime_buf), \
    /* nothing */)

#define _REENT_CHECK_MISC(var) \
    _REENT_CHECK(var, _misc, struct _misc_reent *, sizeof *((var)->_misc),
    _REENT_INIT_MISC(var))

#define _REENT_CHECK_MP(var) \
    _REENT_CHECK(var, _mp, struct _mprec *, sizeof *((var)->_mp), _REENT_INIT_MP(var))

These macros are used throughout the newlib code. Functions such as gmtime, localtime, strtok and others rely on REENT_CHECK to make sure that the second argument ("what") is an allocated object. If the related memory allocation fails, they do not detect this and thus proceed to unsafe access patterns (NULL pointer dereference bugs).

For example, gmtime() is implemented as:

_REENT_CHECK_TM(reent);
return gmtime_r (tim_p, (struct tm *)_REENT_TM(reent));

_REENT_TM is defined as:
#define _REENT_TM(ptr) ((ptr)->_localtime_buf)

If the allocation in REENT_CHECK fails then _localtime_buf will be NULL and will be passed to gmtime_r as the "res" parameter.

There it will lead into a NULL pointer dereference in the following operation:

res->tm_hour = (int) (rem / SECSPERHOUR);

Another example is from the _dtoa_r code:

_REENT_CHECK_MP(ptr);
if (_REENT_MP_RESULT(ptr))

#define _REENT_MP_RESULT(ptr)
((ptr)->_mp->_result)

#define _REENT_MP_RESULT_K(ptr) ((ptr)->_mp->_result_k)

where REENT_MP_RESULT accesses the _mp member of ptr in an unsafe way.

Another example of this pattern is in the _Balloc implementation of newlib/libc/stdlib/mprec.c:

_REENT_CHECK_MP(ptr);
if (_REENT_MP_FREELIST(ptr) == NULL)

where in newlib/libc/include/sys/reent.h we have:

#define _REENT_MP_FREELIST(ptr) ((ptr)->_mp->_freelist)

The same also holds true for the implementation of _Bfree as shown below:

_REENT_CHECK_MP(ptr);
if (v)
{
    v->_next = _REENT_MP_FREELIST(ptr)[v->_k];
    _REENT_MP_FREELIST(ptr)[v->_k] = v;
...

In the same file there is also a vulnerable implementation of __pow5mult:

_REENT_CHECK_MP(ptr);
if (!(p5 = _REENT_MP_P5S(ptr)))

and according to newlib/libc/include/sys/reent.h we have:

#define _REENT_MP_P5S(ptr) ((ptr)->_mp->_p5s)

_dtoa_r null pointer dereference bugs (CVE-2019-14872)

The _dtoa_r function of the "newlib" libc library performs multiple memory allocations without checking their return value.

For example, in newlib/libc/stdlib/dtoa.c:287

b = d2b (ptr, d.d, &be, &bbits);
...
b1 = mult (ptr, mhi, b); // line 644

In the code snippet above, d2b returns an allocated bigint ("big integer") created from a double number. And in the code of line 644 this big integer is used in a multiplication.

In a similar manner, a call to Balloc (which allocates a big integer) in line 426 creates a big integer that is used later in the code without being checked for a NULL value:

_REENT_MP_RESULT(ptr) = Balloc (ptr, _REENT_MP_RESULT_K(ptr));
s = s0 = (char *) _REENT_MP_RESULT(ptr);
...
*s = 0; // line 579

In line 628 i2b is used to transform an integer to a big integer however the return value is again used without checking if it's NULL:

mhi = i2b (ptr, 1);
...
mhi = pow5mult (ptr, mhi, m5); // line 643

In line 654 i2b is again used to create a big integer however the allocated integer is used without checking first if it's NULL:

S = i2b (ptr, 1);
if (s5 > 0)
    S = pow5mult (ptr, S, s5);

In line 746 we have a similar call to Balloc :

mhi = Balloc (ptr, mhi->_k);
Bcopy (mhi, mlo);

Again, the copy operation is performed without checking if Balloc failed.

In line 758 the difference between two big integers allocates a new big integer which is used without first checking for a NULL value:

delta = diff (ptr, S, mhi);
j1 = delta->_sign ? 1 : cmp (b, delta);

__multadd null pointer dereference bug (CVE-2019-14873)

In the __multadd function of the newlib libc library (see newlib/libc/stdlib/mprec.c), Balloc is used to allocate a big integer, however no check is performed to verify if the allocation succeeded or not:

b1 = Balloc (ptr, b->_k + 1);
Bcopy (b1, b);

where Bcopy is defined in newlib/libc/stdlib/mprec.h as:

#define Bcopy(x,y) memcpy((char *)&x->_sign, (char *)&y->_sign, y->_wds*sizeof(__Long) + 2*sizeof(int))

The call to Bcopy will trigger a null pointer dereference bug in case of a memory allocation failure.

__i2b null pointer dereference bug (CVE-2019-14874)

In the __i2b function of the newlib libc library (see newlib/libc/stdlib/mprec.c), Balloc is used to allocate a big integer, however no check is performed to verify if the allocation succeeded or not:

b = Balloc (ptr, 1);
b->_x[0] = i;

The access of _ x[0] will trigger a null pointer dereference bug in case of a memory allocation failure.

__multiply null pointer dereference bug (CVE-2019-14875)

In the __multiply function of the newlib libc library (see newlib/libc/stdlib/mprec.c), Balloc is used to allocate a big integer, however no check is performed to verify if the allocation succeeded or not:

b = Balloc (ptr, 1);
b->_x[0] = i;

The access of _x[0] will trigger a null pointer dereference bug in case of a memory allocation failure.

__lshift null pointer dereference bug (CVE-2019-14876)

In the __lshift function of the newlib libc library (see newlib/libc/stdlib/mprec.c), Balloc is used to allocate a big integer, however no check is performed to verify if the allocation succeeded or not:

b1 = Balloc (ptr, k1);
x1 = b1->_x;

The access to b1 will trigger a null pointer dereference bug in case of a memory allocation failure.

__mdiff null pointer dereference bugs (CVE-2019-14877)

In the __mdiff function of the newlib libc library (see newlib/libc/stdlib/mprec.c), Balloc is used to allocate big integers, however no check is performed to verify if the allocation succeeded or not:

c = Balloc (ptr, 0);
c->_wds = 1;
..
c = Balloc (ptr, a->_k);
c->_sign = i;

The access to _wds and _sign will trigger a null pointer dereference bug in case of a memory allocation failure.

__d2b null pointer dereference bug (CVE-2019-14878)

In the __d2b function of the newlib libc library (see newlib/libc/stdlib/mprec.c), Balloc is used to allocate a big integer, however no check is performed to verify if the allocation succeeded or not:

#ifdef Pack_32
    b = Balloc (ptr, 1);
#else
    b = Balloc (ptr, 2);
#endif
x = b->_x;

Accessing _x will trigger a null pointer dereference bug in case of a memory allocation failure.

Recommendation

CENSUS strongly recommends to update newlib installations to version 3.3.0 as this comes with patches that address all of the abovementioned vulnerabilities. Please note that if newlib is built with the newlib-reent-check-verify 'configure' option disabled, then it carries no protection against CVE-2019-14871.

Disclosure Timeline

newlib Project Contact:October 3rd, 2019
RedHat Assigned CVEs:November 7th, 2019
Vendor Fix Released:January 22nd, 2020
Public Advisory:January 31st, 2020