Assembler¶
Introduction¶
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.
Technical Details¶
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.
Contextual Assembly Patching¶
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:
std::optional<uint64_t> resolve_symbol(const std::string& name) override {
if (name == "a_custom_function") {
return 0x1000;
}
return std::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) {}
std::optional<uint64_t> resolve_symbol(const std::string& name) override {
if (auto addr = target_->get_function_address(name)) {
return *addr;
}
return std::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,
);
Use Cases¶
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.