I want to intercept all file system access that occurs inside of dlopen(). At first, it would seem like LD_PRELOAD
or -Wl,-wrap,
would be viable solutions, but I have had trouble making them work due to some technical reasons:
ld.so has already mapped its own symbols by the time LD_PRELOAD is processed. It's not critical for me to intercept the initial loading, but the _dl_*
worker functions are resolved at this time, so future calls go through them. I think LD_PRELOAD
is too late.
Somehow malloc
circumvents the issue above because the malloc()
inside of ld.so does not have a functional free()
, it just calls memset()
.
The file system worker functions, e.g. __libc_read()
, contained in ld.so
are static so I can't intercept them with -Wl,-wrap,__libc_read
.
This might all mean that I need to build my own ld.so
directly from source instead of linking it into a wrapper. The challenge there is that both libc
and rtld-libc
are built from the same source. I know that the macro IS_IN_rtld
is defined when building rtld-libc
, but how can I guarantee that there is only one copy of static data structures while still exporting a public interface function? (This is a glibc build system question, but I haven't found documentation of these details.)
Are there any better ways to get inside dlopen()
?
Note: I can't use a Linux-specific solution like FUSE
because this is for minimal "compute-node" kernels that do not support such things.
dlopen() The function dlopen() loads the dynamic shared object (shared library) file named by the null-terminated string filename and returns an opaque "handle" for the loaded object. This handle is employed with other functions in the dlopen API, such as dlsym(3), dladdr(3), dlinfo(3), and dlclose().
it would seem like LD_PRELOAD or -Wl,-wrap, would be viable solutions
The --wrap
solution could not possibly be viable: it works only at (static) link time, and your ld.so
and libc.so.6
and libdl.so.2
have all already been linked, so now it is too late to use --wrap
.
The LD_PRELOAD
could have worked, except ... ld.so considers the fact that dlopen()
calls open()
an internal implementation detail. As such, it just calls the internal __open
function, bypassing PLT
, and your ability to interpose open
with it.
Somehow malloc circumvents the issue
That's because libc
supports users who implement their own malloc
(e.g. for debugging purposes). So the call to e.g. calloc
from dlopen
does go through PLT
, and is interposable via LD_PRELOAD
.
This might all mean that I need to build my own ld.so directly from source instead of linking it into a wrapper.
What will the rebuilt ld.so
do? I think you want it to call __libc_open
(in libc.so.6
), but that can't possibly work for obvious reason: it is ld.so
that open
s libc.so.6
in the first place (at process startup).
You could rebuild ld.so
with the call to __open
replaced with a call to open
. That will cause ld.so
to go through PLT
, and expose it to LD_PRELOAD
interposition.
If you go that route, I suggest that you don't overwrite the system ld.so
with your new copy (the chance of making a mistake and rendering the system unbootable is just too great). Instead, install it to e.g. /usr/local/my-ld.so
, and then link your binaries with -Wl,--dynamic-linker=/usr/local/my-ld.so
.
Another alternative: runtime patching. This is a bit of a hack, but you can (once you gain control in main) simply scan the .text
of ld.so
, and look for CALL __open
instructions. If ld.so
is not stripped, then you can find both the internal __open
, and the functions you want to patch (e.g. open_verify
in dl-load.c
). Once you find the interesting CALL
, mprotect
the page that contains it to be writable, and patch in the address of your own interposer (which can in turn call __libc_open
if it needs to), then mprotect
it back. Any future dlopen()
will now go through your interposer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With