elf

Read Linux/Unix executables and shared libraries (ELF) without running them: architecture, type, entry point, sections and segments with entropy and permissions, the libraries it needs, imported and exported symbols, the program interpreter, and suspicious indicators (writable+executable memory, UPX section names, ptrace/dlopen imports, odd interpreters). Pure Python; works on bytes in the playground.

import elf

Example

import binary
import elf

# A tiny 64-bit Linux program, built byte by byte: the ELF header, one
# program header (a loadable, runnable segment) and ten bytes of code.
let data = binary.concat(
  b"\x7fELF",
  binary.pack("<BBBB", 2, 1, 1, 0),
  binary.zeros(8),
  binary.pack("<HHIQQQIHHHHHH", 2, 62, 1, 0x401000, 64, 0, 0, 64, 56, 1, 64, 0, 0),
  binary.pack("<IIQQQQQQ", 1, 5, 0, 0x400000, 0x400000, 130, 130, 0x1000),
  b"\x48\x31\xff\xb8\x3c\x00\x00\x00\x0f\x05",
)

let info = elf.parse_bytes(data)
say(f"{info.format} {info.type} for {info.architecture}, entry {info.entry_hex}")
for seg in info.segments {
  say(f"  segment {seg.type}: {seg.file_size} bytes, {seg.flags.join("+")}")
}
for hit in elf.indicators(info) {
  say(f"  [{hit.level}] {hit.detail}")
}

Functions

parse(path)

returns map

Everything elf knows about a file. Fails if the file isn't ELF.

Fields

  • "format" elf32 or elf64.
  • "architecture" the CPU type, like "x64" or "arm64".
  • "machine" the CPU's longer name, like "x86-64".
  • "type" executable, shared_object, relocatable or core.
  • "os_abi", "endian"
  • "is_pie", "is_static", "is_stripped" true or false.
  • "entry_point" the address where the program starts.
  • "entry_hex" the same address in hex, like "0x401000".
  • "entry_section" the section the entry point is in.
  • "interpreter" the loader the program asks for.
  • "needed" the libraries it needs.
  • "soname", "rpath"
  • "sections" a list of maps with name, type, flags, address, offset, size, entropy, writable and executable.
  • "segments", "imports", "exports"
  • "version_needs" the symbol versions it needs per library, like {"libc.so.6": ["GLIBC_2.34"]}.
  • "version_definitions" the versions a library defines for others.
  • "symbol_versions" dynamic symbol -> version, like {"puts": "GLIBC_2.2.5"}.
  • "hardening" the checksec report: relro (full, partial or none), bind_now, nx, pie, canary, fortify, rpath and runpath.
  • "entropy", "size"
  • "truncated" true if the file is cut off.
  • "indicators" a list of {level, name, detail} findings.
Visualizehow elf.parse works

elf.parse("demo") gives {"format": "elf64", "bits": 64, "endian": "little", "architecture": "x64", "machine": "x8…

elf.parse: inputs flowing into the function and the resultpathdemo7f 45 4c 46 (\x7fELF) …elf.parseresult · everything elf knows (some keys shown)format"elf64"architecture"x64"type"executable"interpreter"/lib64/ld-linux-x86-64.so.2"needed["libc.so.6", "libssl.so.3"]imports["connect", "execve", "puts"]is_strippedfalse

Real result: worked out by running this example with TB while the site was built.

parse_bytes(data)

returns map

Like elf.parse() but for bytes already in memory.

Visualizehow elf.parse_bytes works

elf.parse_bytes(bin) gives {"format": "elf64", "bits": 64, "endian": "little", "architecture": "x64", "machine": "x8…

elf.parse_bytes: inputs flowing into the function and the resultdatabytes in memory7f 45 4c 46 …elf.parse_bytesresult · the same map as parse()format"elf64"endian"little"type"executable"entry_hex"0x400140"

Real result: worked out by running this example with TB while the site was built.

is_elf(path)

returns bool

True if the file starts with the ELF magic bytes.

Visualizehow elf.is_elf works

elf.is_elf("demo") gives true

elf.is_elf: inputs flowing into the function and the resultpathdemoelf.is_elfresult · booltruepathnotes.txtelf.is_elfresult · boolfalse

Real result: worked out by running this example with TB while the site was built.

sections(path)

returns list

Just the sections: name, type, address, size, entropy and flags.

Visualizehow elf.sections works

elf.sections("demo").filter(s => s.name != "") gives [{"name": ".interp", "type": "progbits", "flags": ["alloc"], "address": 4194592, "offset"…

elf.sections: inputs flowing into the function and the resultpathdemoelf.sectionsresult · .text, .rodata, .dynsym ...namesizeentropy.interp283.941.text1250.335.rodata323.311.data642.dynstr433.83… 5 more rows (10 in all)

Real result: worked out by running this example with TB while the site was built.

segments(path)

returns list

The program headers (LOAD, INTERP, DYNAMIC...) with permissions.

Visualizehow elf.segments works

elf.segments("demo") gives [{"type": "load", "flags": ["read", "write", "exec"], "offset": 0, "address": 4194304, "f…

elf.segments: inputs flowing into the function and the resultpathdemoelf.segmentsresult · what gets loaded, with permissionstypefile_sizeload1712interp28dynamic112gnu_stack0

Real result: worked out by running this example with TB while the site was built.

imports(path)

returns list

Undefined dynamic symbols: the functions it gets from shared libraries.

Visualizehow elf.imports works

elf.imports("demo") gives ["connect", "execve", "puts"]

elf.imports: inputs flowing into the function and the resultpathdemoelf.importsresult · functions it gets from libraries"connect""execve""puts"

Real result: worked out by running this example with TB while the site was built.

exports(path)

returns list

Global symbols the file defines for others (what a .so offers).

Visualizehow elf.exports works

elf.exports("libdemo.so") gives ["plugin_init", "plugin_run"]

elf.exports: inputs flowing into the function and the resultpathlibdemo.soelf.exportsresult · what the library offers"plugin_init""plugin_run"pathdemoelf.exportsresult · what the library offers[] (empty list)

Real result: worked out by running this example with TB while the site was built.

needed(path)

returns list

The shared libraries it needs (DT_NEEDED), like ["libc.so.6"].

Visualizehow elf.needed works

elf.needed("demo") gives ["libc.so.6", "libssl.so.3"]

elf.needed: inputs flowing into the function and the resultpathdemoelf.neededresult · shared libraries it needs (DT_NEEDED)"libc.so.6""libssl.so.3"

Real result: worked out by running this example with TB while the site was built.

indicators(path_or_info)

returns list

Suspicious indicators for a file (or for the map elf.parse() gives back): [{"level": high/medium/low, "name", "detail"}].

Visualizehow elf.indicators works

elf.indicators("demo") gives [{"level": "high", "name": "writable_executable_segment", "detail": "A LOAD segment at 0x…

elf.indicators: inputs flowing into the function and the resultpath_or_infodemochecksW+X segments? packed? static?network or exec imports?elf.indicatorsresult · suspicious things, most serious firstlevelnamehighwritable_executable_…lowimports_network

Real result: worked out by running this example with TB while the site was built.

summary(path)

returns text

A short description, like "elf64 x64 executable (PIE), 29 sections, needs 1 library, 12 imports, stripped".

Visualizehow elf.summary works

elf.summary("demo") gives "elf64 x64 executable, 11 sections, needs 2 libraries, 3 imports, not stripped, 1 high-ri…

elf.summary: inputs flowing into the function and the resultpathdemoelf.summaryresult · one line for a reportelf64 x64 executable, 11 sections, needs 2libraries, 3 imports, not stripped, 1high-risk indicator

Real result: worked out by running this example with TB while the site was built.