Assembler


Introduction

In addition to regular file formats modifications, we might want to patch code with custom assembly code. This functionality is available thanks to the function:
import lief

elf = lief.ELF.parse("/bin/hello")

syscall_addresses = [
  inst.address for inst in elf.disassemble(0x400090) if inst.is_syscall
]

for syscall_addr in syscall_addresses:
    elf.assemble(syscall_addr, """
    mov x1, x0;
    str x1, [x2, #8];
    """)

Warning

The assembler is working decently for AArch64/ARM64E and x86/x86-64 but the support is highly limited for the other architectures.

Technical Details

In the same way that the disassembler is based on the LLVM MC layer, this assembler is also based on this component of LLVM.

The assembly text is consumed by the llvm::MCAsmParser object, and we intercept the raw generated assembly bytes from the llvm::MCObjectWriter.

We also resolve llvm::MCFixup for a vast majority of the generated fixups. One important feature that has been introduced in LIEF 0.17.0 is the support for resolving symbols or label on the fly.

Contextual Assembly Patching

Given an assembly code and an address to patch, we might want to use a context that is used to resolve symbols referenced in the assembly listing.

For instance, let’s consider the following patching:

import lief

elf = lief.ELF.parse("/bin/ssh")

elf.assemble(elf.entrypoint, """
  mov rdi, rax;
  call a_custom_function
""")

In this example, a_custom_function is not defined so the assembler engine does not know how to resolve it and raises this error:

warning: Fixup not resolved:
    call a_custom_function
LIEF exposes a interface that can be used to configure the engine and to dynamically resolve symbols used in the assembly listing:
import lief

class MyConfig(lief.assembly.AssemblerConfig):
    def __init__(self):
        super().__init__() # Important!

    @override
    def resolve_symbol(self, name: str) -> int | None:
        if name == "a_custom_function":
            return 0x1000
        return None

elf = lief.ELF.parse("/bin/ssh")

elf.assemble(elf.entrypoint, """
  mov rdi, rax;
  call a_custom_function
""", MyConfig())
This interface can be used to wrap a context which can be, for instance, a generic :
import lief

class MyConfig(lief.assembly.AssemblerConfig):
    def __init__(self, target: lief.Binary):
        super().__init__() # Important!

        self._target = target

    @override
    def resolve_symbol(self, name: str) -> int | None:
        addr = self._target.get_function_address(name)
        if isinstance(addr, lief.lief_errors):
            return None
        return addr

elf = lief.ELF.parse("/bin/ssh")

config = MyConfig(elf)

elf.assemble(elf.entrypoint, """
  mov rdi, rax;
  call a_custom_function
""", config)
The Rust bindings do not offer the same flexibility to capture the . Nevertheless, the closure associated with the lief::assembly::AssemblerConfig::symbol_resolver can capture most of its context:
let mut elf = lief::elf::Binary::parse("/bin/ssh");

let mut config = lief::assembly::AssemblerConfig::default();

let mut sym_map = HashMap::new();

for sym in ls.exported_symbols() {
    sym_map.insert(sym.name(), sym.value());
}

let resolver = Arc::new(move |symbol: &str| {
    sym_map.get(symbol).copied()
});

config.symbol_resolver = Some(resolver);

elf.assemble(addr, r#"
  mov rdi, rax;
  call a_custom_function;
"#, &config);

Python API

C++ API

Rust API