x86 Exploitation 101: “Integer overflow” – adding one more… aaaaaaaaaaand it’s gone

This article is about a vulnerability that is, at the same time, different and close to the previous ones. In fact, it is triggered whenever the code tries to store into a variable a value greater than the maximum that variable can handle. How can this be useful? How overflowing an integer can execute arbitrary code? Well, in fact, it can’t by itself, but it can lead to a stack/heap overflow: once there, we’ve already seen what’s possible.

The earliest paper describing this kind of vulnerability is “Basic Integer Overflows“, written by blexim and dated 2002. In this text, the author shortly describes the vulnerability concept itself and then goes on with some examples. Let’s see if I can pay him some credit by trying to report what I learnt.

Essentially, what happens when we try to store in a variable a value greater than the maximum value that variable can handle? Well, the counting continues from the minimum value it can store. Maybe showing it with an example is easier and, in order to do this, it’s important to keep in mind which are the minimum/maximum values a variable can store, according to the size and to the signedness of that very one.

  • char: -128 – 127
  • unsigned char: 0 – 255
  • int16_t: -32768 – 32767
  • uint16_t: 0 – 65535
  • int32_t: -2147483648 – 2147483647
  • uint32_t: 0 – 4294967295
  • int64_t: -9223372036854775808 – 9223372036854775807
  • uint64_t: 0 – 18446744073709551615

So, the point is that if, for example, an unsigned 16-bit variable contains the value 65534 and we try to add 5 and store the results into the same variable, this one will end up being set to 3: in fact, for a uint16_t, 65535 + 1 = 0 and that’s it.
An analogous thing can be done, for example, with a char variable: if the variable is set to 125 and we add 10, the variable will be set, at the end, to -121 for the same reason.

A very similar thing happens also when we try to store a value that is smaller than the minimum the variable can handle: in a uint32_t world, for example, -2147483645 – 40 = +2147483611.

Now let’s try to imagine what can happen if one of these overflowing integers is used as an index for an array access or to compute the size of buffer to be allocated: the vulnerability could lead to the aforementioned buffer overflows.

blexim’s paper organizes the various scenarios in three main categories:

  • widthness overflows
  • arithmetic overflows
  • signedness bugs

Vulnerabilities belonging to the first category happen when the code tries to store a value in a variable which is too small (in number of bits) to handle it: a typical situation is when a variable of a given type is casted into another one whose type is smaller than the original one. These are the situations when the following GCC warning is generated:

warning: conversion to ‘uint16_t’ from ‘uint32_t’ may alter its value [-Wconversion]

(caused by a uint32_t => uint16_t cast).

When a cast like this is done, the destination variable simply truncates the source value in such a way that it fits the space. An example will better show this concept:

  • (uint32_t)0x11223344 casted to a uint16_t variable becomes 0x3344
  • (uint32_t)0x11223344 casted to a uint8_t variable becomes 0x44
  • (uint16_t)0x1122 casted to a uint8_t variable becomes 0x22

In these days the mobile ITsec world is shaken by this Android vulnerability called “Stagefright“, which allows to execute remote code by just sending a well-forged MMS text. Well, this whole attack is based on a series of vulnerability and most of them are integer overflows (or underflows). So, for this case, I can use a real-life scenario: for example, CVE-2015-3824 “Google Stagefright ‘tx3g’ MP4 Atom Integer Overflow Remote Code Execution”. Without going too deep into details, this vulnerability allows to write into a buffer which is smaller than the amount of data to be written (a classic). The size of the dynamically allocated buffer is computed by adding to variables and, if the values of these variables were correctly chosen, an integer overflow would happen, causing a heap overflow.

The impacted code is the following (taken here from Cyanogenmod, actually):

status_t MPEG4Source::parseChunk(off64_t *offset) {

  [...]

  uint64_t chunk_size = ntohl(hdr[0]);
  uint32_t chunk_type = ntohl(hdr[1]);
  off64_t data_offset = *offset + 8;

  if (chunk_size == 1) {
    if (mDataSource->readAt(*offset + 8, &chunk_size, 8) < 8) {
      return ERROR_IO;
    }
  chunk_size = ntoh64(chunk_size);

  [...]

  switch(chunk_type) {
  [...]

  case FOURCC('t', 'x', '3', 'g'):
  {
    uint32_t type;
    const void *data;
    size_t size = 0;
    if (!mLastTrack->meta->findData(
            kKeyTextFormatData, &type, &data, &size)) {
      size = 0;
    }

    uint8_t *buffer = new (std::nothrow) uint8_t[size + chunk_size];
    if (buffer == NULL) {
      return ERROR_MALFORMED;
    }

    if (size > 0) {
      memcpy(buffer, data, size);
    }

If mLastTrack->meta->findData returns true, then size is guaranteed to be greater than 0. size and chunk_size are added together and the result is used as the parameter of the new operator. Now, chunk_size is read from a file (if some conditions are met) and it’s a uint64_t variable. Keeping in mind that the parameter of the new operator is size_t, there are main two scenarios:

  • 32b architecture: as the calculation involves operands of different types, size is promoted to uint64_t and, then, the calculation is performed. Sadly, the result is uint64_t and size_t is only 32b large: the result is truncated and an integer overflow happens
  • 64b architecture: the parameter is 64b large, but a chunk_size value too high might generate an integer overflow (which actually belongs to the following category: arithmetic overflows)

The following memcpy, then, puts the final word by writing size bytes of data into buffer, which, in case of integer overflow, is too small to hold the stuff.


In the second category there are all the attempts to store into a variable a value greater than the maximum the variable can handle (just like the scenario we saw at the beginning of the post). Usually this happens when an arithmetic operation is involved and the result is to be stored: sums and multiplications can be blamed, but also subtractions (I don’t know if it’s possible with divisions and others).

Again, the Stagefright vulnerability gives us an example belonging to this category (apart from the second scenario of the previous point). The code assigned is CVE-2015-3827 “Google Stagefright ‘covr’ MP4 Atom Integer Underflow Remote Code Execution” and, again, this vulnerability impacts the dynamic allocation of a buffer. The impacted code is:

status_t MPEG4Source::parseChunk(off64_t *offset) {

  [...]

  off64_t chunk_data_size = *offset + chunk_size - data_offset;

  [...]

  switch(chunk_type) {
  [...]

  case FOURCC('c', 'o', 'v', 'r'):
  {
    *offset += chunk_size;

    if (mFileMetaData != NULL) {
      ALOGV("chunk_data_size = %lld and data_offset = %lld",
              chunk_data_size, data_offset);
      sp<ABuffer> buffer = new ABuffer(chunk_data_size + 1);
      if (mDataSource->readAt(
          data_offset, buffer->data(), chunk_data_size) != (ssize_t)chunk_data_size) {
        return ERROR_IO;
      }

Obviously, if chunk_data_size + 1 is greater or equal to SIZE_MAX, the new operator will allocate space for 0 elements for buffer and the following call to readAt will overwrite memory locations.


The last category is represented by the signedness bugs, which usually happen when unsigned and signed integers are compared between them or, in general, when signed integers are used in comparisons. An example of this vulnerability might be represented by the following code:

#define MAX_BUF_SIZE 64 * 1024

void store_into_buffer(const void *src, int num)
{
  char global_buffer[MAX_BUF_SIZE];

  if (num > MAX_BUF_SIZE)
    return;

  memcpy(global_buffer, src, num);

  [...]
}

This code appears OK at first sight, but there’s nice vulnerability in it. In fact, num is a signed variable, but memcpy accepts an unsigned integer as parameter. When num is checked against MAX_BUF_SIZE, if num is a negative number, then it’ll pass the test: the real problem comes when this value is passed to memcpy. In fact, this signed variable will be interpreted as an unsigned one: a negative value becomes a huge positive value (let’s think to -1 that becomes, on 32b, 4294967295). This, of course, causes a stack overflow and the exploitation can follow the patterns we already saw in the previous posts.

Well, this is it, about integer overflows. I know there’s not a single exploit in this post (not even a try), but they would have been a repetition of what we already saw before. Nevertheless, it was interesting to report and analyse the “Stagefright” vulnerable code here, as it’s something really ongoing right now.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s