Modules

Enumerate and locate modules

The following Linux example enumerates the loaded modules, prints their attributes, and selects libc for later inspection. The enumeration API is shared by all supported platforms:

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

The platform-specific module interfaces let you parse an image from its path on disk or from its current mapping:

Operation

Result

Parse from the module’s path

The executable file as stored on disk, using its file layout

Parse from memory

The loaded image, which may contain relocations and runtime changes

Raw bytes from the module’s mapped address range

Parsing produces a LIEF binary object for analysis. Modifying that object does not automatically patch the running module. You should use the memory API when you intend to write to the process’s memory.

A raw memory dump is not necessarily a valid on-disk executable. For offline analysis, use the dump-parsing APIs documented for ELF, PE, and Mach-O.

Linux

On Linux, extends the generic interface with platform-specific helpers. In particular, it exposes the & and can parse the module directly from its path on disk or from its in-memory representation. The following snippet demonstrates these operations on the 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()) is not None and (
    __cxa_finalize := elf_on_disk.get_symbol("__cxa_finalize")
) is not None:
    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}"
            )

Windows

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)

macOS

System libraries can be located in the Dyld shared cache even when their module paths look like ordinary files. In that case, parsing from the path can fail. The example below parses the mapped library from memory and lists its exported symbols:

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}")

Android

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()) is not None and (
    __cxa_finalize := elf_on_disk.get_symbol("__cxa_finalize")
) is not None:
    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}")