diff --git a/api/python/lief/__init__.pyi b/api/python/lief/__init__.pyi index 316c0bd6..46d55e1c 100644 --- a/api/python/lief/__init__.pyi +++ b/api/python/lief/__init__.pyi @@ -416,7 +416,7 @@ class Binary(Object): def disassemble_from_bytes(self, buffer: bytes, address: int = 0) -> Iterator[Optional[assembly.Instruction]]: ... - def assemble(self, address: int, assembly: str) -> bytes: ... + def assemble(self, address: int, assembly: str, config: assembly.AssemblerConfig = ...) -> bytes: ... @property def page_size(self) -> int: ... diff --git a/api/python/lief/assembly/__init__.pyi b/api/python/lief/assembly/__init__.pyi index 2353bdf1..74481704 100644 --- a/api/python/lief/assembly/__init__.pyi +++ b/api/python/lief/assembly/__init__.pyi @@ -102,3 +102,20 @@ class Instruction: def branch_target(self) -> Union[int, lief.lief_errors]: ... def __str__(self) -> str: ... + +class AssemblerConfig: + def __init__(self) -> None: ... + + class DIALECT(enum.Enum): + DEFAULT_DIALECT = 0 + + X86_INTEL = 1 + + X86_ATT = 2 + + @staticmethod + def default_config() -> AssemblerConfig: ... + + dialect: AssemblerConfig.DIALECT + + def resolve_symbol(self, name: str) -> int | None: ... diff --git a/api/python/src/Abstract/pyBinary.cpp b/api/python/src/Abstract/pyBinary.cpp index 5cdb706b..73618f3d 100644 --- a/api/python/src/Abstract/pyBinary.cpp +++ b/api/python/src/Abstract/pyBinary.cpp @@ -367,9 +367,12 @@ void create(nb::module_& m) { )doc"_doc ) - .def("assemble", [] (Binary& self, uint64_t address, const std::string& Asm) { - return nb::to_bytes(self.assemble(address, Asm)); - }, "address"_a, "assembly"_a, + .def("assemble", + [] (Binary& self, uint64_t address, const std::string& Asm, + assembly::AssemblerConfig& config) + { + return nb::to_bytes(self.assemble(address, Asm, config)); + }, "address"_a, "assembly"_a, "config"_a = assembly::AssemblerConfig::default_config(), R"doc( Assemble **and patch** the provided assembly code at the specified address. @@ -383,6 +386,9 @@ void create(nb::module_& m) { xor rax, rbx; mov rcx, rax; """) + + If you need to configure the assembly engine or to define addresses for + symbols, you can provide your own :class:`~.assembly.AssemblerConfig` instance. )doc"_doc ) diff --git a/api/python/src/asm/CMakeLists.txt b/api/python/src/asm/CMakeLists.txt index 261c6df6..e7f2a7e1 100644 --- a/api/python/src/asm/CMakeLists.txt +++ b/api/python/src/asm/CMakeLists.txt @@ -2,6 +2,7 @@ target_sources(pyLIEF PRIVATE init.cpp pyEngine.cpp pyInstruction.cpp + pyAssemblerConfig.cpp ) add_subdirectory(aarch64) diff --git a/api/python/src/asm/init.cpp b/api/python/src/asm/init.cpp index a5169675..766069ee 100644 --- a/api/python/src/asm/init.cpp +++ b/api/python/src/asm/init.cpp @@ -12,6 +12,7 @@ namespace LIEF::assembly { class Engine; class Instruction; +class AssemblerConfig; } namespace LIEF::assembly::py { @@ -20,6 +21,7 @@ void init(nb::module_& m) { create(mod); create(mod); + create(mod); aarch64::py::init(mod); x86::py::init(mod); diff --git a/api/python/src/asm/pyAssemblerConfig.cpp b/api/python/src/asm/pyAssemblerConfig.cpp new file mode 100644 index 00000000..6f829540 --- /dev/null +++ b/api/python/src/asm/pyAssemblerConfig.cpp @@ -0,0 +1,80 @@ +#include "LIEF/asm/AssemblerConfig.hpp" +#include "asm/pyAssembly.hpp" + +#include +#include +#include "nanobind/extra/stl/lief_optional.h" + +#include "nanobind/extra/stl/lief_optional.h" + +namespace LIEF::assembly::py { + +class PyAssemblerConfig : public assembly::AssemblerConfig { + public: + static constexpr auto NB_NUM_SLOTS = 3; + NB_TRAMPOLINE(assembly::AssemblerConfig, NB_NUM_SLOTS); + + optional resolve_symbol(const std::string& name) override { + NB_OVERRIDE(resolve_symbol, name); + } + + ~PyAssemblerConfig() override = default; +}; + +template<> +void create(nb::module_& m) { + nb::class_ obj(m, "AssemblerConfig", + R"doc( + This class exposes the different elements that can be configured to assemble + code. + )doc"_doc + ); + + nb::enum_(obj, "DIALECT", + "The different supported dialects"_doc + ) + .value("DEFAULT_DIALECT", assembly::AssemblerConfig::DIALECT::DEFAULT_DIALECT) + .value("X86_INTEL", assembly::AssemblerConfig::DIALECT::X86_INTEL, + "Intel syntax"_doc) + .value("X86_ATT", assembly::AssemblerConfig::DIALECT::X86_ATT, + "AT&T syntax"_doc) + ; + + obj + .def(nb::init<>()) + .def_static("default_config", &assembly::AssemblerConfig::default_config, + "Default configuration"_doc + ) + .def_rw("dialect", &assembly::AssemblerConfig::dialect, + "The dialect of the input assembly code"_doc + ) + .def("resolve_symbol", &assembly::AssemblerConfig::resolve_symbol, + R"doc( + This function aims to be overloaded in order to resolve symbols used + in the assembly listing. + + For instance, given this assembly code: + + .. code-block:: text + + 0x1000: mov rdi, rbx + 0x1003: call _my_function + + The function ``_my_function`` will remain undefined unless we return its + address in :meth:`~.resolve_symbol`: + + .. code-block:: python + + class MyConfig(lief.assembly.AssemblerConfig): + def __init__(self): + super().__init__() # This is important + + @override + def resolve_symbol(self, name: str) -> int | None: + if name == '_my_function': + return 0x4000 + return None # Or super().resolve_symbol(name) + )doc"_doc, "name"_a) + ; +} +} diff --git a/api/python/src/pyLIEF.cpp b/api/python/src/pyLIEF.cpp index 890ebc60..f3620dea 100644 --- a/api/python/src/pyLIEF.cpp +++ b/api/python/src/pyLIEF.cpp @@ -311,13 +311,14 @@ void init(nb::module_& m) { LIEF::py::init_hash(m); LIEF::py::init_json(m); + LIEF::assembly::py::init(m); + LIEF::py::init_abstract(m); LIEF::dwarf::py::init(m); LIEF::pdb::py::init(m); LIEF::objc::py::init(m); LIEF::dsc::py::init(m); - LIEF::assembly::py::init(m); #if defined(LIEF_ELF_SUPPORT) LIEF::ELF::py::init(m); diff --git a/api/rust/CMakeLists.txt b/api/rust/CMakeLists.txt index 2834e818..33eb6f9d 100644 --- a/api/rust/CMakeLists.txt +++ b/api/rust/CMakeLists.txt @@ -10,7 +10,8 @@ if(LIEF_INSTALL) COMPONENT headers) install( - FILES ${CMAKE_CURRENT_SOURCE_DIR}/autocxx_ffi.rs + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/autocxx_ffi.rs DESTINATION ${CMAKE_INSTALL_LIBDIR}/LIEF/) endif() diff --git a/api/rust/autocxx_ffi.rs b/api/rust/autocxx_ffi.rs index 3595ae4e..6d8cd456 100644 --- a/api/rust/autocxx_ffi.rs +++ b/api/rust/autocxx_ffi.rs @@ -1139,3 +1139,13 @@ include_cpp! { safety!(unsafe) } + +#[autocxx::extern_rust::extern_rust_function] +pub struct AssemblerConfig_r {} + +impl AssemblerConfig_r { + #[autocxx::extern_rust::extern_rust_function] + fn resolve_symbol(&self, name: &str) -> i64 { + unimplemented!(); + } +} diff --git a/api/rust/cargo/lief-ffi/src/lib.rs b/api/rust/cargo/lief-ffi/src/lib.rs index 835bd12f..507c015d 100644 --- a/api/rust/cargo/lief-ffi/src/lib.rs +++ b/api/rust/cargo/lief-ffi/src/lib.rs @@ -1,3 +1,27 @@ include!(concat!(env!("AUTOCXX_RS"), "/", "autocxx-autocxx_ffi-gen.rs")); pub use autocxx_ffi::*; + +#[allow(non_camel_case_types)] +pub struct AssemblerConfig_r { + #[allow(dead_code)] + resolve_symbol_impl: Box Option + Send + Sync + 'static>, +} + +impl AssemblerConfig_r { + #[allow(non_snake_case)] + pub fn new(F: impl Fn(&str) -> Option + Send + Sync + 'static) -> Box { + Box::new(Self { + resolve_symbol_impl: Box::new(F) + }) + } +} + +impl AssemblerConfig_r { + fn resolve_symbol(&self, name: &str) -> i64 { + if let Some(addr) = (self.resolve_symbol_impl)(name) { + return addr as i64; + } + -1 + } +} diff --git a/api/rust/cargo/lief/src/assembly.rs b/api/rust/cargo/lief/src/assembly.rs index 083d4ca8..53aaaf93 100644 --- a/api/rust/cargo/lief/src/assembly.rs +++ b/api/rust/cargo/lief/src/assembly.rs @@ -34,6 +34,10 @@ pub mod powerpc; pub mod riscv; pub mod mips; pub mod ebpf; +pub mod config; #[doc(inline)] pub use instruction::{Instructions, Instruction}; + +#[doc(inline)] +pub use config::AssemblerConfig; diff --git a/api/rust/cargo/lief/src/assembly/config.rs b/api/rust/cargo/lief/src/assembly/config.rs new file mode 100644 index 00000000..e6a85018 --- /dev/null +++ b/api/rust/cargo/lief/src/assembly/config.rs @@ -0,0 +1,92 @@ +use lief_ffi as ffi; +use std::sync::Arc; + +#[derive(Clone)] +/// This structure exposes the different elements that can be configured to assemble +/// code. +pub struct AssemblerConfig { + /// Default configuration + pub dialect: Dialect, + + /// This attribute aims to store a function for resolving symbols in the assembly listing. + /// + /// For instance, given this assembly code: + /// + /// ```text + /// 0x1000: mov rdi, rbx + /// 0x1003: call _my_function + /// ``` + /// + /// The function `_my_function` will remain undefined unless we return its address + /// in a callback defined in this attribute [`AssemblerConfig::symbol_resolver`]: + /// + /// ```rust + /// let mut config = AssemblerConfig::default(); + /// + /// let resolver = Arc::new(move |symbol: &str| { + /// return Some(0x4000); + /// }); + /// + /// config.symbol_resolver = Some(resolver); + /// ``` + pub symbol_resolver: Option Option + Send + Sync + 'static>> +} + +impl Default for AssemblerConfig { + fn default() -> AssemblerConfig { + AssemblerConfig { + dialect: Dialect::DEFAULT_DIALECT, + symbol_resolver: None, + } + } +} + +impl AssemblerConfig { + #[doc(hidden)] + pub fn into_ffi(&self) -> Box { + if let Some(ref resolver) = self.symbol_resolver { + let closure = resolver.clone(); + ffi::AssemblerConfig_r::new(move |s| closure(s)) + } else { + ffi::AssemblerConfig_r::new(|_| None) + } + } +} + +#[allow(non_camel_case_types)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +/// The different supported dialects +pub enum Dialect { + DEFAULT_DIALECT, + + /// Intel syntax + X86_INTEL, + + /// Intel syntax + X86_ATT, + UNKNOWN(u32), +} + +impl From for Dialect { + fn from(value: u32) -> Self { + match value { + 0x00000000 => Dialect::DEFAULT_DIALECT, + 0x00000001 => Dialect::X86_INTEL, + 0x00000002 => Dialect::X86_ATT, + _ => Dialect::UNKNOWN(value), + + } + } +} + +impl From for u32 { + fn from(value: Dialect) -> u32 { + match value { + Dialect::DEFAULT_DIALECT => 0x00000000, + Dialect::X86_INTEL => 0x00000001, + Dialect::X86_ATT => 0x00000002, + Dialect::UNKNOWN(value) => value, + + } + } +} diff --git a/api/rust/cargo/lief/src/generic.rs b/api/rust/cargo/lief/src/generic.rs index 2699e3d2..65c281d7 100644 --- a/api/rust/cargo/lief/src/generic.rs +++ b/api/rust/cargo/lief/src/generic.rs @@ -2,7 +2,7 @@ use lief_ffi as ffi; use bitflags::bitflags; use crate::{to_slice, declare_fwd_iterator}; use crate::common::{into_optional, FromFFI}; -use crate::assembly::Instructions; +use crate::assembly::{Instructions, AssemblerConfig}; use std::pin::Pin; @@ -229,14 +229,21 @@ pub trait Binary { /// let mut bin = get_binary(); /// /// let Vec bytes = bin.assemble(0x12000440, r#" - /// xor rax, rbx; - /// mov rcx, rax; + /// xor rax, rbx; + /// mov rcx, rax; /// "#); /// ``` fn assemble(&mut self, address: u64, asm: &str) -> Vec { Vec::from(self.as_pin_mut_generic().assemble(address, asm).as_slice()) } + /// Same as [`Binary::assemble`] but this function takes an extra [`AssemblerConfig`] that + /// is used to configure the assembly engine: dialect, symbols definitions. + fn assemble_with_config(&mut self, address: u64, asm: &str, config: &AssemblerConfig) -> Vec { + let ffi_config = config.into_ffi(); + Vec::from(self.as_pin_mut_generic().assemble_with_config(address, asm, ffi_config.as_ref()).as_slice()) + } + /// Get the default memory page size according to the architecture and the format of the /// current binary fn page_size(&self) -> u64 { diff --git a/api/rust/cargo/lief/tests/assembler_test.rs b/api/rust/cargo/lief/tests/assembler_test.rs index 788b6f7d..a5a954fc 100644 --- a/api/rust/cargo/lief/tests/assembler_test.rs +++ b/api/rust/cargo/lief/tests/assembler_test.rs @@ -6,6 +6,7 @@ use lief::dwarf::{Parameter, Scope, Type}; use lief::generic::{Binary, Section}; use std::path::{Path, PathBuf}; +use std::sync::Arc; fn get_binary(name: &str) -> lief::Binary { let path = utils::get_sample(Path::new(name)).unwrap(); let path_str = path.to_str().unwrap(); @@ -92,4 +93,23 @@ fn test_api() { reassemble_from("ELF/ELF32_x86_library_libshellx.so", 0x000010c0, 300); //reassemble_from("ELF/libmonochrome-armv7.so", 0x0468b701, 300); // Thumb //reassemble_from("ELF/i872_risv.elf", 0x80000000, 300); + + if let lief::Binary::PE(mut pe) = get_binary("PE/ntoskrnl.exe") { + let imagebase = pe.imagebase(); + + let mut config = lief::assembly::AssemblerConfig::default(); + + let entrypoint = pe.entrypoint(); + let resolver = Arc::new(move |symbol: &str| { + if symbol == "entrypoint" { + return Some(entrypoint); + } + None + }); + + config.symbol_resolver = Some(resolver); + pe.assemble_with_config(pe.entrypoint() - imagebase, r#" + call entrypoint + "#, &config); + } } diff --git a/api/rust/cmake-ffi/CMakeLists.txt b/api/rust/cmake-ffi/CMakeLists.txt index a9c0a55a..28c29984 100644 --- a/api/rust/cmake-ffi/CMakeLists.txt +++ b/api/rust/cmake-ffi/CMakeLists.txt @@ -11,7 +11,11 @@ endif() find_package(LIEF REQUIRED) message(STATUS "Rust FFI source: ${LIEF_RUST_FFI_SRC}") -add_library(lief-sys STATIC ${LIEF_RUST_FFI_SRC}/cxx/gen0.cxx) + +add_library(lief-sys STATIC + ${LIEF_RUST_FFI_SRC}/cxx/gen0.cxx + rust_cpp_bridge.cpp +) set_target_properties(lief-sys PROPERTIES POSITION_INDEPENDENT_CODE ON diff --git a/api/rust/cmake-ffi/rust_cpp_bridge.cpp b/api/rust/cmake-ffi/rust_cpp_bridge.cpp new file mode 100644 index 00000000..458c5383 --- /dev/null +++ b/api/rust/cmake-ffi/rust_cpp_bridge.cpp @@ -0,0 +1,30 @@ +#include + +#include "LIEF/rust/asm/AssemblerConfig.hpp" + +#include + +class RustAssemblerConfig : public LIEF::assembly::AssemblerConfig { + public: + RustAssemblerConfig(const AssemblerConfig_r& impl) : + LIEF::assembly::AssemblerConfig(), + impl_(const_cast(&impl)) + {} + + LIEF::optional resolve_symbol(const std::string& name) override { + int64_t addr = impl_->resolve_symbol(name); + if (addr < 0) { + return LIEF::nullopt(); + } + return addr; + } + + ~RustAssemblerConfig() override = default; + + protected: + AssemblerConfig_r* impl_ = nullptr; +}; + +std::unique_ptr from_rust(const AssemblerConfig_r& config) { + return std::make_unique(config); +} diff --git a/api/rust/include/LIEF/rust/Abstract/Binary.hpp b/api/rust/include/LIEF/rust/Abstract/Binary.hpp index 7e8f8e01..a09d5082 100644 --- a/api/rust/include/LIEF/rust/Abstract/Binary.hpp +++ b/api/rust/include/LIEF/rust/Abstract/Binary.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include "LIEF/rust/error.hpp" @@ -104,6 +105,12 @@ class AbstractBinary : public Mirror { return get().assemble(address, Asm); } + auto assemble_with_config(uint64_t address, std::string Asm, const AssemblerConfig_r& ffi_config) { + std::unique_ptr config = from_rust(ffi_config); + assert(config != nullptr); + return get().assemble(address, Asm, *config); + } + uint64_t page_size() const { return get().page_size(); } diff --git a/api/rust/include/LIEF/rust/asm/AssemblerConfig.hpp b/api/rust/include/LIEF/rust/asm/AssemblerConfig.hpp new file mode 100644 index 00000000..b4a0b619 --- /dev/null +++ b/api/rust/include/LIEF/rust/asm/AssemblerConfig.hpp @@ -0,0 +1,9 @@ +#pragma once +#include "LIEF/visibility.h" +#include +#include "LIEF/asm/AssemblerConfig.hpp" + +struct AssemblerConfig_r; + +LIEF_API std::unique_ptr + from_rust(const AssemblerConfig_r& config); diff --git a/doc/sphinx/_cross_api.rst b/doc/sphinx/_cross_api.rst index 52383bb5..20184a1a 100644 --- a/doc/sphinx/_cross_api.rst +++ b/doc/sphinx/_cross_api.rst @@ -988,6 +988,13 @@ :cpp:class:`LIEF::assembly::ebpf::Instruction` :py:class:`lief.assembly.ebpf.Instruction` + +.. |lief-asm-AssemblerConfig| lief-api:: lief.assembly.AssemblerConfig + + :rust:struct:`lief::assembly::AssemblerConfig` + :cpp:class:`LIEF::assembly::AssemblerConfig` + :py:class:`lief.assembly.AssemblerConfig` + .. COFF Format ================================================================ .. |lief-coff-parse| lief-api:: lief.COFF.parse() diff --git a/doc/sphinx/changelog.rst b/doc/sphinx/changelog.rst index ebcfdf6b..65a29238 100644 --- a/doc/sphinx/changelog.rst +++ b/doc/sphinx/changelog.rst @@ -22,6 +22,10 @@ - :ref:`lief-patchelf ` +:Assembler: + + * Add support for :ref:`Contextual Assembly Patching ` + :DSC: * Add enum for the latest dyld shared cache version introducing @@ -129,7 +133,7 @@ * Introduce :attr:`lief.ELF.Segment.raw_flags` to access the raw (integer) value of the flag * If an ELF binary uses a custom page size, its value can be defined in the - parser configuration: |lief-elf-parserconfig-page_size|. + parser configuration: |lief-elf-parser-config-page_size|. * Add support for SH4 * Add suport for x32/ILP32 ELF binaries (:issue:`1225`) * Add support for S390x diff --git a/doc/sphinx/extended/assembler/cpp.rst b/doc/sphinx/extended/assembler/cpp.rst index de883c58..78898939 100644 --- a/doc/sphinx/extended/assembler/cpp.rst +++ b/doc/sphinx/extended/assembler/cpp.rst @@ -4,3 +4,8 @@ - :cpp:func:`LIEF::Binary::assemble` - :cpp:class:`LIEF::assembly::Engine` + +AssemblerConfig +*************** + +.. doxygenclass:: LIEF::assembly::AssemblerConfig diff --git a/doc/sphinx/extended/assembler/index.rst b/doc/sphinx/extended/assembler/index.rst index 01da12f0..056ab5e3 100644 --- a/doc/sphinx/extended/assembler/index.rst +++ b/doc/sphinx/extended/assembler/index.rst @@ -17,7 +17,7 @@ Introduction ************ In addition to regular file formats modifications, we might want to patch code with -custom assembly. This functionality is available thanks to the |lief-assemble| +custom assembly code. This functionality is available thanks to the |lief-assemble| function: .. tabs:: @@ -93,36 +93,248 @@ Technical Details In the same way that the :ref:`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 assembly text is consumed by the ``llvm::MCAsmParser`` object, and we *intercept* the raw generated assembly bytes from the ``llvm::MCObjectWriter``. -Currently, ``llvm::MCFixup`` are not resolved such as if an assembly instruction -needs some kind of relocation, you can get a warning and the issued bytes be -corrupted: +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**. -.. code-block:: python +.. _extended-assembler-contextual-patching: - import lief +Contextual Assembly Patching +**************************** - macho = lief.MachO.parse("my-ios-app").take(lief.MachO.Header.CPU_TYPE.ARM64) - macho.assemble(0x01665c, "bl _my_function") +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: + +.. tabs:: + + .. tab:: :fa:`brands fa-python` Python + + .. code-block:: python + + import lief + + elf = lief.ELF.parse("/bin/ssh") + + elf.assemble(elf.entrypoint, """ + mov rdi, rax; + call a_custom_function + """) + + .. tab:: :fa:`regular fa-file-code` C++ + + .. code-block:: cpp + + auto elf = LIEF::ELF::Parser::parse("/bin/ssh"); + + elf->assemble(elf->entrypoint(), R"asm( + mov rdi, rax; + call a_custom_function; + )asm"); + + .. tab:: :fa:`brands fa-rust` Rust + + .. code-block:: rust + + let mut elf = lief::elf::Binary::parse("/bin/ssh"); + + elf.assemble(addr, r#" + 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: .. code-block:: text - warning: Fixup not resolved: bl _my_function + warning: Fixup not resolved: + call a_custom_function -LIEF is going to progressively support these fixups and more **importantly**, -it will provide the *binary* context of |lief-abstract-binary| to the assembler. +LIEF exposes a |lief-asm-AssemblerConfig| interface that can be used to +configure the engine and to **dynamically** resolve symbols used in the assembly +listing: -This means that we the binary defines the symbol ``_my_function``, the assembly -engine will be aware of this symbol and could be used in your assembly listing: +.. tabs:: -.. code-block:: + .. tab:: :fa:`brands fa-python` Python + + .. code-block:: python + :emphasize-lines: 3-11,18 + + 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()) + + .. tab:: :fa:`regular fa-file-code` C++ + + .. code-block:: cpp + :emphasize-lines: 1-9,13,18 + + class MyConfig : public LIEF::assembly::AssemblerConfig { + public: + LIEF::optional resolve_symbol(const std::string& name) const override { + if (name == "a_custom_function") { + return 0x1000; + } + return LIEF::nullopt(); + } + }; + + auto elf = LIEF::ELF::Parser::parse("/bin/ssh"); + + MyConfig myconfig; + + elf->assemble(elf->entrypoint(), R"asm( + mov rdi, rax; + call a_custom_function; + )asm", myconfig); + + .. tab:: :fa:`brands fa-rust` Rust + + .. code-block:: rust + :emphasize-lines: 3-10,17 + + let mut elf = lief::elf::Binary::parse("/bin/ssh"); + + let mut config = lief::assembly::AssemblerConfig::default(); + + let resolver = Arc::new(move |symbol: &str| { + if symbol == "a_custom_function" { + return Some(0x1000); + } + None + }); + + config.symbol_resolver = Some(resolver); + + elf.assemble(addr, r#" + mov rdi, rax; + call a_custom_function; + "#, &config); + +This interface can be used to wrap a context which can be, for instance, a +generic |lief-abstract-binary|: + +.. tabs:: + + .. tab:: :fa:`brands fa-python` Python + + .. code-block:: python + :emphasize-lines: 7,11-14,18 + + 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) + + .. tab:: :fa:`regular fa-file-code` C++ + + .. code-block:: cpp + :emphasize-lines: 4-8,11-13,25 + + class MyConfig : public LIEF::assembly::AssemblerConfig { + public: + MyConfig() = delete; + MyConfig(LIEF::Binary& target) : + LIEF::assembly::AssemblerConfig() + { + target_ = ⌖ + } + + LIEF::optional resolve_symbol(const std::string& name) const override { + if (auto addr = target_->get_function_address(name)) { + return *addr; + } + return LIEF::nullopt(); + } + + ~MyConfig() override = default; + + private: + LIEF::Binary* target_ = nullptr; + }; + + auto elf = LIEF::ELF::Parser::parse("/bin/ssh"); + + MyConfig myconfig(*elf); + + elf->assemble(elf->entrypoint(), R"asm( + mov rdi, rax; + call a_custom_function; + )asm", myconfig); + +The Rust bindings do not offer the same flexibility to capture the +|lief-abstract-binary|. Nevertheless, the closure associated with the +:rust:member:`lief::assembly::AssemblerConfig::symbol_resolver [struct]` +can capture most of its context: + +.. tabs:: + + .. tab:: :fa:`brands fa-rust` Rust + + .. code-block:: rust + :emphasize-lines: 5-13 + + 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); - ldr x0, =_my_function; - mov x1, #0xAABB; - str x1, [x0]; - bl _my_function :fa:`brands fa-python` :doc:`Python API ` diff --git a/doc/sphinx/extended/assembler/python.rst b/doc/sphinx/extended/assembler/python.rst index 3115554b..3dd27eee 100644 --- a/doc/sphinx/extended/assembler/python.rst +++ b/doc/sphinx/extended/assembler/python.rst @@ -5,3 +5,8 @@ - :class:`lief.assembly.Engine` + +AssemblerConfig +*************** + +.. autoclass:: lief.assembly.AssemblerConfig diff --git a/include/LIEF/Abstract/Binary.hpp b/include/LIEF/Abstract/Binary.hpp index 0110c91c..61003acc 100644 --- a/include/LIEF/Abstract/Binary.hpp +++ b/include/LIEF/Abstract/Binary.hpp @@ -30,6 +30,7 @@ #include "LIEF/Abstract/Function.hpp" #include "LIEF/asm/Instruction.hpp" +#include "LIEF/asm/AssemblerConfig.hpp" namespace llvm { class MCInst; @@ -357,7 +358,11 @@ class LIEF_API Binary : public Object { /// mov rcx, rax; /// )asm"); /// ``` - std::vector assemble(uint64_t address, const std::string& Asm); + /// + /// If you need to configure the assembly engine or to define addresses for + /// symbols, you can provide your own assembly::AssemblerConfig. + std::vector assemble(uint64_t address, const std::string& Asm, + assembly::AssemblerConfig& config = assembly::AssemblerConfig::default_config()); /// Assemble **and patch** the address with the given LLVM MCInst. /// diff --git a/include/LIEF/asm/AssemblerConfig.hpp b/include/LIEF/asm/AssemblerConfig.hpp new file mode 100644 index 00000000..c9d5b963 --- /dev/null +++ b/include/LIEF/asm/AssemblerConfig.hpp @@ -0,0 +1,91 @@ +/* Copyright 2022 - 2025 R. Thomas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef LIEF_ASM_ASSEMBLER_CONFIG_H +#define LIEF_ASM_ASSEMBLER_CONFIG_H + +#include "LIEF/visibility.h" +#include "LIEF/optional.hpp" + +#include + +namespace LIEF { + +namespace assembly { + +/// This class exposes the different elements that can be configured to assemble +/// code. +class LIEF_API AssemblerConfig { + public: + AssemblerConfig() = default; + + AssemblerConfig(const AssemblerConfig &) = default; + AssemblerConfig &operator=(const AssemblerConfig &) = default; + + AssemblerConfig(AssemblerConfig &&) = default; + AssemblerConfig &operator=(AssemblerConfig &&) = default; + + /// The different supported dialects + enum class DIALECT { + DEFAULT_DIALECT = 0, + /// Intel syntax + X86_INTEL, + + /// AT&T syntax + X86_ATT, + }; + + /// Default configuration + static AssemblerConfig& default_config() { + static AssemblerConfig AC; + return AC; + } + + /// The dialect of the input assembly code + DIALECT dialect = DIALECT::DEFAULT_DIALECT; + + /// This function aims to be overloaded in order to resolve symbols used + /// in the assembly listing. + /// + /// For instance, given this assembly code: + /// + /// ```text + /// 0x1000: mov rdi, rbx + /// 0x1003: call _my_function + /// ``` + /// + /// The function `_my_function` will remain undefined unless we return its address + /// in `resolve_symbol()`: + /// + /// ```cpp + /// class MyConfig : public AssemblerConfig { + /// public: + /// optional resolve_symbol(const std::string& name) { + /// if (name == "_my_function") { + /// return 0x4000; + /// } + /// return nullopt(); // or AssemblerConfig::resolve_symbol(name) + /// } + /// }; + /// ``` + virtual optional resolve_symbol(const std::string& /*name*/) { + return nullopt(); + } + + virtual ~AssemblerConfig() = default; +}; +} +} + +#endif diff --git a/include/LIEF/asm/Engine.hpp b/include/LIEF/asm/Engine.hpp index daee3ce7..291752a2 100644 --- a/include/LIEF/asm/Engine.hpp +++ b/include/LIEF/asm/Engine.hpp @@ -16,7 +16,9 @@ #define LIEF_ASM_ENGINE_H #include "LIEF/visibility.h" #include "LIEF/iterators.hpp" + #include "LIEF/asm/Instruction.hpp" +#include "LIEF/asm/AssemblerConfig.hpp" #include @@ -55,9 +57,11 @@ class LIEF_API Engine { return disassemble(bytes.data(), bytes.size(), addr); } - std::vector assemble(uint64_t address, const std::string& Asm); std::vector assemble(uint64_t address, const std::string& Asm, - LIEF::Binary& bin); + AssemblerConfig& config = AssemblerConfig::default_config()); + + std::vector assemble(uint64_t address, const std::string& Asm, + LIEF::Binary& bin, AssemblerConfig& config = AssemblerConfig::default_config()); std::vector assemble(uint64_t address, const llvm::MCInst& inst, LIEF::Binary& bin); diff --git a/src/asm/asm.cpp b/src/asm/asm.cpp index 87ad6e3d..a0996cbd 100644 --- a/src/asm/asm.cpp +++ b/src/asm/asm.cpp @@ -81,7 +81,9 @@ Binary::instructions_it Binary::disassemble(const uint8_t*, size_t, uint64_t) co return make_empty_iterator(); } -std::vector Binary::assemble(uint64_t/*address*/, const std::string&/*Asm*/) { +std::vector Binary::assemble(uint64_t/*address*/, const std::string&/*Asm*/, + assembly::AssemblerConfig& /*config*/) +{ LIEF_ERR(ASSEMBLY_NOT_SUPPORTED); return {}; } @@ -269,12 +271,14 @@ Engine::instructions_it Engine::disassemble(const uint8_t*, size_t, uint64_t) { return make_empty_iterator(); } -std::vector Engine::assemble(uint64_t/*address*/, const std::string&/*Asm*/) { +std::vector Engine::assemble(uint64_t/*address*/, const std::string&/*Asm*/, + AssemblerConfig& /*config*/) +{ return {}; } std::vector Engine::assemble(uint64_t/*address*/, const std::string&/*Asm*/, - LIEF::Binary&/*bin*/) + LIEF::Binary&/*bin*/, AssemblerConfig& /*config*/) { return {}; } diff --git a/tests/assembly/test_arm64.py b/tests/assembly/test_arm64.py index a77d0d25..34f91438 100644 --- a/tests/assembly/test_arm64.py +++ b/tests/assembly/test_arm64.py @@ -1,3 +1,4 @@ +from typing import override import lief import pytest from utils import get_sample @@ -136,3 +137,37 @@ def test_arm64_operands(): assert isinstance(operands[1], lief.assembly.aarch64.operands.PCRelative) assert operands[1].value == 1 + + +def test_asm_context(): + class Config(lief.assembly.AssemblerConfig): + def __init__(self, elf: lief.ELF.Binary): + super().__init__() + + self._elf = elf + + @override + def resolve_symbol(self, name: str) -> int | None: + sym = self._elf.get_symtab_symbol(name) + if sym is None or sym.type != lief.ELF.Symbol.TYPE.FUNC: + return super().resolve_symbol(name) + return sym.value + + elf = lief.ELF.parse(get_sample("ELF/ELF64_AArch64_piebinary_ndkr16.bin")) + assert elf is not None + + config = Config(elf) + + addr = 0x00000e80 + elf.assemble(addr, """ + adrp x0, _ZNSt6__ndk113basic_ostreamIcNS_11char_traitsIcEEE3putEc; + add x0, x0, :lo12:_ZNSt6__ndk113basic_ostreamIcNS_11char_traitsIcEEE3putEc; + br x0; + bl _ZNSt6__ndk113basic_ostreamIcNS_11char_traitsIcEEE3putEc + """, config) + + insts = list(elf.disassemble(addr)) + assert insts[0].to_string() == "0x000e80: adrp x0, #4096" + assert insts[1].to_string() == "0x000e84: add x0, x0, #1280" + assert insts[2].to_string() == "0x000e88: br x0" + assert insts[3].to_string() == "0x000e8c: bl #1652"