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.

Starting a Linux project?

Leverage Aprioritโ€™s experience in Linux development to secure yourself reliable and robust software aligned with your business vision and technical requirements!

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

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 project

Supporting and Improving Legacy Data Management Software

Find out how we helped our client improve user experience in their legacy data management system. As a result, the client’s team was able to smoothly migrate to their new platform while keeping all end users.

Project details
Supporting and Improving Legacy Data Management Software

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

Practical Comparison of the Most Popular API Hooking Libraries: Microsoft Detours, EasyHook, Nektra Deviare, and Mhook

API hooking is a proven method for tracing tasks, building sandboxes, enhancing browser security, and intercepting OS calls. Read the full article to learn the pros and cons of four popular API hooking libraries and how to use them effectively.

Learn more
Comparison of API hooking libraries

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.

Need assistance with low-level development?

Outsource tricky development tasks to Aprioritโ€™s expert Linux and C++ developers to make your solution work flawlessly!

Have a question?

Ask our expert!

Michael-Teslia
Michael Teslia

Program Manager

Tell us about
your project

...And our team will:

  • Process your request within 1-2 business days.
  • Get back to you with an offer based on your project's scope and requirements.
  • Set a call to discuss your future project in detail and finalize the offer.
  • Sign a contract with you to start working on your project.

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.