import lief
# Using filepath
elf: lief.ELF.Binary | None = lief.ELF.parse("/bin/ls")
# Using a Path from pathlib
elf: lief.ELF.Binary | None = lief.ELF.parse(pathlib.Path(r"C:\Users\test.elf"))
# Using a io object
with open("/bin/ssh", "rb") as f:
elf: lief.ELF.Binary | None = lief.ELF.parse(f)
#include <LIEF/ELF.hpp>
// Using a file path as a std::string
std::unique_ptr<LIEF::ELF::Binary> elf = LIEF::ELF::Parser::parse("/bin/ls");
// Using a vector
std::vector<uint8_t> my_raw_elf;
elf = LIEF::ELF::Parser::parse(my_raw_elf);
let elf: lief::elf::Binary = lief::elf::Binary::parse("/bin/ls").unwrap();
Note
In Python, you can also use lief.parse(), which returns a lief.ELF.Binary object.
elf: lief.ELF.Binary
print(elf.header.entrypoint)
for section in elf.sections:
print(section.name, len(section.content))
std::unique_ptr<LIEF::ELF::Binary> elf;
std::cout << elf->header().entrypoint();
for (const LIEF::ELF::Section& section : elf->sections()) {
std::cout << section.name() << section.content().size() << '\n';
}
let elf: &lief::elf::Binary = some_elf;
println!("{}", elf.header().entrypoint());
for section in elf.sections() {
println!("{} {}", section.name(), section.content().len());
}
elf: lief.ELF.Binary
elf.add_library("libdemo.so")
elf.write("new.elf")
std::unique_ptr<LIEF::ELF::Binary> elf;
elf->add_library("libdemo.so");
elf->write("new.elf");
let elf: &mut lief::elf::Binary = some_elf;
elf.add_library("libdemo.so");
elf.write("new.elf");
See also
Note
elf: lief.ELF.Binary
new_elf: bytes = elf.write_to_bytes()
std::unique_ptr<LIEF::ELF::Binary> elf;
std::ostringstream os;
elf->write(os);
std::string buffer = os.str();
const auto* start = reinterpret_cast<const uint8_t*>(buffer.data());
size_t size = buffer.size();
let elf: &mut lief::elf::Binary = some_elf;
let bytes: Vec<u8> = elf.write_to_bytes();
The ELF format uses two tables to represent different slices of the binary:
The sections table
The segments table
While the sections table offers a detailed view of the binary, it is primarily used by the compiler and the linker. In particular, this table is not required for loading and executing an ELF file. While the Android loader enforces the presence of a sections table and requires specific sections, this table is not used during the actual loading process.
elf: lief.ELF.Binary
segment = lief.ELF.Segment()
segment.type = lief.ELF.Segment.TYPES.LOAD
segment.content = list(b"Hello World")
new_segment = elf.add(segment)
elf.write("new.elf")
std::unique_ptr<LIEF::ELF::Binary> elf;
LIEF::ELF::Segment segment;
segment.type(LIEF::ELF::Segment::TYPE::LOAD);
segment.content({1, 2, 3});
LIEF::ELF::Segment* new_segment = elf->add(segment);
elf->write("new.elf");
let elf: &mut lief::elf::Binary = some_elf;
let mut segment = lief::elf::Segment::new();
segment.set_type(lief::elf::segment::Type::LOAD);
segment.set_content(&[1, 2, 3]);
elf.add_segment(&segment);
elf.write("new.elf");
PT_LOAD segment:elf: lief.ELF.Binary
section = lief.ELF.Section(".lief_demo")
section.content = list(b"Hello World")
new_section = elf.add(section, loaded=True)
elf.write("new.elf")
std::unique_ptr<LIEF::ELF::Binary> elf;
LIEF::ELF::Section section(".lief_demo");
section.content({1, 2, 3});
LIEF::ELF::Section* new_section = elf->add(section, /*loaded=*/true);
elf->write("new.elf");
let elf: &mut lief::elf::Binary = some_elf;
let section = lief::elf::Section::new_with_content(".lief_demo", &[1, 2, 3]);
elf.add_section(
§ion,
/* loaded= */ true,
lief::elf::binary::SecInsertPos::AUTO,
);
elf.write("new.elf");
As mentioned above, the segments table is more critical than the sections table from a loading perspective. Therefore, it is more appropriate to explicitly add a new segment rather than adding a section that implicitly adds a segment.
On the other hand, for debugging purposes or specialized tools, you might want to add a non-loaded section. In this case, the section data is inserted at the end of the binary, immediately after the data wrapped by the segments:
elf: lief.ELF.Binary
section = lief.ELF.Section(".metadata")
section.content = list(b"version: 1.2.3")
# /!\ Note that loaded is set to False here
# ------------------------------------------
new_section = elf.add(section, loaded=False)
elf.write("new.elf")
std::unique_ptr<LIEF::ELF::Binary> elf;
LIEF::ELF::Section section(".metadata");
section.content({1, 2, 3});
LIEF::ELF::Section* new_section = elf->add(section, /*loaded=*/false);
elf->write("new.elf");
let elf: &mut lief::elf::Binary = some_elf;
let section = lief::elf::Section::new_with_content(".metadata", b"version: 1.2.3");
// /!\ Note that loaded is set to false here
// -----------------------------------------
elf.add_section(
§ion,
/* loaded= */ false,
lief::elf::binary::SecInsertPos::AUTO,
);
elf.write("new.elf");
# 0x7f9b98e00000 is the (absolute) address at which the dump was mapped
elf = lief.ELF.parse_from_dump("module.dump", 0x7F9B98E00000)
assert elf is not None
for segment in elf.segments:
print(segment.type, hex(segment.virtual_address))
auto elf = LIEF::ELF::Parser::parse_from_dump("module.dump", 0x7f9b98e00000);
for (const LIEF::ELF::Segment& segment : elf->segments()) {
std::cout << to_string(segment.type()) << '\n';
}
let elf = lief::elf::Binary::parse_from_dump("module.dump", 0x7f9b_98e0_0000).unwrap();
for segment in elf.segments() {
println!("{:?} {:#x}", segment.p_type(), segment.virtual_address());
}
Note
The second parameter must be the (absolute) virtual address at which the dump was mapped. It is used to convert the virtual addresses found in the ELF structures back into an offset within the dump.
Warning
Parsing an ELF from a dump is subject to the same limitations as parsing it from memory: the segments (PT_LOAD) and the program headers are reliable, but the section header table is generally not mapped and the content of the dynamic table (dynamic entries, dynamic symbols and relocations) reflects its runtime state and may not be recoverable. Prefer working with the segments when analyzing a dump.
# Find the module to dump in the current process
mod = lief.runtime.module_from_name("libc.so.6")
assert mod is not None
# Dump the module's memory into a file (the raw bytes are also returned)
data: bytes = mod.dump("module.dump")
# and parse it back using the same imagebase:
elf = lief.ELF.parse_from_dump(data, mod.imagebase)
// Find the module to dump in the current process
auto mod = LIEF::runtime::module_from_name("libc.so.6");
// Dump the module's memory into a file (the raw bytes are also returned)
std::vector<uint8_t> data = mod->dump("module.dump");
auto elf = LIEF::ELF::Parser::parse_from_dump("module.dump", mod->imagebase());
use lief::runtime::Module;
let module = lief::runtime::module_from_name("libc.so.6").unwrap();
// Dump the module's memory into a file (the raw bytes are also returned)
let data = module.dump_to_file("module.dump");
let elf = lief::elf::Binary::parse_from_dump("module.dump", module.imagebase()).unwrap();
Warning
parser_config = lief.ELF.ParserConfig()
parser_config.parse_overlay = False
elf = lief.ELF.parse("my.elf", parser_config)
assert isinstance(elf, lief.ELF.Binary)
builder_config = lief.ELF.Builder.config_t()
builder_config.gnu_hash = False
elf.write("new.elf", builder_config)
LIEF::ELF::ParserConfig parser_config;
parser_config.parse_overlay = false;
auto elf = LIEF::ELF::Parser::parse("my.elf", parser_config);
LIEF::ELF::Builder::config_t builder_config;
builder_config.gnu_hash = false;
elf->write("new.elf", builder_config);
let mut parser_config = lief::elf::ParserConfig::default();
parser_config.parse_overlay = false;
let mut elf = lief::elf::parse_with_config("my.elf", &parser_config).unwrap();
let mut builder_config = lief::elf::builder::Config::default();
builder_config.gnu_hash = false;
elf.write_with_config("new.elf", builder_config);
Note that this support is only available in the Extended version of LIEF.
LIEF provides comprehensive facilities for manipulating a binary’s RPATH/RUNPATH.
DT_RPATH vs DT_RUNPATH
DT_RPATH and DT_RUNPATH are both dynamic tags used to specify runtime library search paths.
DT_RPATH is now considered legacy because it does not respect the precedence of the LD_LIBRARY_PATH environment variable. This means that if LD_LIBRARY_PATH is set to a valid directory where the library can be found, it will be ignored in favor of the DT_RPATH value. Therefore, the DT_RUNPATH tag should be preferred over DT_RPATH.
Note that if both tags are present, the loader will use the DT_RUNPATH entry over the legacy DT_RPATH.
DT_RPATH tag is represented by the interface, and the DT_RUNPATH tag by .The RPATH/RUNPATH modifications supported by LIEF include:
Adding a new entry
elf: lief.ELF.Binary
runpath = lief.ELF.DynamicEntryRunPath("$ORIGIN:/opt/lib64")
elf.add(runpath)
other_runpath = lief.ELF.DynamicEntryRunPath(["$ORIGIN", "/opt/lib64"])
elf.add(other_runpath)
elf.write("updated.elf")
std::unique_ptr<LIEF::ELF::Binary> elf;
LIEF::ELF::DynamicEntryRunPath runpath("$ORIGIN:/opt/lib64");
elf->add(runpath);
LIEF::ELF::DynamicEntryRunPath other_runpath(
std::vector<std::string>{"$ORIGIN", "/opt/lib64"}
);
elf->add(other_runpath);
elf->write("updated.elf");
let elf: &mut lief::elf::Binary = some_elf;
let runpath = lief::elf::dynamic::RunPath::new("$ORIGIN:/opt/lib64");
elf.add_dynamic_entry(&runpath);
let other_runpath = lief::elf::dynamic::RunPath::with_paths(&["$ORIGIN", "/opt/lib64"]);
elf.add_dynamic_entry(&other_runpath);
let output = PathBuf::from("updated.elf");
elf.write(output.as_path());
Changing an entry
elf: lief.ELF.Binary
runpath = elf.get(lief.ELF.DynamicEntry.TAG.RUNPATH)
assert runpath is not None
runpath.runpath = "$ORIGIN:/opt/lib64"
runpath.append("lib-x86_64-gnu")
elf.write("updated.elf")
std::unique_ptr<LIEF::ELF::Binary> elf;
auto* runpath = elf->get(LIEF::ELF::DynamicEntry::TAG::RUNPATH)
->cast<LIEF::ELF::DynamicEntryRunPath>();
assert(runpath != nullptr);
runpath->runpath("$ORIGIN:/opt/lib64");
runpath->append("lib-x86_64-gnu");
elf->write("updated.elf");
let elf: &mut lief::elf::Binary = some_elf;
if let Some(dynamic::Entries::RunPath(mut runpath)) =
elf.dynamic_entry_by_tag(dynamic::Tag::RUNPATH)
{
runpath.set_runpath("$ORIGIN:/opt/lib64");
runpath.append("lib-x86_64-gnu");
}
let output = PathBuf::from("updated.elf");
elf.write(output.as_path());
Removing entries
elf: lief.ELF.Binary
# Remove **all** DT_RUNPATH entries
elf.remove(lief.ELF.DynamicEntry.TAG.RUNPATH)
# Remove all entries that contain '$ORIGIN'
to_remove: list[lief.ELF.DynamicEntryRunPath] = []
for dt_entry in elf.dynamic_entries:
if not isinstance(dt_entry, lief.ELF.DynamicEntryRunPath):
continue
if "$ORIGIN" in dt_entry.runpath:
to_remove.append(dt_entry)
for entry in to_remove:
elf.remove(entry)
elf.write("updated.elf")
std::unique_ptr<LIEF::ELF::Binary> elf;
// Remove **all** DT_RUNPATH entries
elf->remove(LIEF::ELF::DynamicEntry::TAG::RUNPATH);
// Remove all entries that contain '$ORIGIN'
std::vector<LIEF::ELF::DynamicEntryRunPath*> to_remove;
for (DynamicEntry& entry : elf->dynamic_entries()) {
if (auto* dt_entry = entry.cast<LIEF::ELF::DynamicEntryRunPath>()) {
if (dt_entry->runpath().find("$ORIGIN") != std::string::npos) {
to_remove.push_back(dt_entry);
}
}
}
for (LIEF::ELF::DynamicEntryRunPath* entry : to_remove) {
elf->remove(*entry);
}
elf->write("updated.elf");
let elf: &mut lief::elf::Binary = some_elf;
// Remove **all** DT_RUNPATH entries
elf.remove_dynamic_entries_by_tag(dynamic::Tag::RUNPATH);
// Remove all entries that contain '$ORIGIN'
elf.remove_dynamic_entry_if(|e| {
if let dynamic::Entries::RunPath(runpath) = e {
return runpath.runpath().contains("$ORIGIN");
}
false
});
let output = PathBuf::from("updated.elf");
elf.write(output.as_path());
You can also check the lief-patchelf section for a command-line interface.
The ELF format supports symbol versioning, allowing multiple versions of the same function or variable to coexist within a single shared object.
During compilation, the linker selects the appropriate symbols and versions based on the libraries provided as input. For example, if a program uses the printf function and is linked with a version of libc.so that exposes printf@@GLIBC_2.40, the compiled executable will require at least that version of the libc to run.
These versioning requirements can be problematic when creating executables or libraries intended for a wide range of Linux distributions.
The best way to ensure maximum compatibility is to target the minimum supported version of Glibc. For instance, if you aim to support Linux distributions with at least Glibc version 2.28 (released in 2018), you should specifically provide that version of libc.so during linking:
$ ld --sysroot=/sysroot/glibc-2.28/ my_program.o -o my_program.elf
$ ld -L /sysroot/glibc-2.28/lib64/ my_program.o -o my_program.elf -lc
In situations where you lack control over the link step, you may want to change the versioning post-compilation. LIEF can be used in these situations to perform the following modifications on symbol versions.
Remove the version for a specific symbol
In this example, we remove the version attached to the printf symbol by setting the versioning as global (the default setting for imported functions).
elf: lief.ELF.Binary
sym = elf.get_dynamic_symbol("printf")
assert sym is not None and sym.symbol_version is not None
sym.symbol_version.as_global()
elf.write("updated.elf")
std::unique_ptr<LIEF::ELF::Binary> elf;
LIEF::ELF::Symbol* sym = elf->get_dynamic_symbol("printf");
assert(sym != nullptr);
sym->symbol_version()->as_global();
elf->write("updated.elf");
let elf: &mut lief::elf::Binary = some_elf;
if let Some(sym) = elf.dynamic_symbol_by_name("printf")
&& let Some(mut symver) = sym.symbol_version()
{
symver.as_global();
}
let output = PathBuf::from("updated.elf");
elf.write(output.as_path());
Remove all the versions for a specific library
In this example, we remove all the symbol versions associated with an imported library (libm.so.6):
elf: lief.ELF.Binary
elf.remove_version_requirement("libm.so.6")
elf.write("updated.elf")
std::unique_ptr<LIEF::ELF::Binary> elf;
elf->remove_version_requirement("libm.so.6");
elf->write("updated.elf");
let elf: &mut lief::elf::Binary = some_elf;
elf.remove_version_requirement("libm.so.6");
let output = PathBuf::from("updated.elf");
elf.write(output.as_path());
$ readelf -V input.elf
Version symbols section '.gnu.version' contains 48 entries:
Addr: 00000000000009bc Offset: 0x0009bc Link: 6 (.dynsym)
000: 0 (*local*) 2 (GLIBC_2.2.5) 3 (GLIBC_2.2.5) 2 (GLIBC_2.2.5)
004: 2 (GLIBC_2.2.5) 0 (*local*) 4 (GLIBC_2.17) 3 (GLIBC_2.2.5)
008: 2 (GLIBC_2.2.5) 5 (GLIBC_2.27) 2 (GLIBC_2.2.5) 3 (GLIBC_2.2.5)
00c: 3 (GLIBC_2.2.5) 2 (GLIBC_2.2.5) 6 (GLIBC_2.4) 2 (GLIBC_2.2.5)
010: 3 (GLIBC_2.2.5) 2 (GLIBC_2.2.5) 3 (GLIBC_2.2.5) 3 (GLIBC_2.2.5)
014: 2 (GLIBC_2.2.5) 3 (GLIBC_2.2.5) 3 (GLIBC_2.2.5) 0 (*local*)
018: 3 (GLIBC_2.2.5) 3 (GLIBC_2.2.5) 2 (GLIBC_2.2.5) 3 (GLIBC_2.2.5)
01c: 3 (GLIBC_2.2.5) 3 (GLIBC_2.2.5) 2 (GLIBC_2.2.5) 2 (GLIBC_2.2.5)
020: 2 (GLIBC_2.2.5) 3 (GLIBC_2.2.5) 2 (GLIBC_2.2.5) 2 (GLIBC_2.2.5)
024: 3 (GLIBC_2.2.5) 3 (GLIBC_2.2.5) 2 (GLIBC_2.2.5) 3 (GLIBC_2.2.5)
028: 3 (GLIBC_2.2.5) 0 (*local*) 3 (GLIBC_2.2.5) 3 (GLIBC_2.2.5)
02c: 3 (GLIBC_2.2.5) 7 (GLIBC_2.29) 2 (GLIBC_2.2.5) 2 (GLIBC_2.2.5)
Version needs section '.gnu.version_r' contains 2 entries:
Addr: 0000000000000a20 Offset: 0x000a20 Link: 7 (.dynstr)
0x0000: Version: 1 File: libm.so.6 Cnt: 3
0x0010: Name: GLIBC_2.29 Flags: none Version: 7
0x0020: Name: GLIBC_2.27 Flags: none Version: 5
0x0030: Name: GLIBC_2.2.5 Flags: none Version: 3
0x0040: Version: 1 File: libc.so.6 Cnt: 3
0x0050: Name: GLIBC_2.4 Flags: none Version: 6
0x0060: Name: GLIBC_2.17 Flags: none Version: 4
0x0070: Name: GLIBC_2.2.5 Flags: none Version: 2
$ readelf -V updated.elf
Version symbols section '.gnu.version' contains 48 entries:
Addr: 00000000000009bc Offset: 0x0009bc Link: 6 (.dynsym)
000: 0 (*local*) 1 (*global*) 1 (*global*) 1 (*global*)
004: 1 (*global*) 0 (*local*) 4 (GLIBC_2.17) 1 (*global*)
008: 1 (*global*) 1 (*global*) 1 (*global*) 1 (*global*)
00c: 1 (*global*) 1 (*global*) 6 (GLIBC_2.4) 1 (*global*)
010: 1 (*global*) 1 (*global*) 1 (*global*) 1 (*global*)
014: 1 (*global*) 1 (*global*) 1 (*global*) 0 (*local*)
018: 1 (*global*) 1 (*global*) 1 (*global*) 1 (*global*)
01c: 1 (*global*) 1 (*global*) 1 (*global*) 1 (*global*)
020: 1 (*global*) 1 (*global*) 1 (*global*) 1 (*global*)
024: 1 (*global*) 1 (*global*) 1 (*global*) 1 (*global*)
028: 1 (*global*) 0 (*local*) 1 (*global*) 1 (*global*)
02c: 1 (*global*) 1 (*global*) 1 (*global*) 1 (*global*)
Version needs section '.gnu.version_r' contains 1 entries:
Addr: 0000000000000a20 Offset: 0x000a20 Link: 7 (.dynstr)
0x0000: Version: 1 File: libc.so.6 Cnt: 3
0x0010: Name: GLIBC_2.4 Flags: none Version: 6
0x0020: Name: GLIBC_2.17 Flags: none Version: 4
0x0030: Name: GLIBC_2.2.5 Flags: none Version: 2