Latest Tweets

Defeating GHOST on old glibc implementations

The problem

So GHOST is here. It is certainly a critical vulnerability that, on some old GNU/Linux distros, simply does not have any available patch to fix it. Of course it is always a very bad idea to keep old systems running with no available patches, but it is equally true that the opposite does not mean we are more secure, either. This particular vulnerability belongs to the past, and yet it is affecting myriads of modern GNU/Linux distributions like Debian Wheezy and the like. A classic buffer overflow vulnerability, one of those that have not been read about since the epic Smashing The Stack For Fun and Profit. And yet, here we are: year 2015, with a lot of modern protections against memory corruption issues like ASLR and PIE, DEP/NX, RELRO, SSP, and who knows what more is in store, available … and it is still feasible to take over a server by crafting a malicious hostname. Ouch.

Well, okay, so let’s patch our GNU/Linux servers and that’s it. Not quite so, I am afraid: how about those thousands of embedded systems that are still running a buggy libc version? I mean Access Points, Routers, and so on. Or what happens to those servers that for some obscure – although sometimes practical – reasons cannot be upgraded to modern GNU/Linux distros with the correspondent patch to fix this GHOST vulnerability? For the latter I do have an answer. And it is trivial: let’s make our own patch.

The solution

 No matter which version you have on your old GNU/Linux box. What we need to solve is basically two main problems:

  • Compute the correct size for the vulnerable buffer.
  • Return the right value whenever this buffer is not large enough to hold the user’s input data (ERANGE instead of EINVAL).

In order to achieve that, we need to edit two files: nss/digit_dots.c and nss/getXXbyYY_r.c inside the GNU libc6 source directory.

Patching the digits_dots.c and getXXbyYY_r.c files

The vulnerability resides right in the  __nss_hostname_digits_dots function. So we need to look for the lines where the necessary buffer length is computed. This value will be hold by the size_needed variable. On some GNU libc6 versions, there are redundant pieces of code and this computation and checking can be present twice in the nss/digits_dots.c file, as shown below:

      size_needed = (sizeof (*host_addr)
                     + sizeof (*h_addr_ptrs)
                     + sizeof (*h_alias_ptr) + strlen (name) + 1);
 
      if (buffer_size == NULL)
        {
          if (buflen < size_needed)
            {
              *status = NSS_STATUS_TRYAGAIN;
              if (h_errnop != NULL)
                *h_errnop = NETDB_INTERNAL;
              __set_errno (ERANGE);
              goto done;
            }
        }
        ...
        size_needed = (sizeof (*host_addr)
                         + sizeof (*h_addr_ptrs) 
                         + sizeof(*h_alias_ptr) + strlen (name) + 1); 
 
          if (buffer_size == NULL && buflen < size_needed)
            {   
              *status = NSS_STATUS_TRYAGAIN;
              if (h_errnop != NULL)
                *h_errnop = NETDB_INTERNAL;
              __set_errno (ERANGE);
              goto done;
            }

Basically, we compute the right buffer’s length by including the size for a char pointer with sizeof(*h_alias_ptr). On 32 bit architectures, this size is 4 bytes and on 64 bit architectures is 8 bytes. Besides, we need to return the right value whenever this size is not large enough, that is a ERANGE value. Reading up the nss/getXXbyYY_r.c file, we realize that the only way of returning the ERANGE is whenever none of these conditions meet:

  int res;
  if (status == NSS_STATUS_SUCCESS || status == NSS_STATUS_NOTFOUND)
    res = 0;
  /* Don't pass back ERANGE if this is not for a too-small buffer.  */
  else if (errno == ERANGE && status != NSS_STATUS_TRYAGAIN)
    res = EINVAL;
#ifdef NEED_H_ERRNO
  /* These functions only set errno if h_errno is NETDB_INTERNAL.  */
  else if (status == NSS_STATUS_TRYAGAIN && *h_errnop != NETDB_INTERNAL)
    res = EAGAIN;
#endif
  else
    return errno;
 
  __set_errno (res);
  return res;

According to the previous code snippet, errno will be returned “as it is” whenever none of the previous conditions meet. In our case, we need to set the status pointer to the  NSS_STATUS_TRYAGAIN value back in the nss/digit_dots.c file to achieve precisely this.

In order to break the execution path for this reentrant function whenever our buffer is not large enough, we have to add a conditional compilation directive in the nss/getXXbyYY_rc.c file as well:

#ifdef HANDLE_DIGITS_DOTS
  switch (__nss_hostname_digits_dots (name, resbuf, &buffer, NULL,
                                      buflen, result, &status, AF_VAL,
                                      H_ERRNO_VAR_P))
    {
    case -1:
      return errno;
    case 1:
#ifdef NEED_H_ERRNO
      any_service = true;
#endif
      goto done;
    }
#endif

Now it is all set. What we need to do now is to compile and install the patched library. We are now going to show how to achieve that in Debian-based,  Fedora Cores and OpenSuse boxes. The least intrusive way of fixing these systems would be by patching their own GNU libc6 implementations, rather than upgrading the whole library to the newest version available. This way, the entire process will be based on a trivial problem for most system administrators, that is: to get the sources for a particular package, change some code on it, and re-generate the new binary package for installation. All in all, an actually quick and easy step.

Patching an old Debian GNU/Linux box

First, we need to get all the GNU libc6 build dependencies and then its sources:

# apt-get build-dep libc6 && apt-get source libc6

After that, we need to decompress the entire libc6 source code and let the debian build system apply any configured patch:

# cd /path/to/the/decompressed libc6 sources

# debian/rules patch

Once the debian build system has decompressed and patched the libc6 sources, they will be placed inside the build-tree directory. Now we need to change the nss/getXXbyYY_r.c and nss/digit_dots.c files accordingly, as explained above.

After that, we have to compile and re-generate the binary packages to be installed. If we want to use more than just one processor to perform the compilation, we can export the DEB_BUILD_OPTIONS variable with the total amount of cores we want to use, i.e:

# export DEB_BUILD_OPTIONS=parallel=2

#debian/rules build

# debian/rules binary

Once the compilation process is finished, we will have some debian binary packages readily available to be installed on the same directory as where the GNU libc6 library was downloaded. All we need to do now is to install them and reboot:

# dpkg -i -R *.deb && reboot

Patching and old Fedora Core GNU/Linux box

Some old versions of the Fedora Core GNU/Linux distributions does not allow the rpmbuild command to run “short-circuited” when the -bb flag is set. Therefore, we need to alter the sources before allowing the redhat build system to decompress, build and generate the new rpm binary packages. Otherwise, any change made to the original libc6 sources will be lost as soon as a rpmbuild -bb command is issued. The first step will be to install the rpm source package for our libc6 implementation, for example by downloading it from an old fedora core mirror and then running the rpm command this way:

# rpm -iv glibc-VERSION.src.rpm

After that, we need to decompress the libc6 tarball and made the changes discussed some lines above:

# cd /usr/src/redhat/SOURCES && tar xvfj glibc-20060306T1239.tar.bz2

Once the changes are made, we need to re-tar the entire directory to allow the redhat build system to use it:

# rm glibc-20060306T1239.tar.bz2 && tar cvfj glibc-20060306T1239.tar.bz2 glibc-20060306T1239/* && rm -rf glibc-20060306T1239

Now it is all set to start building the new patched library and generate some rpm binary packages ready to install. If we want to use more than just one processor to do the job, we have to create the /root/.rpmacros file and add the total desired number of processors to use, i.e: %__make /usr/bin/make -j 4. Then, we run the compilation process this way:

# rpmbuild -bb SPECS/glibc.spec

If the compilation ends successfully, we will have the packages in /usr/src/redhat/RPMS. All we need to do now is to install them and reboot:

# cd /usr/src/redhat/RPMS && rpm -iv *.rpm && reboot

Patching and old OpenSuse GNU/Linux box

Up to a point, the same procedure as described in the previous section, can be used to patch an old OpenSuse GNU/Linux distribution. However, there are certain considerations to make:

  1. The sources are placed in the /usr/src/packages/ directory, instead of the /usr/src/redhat one.
  2. Probably OpenSuse would have a newer version for the rpmbuild command, therefore it is feasible to run every single step of the building process one-at-a-time by issuing the –short-circuit flag to the rpmbuild command.

Testing

The test C program written to check whether our GNU/Linux does hold the vulnerability still applies here. So, we need to get this program, compile it and run it. If we’ve got the “not vulnerable” string, then it means we have patched the GNU libc6 library correctly.