Talos Vulnerability Report

TALOS-2020-1019

GNU glibc ARMv7 memcpy() memory corruption vulnerability

May 21, 2020
CVE Number

CVE-2020-6096

Summary

An exploitable signed comparison vulnerability exists in the ARMv7 memcpy() implementation of GNU glibc. Calling memcpy() (on ARMv7 targets that utilize the GNU glibc implementation) with a negative value for the 'num' parameter results in a signed comparison vulnerability.

If an attacker underflows the 'num' parameter to memcpy(), this vulnerability could lead to undefined behavior such as writing to out-of-bounds memory and potentially remote code execution. Furthermore, this memcpy() implementation allows for program execution to continue in scenarios where a segmentation fault or crash should have occurred. The dangers occur in that subsequent execution and iterations of this code will be executed with this corrupted data.

Tested Versions

GNU glibc 2.30.9000

Product URLs

https://www.gnu.org/software/libc/

CVSSv3 Score

8.1 - CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-195 - Signed to Unsigned Conversion Error

Details

The GNU C Library project provides the core libraries for the GNU system and GNU/Linux systems, as well as many other systems that use Linux as the kernel. These libraries provide critical APIs including ISO C11, POSIX.1-2008, BSD, OS-specific APIs and more. These APIs include such foundational facilities as open, read, write, malloc, printf, getaddrinfo, dlopen, pthread_create, crypt, login, exit and more.

If an attacker underflows the num parameter to memcpy(), this vulnerability could lead to undefined behavior such as writing to out-of-bounds memory and potentially remote code execution. Furthermore, this memcpy() implementation allows for program execution to continue in scenarios where a segmentation fault or crash should have occurred. The dangers occur in that subsequent execution and iterations of this code will be executed with this corrupted data.

By definition, memcpy() takes three parameters. The third parameter, num, is a size_t data type that contains the number of bytes to copy (http://man7.org/linux/man-pages/man3/memcpy.3.html). By definition, size_t data types are treated as unsigned integers. The implementation of memcpy() for ARMv7 targets does not properly handle the size_t data type for the num parameter. Instead of treating size_t data types as unsigned, the ARMv7 memcpy() implementation utilizes signed branch and arithmetic operations while operating on the num parameter.

269 mov     r12, r0
270 cmp     r2, #64
271 bge     0x405ffcb4

At the beginning of the ARMv7 memcpy() implementation, the function parameter num (number of bytes to copy) is compared with 64. If 64 or more bytes need to be copied, then program execution will branch, otherwise, execution will continue downward to copy under 64 bytes before returning.

By definition, the CMP instruction will subtract the value of the operand (64) from the register value (parameter num, or register r2) to generate the appropriate condition codes. If num contains a negative value, the CMP instruction will result in a negative result, thus setting the negative condition code. The before and after condition codes are shown below.

      n z c v
(Before)  0 0 1 0
(After)   1 0 1 0

Because bge is a signed branch (signed greater than or equal), it will not branch if the n condition code is set. As a result, when presented with a negative value for num, the ARMv7 memcpy() function will not take this branch and will continue downward to copy less than 64 bytes.

To exhibit the differences between the memcpy() implementation on ARMv7 versus other platforms and the vulnerability, a small test program was written. This program attempts to copy a total of 0xfffffd37 (+4294966583 or -713 in unsigned and signed decimal form respectively) bytes to a location in memory.

#include <string.h>
#include <stdio.h>

struct linebuffer {
    int bufsz;
    int nl_pos;
    int len;
    char buf[2048];
};

void do_memcpy(void)
{
    struct linebuffer lb;
    memset(&lb, 0, sizeof(lb));
    memset(lb.buf, 'A', 2048);
    lb.len = 0xfffffd37;

    memcpy(lb.buf, lb.buf + 1024, lb.len);
}

int main(int argc, char **argv)
{
    puts("before memcpy");
    do_memcpy();
    puts("after memcpy");
}

When run on x86 platforms, this program segmentation faults, because the num argument to memcpy(), 0xfffffd37, is interpreted properly as a size_t value (which is an unsigned integer).

When run on ARMv7 hosts, memcpy() does not treat the num parameter as a size_t value, but rather as a signed integer. The num value of 0xfffffd37 is interpreted as -713 which means only 55 bytes will be copied. Once copied, this program will successfully complete and the program will exit.

Mitigation

By the memcpy() definition, the number of bytes to copy (num) is expected to be an unsigned integer. To account for this, signed branch operations should not be performed on the num parameter at any point during the memcpy() implementation. Instead of bge, the unsigned greater than or equal comparison, bhs/bcs, should be used. This would ensure that the num parameter is treated as unsigned.

Several instances were found throughout the ARMv7 memcpy() implementation (located in sysdeps/arm/armv7/multiarch/memcpy_impl.S) of signed branches being used in place of their unsigned branch equivalents. These instances are chronicled below.

Line 271:   bge .Lcpy_not_short
Line 354:   blt .Ltail63aligned
Line 357:   bge .Lcpy_body_long
Line 381:   bge 1b
Line 415:   bge 1b
Line 485:   blt 2f
Line 497:   bge 1b

Timeline

2020-03-02 - Vendor Disclosure

2020-05-21 - Public Release

Credit

Discovered by Jason Royes and Samuel Dytrych of Cisco Security Assessment and Penetration Team.