┌───────────────────────┐ ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ │ │ █ █ █ █ █ █ │ │ █ █ █ █ █▀▀▀▀ │ │ █ █ █ █ ▄ │ │ ▄▄▄▄▄ │ │ █ █ │ │ █ █ │ │ █▄▄▄█ │ │ ▄ ▄ │ │ █ █ │ │ █ █ │ │ █▄▄▄█ │ │ ▄▄▄▄▄ │ Fine grained load time ASLR for ELF executables │ █ │ in X86_64 Linux │ █ │ ~ elfmaster └───────────────────█ ──┘ ## Table of Contents - Fine grained load time ASLR for ELF executables in X86_64 Linux - Introduction with a little background - Origins of this paper - Original research goals - Shiva as a binary patching engine - 1. ELF Microprograms - 2. ELF Micropatches - Illustration 1.0: Loaded Shiva module in process image - ELF microprograms for security hardening in the process image - What is ASLR and gASLR? - ASLR definition - gASLR definition - Chained dynamic linking concept - Illustration 1.1: Chained dynamic linking execution flow - Writing a basic Shiva module in C - Build Shiva for x86_64 Linux - Build for x86_64 Linux - Source code for our first Shiva module: sections.c - Compiling our first Shiva module (aka ELF microprogram) - Testing our sections.o module. - Using shiva-ld to "prelink" our sections.o module to /bin/ls - shiva-ld usage - Implementing gASLR as a Shiva module - Design and implementation - Our gASLR requires preserved text relocations - Emit a preserved text relocation section - Source code snippet of gASLR.c line 466: move_function() - ELF executable must have a large code model - How to build an executable for gASLR compatibility - gASLR source code - Summary of gASLR.c source code - gASLR demo - Source code for test.c in gASLR demo - Running ./test with standard ASLR - gASLR in action - Future work on gASLR - Closing thoughts - ELF binary hacking workshops - Contact ## Introduction with a little background Good day ladies and gentlemen. It is my honor to share some of my research into writing custom ELF dynamic linkers, ELF ABI enhancements, and chained dynamic linking (Multiple dynamic linkers simultaneously) for solving complex security problems such as process-image hardening with load-time granular ASLR-- which is a security feature that I will be demonstrating in this paper. ### Origins of this paper This paper, in many respects, is a follow up to my paper in tmpout issue 2, "preloading the linker for fun and profit"[1] which illustrates how a custom dynamic linker can be chained together with ld-linux.so and load ET_REL objects to implement ELF viruses. That research ultimately led into the project known as Shiva[2] and also see Shiva Github[3]. ### Original research goals The original paper showed how chaining a custom ELF interpreter could be used as a slick new Linux virus technique, yes, but I knew that it had strong potential for much more: security hardening modules , debugging modules, and later realized that it could be adapted for ELF binary patching. Shiva was born... and it was a custom ELF dynamic linker that was originally designed for DevSecOps instrumentation. I wanted to give developers the ability to rapidly design complex in-process security features and hardening modules. I also gave early versions of Shiva a PTRACE-similar interface known as ShivaTrace that gives lightning fast instrumentation for custom tracers and debugging engines. See my Toorcamp 2022 talk "Advancing the programmability and security of the Linux userland runtime"[4]. ### Shiva as a binary patching engine Shiva eventually became funded by DARPA AMP and it evolved into a binary patching engine allowing developers with no reverse engineering knowledge to write powerful binary patches in C. We will not be exploring Shiva's binary patching capabilities today. In the mean time feel free to read "A friendly user-manual to ELF micropatching with Shiva", which demonstrates Shiva's ELF binary patching capabilities for AArch64, or take a peek at the 2023 DEFCON talk "Revolutionizing ELF binary patching with Shiva"[6] -- I am waiting on approval from DARPA to release my x86_64 Shiva blog-post which will show users an in-depth guide to binary patching with Shiva. Meanwhile... let's stick to it's other capabilities... Shiva is a multi-purpose dynamic linker with two major module types. Modules are ELF relocatable objects that come in two varieties for Shiva loading: #### 1. ELF Microprograms These are programs written in C and compiled into ET_REL objects that are installed by Shiva at load-time. These ELF microprograms are powerful for writing security modules, as I illustrated in my 2022 Toorcamp presentation: Debugging engines, exploit mitigations, and tracers. #### 2. ELF Micropatches ELF Micropatches in Shiva are similar to ELF microprograms but they have no init function; instead they use symbolic definition and custom Shiva macros to interpose and transform code and data intuitively. This allows developers to patch software that is already compiled-- allowing developers with little to no reverse engineering knowledge to apply complex binary patches written in C. Shiva uses ET_REL objects as modules because this type of ELF file contains not only the code and data but the meta-data that is necessary to build a program image from scratch. The .text relocations are incredibly valuable for re-linking and transforming code, thus allowing Shiva to modify, transform, and re-program various parts of the program image at load-time with extreme flexibility, descriptiveness, and power. In todays paper we will be using the ELF microprogram loading capabilities. It is different than the ELF patching capabilities, although both use relocatable modules. ### Illustration 1.0: Loaded Shiva module in process image ELF ET_DYN EXECUTABLE (PIE) ┌────────────────────┐ │ TEXT SEGMENT │ ├────────────────────┤ SHIVA MODULE │ .interp │ ├────────────────────┤ Ox6000000 ┌────────────────────┐ │ .gnu.hash │ │ TEXT SEGMENT │ ├────────────────────┤ ├────────────────────┤ ▶▶▶▶▶▶▶▶▶▶▶▶▶ │ .dynsym │ │ .text │ ├────────────────────┤ ├────────────────────┤ The patch code │ .dynstr │ │ .rodata │ and executable ├────────────────────┤ ├────────────────────┤ code are linked │ .rela.dyn │ │ .plt │ together at runtime ├────────────────────┤ 0x8000000 ├────────────────────┤ │ .rela.plt │ │ DATA SEGMENT │ ◀◀◀◀◀◀◀◀◀◀◀◀◀ ├────────────────────┤ ├────────────────────┤ │ .init │ │ .data │ ├────────────────────┤ ├────────────────────┤ ▼ etc... ▼ │ .bss │ ├────────────────────┤ ├────────────────────┤ │ DATA SEGMENT │ │ .got.plt │ ├────────────────────┤ └────────────────────┘ │ .init_array │ The Shiva modules use ├────────────────────┤ a large code model; │ .fini_array │ all call instructions ├────────────────────┤ are indirect through │ .dynamic │ the PLT/GOT and so are ├────────────────────┤ global variable │ .got │ references ├────────────────────┤ │ .data │ ├────────────────────┤ │ .bss │ └────────────────────┘ Shiva module process image - original[7] ## ELF microprograms for security hardening in the process image This fascinating concept of chaining a custom dynamic linker into the execution flow helps to bridge the gap that allows developers to build sophisticated security features without being a developer of glibc, gcc, binutils, ld, ld-linux.so, and Linux. Hardening the process image in Linux is currently done with a combination of binary mitigations and security features that have been implemented at various layers throughout the compilers, linkers, libraries and OS code, most often requiring coordinated efforts between multiple projects. ELF microprograms can load and execute before ld-linux.so has even had a chance to run, giving the developer execution precedence over the entire process image. I have designed extremely fast function tracers, specialized debuggers, exploit mitigations, viruses, and yes backdoors! Among many other fascinating magical concepts while experimenting with my programmable and custom dynamic linker that I now call Shiva. I even wrote a fuzzer for ld-linux.so in 2022. ### What is ASLR and gASLR? #### ASLR definition ASLR (Address space layout randomization) is a feature in modern OS's such as Linux that randomizes the base address of the executable and libraries at each load-time of the program, thus discouraging exploits from being able to use hardcoded addresses and making memory corruption exploitation more difficult in general. ASLR works with PIE ELF Executables so that they can be randomly loaded without the need for text relocations at runtime. The code is all position independent and the file type is ET_DYN. The weakness is such that ASLR only randomizes the base address. The base address can be leaked by an exploit that is able to gain a read-primitive. Once an attacker determines the base address it is trivial to know the location of code gadgets and global data. #### gASLR definition gASLR is "Granular ASLR". It randomizes the program at a granular level. In other words it does fine-grained randomization. Instead of just randomizing the base address it randomizes the address of every function at load-time, and optionally can randomize the location of PLT entries and global data. This aims to harden the process-image from memory corruption attacks like ASLR but if an attacker leaks the base address they won't be able to use that information to locate code or data by relative offsets anymore. Today we will be introducing a prototype for gASLR[8] in the form of a Shiva ELF microrogram. Our current implementation re-writes every function within the main executable at load-time at a random location in memory and re-links the code and data just in time before program execution. ## Chained dynamic linking concept I highly recommend that you read my original paper[1] to understand the concept of chaining in a custom dynamic linker. There's an entire algorithm I documented on replacing PT_INTERP with "/lib/shiva" instead of "/lib/x86_64-linux-gnu/ld-linux.so". This replaces the primary ELF Interpreter. The new ELF interpreter, once finished with it's work, must be responsible for mapping the original ld-linux.so into memory and transferring control to it when all is said in done. Generally speaking there has always been one dynamic linker, it is the ELF interpreter and it's pathname is set as a string in the PT_INTERP segment of the executable file. The Linux kernel opens the ELF executable and then searches for it's interpreter via the PT_INTERP segment. Usually we see something like "/lib/x86_64-linux-gnu/ld-linux.so.1". We are going to replace this with the path to our new custom ELF interpreter, in this case "/lib/shiva". [Linux kernel] -> [New ELF Interpreter] -> [Microprograms init function] -> [ld-linux.so] -> [_start()] ### Illustration 1.1: Chained dynamic linking execution flow Shiva's dynamic linking workflow - original[9] LINUX KERNEL (1) SYS_EXEC │ │ Shiva passes control (4) │ to the RTLD for ▼ (2) further setup (5) (6) ┌─ Shiva ───────────────┐ ┌─ RTLD ───────────────┐ ┌─ Target Program ─┐ │ │ │ │ │ │ │ /lib/shiva │──────▶│ /lib64/ld-linux.so │──▶│ /bin/test │┐ │ │ │ │ │ ││ └───────────────────────┘ └──────────────────────┘ └──────────────────┘│ │ ▲│ │ │ │ Shiva links ││ Shiva maps RTLD maps │ RTLD /bin/test is now│ │ the patch ││ the RTLD dependencies │ applies patched and │ │ module into ││ "ld-linux.so" │ relocations linked with │ │ the process ││ into memory │ "patch1.o" │ │ │▼ ▼ │ │(3)┌─ MODULES ────────────────────┐(8) ┌─ LIBS ───────────┐ │ │ │ │ │ │ │ └──▶│ /opt/shiva/modules/patch1.o │ │ /lib64/libc.so │◀────────┬───────┘ │ │ │ │ │ └─ Shiva patch module ─────────┘ └─ Shared library ─┘ │ ▲ │ └──────────────────────────────────────┘ ## Writing a basic Shiva module in C Similarly to Linux LKM's, Shiva modules have an init function that will execute as the module entry point. Shiva modules can execute this init routine either before ld-linux.so runs or after ld-linux.so runs. Depending on what the developer is trying to accomplish. For example if the developer was trying to write a Shiva module that fuzzes ld-linux.so then you would want the module to run before ld-linux.so so that it could mutate the Dynamic auxiliary data before ld-linux.so is executed. See `shiva/modules/include/shiva_module.h` `SHIVA_MODULE_PRE_LDSO` or `SHIVA_MODULE_POST_LDSO` If you do not specify one then Shiva will default to `SHIVA_MODULE_POST_LDSO`. Shiva modules have access to the following structures and code libraries that are statically linked into the binary of the Shiva interpreter itself: #### 1. struct shiva_ctx from shiva/shiva.h This struct is the main context structure for Shiva, it contains all other important structs within it such as `struct shiva_module`. The Shiva module's `shiva_init` function takes this as it's one and only argument, giving the developer of a Shiva module complete access to Shiva's internal ELF linking and binary routines. ####2. libelfmaster This library[10] is used by Shiva for all of the internal ELF heavy lifting, parsing, etc. See https://github.com/elfmaster/libelfmaster ####3. musl-libc This is the C libary used to compile Shiva. It is baked into the Shiva binary and can be accessed in addition to glibc from within the scope of Shiva modules. In order to force musl-libc resolution over glibc, use the `SHIVA_MODULE_FORCE_MUSL_RESOLUTION` macro. This can be necessary at time to avoid certain glibc functions that use STT_IFUNC relocations, which Shiva does not support. ####4. Dynamically linked symbols (Functions and global variables) If the program that you are writing a module for has access to a specific shared library at load-time then so do you. If a library is loaded by `dlopen()` then Shiva modules cannot access that symbol without special hacking. This will be fixed in a future version. ####5. libcapstone Shiva uses libcapstone internally to disassemble code. Shiva modules can use libcapstone to disassemble the code of the target process, or anything else that they choose to open. ### Build Shiva for x86_64 Linux If you haven't already, checkout the source code to Shiva's `x86_64_port` branch. ### Build for x86_64 Linux Clone Shiva[11] and read README.md for the x86_64 build instructions.

git clone https://github.com/advanced-microcode-patching/shiva
### Source code for our first Shiva module: sections.c Our first Shiva module makes use of the Shiva context pointer `shiva_ctx_t *ctx` and the `libelfmaster` API to iterate over the section headers of the loaded ELF executable, printing them to stdout before the target program runs. Navigate to `shiva/modules/x86_64_modules/sections` The `sections.c` file contains the source code for our first module.

#define _GNU_SOURCE
#include "shiva_module.h"
#include "shiva.h"
#include "libelfmaster.h"

SHIVA_MODULE_POST_LDSO; // Tell Shiva to execute shiva_init() after ld-linux.so runs

int
shiva_init(struct shiva_ctx *ctx)
{
    elfobj_t *elfobj = &ctx->elfobj;
    elf_section_iterator_t shdr_iter;
    struct elf_section shdr;
    size_t i = 0;

    printf("Printing ELF section headers of program before runtime!\n");

    elf_section_iterator_init(elfobj, &shdr_iter);
    while (elf_section_iterator_next(&shdr_iter, &shdr) == ELF_ITER_OK) {
            printf("[%02x] %s\n", i++, shdr.name);
    }
}
### Compiling our first Shiva module (aka ELF microprogram) The Shiva modules must be compiled with a large code model so that they can properly link into a process image where the module is further than 2GB from the main executable in memory.

$ gcc -I /opt/shiva/include -I /opt/elfmaster/include -mcmodel=large -c sections.c
You can also run the Makefile that is included in this directory. Simply type `make module` to build the sections.o module. ### Testing our sections.o module. Shiva is both a regular program and an interpreter. Just like `ld-linux.so` it can be run directly or used as the ELF program interpreter. When testing the effects of a module it can be useful to simply run Shiva directly as the program loader. See the Shiva UserlandExec source code [16]. Let's first run /bin/ls by itself to get a baseline and then again with Shiva to see the effect that the module has on our program. We are expecting it to print out the ELF section headers to `/bin/ls` or whatever executable you choose to run with the module.

$ /bin/ls
Makefile  prog.c  sections.c  sections.o
Set the `sections.o` module with the `SHIVA_MODULE_PATH` environment variable and run `/bin/ls` again but this time with Shiva as the loader.

$ export SHIVA_MODULE_PATH=./sections.o
$ shiva /bin/ls
Printing ELF section headers of program before runtime!
[00] 
[01] .interp
[02] .note.gnu.property
[03] .note.gnu.build-id
[04] .note.ABI-tag
[05] .gnu.hash
[06] .dynsym
[07] .dynstr
[08] .gnu.version
[09] .gnu.version_r
[0a] .rela.dyn
[0b] .rela.plt
[0c] .init
[0d] .plt
[0e] .plt.got
[0f] .plt.sec
[10] .text
[11] .fini
[12] .rodata
[13] .eh_frame_hdr
[14] .eh_frame
[15] .init_array
[16] .fini_array
[17] .data.rel.ro
[18] .dynamic
[19] .got
[1a] .data
[1b] .bss
[1c] .gnu_debugaltlink
[1d] .gnu_debuglink
[1e] .shstrtab
Makefile  prog.c  sections.c  sections.o
$ 
The `shiva_init()` function in in `sections.o` module is executed and it iterates over the section header table printing each section string name of the loaded ELF executable. For real-world modules we don't want to have to invoke Shiva directly everytime we run the program, it would defeat the purpose of any kind of security features that we add since a user could simply run the program without Shiva. ### Using shiva-ld to "prelink" our sections.o module to /bin/ls Create a local copy of /bin/ls in our CWD (In `shiva/modules/x86_64_modules/sections`)

$ cp /bin/ls .
#### shiva-ld usage

-=[Shiva Prelinker v2.0.]=-Usage: shiva-ld -e test_bin -p patch1.o -i /lib/shiva -s /opt/shiva/modules/ -o test_bin_final [-cdN]
[-e] --input_exec   Input ELF executable
[-p] --input_patch  Input ELF patch (NOTE: should be a .so patch when the -N flag is used)
[-i] --interp_path  Interpreter search path, i.e. "/lib/shiva"
[-s] --search_path  Module search path (For patch object)
[-o] --output_exec  Output executable
[-d] --disable-cflow    Do not generate CFG data (i.e. .shiva.xref and .shiva.branch)
[-N] --needed-injection Injects shared object dependency via DT_NEEDED entry
The following command will copy `ls` to `ls.new`. The new version `ls.new` will have the path `/lib/shiva` set as the primary ELF interpreter and the dynamic segment will be moved into a newly added PT_LOAD segment in order to make room for several new custom dynamic segment types. There are also three new ELF sections that you will see in `ls.new` which are `.shiva.xref`, `.shiva.branch`, and `.shiva.strtab`. These sections live within the new PT_LOAD segment and contains control flow information that Shiva uses to generate relocation meta-data at load-time.

$ shiva-ld -e ls -p sections.o -i /lib/shiva -s /opt/shiva/modules -o ls.new
................................................................................
[+] Input executable: ls
[+] Input search path for patch: /opt/shiva/modules
[+] Basename of patch: sections.o
[+] Output executable: ls.new
Updating dynamic segment vaddr: 0x24000 offset: 0x22000 size: 0x1f0
Finished prelinking.
Copy the Shiva module `/opt/shiva/modules` because that's where we specified the module search path with the `-s` flag above.

$ sudo cp sections.o /opt/shiva/modules
Now run `ls.new` directly. The Linux kernel will invoke `/lib/shiva` as the primary ELF interpreter. Shiva will check the dynamic segment for `SHIVA_DT_NEEDED` and `SHIVA_DT_SEARCH` to find the basename of our module `sections.o` and the search path of our module `/opt/shiva/modules`. Once Shiva has finished loading and relocating this module it will map the original interpreter into memory and eventually pass control to it for finalizing the loading and linking of shared libraries. In this module we specified `SHIVA_MODULE_POST_LDSO` which instructs Shiva to run the module after `ld-linux.so` has finished.

$ ./ls.new
Printing ELF section headers of program before runtime!
[00] 
[01] .interp
[02] .note.gnu.property
[03] .note.gnu.build-id
[04] .note.ABI-tag
[05] .gnu.hash
[06] .dynsym
[07] .dynstr
[08] .gnu.version
[09] .gnu.version_r
[0a] .rela.dyn
[0b] .rela.plt
[0c] .init
[0d] .plt
[0e] .plt.got
[0f] .plt.sec
[10] .text
[11] .fini
[12] .rodata
[13] .eh_frame_hdr
[14] .eh_frame
[15] .init_array
[16] .fini_array
[17] .data.rel.ro
[18] .dynamic
[19] .got
[1a] .data
[1b] .bss
[1c] .gnu_debugaltlink
[1d] .gnu_debuglink
[1e] .shstrtab
[1f] .shiva.strtab
[20] .shiva.xref
[21] .shiva.branch
ls  ls.new  Makefile  prog.c  sections.c  sections.o
We were able to run the executable `ls.new` directly and no longer need to run `/bin/shiva` as the loader. Shiva gets automatically loaded by the kernel as the ELF interpreter when we use the prelinker `shiva-ld` to attach the necessary meta-data to the target executable. Let me show you the modifications to the `PT_INTERP` and `PT_DYNAMIC` segments...

$ readelf -l ls.new | grep interpreter
      [Requesting program interpreter: /lib/shiva]
$ readelf -d ls.new
Dynamic section at offset 0x22000 contains 31 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libselinux.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x4000
 0x000000000000000d (FINI)               0x17084
 0x0000000000000019 (INIT_ARRAY)         0x20fd0
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x20fd8
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x3b0
 0x0000000000000005 (STRTAB)             0xf88
 0x0000000000000006 (SYMTAB)             0x400
 0x000000000000000a (STRSZ)              1446 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x21c58
 0x0000000000000002 (PLTRELSZ)           2400 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x2af8
 0x0000000000000007 (RELA)               0x16e8
 0x0000000000000008 (RELASZ)             5136 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000000000001e (FLAGS)              BIND_NOW
 0x000000006ffffffb (FLAGS_1)            Flags: NOW PIE
 0x000000006ffffffe (VERNEED)            0x1628
 0x000000006fffffff (VERNEEDNUM)         2
 0x000000006ffffff0 (VERSYM)             0x152e
 0x000000006ffffff9 (RELACOUNT)          201
 0x0000000060000018 (Operating System specific: 60000018)                0x241f0
 0x0000000060000017 (Operating System specific: 60000017)                0x24203
 0x0000000060000019 (Operating System specific: 60000019)                0x2420e
 0x0000000000000000 (NULL)               0x0
Congratulations, you now know how to build a basic ELF microprogram as a Shiva module. ## Implementing gASLR as a Shiva module OK let's get to the cool stuff! gASLR (Granular ASLR) at load-time, implemented as an ELF microprogram that get's loaded and installed by Shiva at runtime. ### Design and implementation Although our gASLR can technically be applied to existing ELF executables, say on an ubuntu 24 install, it would be slow since those ELF binaries do not have a preserved `.rela.text` section by default, and it will be unreliable since they are built with a small code model which may prevent functions that are randomly moved more than 2GB away from eachother from being properly re-linked. ### Our gASLR requires preserved text relocations Generally speaking the ELF relocations that apply to the `.text` section are used for linking the programs compilation units with `/bin/ld`. The ELF relocations of this type only apply to ELF relocatable objects by default. Notice that `.o` files have `.rela.text` sections but executables do not. This is because executables don't require .text relocations since the compilation units (Of code and data) are already linked into the ELF executable. For those of us crazy enough to want to re-link the `.text` section at program load-time we ideally want there to exist a preserved `.rela.text` section in the executable we are applying gALSR too. Our Shiva module needs to move the location of every function of the target program into a random memory location which requires the re-linking of every function. This means that in order to efficiently re-write every function into a new address range we will need the **text relocations** of the executable that we are applying gASLR too. #### Emit a preserved text relocation section

$ gcc -Wl,--emit-relocs test.c -o test
The `.rela.text` section in an executable is slightly different than those in an ET_REL file. The text relocations in an ET_REL object have `r_offset` values that are offsets relative to the start of the `.text` section. In an ELF executable the `.rela.text` has `r_offset` values that are relative from the base of the program. Since the text relocations are relative to the base of the program then the `r_offset` values will become invalid once we randomize the location of each function. So our gASLR module must update the relocation records for each function before using it to re-link them. #### Source code snippet of gASLR.c line 466: move_function()

bool
move_function(struct shiva_ctx *ctx, struct aslr_ctx *aslr, struct func_entry *fe)
{

        size_t delta;
        struct reloc_entry *rel_entry;

        aslr_debug("Moving function %s to %p\n", fe->symbol.name, fe->n_mem);
        /*
         * Copy function code from its old address to its new address
         */
        memcpy(fe->n_mem, (uint8_t *)fe->runtime_vaddr, fe->func_len);
        /*
         * Update the relocation entries for that function so that they
         * reflect the correct r_offset's after it is moved.
         */
        TAILQ_FOREACH(rel_entry, &fe->reloc_list, _linkage) {
                delta = rel_entry->rel.offset - fe->base_vaddr;
                rel_entry->rel.offset = delta;
        }
        /*
         * Now that function has been moved to a new location
         * fixup the function using the modified relocation records.
         */
        return relocate_function(ctx, aslr, fe);
}
In the source code above we simply subtract the base address of each function from it's `r_offset` values to achieve the new r_offset values. Our current implementation of gASLR moves each function it's own random base address that was mmap'd. In future versions I will make sure that each function is placed at a random offset of the new base address to make locating functions less predictable at a page boundary. ### ELF executable must have a large code model As we mentioned earlier Shiva modules must be compiled with the large code model, while typical ELF executables use the small code model by default. The gASLR implementation randomly relocates the executable’s `.text` section, and mmap() may place code more than 2 GB apart. Workarounds such as MAP_32BIT reduce available entropy and weaken security. Therefore, the current gASLR implementation requires the target executable to be built with the large code model. In the future I have some hacks that will get gASLR working with executable's that have a small code model too, but in the meantime we only support executables with large code models. #### How to build an executable for gASLR compatibility 1. Preserved text relocations 2. Large code model

gcc -mcmodel=large -Wl,--emit-relocs test.c -o test
## gASLR source code Change to the directory `shiva/modules/x86_64_modules/aslr` $ cat gASLR.c

/*
 * X86_64 gASLR by Ryan O'Neill
 * A 2025 Shiva module
 *
 * Target program must be built with a large code model:
 *  gcc -mcmodel=large
 * Target program must be built with preserved text relocations:
 *  gcc -Wl,--emit-relocs
 *
 * cp gASLR.o /opt/shiva/modules
 * shiva-ld -e <binary> -p gASLR.o -s /opt/shiva/modules -i /lib/shiva -o test -d
 *
 */

#define _GNU_SOURCE
#include "../../include/shiva_module.h"
#include "../../../shiva.h"
#include "/opt/elfmaster/include/libelfmaster.h"

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/queue.h>

#include <stdarg.h>
#include <stdio.h>

#if defined DEBUG
    #define aslr_debug(...) {\
    do {\
        fprintf(stdout, "[%s:%s:%d] ", __FILE__, __func__, __LINE__); \
        fprintf(stdout, __VA_ARGS__);   \
    } while(0); \
}
#else
      #define aslr_debug(...)
#endif

typedef struct reloc_entry {
    struct elf_relocation rel;
    TAILQ_ENTRY(reloc_entry) _linkage;
} reloc_entry_t;

#define ASLR_FUNC_F_ENTRYPOINT  (1 << 0) // is pointed to by ehdr->e_entry

#define ELF_RUNTIME_BASE(x) (x + ctx->ulexec.base_vaddr)

typedef struct func_entry {
    uint64_t base_vaddr;    // on disk
    uint64_t runtime_vaddr; // after initial ASLR at runtime
    uint64_t new_base_vaddr; // at runtime after Granular ASLR (New location)
    uint8_t *o_mem;     // memory of old location
    uint8_t *n_mem;     // memory mapping of new location
    struct elf_symbol symbol;
    struct elf_section section;
    size_t func_len;
    uint64_t flags;
    TAILQ_HEAD(, reloc_entry) reloc_list;
    TAILQ_ENTRY(func_entry) _linkage;
} func_entry_t;

typedef struct aslr_ctx {
    TAILQ_HEAD(, func_entry) orig_func_list; // orig funclist
    uint64_t base_vaddr;
} aslr_ctx_t;

#define HEAP_INITIALIZER NULL

struct elf_section text_section, got_section;

/*
 * Our modules shiva_init() function should run after ld-linux.so.
 * This phase is implicit if you don't define this so it's technically
 * not necessary, but it's important to be explicit.
 */
SHIVA_MODULE_POST_EXEC_PHASE;

bool
build_func_list(struct shiva_ctx *ctx, struct aslr_ctx *aslr,
    size_t *fn_count)
{
    size_t text_size;
    uint64_t text_addr;
    struct elf_section text;
    struct elf_symbol symbol;
    elf_relocation_iterator_t rel_iter;
    elf_symtab_iterator_t sym_iter;
    struct elf_relocation rel;

    TAILQ_INIT(&aslr->orig_func_list);

    if (elf_section_by_name(&ctx->elfobj, ".text", &text) == false) {
        fprintf(stderr, "Failed to get section .text\n");
        return false;
    }
    text_section = text;

    if (elf_section_by_name(&ctx->elfobj, ".got", &got_section) == false ) {
        fprintf(stderr, "Failed to get section .got\n");
        return false;
    }
    elf_symtab_iterator_init(&ctx->elfobj, &sym_iter);
    while (elf_symtab_iterator_next(&sym_iter, &symbol) == ELF_ITER_OK) {
        if (symbol.type != STT_FUNC)
            continue;
        if (symbol.bind != STB_GLOBAL)
            continue;
        if (symbol.value >= text.address &&
            symbol.value < text.address + text.size) {

            struct func_entry *fe;

            fe = calloc(1, sizeof(*fe));
            if (fe == NULL) {
                perror("calloc");
                return false;
            }

            /*
             * This function lives in the .text
             */
            if (symbol.value == elf_entry_point(&ctx->elfobj)) {
                fe->flags |= ASLR_FUNC_F_ENTRYPOINT;
            }
            fe->symbol = symbol;
            fe->section = text;
            fe->base_vaddr = symbol.value;
            fe->func_len = symbol.size;

            TAILQ_INIT(&fe->reloc_list);

            /*
             * The .rela.text section in an ELF executable will
             * contain r_offset values that are absolute values vs.
             * values that are relative to the beginning of the
             * .text section as is the case with ET_REL objects.
             * This makes sense since the absolute addresses didn't
             * exist until the ET_REL objects were linked into a
             * final executable, and thus the relocation tables get
             * updated with the absolute r_offset's that are relative
             * to the base (instead of relative to the .text section).
             *
             * NOTE: See -z --emit-relocs
             */
            elf_relocation_iterator_init(&ctx->elfobj, &rel_iter);
            while (elf_relocation_iterator_next(&rel_iter, &rel)
                == ELF_ITER_OK) {
                if (strcmp(rel.shdrname, ".rela.text") != 0)
                    continue;
                if (rel.offset < fe->base_vaddr)
                    continue;
                if (rel.offset >= fe->base_vaddr + fe->func_len)
                    continue;
                /*
                 * Only process relocs that are fixing up the
                 * current function.
                 */
                struct reloc_entry *re = malloc(sizeof(*re));
                if (re == NULL) {
                    perror("malloc");
                    return false;
                }
                memcpy(&re->rel, &rel, sizeof(struct elf_relocation));
                TAILQ_INSERT_TAIL(&fe->reloc_list, re, _linkage);
            }
            fe->runtime_vaddr = fe->base_vaddr + ctx->ulexec.base_vaddr;
            /*
             * Create new memory mapping to move function into.
             */
            fe->n_mem = mmap(NULL, fe->func_len, PROT_READ|PROT_WRITE,
                MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
            if (fe->n_mem == MAP_FAILED) {
                perror("mmap");
                return false;
            }
            fe->new_base_vaddr = (uint64_t)fe->n_mem;
            fe->o_mem = (uint8_t *)fe->runtime_vaddr;
            TAILQ_INSERT_TAIL(&aslr->orig_func_list, fe, _linkage);
            *fn_count++;
        }
    }
    return true;
}

/*
 * returns true or false
 * stores the offset of a given GOT entry (From the beginning of the GOT)
 * into uint64_t *gotoff
 */
bool
find_gotoff_by_symbol(struct shiva_ctx *ctx, const char *symname, uint64_t *gotoff)
{
    struct elf_section got;

    if (elf_section_by_name(&ctx->elfobj, ".got", &got) == false) {
        fprintf(stderr, "elf_section_by_name() failed on .got\n");
        return false;
    }


    return true;
}

#define ASLR_REL_F_NEEDS_PLTGOT (1 << 0)

bool
relocate_function(struct shiva_ctx *ctx, struct aslr_ctx *aslr, struct func_entry *fe)
{
    uint64_t page_vaddr;
    struct reloc_entry *rel_entry;
    bool res;
    uint8_t movabs_rdi[] = "\x48\xbf\x00\x00\x00\x00\x00\x00\x00\x00";
    uint8_t rip_call[] = "\xff\x15\x00\x00\x00\x00";
    uint64_t rel_flags = 0;
    elf_dynsym_iterator_t dsym_iter;
    size_t symoffset = 0;

    if (fe->flags & ASLR_FUNC_F_ENTRYPOINT) {
        aslr_debug("Relocating entry point  %s\n", fe->symbol.name);
    }
    TAILQ_FOREACH(rel_entry, &fe->reloc_list, _linkage) {
        uint8_t *r_ptr = (fe->flags & ASLR_FUNC_F_ENTRYPOINT) ?
            (uint8_t *)(ctx->ulexec.base_vaddr + rel_entry->rel.offset) :
            fe->n_mem + rel_entry->rel.offset;
        uint64_t rel_addr = (uint64_t)r_ptr;
        uint64_t rel_val;
        uint64_t plt_addr;
        struct elf_plt plt;
        struct elf_section shdr, got;
        uint64_t symval;
        struct elf_symbol symbol;
        char *p;

        page_vaddr = (uint64_t)r_ptr & ~4095;

        p = strchr(rel_entry->rel.symname, '@');
        if (p != NULL)
            *p = '\0';

        aslr_debug("Relocation type: %lu\n", rel_entry->rel.type);
        aslr_debug("Relocation offset: %#lx\n", rel_entry->rel.offset);
        aslr_debug("Relunit: %p\n", r_ptr);
        aslr_debug("Symbol name: %s\n", rel_entry->rel.symname);

        (void )mprotect((void *)page_vaddr, 4096, PROT_READ|PROT_WRITE|PROT_EXEC);

        if (elf_section_by_name(&ctx->elfobj, ".got", &got) == false) {
            fprintf(stderr, "elf_section_by_name() failed on .got\n");
            return false;
        }

        switch(rel_entry->rel.type) {
        case R_X86_64_GOTPCRELX:
        case R_X86_64_GOTPCREL:
            break; /* GOTPCREL relocs are still unfinished, break out here. */
            struct elf_symbol tmp;
            elf_dynsym_iterator_init(&ctx->elfobj, &dsym_iter);
            while (elf_dynsym_iterator_next(&dsym_iter, &tmp) == ELF_ITER_OK) {
                if (strcmp(tmp.name, rel_entry->rel.symname) == 0) {
                    uint64_t got_entry; // address of the GOT entry for the symbol
                    struct elf_symbol sym;
                    aslr_debug("R_X86_64_GOTPCREL(X) processing symbol %s\n", tmp.name);
                    /*
                     * First 3 entries of GOT[0, 1, 2] are reserved (hence the "sizeof(uintptr_t) * 3")
                     */
                    got_entry = ELF_RUNTIME_BASE(got.address) + symoffset + (sizeof(uintptr_t) * 3);
                    if (elf_symbol_by_name(&ctx->elfobj, tmp.name, &sym) == false) {
                        fprintf(stderr, "elf_symbol_by_name() failed to resolve symbol %s\n", tmp.name);
                        return false;
                    }
                    rel_val = got_entry - ELF_RUNTIME_BASE(sym.value);
                    aslr_debug("rel_val = %#lx - %#lx\n", got_entry, ELF_RUNTIME_BASE(sym.value));
                    aslr_debug("symoffset in got is %zu\n", symoffset);
                    aslr_debug("Setting reloc value to %#lx\n", rel_val);

                    *(uint64_t *)r_ptr = rel_val;
                    goto success;
                }
                symoffset += sizeof(uintptr_t);
             }
            fprintf(stderr, "failed to resolve R_X86_64_GOTPCREL\n");
            return false;
            break;
        case R_X86_64_GOT64:

            elf_dynsym_iterator_init(&ctx->elfobj, &dsym_iter);
            while (elf_dynsym_iterator_next(&dsym_iter, &tmp) == ELF_ITER_OK) {
                struct elf_plt plt_entry;

                if (elf_section_by_name(&ctx->elfobj, ".got", &got) == false) {
                    fprintf(stderr, "elf_section_by_name() failed on .got\n");
                    return false;
                }

                /* This symbol should be related to a GLOB_DAT or JUMPSLOT
                 * relocation.
                 */

                printf("Comparing %s and %s\n", tmp.name, rel_entry->rel.symname);
                if (strcmp(tmp.name, rel_entry->rel.symname) == 0) {
                    aslr_debug("R_X86_64_GOT64 processing symbol %s\n", tmp.name);
                    /*
                     * First 3 entries of GOT[0, 1, 2] are reserved
                     */
                    rel_val = symoffset + (sizeof(uintptr_t) * 3);
                    aslr_debug("symoffset in got is %zu\n", symoffset);
                    aslr_debug("Setting reloc value to %#lx\n", rel_val);
                    *(uint64_t *)r_ptr = rel_val;
                    break;
                }
                symoffset += sizeof(uintptr_t);
            }
            fprintf(stderr, "Failed to find symbol for R_X86_64_GOT64 reloc entry\n");
            return false;
            break;
        case R_X86_64_GOTPC64: /* GOT - P + A */
            if (elf_section_by_name(&ctx->elfobj, ".got", &got) == false) {
                fprintf(stderr, "elf_section_by_name() failed on .got\n");
                return false;
            }
            aslr_debug("R_X86_64_GOTPC64\n");
            rel_val = ELF_RUNTIME_BASE(got.address) - rel_addr + rel_entry->rel.addend;
            aslr_debug("rel_val = %#lx - %#lx + %#lx\n", ELF_RUNTIME_BASE(got.address),
                rel_addr, rel_entry->rel.addend);
            aslr_debug("Setting %p to %#lx\n", r_ptr, rel_val);
            *(uint64_t *)r_ptr = rel_val;
            break;
        case R_X86_64_GOTOFF64:
            aslr_debug("R_X86_64_GOTOFF64\n");
            if (elf_symbol_by_name(&ctx->elfobj, rel_entry->rel.symname, &symbol) == false) {
                fprintf(stderr, "elf_symbol_by_name failed on %s\n", symbol.name);
                return false;
            }
            if (symbol.type == STT_FUNC && symbol.bind == STB_GLOBAL) {
                struct func_entry *tmp;

                TAILQ_FOREACH(tmp, &aslr->orig_func_list, _linkage) {
                    if (strcmp(tmp->symbol.name, rel_entry->rel.symname) != 0)
                        continue;
                    symval = tmp->new_base_vaddr;
                    aslr_debug("symval set to %#lx\n", symval);
                    break;
                }
            } else {
                symval = ELF_RUNTIME_BASE(symbol.value);
                aslr_debug("symval set to %#lx\n", symval);
            }

            rel_val = symval + rel_entry->rel.addend -
                ELF_RUNTIME_BASE(got.address);

            aslr_debug("R_X86_64_GOTOFF64 setting r_ptr(%p) to rel_val: %#x\n",
                r_ptr, rel_val);

            *(int64_t *)r_ptr = rel_val;
            break;
        case R_X86_64_PLTOFF64: /* L - GOT + A */
            aslr_debug("R_X86_64_PLTOFF64\n");
            if (elf_plt_by_name(&ctx->elfobj, rel_entry->rel.symname,
                &plt) == false) {
                fprintf(stderr, "elf_plt_by_name() failed on %s\n",
                    rel_entry->rel.symname);
                return false;
            }
            if (elf_section_by_name(&ctx->elfobj, ".got", &got) == false) {
                fprintf(stderr, "elf_section_by_name() failed on .got\n");
                return false;
            }
            symval = plt.addr + ctx->ulexec.base_vaddr;
            aslr_debug("symval:(%#lx) - got:(%#lx) + addend(%#lx)\n",
                symval, ELF_RUNTIME_BASE(got.address), rel_entry->rel.addend);
            rel_val = symval - ELF_RUNTIME_BASE(got.address) + rel_entry->rel.addend;
            aslr_debug("rel_val: %#x\n", rel_val);
            aslr_debug("Setting PLT encoded-offset to GOT offset %#lx\n", got.address +
                rel_entry->rel.addend);
            *(uint32_t *)r_ptr = rel_val;
            break;
        case R_X86_64_PC32: /* S + A - P */
            aslr_debug("R_X86_64_PC32\n");
            if (rel_entry->rel.symname[0] == '.') {
                res = elf_section_by_name(&ctx->elfobj, rel_entry->rel.symname,
                    &shdr);
                if (res == false) {
                    fprintf(stderr, "elf_section_by_name() on %s failed\n",
                        rel_entry->rel.symname);
                    return false;
                }
                symval = ELF_RUNTIME_BASE(shdr.address);
                rel_val = symval + rel_entry->rel.addend - rel_addr;
                aslr_debug("Setting R_X86_64_PC32(1) reloc value (r_ptr: %p) to rel_val: %#x (destination symbol %s:%#lx)\n",
                   r_ptr, rel_val, rel_entry->rel.symname, symval);
                *(uint32_t *)&r_ptr[0] = rel_val;
                break;
            } else {
                if (elf_symbol_by_name(&ctx->elfobj, rel_entry->rel.symname,
                    &symbol) == false) {
                    fprintf(stderr, "elf_symbol_by_name() failed to find symbol %s\n",
                        symbol.name);
                    return false;
                }
                struct func_entry *tmp;

                if (symbol.type != STT_FUNC)
                    break;

                aslr_debug("Searching for symbol %s\n", symbol.name);
                TAILQ_FOREACH(tmp, &aslr->orig_func_list, _linkage) {
                    if (strcmp(tmp->symbol.name, rel_entry->rel.symname) != 0)
                        continue;
                    if (fe->flags & ASLR_FUNC_F_ENTRYPOINT) {
                        /*
                         * Instead of solving the normal relocation for
                         * a R_X86_64_PC32 here, we actually replace an
                         * entire 'lea 0x0(%rip), $rdi' instruction with
                         * a 'movabs <new_main> $rdi'. The memory mapping
                         * where main() lives will likely exceed what can
                         * be encoded into a 4 byte offset.
                         *
                         * init routes (i.e. _start, __libc_start_main, etc.)
                         * are all already compiled into the crt*.o files. So
                         * while main() and all other functions compiled may
                         * be in a large code model, the init routines are not.
                         */ 
                        if (strcmp(tmp->symbol.name, "main") == 0) {
                            uint8_t *new_r_ptr;

                            new_r_ptr = r_ptr + 6;
                            uint32_t offset = *(uint32_t *)new_r_ptr;
                            *(uint32_t *)&rip_call[2] = offset - 3;
                            *(uint64_t *)&movabs_rdi[2] = tmp->new_base_vaddr;
                            new_r_ptr = r_ptr - 3;
                            memcpy(new_r_ptr, movabs_rdi, sizeof(movabs_rdi));
                            new_r_ptr += sizeof(movabs_rdi) - 1;
                            memcpy(new_r_ptr, rip_call, sizeof(rip_call));
                            break;
                        }
                    }
                    symval = tmp->new_base_vaddr;
                    rel_val = symval + rel_entry->rel.addend - rel_addr;
                    aslr_debug("Setting X86_64_PC32(2) reloc value to rel_val: %#x"
                        " destination symbol %s:%#lx)\n", rel_val,
                        rel_entry->rel.symname, symval);
                    *(uint32_t *)r_ptr = rel_val;
                }
            }
            break;
        default:
            aslr_debug("Unhandled relocation type %d: %s\n", rel_entry->rel.type,
                elf_reloc_type_string(&ctx->elfobj, rel_entry->rel.type));
            break;
    }
}

success:
    aslr_debug("Setting mprotect PROT_READ|PROT_EXEC on %p\n", (void *)page_vaddr);
    (void)mprotect((void *)page_vaddr,
        4096,
        PROT_READ|PROT_EXEC);

    aslr_debug("Returning\n");
    return true;
}

bool
move_function(struct shiva_ctx *ctx, struct aslr_ctx *aslr, struct func_entry *fe)
{

    size_t delta;
    struct reloc_entry *rel_entry;

    aslr_debug("Moving function %s to %p\n", fe->symbol.name, fe->n_mem);
    /*
     * Copy function code from its old address to its new address
     */
    memcpy(fe->n_mem, (uint8_t *)fe->runtime_vaddr, fe->func_len);
    /*
     * Update the relocation entries for that function so that they
     * reflect the correct r_offset's after it is moved.
     */
    TAILQ_FOREACH(rel_entry, &fe->reloc_list, _linkage) {
        delta = rel_entry->rel.offset - fe->base_vaddr;
        rel_entry->rel.offset = delta;
    }
    /*
     * Now that function has been moved to a new location
     * fixup the function using the modified relocation records.
     */
    return relocate_function(ctx, aslr, fe);
}

bool
remove_old_function(struct shiva_ctx *ctx, struct func_entry *fe)
{
    int ret;
    size_t mlen;
    size_t pgoff;

    /*
     * Scrub old binary with zeroes. Perhaps this should be random
     * bytes in the future.
     */
    pgoff = ELF_PAGEOFFSET(fe->runtime_vaddr);

    ret = mprotect((void *)(fe->runtime_vaddr & ~4095), fe->func_len + pgoff, PROT_READ|PROT_WRITE|PROT_EXEC);
    memset((void *)fe->runtime_vaddr, 0, fe->func_len - 1);
    ret = mprotect((void *)(fe->runtime_vaddr & ~4095), fe->func_len + pgoff, PROT_READ|PROT_EXEC);

    return ret ? false : true;
}

bool
randomize_func_locations(struct shiva_ctx *ctx, struct aslr_ctx *aslr,
    size_t fn_count)
{
    struct func_entry *fe;
    bool res;

    (void)fn_count;

    TAILQ_FOREACH(fe, &aslr->orig_func_list, _linkage) {
        if (fe->flags & ASLR_FUNC_F_ENTRYPOINT) {
            /*
             * This function is probably _start and
             * we therefore do not move it, we leave it
             * as the entrypoint, but we must fixup its
             * relocations to point to the new main() etc.
             */
            res = relocate_function(ctx, aslr, fe);
            if (res == false) {
                fprintf(stderr, "Failed to relocate entrypoint function %s\n",
                    fe->symbol.name);
                return false;
            }
            continue;
        }
        aslr_debug("Moving function: %s\n", fe->symbol.name);
        res = move_function(ctx, aslr, fe);     
        if (res == false) {
            fprintf(stderr, "Failed to move function %s\n", fe->symbol.name);
            return false;
        }
        aslr_debug("Function %s was moved sucessfuly, now lets scrub the old version\n",
            fe->symbol.name);
        res = remove_old_function(ctx, fe);
        aslr_debug("Function %s was scrubbed from its original location\n", fe->symbol.name);
    }
    return true;
}

int
shiva_init(struct shiva_ctx *ctx)
{
    struct aslr_ctx aslr;
    size_t fn_count;

    aslr_debug("Building func list\n");

    if (build_func_list(ctx, &aslr, &fn_count) == false) {
        fprintf(stderr, "build_func_list() failed on .text\n");
        return -1;
    }

    aslr_debug("Randomizing func locations\n");
    if (randomize_func_locations(ctx, &aslr, fn_count) == false) {
        fprintf(stderr, "randomize_func_locations() failed\n");
        return -1;
    }
    aslr_debug("Leaving module\n");
}
### Summary of gASLR.c source code 1. Move every function within the .text section to a new location in memory 2. Update the `.rela.text` relocation entries `r_offset` values to align with the new function locations 3. Apply each `.rela.text` relocation to each function that has been moved. You might call this technique **JIT function transplantation**. ## gASLR demo From the `shiva/modules/x86_64_modules/aslr` directory you will see several files including test.c, gASLR.c and a Makefile. To illustrate the effects of gASLR I created a simple program called `test.c` that prints the address of it's functions at runtime. With standard ASLR in effect you will notice that the two functions `main()` and `test1()` are always at a fixed offset from the base address of the executable, whereas with gASLR applied to ./test the functions will be mapped to their own base address. ### Source code for test.c in gASLR demo gaslr_test_source - test.c - original [12]

#include <stdio.h>
#include <stdlib.h›

int test1(void)
{
    int i = 0;
    return 0;
}

static int ignore_me(void) // gASLR.o will not relocate static functions
{
    int i = 7;
    return 0;
}

int main(void)
{
    printf("base address: %p\n", (unsigned long)&ignore_me & ~4095);
    printf("main() is at %p\n" &main);
    printf("test1() is at %p\n", &test1);
    test1();
}
Let's compile this program and run it with standard ASLR in effect. ### Running ./test with standard ASLR

$ gcc -Wl,--emit-relocs -mcmodel=large test.c-o test 
$ ./test 
base address: 0x5a3b72d0a000
main() is at 0x5a3b72d0a19d 
test1() is at 0x5a3b72d0a149
test_without_gaslr - original [13] The program output shows that the address of `main()` and of `test1()` stay at the same fixed offsets from the base address `0x5a3b72d0a000` during each run. Let's apply gASLR to the program and see the difference in the output... ### gASLR in action

elfmaster@arcana-laptop:~/amp/shiva/modules/x86_64_modules/aslr$ shiva-ld -e ./test -i /lib/shiva \
> -5 $PWD -P gASLR.o -o test.gASLR
...............
[+] Input executable: ./test
[+] Input search path for patch: /home/elfmaster/amp/shiva/modules/x86_64_modules/aslr
[+] Basename of patch: gASLR.o
[+] Output executable: test.gASLR
Üpdating dynamic segment vaddr: 0x5000 offset: Ox5000 size: 0x1e0
Finished prelinking.
elfmaster@arcana-laptop:~/amp/shiva/modules/x86_64_modules/aslr$ ./test.gASLR
base address: 0x62e0aa32d000
main() is at 0x790adc599000
test1() is at 0x790adc597000
elfmaster@arcana-laptop:~/amp/shiva/modules/x86_64_modules/aslr$./test.gASLR
base address: 0x55d1bb26e000
main() is at 0x705a9c5f5000
test1() is at 0x705a9c5f3000
elfmaster@arcana-laptop:~/amp/shiva/modules/x86_64_modules/aslr$ •/test.gASLR
base address: 0x5e8f57b1a000
main() is at 0x7ee27abc6000
test1() is at 0x7ee279f61000
gASLR_demo - original [14] In the program output above we can see that the function addresses change each time to addresses that are at a different offset from the executable base address. The gASLR module is using `mmap()` to create a unique base mapping for each function individually. ## Future work on gASLR Presently this is just a prototype and is lacking the code to handle all possible relocation types and only accomplishes the basic security goals-- in a better version we would also add in the following features: - Randomized PLT entries - Randomized global data - Randomization of locally bound functions - Place function at a random offset from its new memory address - Ability to work on small-code-model programs Given a little bit more time I could have it polished up and ready for production use. I just need the time to get it done! ## Closing thoughts Shiva bridges the gap between binary patching and dynamic instrumentation and is extremely powerful for building ELF microprograms that solve hard security problems! Thank you so much for taking the time to read this... ## ELF binary hacking workshops For those interested in learning more about Shiva and how to use it for binary patching as well as for building security features, fuzzers, debuggers, and more... please feel free to check out the training description[15] on the Arcana webpage. I'd love to train more people to see what kind of awesome projects come out of Shiva. ## Contact Please contact me with questions, comments, collaboration ideas, etc. elfmaster [at] arcana-research.io ## References [1] https://tmpout.sh/2/6.html [2] https://arcana-research.io/shiva [3] https://github.com/advanced-microcode-patching/shiva [4] https://infocondb.org/con/toorcon/toorcamp-2022/shiva-advancing-the-programmability-and-security-of-the-linux-userland-runtime [5] https://github.com/advanced-microcode-patching/shiva_user_manual [6] https://www.youtube.com/watch?v=TDMWejaucdg [7] https://arcana-research.io/static/shiva_module_process_image.png [8] https://github.com/advanced-microcode-patching/shiva/blob/x86_64_port/modules/x86_64_modules/aslr/gASLR.c [9] https://arcana-research.io/static/shiva-dynamic-linking-workflow.png [10] https://github.com/elfmaster/libelfmaster [11] https://github.com/advanced-microcode-patching/shiva [12] https://arcana-research.io/static/gaslr_test_source.png [13] https://arcana-research.io/static/test_without_gaslr.png [14] https://arcana-research.io/static/gaslr_prelink.png [15] https://arcana-research.io/blog/the-art-of-elf-binary-patching-in-linux [16] https://github.com/advanced-microcode-patching/shiva/blob/x86_64_port/shiva_ulexec.c --[ PREV | HOME | NEXT ]--