The following snippet shows how to iterate over the modules loaded in the current process and how to access common attributes such as the imagebase or the name:
for mod in lief.runtime.modules():
print(mod)
# Look for the libc
if mod.name.startswith("libc.so"):
print(
f"libc.so found: {mod.path} ([{mod.imagebase:#010x}, {mod.end:#010x}])"
)
assert isinstance(mod, lief.runtime.linux.Module)
libc = mod
for (const LIEF::runtime::Module& mod : LIEF::runtime::modules()) {
info("{}", mod.to_string());
// Look for the libc
if (mod.name().ends_with("libc.so")) {
info("libc.so found: {} ([{}, {}])", mod.path(), hex(mod.imagebase()),
hex(mod.end()));
libc.reset(mod.clone().release()->as<LIEF::runtime::Linux::Module>());
}
}
for module in runtime::modules() {
println!(
"{} ({:#010x} - {:#010x})",
module.name(),
module.imagebase(),
module.end()
);
if let runtime::Modules::Linux(linux_mod) = module {
if linux_mod.name().starts_with("libc.so") {
println!(
"libc.so found: {} ([{:#010x}, {:#010x}])",
linux_mod.path(),
linux_mod.imagebase(),
linux_mod.end()
);
libc = Some(linux_mod);
}
}
}
libc module:print(f"libc path: {libc.path}")
if __cxa_finalize := libc.dlsym("__cxa_finalize"):
__cxa_finalize_addr = lief.to_int(__cxa_finalize)
print(f"{libc.name}!__cxa_finalize: {__cxa_finalize_addr:#010x}")
if malloc := libc.dlsym("malloc"):
malloc_addr = lief.to_int(malloc)
# lief.to_int is used to convert an opaque pointer (void*) into a
# regular Python int
print(f"{libc.name}!malloc: {malloc_addr:#010x}")
# With LIEF extended, we can disassemble the function from its
# absolute memory address:
if lief.__extended__:
for inst in islice(lief.runtime.disassemble(malloc_addr), 4):
print(" ", inst)
# This parses the ELF from its path on the disk
if elf_on_disk := libc.parse_from_path():
if __cxa_finalize := elf_on_disk.get_symbol("__cxa_finalize"):
print(f"__cxa_finalize: {__cxa_finalize.value:#010x}")
# This code loads 'librt.so' and wrap the dlopen handler in a Module.
if librt := lief.runtime.linux.dlopen("librt.so.1"):
print(
f"librt loaded: {librt.path} (dlopen handler={lief.to_int(librt.handle):#010x})"
)
# Parse the ELF directly from memory.
if librt_mem := librt.parse_from_memory():
# This code looks for the relocation associated with the
# **imported** symbol `__cxa_finalize` that is defined in the libc.
# The relocation holds the address where the function is resolved.
# Therefore, by reading this relocation address we can access the
# resolved address of `__cxa_finalize`. This address should match
# the value previously returned by dlsym
__cxa_finalize_reloc = next(
(
reloc
for reloc in librt_mem.relocations
if reloc.symbol is not None
and reloc.symbol.name == "__cxa_finalize"
),
None,
)
if __cxa_finalize_reloc is not None:
abs_reloc_addr = librt.imagebase + __cxa_finalize_reloc.address
# We assume a 64-bit architecture
__cxa_finalize_addr = lief.runtime.Memory.read_u64(abs_reloc_addr)
print(
f"{librt.name} -> {abs_reloc_addr:#010x} -> {__cxa_finalize_addr:#010x}"
)
info("libc path: {}", libc->path());
if (void* cxa_finalize = libc->dlsym("__cxa_finalize")) {
info("{}!__cxa_finalize: {}", libc->name(),
hex(reinterpret_cast<uintptr_t>(cxa_finalize)));
}
if (void* malloc_fn = libc->dlsym("malloc")) {
auto malloc_ptr = reinterpret_cast<uintptr_t>(malloc_fn);
info("{}!malloc: {}", libc->name(),
hex(reinterpret_cast<uintptr_t>(malloc_fn)));
auto malloc_instructions = LIEF::runtime::disassemble(malloc_ptr);
// Dump the first instructions of malloc
for (const LIEF::assembly::Instruction& inst :
malloc_instructions | std::views::take(4))
{
info(" {}", inst.to_string());
}
}
// This parses the ELF from its path on the disk
if (std::unique_ptr<LIEF::ELF::Binary> elf_on_disk = libc->parse_from_path()) {
if (const LIEF::Symbol* sym = elf_on_disk->get_symbol("__cxa_finalize")) {
info("__cxa_finalize: {}", hex(sym->value()));
}
}
// This code loads 'librt.so' and wraps the dlopen handle in a Module.
if (std::unique_ptr<LIEF::runtime::Linux::Module> librt =
LIEF::runtime::Linux::dlopen("librt.so.1"))
{
info("librt loaded: {} (dlopen handle={})", librt->path(),
hex(reinterpret_cast<uintptr_t>(librt->handle())));
// Alternately you could do this:
{
void* handle = dlopen("librt.so.1", RTLD_NOW);
auto librt_alt = LIEF::runtime::Linux::Module::from_handle(handle);
}
// Parse the ELF directly from memory.
if (std::unique_ptr<LIEF::ELF::Binary> librt_mem = librt->parse_from_memory())
{
// Look for the relocation associated with the **imported** symbol
// `__cxa_finalize` that is defined in the libc. The relocation holds
// the address where the function is resolved. Reading this address
// gives back the same value previously returned by dlsym.
for (const auto& reloc : librt_mem->relocations()) {
const auto* sym = reloc.symbol();
if (sym == nullptr || sym->name() != "__cxa_finalize") {
continue;
}
uintptr_t abs_reloc_addr = librt->imagebase() + reloc.address();
auto resolved = LIEF::runtime::Memory::read<uint64_t>(abs_reloc_addr);
info("{} -> {} -> {}", librt->name(), hex(abs_reloc_addr), hex(resolved));
break;
}
}
}
println!("libc path: {}", libc.path());
let cxa_finalize = libc.dlsym("__cxa_finalize".to_string());
if !cxa_finalize.is_null() {
println!(
"{}!__cxa_finalize: {:#010x}",
libc.name(),
cxa_finalize as usize
);
}
let malloc = libc.dlsym("malloc".to_string());
if !malloc.is_null() {
println!("{}!malloc: {:#010x}", libc.name(), malloc as usize);
}
// Parse the ELF from its path on the disk
if let Some(elf_on_disk) = libc.parse_from_path() {
for sym in elf_on_disk.dynamic_symbols() {
if sym.name() == "__cxa_finalize" {
println!("__cxa_finalize: {:#010x}", sym.value());
break;
}
}
}
// Load 'librt.so' and wrap the dlopen handle in a Module.
if let Some(librt) = runtime::linux::dlopen("librt.so.1") {
println!(
"librt loaded: {} (dlopen handle={:#010x})",
librt.path(),
librt.handle() as usize
);
// Parse the ELF directly from memory.
if let Some(librt_mem) = librt.parse_from_memory() {
// Find the relocation for the imported symbol `__cxa_finalize`
// defined in libc. Reading the relocation address returns the
// resolved address, which should match what dlsym returned.
for reloc in librt_mem.relocations() {
let sym = match reloc.symbol() {
Some(s) => s,
None => continue,
};
if sym.name() != "__cxa_finalize" {
continue;
}
let abs_reloc_addr = librt.imagebase() + reloc.address();
// Assume a 64-bit architecture
let resolved: u64 = unsafe { runtime::Memory::read(abs_reloc_addr) };
println!(
"{} -> {:#010x} -> {:#010x}",
librt.name(),
abs_reloc_addr,
resolved
);
break;
}
}
}
On Windows, you can perform similar operations such as in-memory parsing, accessing the HMODULE handle, and resolving functions through a dlsym-like helper:
print(f"ntdll path: {ntdll.path}")
if nt_query := ntdll.dlsym("NtQueryInformationProcess"):
# lief.to_int converts an opaque pointer (void*) into a regular Python int
nt_query_addr = lief.to_int(nt_query)
print(f"{ntdll.name}!NtQueryInformationProcess: {nt_query_addr:#010x}")
# This parses the PE from its path on the disk
if pe_on_disk := ntdll.parse_from_path():
print(f"on-disk imagebase: {pe_on_disk.optional_header.imagebase:#010x}")
if sym := pe_on_disk.get_symbol("NtQueryInformationProcess"):
print(f"NtQueryInformationProcess: {sym.value:#010x}")
# This parses the PE directly from memory
if pe_memory := ntdll.parse_from_memory():
imagebase = pe_memory.optional_header.imagebase
print(f"in-memory imagebase: {imagebase:#010x}")
if sym := pe_memory.get_symbol("RtlFreeHeap"):
print(f"RtlFreeHeap: {sym.value:#010x}")
# With LIEF extended, we can disassemble the function directly from
# its absolute memory address:
if lief.__extended__:
for inst in islice(lief.runtime.disassemble(imagebase + sym.value), 4):
print(" ", inst)
info("ntdll path: {}", ntdll->path());
if (void* _NtQueryInformationProcess = ntdll->dlsym("NtQueryInformationProcess"))
{
info("{}!NtQueryInformationProcess: {}", ntdll->name(),
hex(reinterpret_cast<uintptr_t>(_NtQueryInformationProcess)));
}
if (std::unique_ptr<LIEF::PE::Binary> pe_on_disk = ntdll->parse_from_path()) {
info("on-disk imagebase: {}", hex(pe_on_disk->optional_header().imagebase()));
if (const LIEF::Symbol* sym =
pe_on_disk->get_symbol("NtQueryInformationProcess"))
{
info("NtQueryInformationProcess: {}", hex(sym->value()));
}
}
if (std::unique_ptr<LIEF::PE::Binary> pe_memory = ntdll->parse_from_memory()) {
const uintptr_t imagebase = pe_memory->optional_header().imagebase();
info("in-memory imagebase: {}", hex(imagebase));
if (const LIEF::Symbol* sym = pe_memory->get_symbol("RtlFreeHeap")) {
info("RtlFreeHeap: {}", hex(sym->value()));
// Disassemble RtlFreeHeap from memory
auto instructions = LIEF::runtime::disassemble(imagebase + sym->value());
auto first_instructions = instructions | std::views::take(4);
for (const LIEF::assembly::Instruction& inst : first_instructions) {
info(" {}", inst.to_string());
}
}
}
println!("ntdll path: {}", ntdll.path());
let nt_query = ntdll.dlsym("NtQueryInformationProcess".to_string());
if !nt_query.is_null() {
println!(
"{}!NtQueryInformationProcess: {:#010x}",
ntdll.name(),
nt_query as usize
);
}
// Parse the PE from its path on the disk
if let Some(pe_on_disk) = ntdll.parse_from_path() {
println!(
"on-disk imagebase: {:#010x}",
pe_on_disk.optional_header().imagebase()
);
if let Some(export) = pe_on_disk.export() {
if let Some(entry) = export.entry_by_name("NtQueryInformationProcess") {
println!("NtQueryInformationProcess: {:#010x}", entry.address());
}
}
}
// Parse the PE directly from memory
if let Some(pe_memory) = ntdll.parse_from_memory() {
let imagebase = pe_memory.optional_header().imagebase();
println!("in-memory imagebase: {:#010x}", imagebase);
if let Some(export) = pe_memory.export() {
if let Some(entry) = export.entry_by_name("RtlFreeHeap") {
let rva = entry.address() as u64;
println!("RtlFreeHeap: {:#010x}", rva);
// Disassemble the first instructions of RtlFreeHeap from memory
for inst in runtime::disassemble(imagebase + rva).take(4) {
println!(" {}", inst.to_string());
}
}
}
}
print(f"libsystem_c path: {libsystem.path}")
if arc4_init := libsystem.dlsym("arc4_init"):
# lief.to_int converts an opaque pointer (void*) into a regular Python int
arc4_init_addr = lief.to_int(arc4_init)
print(f"{libsystem.name}!arc4_init: {arc4_init_addr:#010x}")
# With LIEF extended, we can disassemble the function directly from its
# absolute memory address:
if lief.__extended__:
for inst in islice(lief.runtime.disassemble(arc4_init_addr), 4):
print(" ", inst)
# libsystem.path looks like a valid path but libsystem_c.dylib lives in the
# dyld-shared-cache. Therefore, it's pointless to try to parse the library
# from its filepath as the library does not exist on the disk.
macho_on_disk: lief.MachO.Binary | None = None
with lief.logging.level_scope(lief.logging.LEVEL.OFF):
macho_on_disk = libsystem.parse_from_path()
if macho_on_disk is None:
print(f"As expected, {libsystem.path} is not present in the filesystem")
# But we can parse it from memory:
if macho_memory := libsystem.parse_from_memory():
print(f"in-memory imagebase: {macho_memory.imagebase:#010x}")
# List 'exported' symbols
for sym in macho_memory.exported_symbols:
print(f"{sym.name:10}: {sym.value:#010x}")
info("libsystem_c path: {}", libsystem->path());
if (void* p_arc4_init = libsystem->dlsym("arc4_init")) {
info("{}!arc4_init: {}", libsystem->name(),
hex(reinterpret_cast<uintptr_t>(p_arc4_init)));
// Disassemble malloc directly from its resolved memory address
auto instructions =
LIEF::runtime::disassemble(reinterpret_cast<uintptr_t>(p_arc4_init));
for (const LIEF::assembly::Instruction& inst :
instructions | std::views::take(4))
{
info(" {}", inst.to_string());
}
}
// libsystem->path() looks like a valid path but libsystem_c.dylib lives
// in the dyld-shared-cache. Therefore, it's pointless to try to parse the
// library from its filepath as the library does not exist on the disk
std::unique_ptr<LIEF::MachO::Binary> macho_on_disk;
{
LIEF::logging::Scoped disable_logger(LIEF::logging::LEVEL::OFF);
macho_on_disk = libsystem->parse_from_path();
}
if (macho_on_disk == nullptr) {
info("As expected, {} is not present in the filesystem", libsystem->path());
}
// But we can parse it from memory:
if (std::unique_ptr<LIEF::MachO::Binary> macho_memory =
libsystem->parse_from_memory())
{
info("in-memory imagebase: {}", hex(macho_memory->imagebase()));
for (const LIEF::MachO::Symbol& S : macho_memory->exported_symbols()) {
info(std::format("{:10}: {:#010x}", S.name(), S.value()));
}
}
println!("libsystem_c path: {}", libsystem.path());
let arc4_init = libsystem.dlsym("arc4_init".to_string());
if !arc4_init.is_null() {
println!(
"{}!arc4_init: {:#010x}",
libsystem.name(),
arc4_init as usize
);
// Disassemble arc4_init directly from its resolved memory address
for inst in runtime::disassemble(arc4_init as u64).take(4) {
println!(" {}", inst.to_string());
}
}
// libsystem.path() looks like a valid path but libsystem_c.dylib lives in
// the dyld-shared-cache. Therefore, it's pointless to try to parse the
// library from its filepath as the library does not exist on the disk.
let macho_on_disk = {
let _scoped = lief::logging::Scoped::new(lief::logging::Level::OFF);
libsystem.parse_from_path()
};
if macho_on_disk.is_none() {
println!(
"As expected, {} is not present in the filesystem",
libsystem.path()
);
}
// But we can parse it from memory:
if let Some(macho_memory) = libsystem.parse_from_memory() {
println!("in-memory imagebase: {:#010x}", macho_memory.imagebase());
// List 'exported' symbols
for sym in macho_memory.exported_symbols() {
println!("{:10}: {:#010x}", sym.name(), sym.value());
}
}
libc and liblog modules:print(f"libc path: {libc.path}")
if __cxa_finalize := libc.dlsym("__cxa_finalize"):
__cxa_finalize_addr = lief.to_int(__cxa_finalize)
print(f"{libc.name}!__cxa_finalize: {__cxa_finalize_addr:#010x}")
if malloc := libc.dlsym("malloc"):
malloc_addr = lief.to_int(malloc)
print(f"{libc.name}!malloc: {malloc_addr:#010x}")
# With LIEF extended, we can disassemble the function from its
# absolute memory address:
if lief.__extended__:
for inst in islice(lief.runtime.disassemble(malloc_addr), 4):
print(" ", inst)
if elf_on_disk := libc.parse_from_path():
if __cxa_finalize := elf_on_disk.get_symbol("__cxa_finalize"):
print(f"__cxa_finalize: {__cxa_finalize.value:#010x}")
if liblog := lief.runtime.android.dlopen("liblog.so"):
print(
f"liblog loaded: {liblog.path} "
f"(dlopen handler={lief.to_int(liblog.handle):#010x})"
)
if log_print := liblog.dlsym("__android_log_print"):
log_print_addr = lief.to_int(log_print)
print(f"{liblog.name}!__android_log_print: {log_print_addr:#010x}")
info("libc path: {}", libc->path());
if (void* cxa_finalize = libc->dlsym("__cxa_finalize")) {
info("{}!__cxa_finalize: {}", libc->name(),
hex(reinterpret_cast<uintptr_t>(cxa_finalize)));
}
if (void* malloc_fn = libc->dlsym("malloc")) {
auto malloc_ptr = reinterpret_cast<uintptr_t>(malloc_fn);
info("{}!malloc: {}", libc->name(), hex(malloc_ptr));
auto malloc_instructions = LIEF::runtime::disassemble(malloc_ptr);
// Dump the first instructions of malloc
for (const LIEF::assembly::Instruction& inst :
malloc_instructions | std::views::take(4))
{
info(" {}", inst.to_string());
}
}
if (std::unique_ptr<LIEF::ELF::Binary> elf_on_disk = libc->parse_from_path()) {
if (const LIEF::Symbol* sym = elf_on_disk->get_symbol("__cxa_finalize")) {
info("__cxa_finalize: {}", hex(sym->value()));
}
}
if (std::unique_ptr<LIEF::runtime::android::Module> liblog =
LIEF::runtime::android::dlopen("liblog.so"))
{
info("liblog loaded: {} (dlopen handle={})", liblog->path(),
hex(reinterpret_cast<uintptr_t>(liblog->handle())));
if (void* log_print = liblog->dlsym("__android_log_print")) {
info("{}!__android_log_print: {}", liblog->name(),
hex(reinterpret_cast<uintptr_t>(log_print)));
}
}
println!("libc path: {}", libc.path());
let cxa_finalize = libc.dlsym("__cxa_finalize".to_string());
if !cxa_finalize.is_null() {
println!(
"{}!__cxa_finalize: {:#010x}",
libc.name(),
cxa_finalize as usize
);
}
let malloc = libc.dlsym("malloc".to_string());
if !malloc.is_null() {
println!("{}!malloc: {:#010x}", libc.name(), malloc as usize);
}
// Parse the ELF from its path on the disk
if let Some(elf_on_disk) = libc.parse_from_path() {
for sym in elf_on_disk.dynamic_symbols() {
if sym.name() == "__cxa_finalize" {
println!("__cxa_finalize: {:#010x}", sym.value());
break;
}
}
}
if let Some(liblog) = runtime::android::dlopen("liblog.so") {
println!(
"liblog loaded: {} (dlopen handle={:#010x})",
liblog.path(),
liblog.handle() as usize
);
let log_print = liblog.dlsym("__android_log_print".to_string());
if !log_print.is_null() {
println!(
"{}!__android_log_print: {:#010x}",
liblog.name(),
log_print as usize
);
}
}