$ elf_files=( =uname =busybox /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libncurses.so.6.5
core file.o )
$ for f ($elf_files) readelf -h $f | grep -H --label=$f Type
/usr/bin/uname: Type: DYN (Position-Independent Executable file)
/usr/bin/busybox: Type: EXEC (Executable file)
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2: Type: DYN (Shared object file)
/lib/x86_64-linux-gnu/libc.so.6: Type: DYN (Shared object file)
/lib/x86_64-linux-gnu/libncurses.so.6.5: Type: DYN (Shared object file)
core: Type: CORE (Core file)
file.o: Type: REL (Relocatable file)
So if you're looking for ELF executables but are matching on DYN
in the output of readelf -h
, you'll have false positives such as /lib/x86_64-linux-gnu/libncurses.so.6.5
which is not meant to be executed and false negatives such as busybox
which here is statically linked but is definitely an ELF executable.
Technically, some shared libraries can be executed.
For instance the GNU libc, when executed, reports its version on stdout:
$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Debian GLIBC 2.40-3) stable release version 2.40.
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 14.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
Minimum supported kernel: 3.2.0
For bug reporting instructions, please see:
<http://www.debian.org/Bugs/>.
You'll see that it has a .interp
section. And its entry point in the ELF header is not 0.
ld-linux-x86-64.so.2
is also executable, and can also be seen as a shared lib, but has no .interp
section, it is actually the interpreter of dynamically linked executables, but can also be executed directly.
$ file --mime-type $elf_files
/usr/bin/uname: application/x-pie-executable
/usr/bin/busybox: application/x-executable
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2: application/x-sharedlib
/lib/x86_64-linux-gnu/libc.so.6: application/x-sharedlib
/lib/x86_64-linux-gnu/libncurses.so.6.5: application/x-sharedlib
core: application/x-coredump
file.o: application/x-object
Still doesn't identify ld-linux.so and libc as executable, and doesn't show they are ELF files.
$ file $elf_files
/usr/bin/uname: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=5fe9376fe34784ee78ef01f4d4d5b8c8d51fc12d, for GNU/Linux 3.2.0, stripped
/usr/bin/busybox: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=8d6280f3f338cc3c3aa826b100ddf71e5deef2b6, for GNU/Linux 3.2.0, stripped
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), static-pie linked, BuildID[sha1]=9e7570f03ee32b31ce7ab60df72e19390657a7e2, stripped
/lib/x86_64-linux-gnu/libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=522aec690df2798fac29fc4608b2c28fd5968271, for GNU/Linux 3.2.0, stripped
/lib/x86_64-linux-gnu/libncurses.so.6.5: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=a29e3368f1d02204459225efde34fbb1fdf4b795, stripped
core: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from 'sleep 1', real uid: 1000, effective uid: 1000, real gid: 1000, effective gid: 1000, execfn: '/usr/bin/sleep', platform: 'x86_64'
file.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
Better, as you see they're ELF files and that libc has an interpreter, but ld-linux.so
is still not clearly identified as executable.
$ for f ($elf_files) readelf -h $f | grep -H --label=$f Entry
/usr/bin/uname: Entry point address: 0x2730
/usr/bin/busybox: Entry point address: 0x4a3080
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2: Entry point address: 0x1bb00
/lib/x86_64-linux-gnu/libc.so.6: Entry point address: 0x29f30
/lib/x86_64-linux-gnu/libncurses.so.6.5: Entry point address: 0x0
core: Entry point address: 0x0
file.o: Entry point address: 0x0
There, the executable files and only them have an entry point other than 0x0, so it looks like a more reliable way to find ELF executables.
But readelf
may not be the best tool for that. For instance, readelf -h
will report the ELF header in every file in a ar
archive:
$ ar r binaries.a =uname =busybox
ar: creating binaries.a
$ readelf -h binaries.a | grep -e Type -e Entry
Type: DYN (Position-Independent Executable file)
Entry point address: 0x2730
Type: EXEC (Executable file)
Entry point address: 0x4a3080
So will objdump -f
:
$ objdump -f binaries.a
In archive binaries.a:
uname: file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000150:
HAS_SYMS, DYNAMIC, D_PAGED
start address 0x0000000000002730
busybox: file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000102:
EXEC_P, D_PAGED
start address 0x00000000004a3080
But at least the format makes it easier to detect archives. So you can do:
is_ELF_executable() (
set -o pipefail
LC_ALL=C objdump -f -- "${1-$REPLY}" 2> /dev/null | awk '
NR == 1 && /^In archive/ {exit}
/^start address 0x.*[^0]/ {found = 1; exit}
END {exit (!found)}'
)
Then:
if is_ELF_executable /bin/ls; then
echo Yep.
fi
By using ${1-$REPLY}
, I can also use it as a zsh qualifier function. For instance, to find the ELF executables with some execute permissions in /lib/x86_64-linux-gnu
:
$ print -rl /lib/x86_64-linux-gnu/*(*+is_ELF_executable)
/lib/x86_64-linux-gnu/cups-pk-helper-mechanism
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/libc.so.6
If I don't limit to the ones with execute permissions, I find more:
$ print -rl /lib/x86_64-linux-gnu/*(.+is_ELF_executable)
/lib/x86_64-linux-gnu/cups-pk-helper-mechanism
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/libcap.so.2.66
/lib/x86_64-linux-gnu/libc.so.6
/lib/x86_64-linux-gnu/libfreehand-0.1.so.1.0.2
/lib/x86_64-linux-gnu/libiptcdata.so.0.3.3
/lib/x86_64-linux-gnu/libpsx.so.2.66
/lib/x86_64-linux-gnu/libptytty.so.0
/lib/x86_64-linux-gnu/libQt5Core.so.5.15.13
/lib/x86_64-linux-gnu/libQt6Core.so.6.7.2
/lib/x86_64-linux-gnu/libvidstab.so.1.1
/lib/x86_64-linux-gnu/libx86.so.1
/lib/x86_64-linux-gnu/libxcb-xrm.so.0.0.0
Interestingly, not all have a .interp
section. Except for ld-linux.so
, all those that don't segfault when executed (here tested by making an executable copy).
Taking libptytty.so.0
as an example, if I build it from source with dpkg-buildpackage
, I get a 0x0 entry point, so I don't know why it's not 0 in the system's one, but in any case, that comes to show that the heuristic can still be refined any improved.
Edit. Looks like those shared libraries with an entry point that is not 0 are those that were built by older versions of GNU ld
(2.37 for that libptytty.so.0). On Debian 11, I find that all shared libraries had a non-zero entry point, so while that method appears to work on FreeBSD or recent GNU/Linux systems, it doesn't in older systems.
A method checking:
- "executable" mentioned in the ELF type
- presence of a
.interp
section
as heuristics would be more reliably, but give a false negative for the dynamic linker.
is_ELF_executable() {
LC_ALL=C readelf -hS -- "${1-$REPLY}" 2> /dev/null | perl -lne '
if ($. == 1) {
last unless /^ELF/; # rule out non-ELF including archives
} elsif (/^\s*\[ *\d+\]\s*\.interp\s/ || /^\s*Type:.*(?i:exec)/) {
$ok = 1; last;
}
END {exit !$ok}'
}