Logo
blank Skip to main content

Using the GCC Attribute Constructor with LD_PRELOAD

C++

Linux has a wide variety of tools that allow you to fully control what’s happening. One of them is LD_PRELOAD, which is an environmental variable that allows you to load any library of your choice before anything else. There are a number of LD_PRELOAD tricks that you can use to control and modify software within your environment.

At Apriorit, we specialize in cybersecurity and virtualization and often use hooks for monitoring and system management. We had one case, where we tried to install hooks using the method with the constructor attribute. But when adding hooks for the read method, we encountered a problem where the read method was called earlier than the method with the constructor attribute. As a result, the hooks weren’t installed and our application crashed.

This LD_PRELOAD example was born from our search for a solution to this problem. When searching for the solution, we conducted detailed research of the constructor attribute and how to use it. Below you will find our results.

What is the constructor attribute?

The GCC website provides a detailed description of the constructor attribute. The gist is that the constructor attribute works similarly to the destructor attribute, only they do opposite things. The constructor makes it so that a function is called automatically while the execution enters main(). The destructor makes it so that a function is called when exit() is called or when main() has finished. Both of these functions are useful for initializing data that will be used by your program.

To control the order in which constructors and destructors run, you need to provide an integer to define the priority. A destructor with a higher priority number will run before a destructor with a lower number. The opposite is true for constructors – a constructor with a lower number will run earlier.

If you need both a constructor and destructor to handle the same resource, you would usually assign them the same priority. The properties of destructors and constructors are similar to those specified for namespace-scope C++ objects.

Related services

Outsource Software Development in C/C++

How the constructor attribute works

The constructor attribute guarantees that all methods with this attribute will be called before main() but does not guarantee that the method with the attribute will be called before other methods.

Here’s a short example that illustrates the behavior of the constructor attribute:

C
ssize_t read(int fd, void *buf, size_t len)
{
    printf("read was called\n");
    if (!orig_read)
    {
        printf("orig_read was not initialized\n");
        return -1;
    }
  
    return orig_read(fd, buf, len);
}
  
static __attribute__((constructor))) void init_method2(void)
{
    printf("init_method2 was called\n");
    char sym;
    read(0, &sym, sizeof(sym));
}
  
static __attribute__((constructor)) void init_method(void)
{
    printf("init_method was called\n");
    orig_read = dlsym(RTLD_NEXT, "read");
    printf("read was initialized\n");
}

Here’s the result that you get after launching the application linked with the library from the example above:

ShellScript
init_method2 was called
read was called
orig_read was not initialized
init_method was called
read was initialized

Read also:
Driver Matching with I/O Kit: Building a USB Device Filter Driver

Setting constructor priorities

In this case, all constructors are called sequentially. When init_method2 is called by the read method (and since the init_method constructor hasn’t been called yet), orig_read is not initialized, and thus you’ll get this message:

ShellScript
orig_read was not initialized

In this situation, the problem of launch order can be solved by setting constructor priorities:

C
static __attribute__((constructor (200))) void init_method2(void)
{
    printf("init_method2 was called\n");
    char sym;
    read(0, &sym, sizeof(sym));
}
  
static __attribute__((constructor (150))) void init_method(void)
{
    printf("init_method was called\n");
    orig_read = dlsym(RTLD_NEXT, "read");
    printf("read was initialized\n");
}

First, the constructor with the lowest priority number will be called. In this case, it’s init_method. You can use numbers higher than 100 to set priorities. Constructor priorities from 0 to 100 are reserved for the implementation.

Related services

Kernel and Driver Development

Constructor priorities within several interacting libraries

The example described above is far removed from real cases that we encounter in practice, since we can clearly see all dependencies. Let’s take a look at a more realistic case where several libraries are interacting. For this, we’ll leave only one method with the constructor attribute in the first library.

C
static __attribute__((constructor)) void init_method(void)
{
    printf("init_method was called\n");
    orig_read = dlsym(RTLD_NEXT, "read");
    printf("read was initialized\n");
}

We’ll also add a hook for write to this library that will use the test_func method from another library.

C
ssize_t write(int fd, const void *buf, size_t len)
{
    printf("write was called\n");
    test_func();
    if (!orig_write)
    {
        orig_write = dlsym(RTLD_NEXT, "read");
    }
  
    return orig_write(fd, buf, len);
}

Here’s the code of the library that defines test_func:

C
void read_first_byte(int fd)
{
    printf("read_first_byte was called\n");
    const size_t size = 1;
    char buf[size];
    int res = read(fd, buf, size);
    if (res < -1)
    {
        printf("Failed to read from file\n");
        return;
    }
}
  
void test_func()
{
    printf("test_func was called\n");
}
  
static __attribute__((constructor)) void init_test_lib(void)
{
    printf("init_test_lib was called\n");
    read_first_byte(0);
}

In this library there’s a method with the constructor attribute, with the init_test_lib inside the read_first_byte method using the read call. When running the test app linked to these libraries, you’ll get the following result:

ShellScript
init_test_lib was called
read_first_byte was called
read was called
orig_read was not initialized
init_method was called
read was initialized

In order to fully understand what’s going on, you can use LD_DEBUG=all and check what the loader does:

ShellScript
   23486:   
     23486:    calling init: /home/user/constructor/build-test_lib/libtest_lib.so
     23486:   
     23486:    symbol=puts;  lookup in file=./constructor_test [0]
     23486:    symbol=puts;  lookup in file=/home/user/constructor/bin/Debug/libconstructor.so [0]
     23486:    symbol=puts;  lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
     23486:    binding file /home/user/constructor/build-test_lib/libtest_lib.so [0] to /lib/x86_64-linux-gnu/libc.so.6 [0]: normal symbol `puts' [GLIBC_2.2.5]
     23486:    symbol=_dl_find_dso_for_object;  lookup in file=./constructor_test [0]
     23486:    symbol=_dl_find_dso_for_object;  lookup in file=/home/user/constructor/bin/Debug/libconstructor.so [0]
     23486:    symbol=_dl_find_dso_for_object;  lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
     23486:    symbol=_dl_find_dso_for_object;  lookup in file=/lib/x86_64-linux-gnu/libdl.so.2 [0]
     23486:    symbol=_dl_find_dso_for_object;  lookup in file=/home/user/constructor/build-test_lib/libtest_lib.so [0]
     23486:    symbol=_dl_find_dso_for_object;  lookup in file=/lib64/ld-linux-x86-64.so.2 [0]
     23486:    binding file /lib/x86_64-linux-gnu/libc.so.6 [0] to /lib64/ld-linux-x86-64.so.2 [0]: normal symbol `_dl_find_dso_for_object' [GLIBC_PRIVATE]
init_test_lib was called
     23486:    symbol=read_first_byte;  lookup in file=./constructor_test [0]
     23486:    symbol=read_first_byte;  lookup in file=/home/user/constructor/bin/Debug/libconstructor.so [0]
     23486:    symbol=read_first_byte;  lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
     23486:    symbol=read_first_byte;  lookup in file=/lib/x86_64-linux-gnu/libdl.so.2 [0]
     23486:    symbol=read_first_byte;  lookup in file=/home/user/constructor/build-test_lib/libtest_lib.so [0]
     23486:    binding file /home/user/constructor/build-test_lib/libtest_lib.so [0] to /home/user/constructor/build-test_lib/libtest_lib.so [0]: normal symbol `read_first_byte'
read_first_byte was called
     23486:    symbol=read;  lookup in file=./constructor_test [0]
     23486:    symbol=read;  lookup in file=/home/user/constructor/bin/Debug/libconstructor.so [0]
     23486:    binding file /home/user/constructor/build-test_lib/libtest_lib.so [0] to /home/user/constructor/bin/Debug/libconstructor.so [0]: normal symbol `read' [GLIBC_2.2.5]
     23486:    symbol=puts;  lookup in file=./constructor_test [0]
     23486:    symbol=puts;  lookup in file=/home/user/constructor/bin/Debug/libconstructor.so [0]
     23486:    symbol=puts;  lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
     23486:    binding file /home/user/constructor/bin/Debug/libconstructor.so [0] to /lib/x86_64-linux-gnu/libc.so.6 [0]: normal symbol `puts' [GLIBC_2.2.5]
read was called
orig_read was not initialized
     23486:   
     23486:    calling init: /lib/x86_64-linux-gnu/libdl.so.2
     23486:   
     23486:   
     23486:    calling init: /home/user/constructor/bin/Debug/libconstructor.so
     23486:   
init_method was called
     23486:    symbol=dlsym;  lookup in file=./constructor_test [0]
     23486:    symbol=dlsym;  lookup in file=/home/user/constructor/bin/Debug/libconstructor.so [0]
     23486:    symbol=dlsym;  lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
     23486:    symbol=dlsym;  lookup in file=/lib/x86_64-linux-gnu/libdl.so.2 [0]
     23486:    binding file /home/user/constructor/bin/Debug/libconstructor.so [0] to /lib/x86_64-linux-gnu/libdl.so.2 [0]: normal symbol `dlsym' [GLIBC_2.2.5]
     23486:    symbol=_dl_sym;  lookup in file=./constructor_test [0]
     23486:    symbol=_dl_sym;  lookup in file=/home/user/constructor/bin/Debug/libconstructor.so [0]
     23486:    symbol=_dl_sym;  lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
     23486:    binding file /lib/x86_64-linux-gnu/libdl.so.2 [0] to /lib/x86_64-linux-gnu/libc.so.6 [0]: normal symbol `_dl_sym' [GLIBC_PRIVATE]
     23486:    symbol=read;  lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
     23486:    binding file /home/user/constructor/bin/Debug/libconstructor.so [0] to /lib/x86_64-linux-gnu/libc.so.6 [0]: normal symbol `read'
read was initialized
     23486:    symbol=__libc_start_main;  lookup in file=./constructor_test [0]
     23486:    symbol=__libc_start_main;  lookup in file=/home/user/constructor/bin/Debug/libconstructor.so [0]
     23486:    symbol=__libc_start_main;  lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
     23486:    binding file ./constructor_test [0] to /lib/x86_64-linux-gnu/libc.so.6 [0]: normal symbol `__libc_start_main' [GLIBC_2.2.5]
     23486:   
     23486:    initialize program: ./constructor_test
     23486:

Read also:
Hooking Linux Kernel Functions, Part 1: Looking for the Perfect Solution

Initialization occurs in the following sequence:

1. libtest_lib.so is initialized (this is the library that defines the test_func method)

ShellScript
calling init: /home/user/constructor/build-test_lib/libtest_lib.so

2. Symbols are searched for puts and _dl_find_dso_for_object

ShellScript
23486:    binding file /home/user/constructor/build-test_lib/libtest_lib.so [0] to /lib/x86_64-linux-gnu/libc.so.6 [0]: normal symbol `puts' [GLIBC_2.2.5]
23486:    binding file /lib/x86_64-linux-gnu/libc.so.6 [0] to /lib64/ld-linux-x86-64.so.2 [0]: normal symbol `_dl_find_dso_for_object' [GLIBC_PRIVATE]

3. The init_test_lib constructor is called

4. Symbols are searched for read_first_byte

ShellScript
23486:    binding file /home/user/constructor/build-test_lib/libtest_lib.so [0] to /home/user/constructor/build-test_lib/libtest_lib.so [0]: normal symbol `read_first_byte'

5. Symbols are searched for reads and puts

ShellScript
23486:    binding file /home/user/constructor/build-test_lib/libtest_lib.so [0] to /home/user/constructor/bin/Debug/libconstructor.so [0]: normal symbol `read' [GLIBC_2.2.5]
23486:    binding file /home/user/constructor/bin/Debug/libconstructor.so [0] to /lib/x86_64-linux-gnu/libc.so.6 [0]: normal symbol `puts' [GLIBC_2.2.5]

In this case, read is attached to the uninitialized library libconstructor.so

6. The read method from libconstructor.so is called

7. The /home/user/constructor/bin/Debug/libconstructor.so initialization is called

8. The init_method constructor is called

Thus, we can conclude that if you plan on reloading certain methods in the dynamic library you shouldn’t do it in a method with the constructor attribute. You can transfer the load right into the reloaded method. For example, for read you can do the following:

C
ssize_t read(int fd, void *buf, size_t len)
{
    printf("read was called\n");
    if (!orig_read)
    {
        orig_read = dlsym(RTLD_NEXT, "read");
    }
  
    return orig_read(fd, buf, len);
}

Conclusion

We hope that this article has cleared up some of your questions on how to use LD_PRELOAD with the constructor attribute. This is a powerful tool, and a single LD_PRELOAD exploit can be used to gain full control over an application, so make sure to use it responsibly. If you ever need an experienced development team with great knowledge of Linux and low-level development, you can always send us your request for proposal

Tell us about your project

Send us a request for proposal! We’ll get back to you with details and estimations.

By clicking Send you give consent to processing your data

Book an Exploratory Call

Do not have any specific task for us in mind but our skills seem interesting?

Get a quick Apriorit intro to better understand our team capabilities.

Book time slot

Contact us