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.
Contents
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:
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:
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:
orig_read was not initialized
In this situation, the problem of launch order can be solved by setting constructor priorities:
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.
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.
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.
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:
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:
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:
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.
Initialization occurs in the following sequence:
1. libtest_lib.so is initialized (this is the library that defines the test_func method)
calling init: /home/user/constructor/build-test_lib/libtest_lib.so
2. Symbols are searched for puts and _dl_find_dso_for_object
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
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
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:
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!