I would like to verify that the dynamic linker used when a program is run is the one mentioned via file
, readelf -l
, or ldd
. My motivation stems from having multiple dynamic linkers that exist in separate spaces on the machine and they should never mix-and-match.
Thus far, the best way I have found to verify the dynamic linker is via gdb
. By looking at output of info proc mappings
, I can determine which dynamic linker was mapped into the address space and is in use. I am trying to avoid using gdb
as it would require me to run test suites and other things through it.
Using the LD_DEBUG
environment variable seems like it could be an alternate solution which would allow me to easily save logs for verification after (or during) program execution. However, I am unsure which option would give me the best information. I thought scopes
or libs
could be good options but libs
doesn’t always mention the dynamic linker. For example, this is the output of a simple hello world program:
$ LD_DEBUG=libs ./test0
24579: find library=libc.so.6 [0]; searching
24579: search cache=/etc/ld.so.cache
24579: trying file=/lib/x86_64-linux-gnu/libc.so.6
24579:
24579:
24579: calling init: /lib/x86_64-linux-gnu/libc.so.6
24579:
24579:
24579: initialize program: ./test0
24579:
24579:
24579: transferring control: ./test0
24579:
hello world
24579:
24579: calling fini: ./test0 [0]
24579:
$ LD_DEBUG=libs ./test0-gnu-cross
24581: find library=libc.so.6 [0]; searching
24581: search path=/usr/local/gnu-cross/x86_64-linux-gnu/lib/glibc-hwcaps/x86-64-v4:/usr/local/gnu-cross/x86_64-linux-gnu/lib/glibc-hwcaps/x86-64-v3:/usr/local/gnu-cross/x86_64-linux-gnu/lib/glibc-hwcaps/x86-64-v2:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell/avx512_1/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell/avx512_1:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/avx512_1/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/avx512_1:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls:/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell/avx512_1/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell/avx512_1:/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell:/usr/local/gnu-cross/x86_64-linux-gnu/lib/avx512_1/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib/avx512_1:/usr/local/gnu-cross/x86_64-linux-gnu/lib/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib (RPATH from file ./test0-gnu-cross)
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/glibc-hwcaps/x86-64-v4/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/glibc-hwcaps/x86-64-v3/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/glibc-hwcaps/x86-64-v2/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell/avx512_1/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell/avx512_1/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/avx512_1/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/avx512_1/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell/avx512_1/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell/avx512_1/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/avx512_1/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/avx512_1/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/libc.so.6
24581:
24581:
24581: calling init: /usr/local/gnu-cross/x86_64-linux-gnu/lib/ld-linux-x86-64.so.2
24581:
24581:
24581: calling init: /usr/local/gnu-cross/x86_64-linux-gnu/lib/libc.so.6
24581:
24581:
24581: initialize program: ./test0-gnu-cross
24581:
24581:
24581: transferring control: ./test0-gnu-cross
24581:
hello world
24581:
24581: calling fini: ./test0-gnu-cross [0]
24581:
As you can see, the program test0
which is built with a standard Debian/GNU toolchain and uses the system’s dynamic linker doesn’t state that.
The scopes
option looks more helpful but I don’t understand what the output is saying:
$ LD_DEBUG=scopes ./test0
24577:
24577: Initial object scopes
24577: object=./test0 [0]
24577: scope 0: ./test0 /lib/x86_64-linux-gnu/libc.so.6 /lib64/ld-linux-x86-64.so.2
24577:
24577: object=linux-vdso.so.1 [0]
24577: scope 0: ./test0 /lib/x86_64-linux-gnu/libc.so.6 /lib64/ld-linux-x86-64.so.2
24577: scope 1: linux-vdso.so.1
24577:
24577: object=/lib/x86_64-linux-gnu/libc.so.6 [0]
24577: scope 0: ./test0 /lib/x86_64-linux-gnu/libc.so.6 /lib64/ld-linux-x86-64.so.2
24577:
24577: object=/lib64/ld-linux-x86-64.so.2 [0]
24577: no scope
24577:
hello world
$ LD_DEBUG=scopes ./test0-gnu-cross
24576:
24576: Initial object scopes
24576: object=./test0-gnu-cross [0]
24576: scope 0: ./test0-gnu-cross /usr/local/gnu-cross/x86_64-linux-gnu/lib/libc.so.6 /usr/local/gnu-cross/x86_64-linux-gnu/lib/ld-linux-x86-64.so.2
24576:
24576: object=linux-vdso.so.1 [0]
24576: scope 0: ./test0-gnu-cross /usr/local/gnu-cross/x86_64-linux-gnu/lib/libc.so.6 /usr/local/gnu-cross/x86_64-linux-gnu/lib/ld-linux-x86-64.so.2
24576: scope 1: linux-vdso.so.1
24576:
24576: object=/usr/local/gnu-cross/x86_64-linux-gnu/lib/libc.so.6 [0]
24576: scope 0: ./test0-gnu-cross /usr/local/gnu-cross/x86_64-linux-gnu/lib/libc.so.6 /usr/local/gnu-cross/x86_64-linux-gnu/lib/ld-linux-x86-64.so.2
24576:
24576: object=/usr/local/gnu-cross/x86_64-linux-gnu/lib/ld-linux-x86-64.so.2 [0]
24576: no scope
24576:
hello world
In summary, I would like to find a good way to verify the dynamic linker that is being used. Unless you can think of a better option, LD_DEBUG
seems like a good bet, but I struggle to understand how to use it effectively in this case.
Thank you for your help 🙂
2
Answers
You can use
LD_DEBUG=scopes
for thisSample output from my machine:
Look for the object with no scope.
Also, there are only a couple of values for LD_DEBUG, check them here and experiment.
There is no need to actually run the executable to determine the ELF interpreter that it will use.
We can use static tools and be guaranteed that we can get the full path.
We can use a combination of
readelf
andldd
.If we use
readelf -a
, we can parse the output.One part of the
readelf
output:Note the address of the
.interp
section. It is0x2e0
.If we open the executable and do a seek to that offset, we can read the ELF interpreter string. For example, here is [what I’ll call]
fileBad
:Note that the string seems a little odd … More on that later …
Under the "Program Headers:" section, we have:
Again, note the
0x2e0
file offset. This may be an easier way to get the path to the ELF interpreter.Now we have the full path to the ELF interpreter.
We can now do
ldd /path/to/executable
and we’ll get a list of the shared libraries it is/will be using. We’ll do this forfileGood
. Normally, this looks like [redacted]:That’s for a normal executable. Here’s the
ldd
output forfileBad
:Okay, to explain …
fileGood
is a standard executable [/bin/vi
on my system]. However,fileBad
is a copy that I made where I patched the interpreter path to a non-existent file.From the
readelf
data, we know the interpreter path. We can check for existence of that file. If it doesn’t exist things are [obviously] bad.With the interpreter path we got from
readelf
, we can find the output line fromldd
for the interpreter.For the good file,
ldd
gave us the simple interpreter resolution:For the bad file,
ldd
gave us:So, either
ldd
or the kernel detected the missing interpreter and substituted the default one.If we try to exec
fileBad
from the shell we get:If we try to exec
fileBad
from a C program we get anENOENT
error:From this we know that the kernel did not try to use a "default" interpreter when we did an
exec*
syscall.So, we now know that the static analysis we did to determine the ELF interpreter path is valid.
We can be assured that the path we came up with is [will be] the path to the ELF interpreter that the kernel will map into the process address space.
For further assurance, if you need to, download the kernel source code. Look in the file:
fs/binfmt_elf.c
I think that’s sufficient, but to answer the question in your top comment
There’s no need to race.
We can control the
fork
process. We can set up the child to run under [the syscall]ptrace
, so we can control its execution (Note thatptrace
is whatgdb
andstrace
use).After we
fork
, but before weexec
, the child can request that the target of theexec
sleep until a process attaches to it viaptrace
.So, the parent can examine
/proc/pid/maps
[or whatever else] before the target executable has executed a single instruction. It can control execution viaptrace
[and, eventually, detach to allow the target to run normally].Given the answer to the first part of your question, this is a bit of a moot point.
There is no way to [accurately] predict the
pid
of a process wefork
. If we could determine thepid
that the system would use next, there is no guarantee that we will win the race against another process doing afork
[before us] and "getting" thepid
we "thought" would be ours.