Imagine the following C code:

#include <unistd.h>
#include <string.h>

int main()
{
    const char a[] = "hello\n";
    write(1, a, sizeof(a));
}

It is simply compiled with GCC and lives as a binary executable a.out. How can we hook that libc write function in such a way that we can run our own code without the application breaking?

First lets list the symbols that glibc exports.

$ readelf -Ws /lib/libc.so.6 | grep -i write
    99: 00000000000746d0   495 FUNC    GLOBAL DEFAULT   11 _IO_wdo_write@@GLIBC_2.2.5
   168: 00000000000e7640    90 FUNC    WEAK   DEFAULT   11 __write@@GLIBC_2.2.5
   286: 000000000007a430    57 FUNC    GLOBAL DEFAULT   11 _IO_do_write@@GLIBC_2.2.5
   487: 00000000000f6340    36 FUNC    GLOBAL DEFAULT   11 process_vm_writev@@GLIBC_2.15
   489: 00000000000e5b70    96 FUNC    WEAK   DEFAULT   11 __pwrite64@@GLIBC_2.2.5
   843: 00000000000ed2d0    90 FUNC    WEAK   DEFAULT   11 writev@@GLIBC_2.2.5
  1239: 00000000000e5b70    96 FUNC    GLOBAL DEFAULT   11 __libc_pwrite@@GLIBC_PRIVATE
  1500: 00000000000ed3e0   166 FUNC    GLOBAL DEFAULT   11 pwritev@@GLIBC_2.10
  1552: 00000000000f59f0    55 FUNC    GLOBAL DEFAULT   11 eventfd_write@@GLIBC_2.7
  1567: 000000000006e4e0   494 FUNC    WEAK   DEFAULT   11 fwrite@@GLIBC_2.2.5
  1840: 00000000000ed3e0   166 FUNC    GLOBAL DEFAULT   11 pwritev64@@GLIBC_2.10
  1986: 00000000000793f0   164 FUNC    GLOBAL DEFAULT   11 _IO_file_write@@GLIBC_2.2.5
  2005: 000000000006e4e0   494 FUNC    GLOBAL DEFAULT   11 _IO_fwrite@@GLIBC_2.2.5
  2024: 00000000000e5b70    96 FUNC    WEAK   DEFAULT   11 pwrite@@GLIBC_2.2.5
  2082: 00000000000785b0   133 FUNC    GLOBAL DEFAULT   11 fwrite_unlocked@@GLIBC_2.2.5
  2091: 00000000000e5b70    96 FUNC    WEAK   DEFAULT   11 pwrite64@@GLIBC_2.2.5
  2138: 00000000000e7640    90 FUNC    WEAK   DEFAULT   11 write@@GLIBC_2.2.5

Notice how there is a write and a __write symbol? What’s all that about? Let’s look at the source code.

ssize_t
__libc_write (int fd, const void *buf, size_t nbytes)
{
    if (nbytes == 0)
        return 0;
    if (fd < 0)
    {
        __set_errno (EBADF);
        return -1;
    }
    if (buf == NULL)
    {
        __set_errno (EINVAL);
        return -1;
    }

    __set_errno (ENOSYS);
    return -1;
}
libc_hidden_def (__libc_write)
stub_warning (write)

weak_alias (__libc_write, __write)
libc_hidden_weak (__write)
weak_alias (__libc_write, write)

Notice in particular the weak_alias macros near the bottom. It wouldn’t be too much of a stretch to conclude from this that write and __write will both do the same thing; although you’re free to dig further into the code if you’d like.

Let’s put this hypothosis to the test by creating a shared library.

#include <stdio.h>
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count)
{
        printf("Writing ... <%s>\n", buf);
        return __write(fd, buf, count);
}

We can compile this as a shared library like so.

$ gcc -fPIC -shared -o libhook.so hook.c

Unix-like operating systems include an environmental variable called LD_PRELOAD which allows us t specify a shared library which will be loaded ahead of all others, including libc. Using this method we can trick the application into using the write function we wrote instead of the official libc one.

$ LD_PRELOAD=$(pwd)/libhook.so ./a.out
Writing ... <hello
>
hello

This approach may seem flakly since we are relying on some illicit knowledge of glibc’s internal workings however, the official documentation states that this is actually part of the reason that double underscore functions exist in the library.

Unfortunantly not all libraries offer duplicate symbols in this way. Is there a another way to hook library functions? That’s an answer we will answer in part 2.