elf: lief.ELF.Binary
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];
""",
)
std::unique_ptr<LIEF::ELF::Binary> elf;
std::vector<uint64_t> syscall_addresses;
for (const auto& inst : elf->disassemble(0x400090)) {
if (inst.is_syscall()) {
syscall_addresses.push_back(inst.address());
}
}
for (uint64_t addr : syscall_addresses) {
elf->assemble(addr, R"asm(
mov x1, x0;
str x1, [x2, #8];
)asm");
}
let elf: &mut lief::elf::Binary = some_elf;
let syscall_addresses: Vec<u64> = elf
.disassemble_address(0x400090)
.filter(|inst| inst.is_syscall())
.map(|inst| inst.address())
.collect();
for addr in syscall_addresses {
elf.assemble(
addr,
r#"
mov x1, x0;
str x1, [x2, #8];
"#,
);
}
Warning
The assembler works well for AArch64/ARM64E, x86/x86-64, and RISC-V but support for other architectures is currently limited.
Similar to the disassembler, this assembler is based on the LLVM MC layer.
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. An important feature introduced in LIEF 0.17.0 is support for resolving symbols or labels on the fly.
Given assembly code and a target address, we might want to use a context to resolve symbols referenced in the assembly listing.
For example, consider the following patch:
elf: lief.ELF.Binary
elf.assemble(
elf.entrypoint,
"""
mov rdi, rax;
call a_custom_function;
""",
)
std::unique_ptr<LIEF::ELF::Binary> elf;
elf->assemble(elf->entrypoint(), R"asm(
mov rdi, rax;
call a_custom_function;
)asm");
let elf: &mut lief::elf::Binary = some_elf;
elf.assemble(
elf.entrypoint(),
r#"
mov rdi, rax;
call a_custom_function;
"#,
);
In this example, a_custom_function is undefined, so the assembler engine cannot resolve it and raises the following error:
warning: Fixup not resolved:
call a_custom_function
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.Binary
elf.assemble(
elf.entrypoint,
"""
mov rdi, rax;
call a_custom_function;
""",
MyConfig(),
)
class MyConfig : public LIEF::assembly::AssemblerConfig {
public:
LIEF::optional<uint64_t> resolve_symbol(const std::string& name) override {
if (name == "a_custom_function") {
return 0x1000;
}
return LIEF::nullopt();
}
};
MyConfig myconfig;
std::unique_ptr<LIEF::ELF::Binary> elf;
elf->assemble(elf->entrypoint(), R"asm(
mov rdi, rax;
call a_custom_function;
)asm",
myconfig);
let elf: &mut lief::elf::Binary = some_elf;
let mut config = lief::assembly::AssemblerConfig::default();
let resolver = Arc::new(|symbol: &str| {
if symbol == "a_custom_function" {
return Some(0x1000);
}
None
});
config.symbol_resolver = Some(resolver);
elf.assemble_with_config(
elf.entrypoint(),
r#"
mov rdi, rax;
call a_custom_function;
"#,
&config,
);
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.Binary
config = MyConfig(elf)
elf.assemble(
elf.entrypoint,
"""
mov rdi, rax;
call a_custom_function;
""",
config,
)
class MyConfig : public LIEF::assembly::AssemblerConfig {
public:
MyConfig() = delete;
MyConfig(LIEF::Binary& target) :
LIEF::assembly::AssemblerConfig(),
target_(&target) {}
LIEF::optional<uint64_t> resolve_symbol(const std::string& name) override {
if (auto addr = target_->get_function_address(name)) {
return *addr;
}
return LIEF::nullopt();
}
~MyConfig() override = default;
private:
LIEF::Binary* target_ = nullptr;
};
std::unique_ptr<LIEF::ELF::Binary> elf;
MyConfig myconfig(*elf);
elf->assemble(elf->entrypoint(), R"asm(
mov rdi, rax;
call a_custom_function;
)asm",
myconfig);
lief::assembly::AssemblerConfig::symbol_resolver can capture most of its context:let elf: &mut lief::elf::Binary = some_elf;
let mut config = lief::assembly::AssemblerConfig::default();
let sym_map: HashMap<String, u64> = elf
.exported_symbols()
.map(|sym| (sym.name(), sym.value()))
.collect();
let resolver = Arc::new(move |symbol: &str| sym_map.get(symbol).copied());
config.symbol_resolver = Some(resolver);
elf.assemble_with_config(
elf.entrypoint(),
r#"
mov rdi, rax;
call a_custom_function;
"#,
&config,
);
Patching code with the assembler is a fast alternative to editing raw bytes by hand: it is useful for bypassing a check while reverse engineering, hot-patching a bug in a shipped executable, or inserting instrumentation. Note The snippets above assume Disabling an Instruction¶
nop bytes as it occupied. Pairing the disassembler with lets you do this:elf: lief.ELF.Binary
# Overwrite the first call in the region (e.g. a call to an anti-debugging
# routine) with nops
for inst in elf.disassemble(0x401200):
if inst.is_call:
elf.assemble(inst.address, "nop\n" * inst.size)
break
elf.write("patched.bin")
std::unique_ptr<LIEF::ELF::Binary> elf;
// Overwrite the first call in the region (e.g. a call to an anti-debugging
// routine) with nops
for (const auto& inst : elf->disassemble(0x401200)) {
if (inst.is_call()) {
std::string nops;
for (size_t i = 0; i < inst.size(); ++i) {
nops += "nop\n";
}
elf->assemble(inst.address(), nops);
break;
}
}
elf->write("patched.bin");
let elf: &mut lief::elf::Binary = some_elf;
// Overwrite the first call in the region (e.g. a call to an anti-debugging
// routine) with nops
let target = elf
.disassemble_address(0x401200)
.find(|inst| inst.is_call())
.map(|inst| (inst.address(), inst.size()));
if let Some((address, size)) = target {
elf.assemble(address, &"nop\n".repeat(size as usize));
}
elf.write("patched.bin");
x86/x86-64, where a nop is a single byte, so inst.size of them fill the slot exactly. On fixed-width instruction set such as AArch64 every instruction is 4 bytes, so we would emit inst.size / 4 instead.