Memory

The interface exposes an API to allocate, inspect and manipulate memory in the current process. It can be used to map anonymous pages, change their protection and read or write from any virtual address.

Linux x86-64

The following snippet allocates a writable and executable page, assembles a small x86-64 stub inside it that prints LIEF Runtime Extended Demo, calls the resulting function, and rewrites part of the buffer before invoking it again:

chunk = lief.runtime.Memory.mmap(
    lief.runtime.Process.page_size,
    lief.runtime.Memory.ANONYMOUS | lief.runtime.Memory.PRIVATE,
    lief.runtime.Memory.READ | lief.runtime.Memory.WRITE | lief.runtime.Memory.EXEC,
)

raw_inst = lief.runtime.assemble(
    chunk.addr,
    r"""
    .global _start

    .text
    _start:
        push 1
        pop  rax
        mov  edi, eax
        lea  rsi, [rip + msg]
        mov  rdx, 28
        syscall
        ret

    msg:
        .ascii "LIEF Runtime Extended Demo\n"
    """,
)

# Ensure the assembly is correctly JITed at the beggining of the allocated
# memory
for inst in islice(lief.runtime.disassemble(chunk.addr), 7):
    print(inst)

# Dump as raw bytes
raw_bytes = lief.runtime.Memory.read(chunk.addr, len(raw_inst))
print(raw_bytes.hex(":"))

# Change the permission to R-X
chunk.make_rx()

VoidVoidFunc = ctypes.CFUNCTYPE(None)  # void(*)()
hello_jit = VoidVoidFunc(chunk.addr)
hello_jit()

# Change the message
pos = raw_bytes.find(b"LIEF Runtime")
chunk.make_rw()
lief.runtime.Memory.write(b"Hello Python ctypes World!\n", chunk.addr + pos)
chunk.make_rx()

# Print again
hello_jit()

# Don't miss good practices!
chunk.deallocate()

Windows ARM64

x86-64

This part focuses on the ARM64 architecture but it works in the exact same way for Windows x86-64.

Similarly, we can JIT a Hello World shellcode on Windows ARM64 with the following snippet:

chunk = lief.runtime.Memory.mmap(
    lief.runtime.Process.page_size,
    lief.runtime.Memory.ANONYMOUS | lief.runtime.Memory.PRIVATE,
    lief.runtime.Memory.READ | lief.runtime.Memory.WRITE | lief.runtime.Memory.EXEC,
)

# The message printed by the shellcode. It must stay alive while the JITed
# function runs, so we keep a reference for the whole function.
MSG = b"Hello World\n"
msg = ctypes.create_string_buffer(MSG)

# The assembler resolves the symbols referenced by the shellcode through
# this config, mirroring the C++ ``AssemblerConfig::resolve_symbol`` override.
class Config(lief.assembly.AssemblerConfig):
    def resolve_symbol(self, name: str) -> int | None:
        kernel32 = lief.runtime.windows.dlopen("kernel32.dll")
        if kernel32 is None:
            print("Failed to dlopen kernel32.dll", file=sys.stderr)
            return None

        # Resolve the symbols that are used by the shellcode
        if name == "GetStdHandle":
            return lief.to_int(kernel32.dlsym("GetStdHandle"))
        if name == "WriteFile":
            return lief.to_int(kernel32.dlsym("WriteFile"))
        if name == "var_msg":
            return ctypes.addressof(msg)
        if name == "var_msg_len":
            return len(MSG)
        return None

config = Config()

# Shellcode dynamically compiled and whose referenced symbols are resolved
# at runtime by the provided config
lief.runtime.assemble(
    chunk.addr,
    r"""
    .text
        .global main
        .align 2

    main:
        stp     x29, x30, [sp, -64]!
        mov     x29, sp
        stp     x19, x20, [sp, 16]
        stp     x21, x22, [sp, 32]

        // Here we use symbols that are **dynamically** resolved by the
        // AssemblerConfig
        ldr     x19, =GetStdHandle
        ldr     x20, =WriteFile
        ldr     x21, =var_msg
        ldr     w22, =var_msg_len

        // Call GetStdHandle(STD_OUTPUT_HANDLE)
        // STD_OUTPUT_HANDLE = -11
        // GetStdHandle is stored in x19
        mov     w0, -11
        blr     x19

        // Call ---> bool WriteFile(
        //   HANDLE  hConsoleOutput,        // x0: from GetStdHandle (already set)
        //   const VOID *lpBuffer,          // x1: pointer to string
        //   DWORD   nNumberOfCharsToWrite, // w2: length of string
        //   LPDWORD lpNumberOfCharsWritten,// x3: pointer to written count
        //   LPVOID  lpReserved             // x4: NULL
        // );
        mov     x1, x21
        mov     w2, w22
        add     x3, sp, 48
        mov     x4, xzr
        blr     x20

        mov     x0, xzr
        ldp     x21, x22, [sp, 32]
        ldp     x19, x20, [sp, 16]
        ldp     x29, x30, [sp], 64
        ret
    """,
    config,
)

# Flush the instruction cache
chunk.cache_flush()

# Change the permission to R-X and invoke the JITed function
chunk.make_rx()

void_void_func = ctypes.CFUNCTYPE(None)  # void(*)()
hello_jit = void_void_func(chunk.addr)

# This call prints the message "Hello World" on the console
hello_jit()

# Don't miss good practices!
chunk.deallocate()

Note that this shellcode uses the external functions GetStdHandle and WriteFile that are dynamically resolved and injected into the shellcode at runtime. The shellcode is thus generated with dynamic information that would otherwise have been tedious to resolve at the assembly level.

OSX ARM64

SIP

Allocating and executing JIT memory on macOS requires System Integrity Protection (SIP) to be disabled. The snippet below therefore only runs the JIT part when reports that SIP is disabled.

Similarly, we can JIT a Hello World shellcode on macOS ARM64. The shellcode calls write, which is dynamically resolved from libSystem at runtime:

# The JIT below allocates and executes RWX memory. On macOS this is only
# permitted when SIP is disabled, so we check it before.
if lief.runtime.osx.Host.is_sip_enabled:
    print("SIP is enabled: skipping the JIT memory example", file=sys.stderr)
    return

# Note the `JIT` flag here that is used to switch the allocated chunk into
# R-X once the shellcode is committed.
chunk = lief.runtime.Memory.mmap(
    lief.runtime.Process.page_size,
    lief.runtime.Memory.ANONYMOUS
    | lief.runtime.Memory.PRIVATE
    | lief.runtime.Memory.JIT,
    lief.runtime.Memory.READ | lief.runtime.Memory.WRITE,
)

# The message printed by the shellcode. It must stay alive while the JITed
# function runs, so we keep a reference for the whole function.
MSG = b"Hello World\n"
msg = ctypes.create_string_buffer(MSG)

# libSystem re-exports the libc symbols (write, ...)
libsystem = lief.runtime.osx.dlopen("libSystem.B.dylib")

# The assembler resolves the symbols referenced by the shellcode through
# this config, mirroring the C++ ``AssemblerConfig::resolve_symbol`` override.
class Config(lief.assembly.AssemblerConfig):
    def resolve_symbol(self, name: str) -> int | None:
        if libsystem is None:
            print("Failed to dlopen libSystem.B.dylib", file=sys.stderr)
            return None

        # Resolve the symbols that are used by the shellcode
        if name == "write":
            return lief.to_int(libsystem.dlsym("write"))
        if name == "var_msg":
            return ctypes.addressof(msg)
        if name == "var_msg_len":
            return len(MSG)
        return None

config = Config()

# Shellcode dynamically compiled and whose referenced symbols are resolved
# at runtime by the provided config
lief.runtime.assemble(
    chunk.addr,
    r"""
    .text
        .global main
        .align 2

    main:
        stp     x29, x30, [sp, -16]!
        mov     x29, sp

        // Here we use symbols that are **dynamically** resolved by the
        // AssemblerConfig
        ldr     x8, =write          // write(2) libc wrapper
        ldr     x1, =var_msg        // buffer
        ldr     w2, =var_msg_len    // length

        // write(STDOUT_FILENO, msg, len)
        mov     x0, 1
        blr     x8

        mov     x0, xzr
        ldp     x29, x30, [sp], 16
        ret
    """,
    config,
)

# Flush the instruction cache
chunk.cache_flush()

# Change the permission to R-X and invoke the JITed function
chunk.make_rx()

void_void_func = ctypes.CFUNCTYPE(None)  # void(*)()
hello_jit = void_void_func(chunk.addr)

# This call prints the message "Hello World" on the console
hello_jit()

# Don't miss good practices!
chunk.deallocate()

Android ARM64

Similarly, we can JIT a small AArch64 stub on Android to print LIEF Runtime Extended Demo. The Python tab calls write, which is dynamically resolved from Bionic libc at runtime:

chunk = lief.runtime.Memory.mmap(
    lief.runtime.Process.page_size,
    lief.runtime.Memory.ANONYMOUS | lief.runtime.Memory.PRIVATE,
    lief.runtime.Memory.READ | lief.runtime.Memory.WRITE | lief.runtime.Memory.EXEC,
)

class Config(lief.assembly.AssemblerConfig):
    def resolve_symbol(self, name: str) -> int | None:
        libc = lief.runtime.android.dlopen("libc.so")
        if libc is None:
            print("Failed to dlopen libc.so", file=sys.stderr)
            return None

        if name == "write":
            return lief.to_int(libc.dlsym("write"))
        return None

config = Config()

raw_inst = lief.runtime.assemble(
    chunk.addr,
    r"""
    .text
        .global main
        .align 2

    main:
        stp     x29, x30, [sp, -16]!
        mov     x29, sp

        ldr     x8, =write

        mov     x0, 1    // STDOUT_FILENO
        adr     x1, msg  // buf = &msg
        mov     x2, 27   // len = len("LIEF Runtime Extended Demo\n")
        blr     x8       // write(STDOUT_FILENO, msg, 27)

        ldp     x29, x30, [sp], 16
        ret

    msg:
        .ascii "LIEF Runtime Extended Demo\n"
    """,
    config,
)

# Disassemble the JITed stub from memory
for inst in islice(lief.runtime.disassemble(chunk.addr), 9):
    print(inst)

# Dump as raw bytes
raw_bytes = lief.runtime.Memory.read(chunk.addr, len(raw_inst))
print(raw_bytes.hex(":"))

# Flush the instruction cache before executing the freshly written code
# (this is required on AArch64).
chunk.cache_flush()

# Change the permission to R-X
chunk.make_rx()

VoidVoidFunc = ctypes.CFUNCTYPE(None)  # void(*)()
hello_jit = VoidVoidFunc(chunk.addr)
hello_jit()

# Change the message
pos = raw_bytes.find(b"LIEF Runtime")
chunk.make_rw()
lief.runtime.Memory.write(b"Hello Python ctypes World!\n", chunk.addr + pos)
chunk.make_rx()
chunk.cache_flush()

# Print again
hello_jit()

# Don't miss good practices!
chunk.deallocate()