Repository: gamozolabs/elfloader Branch: main Commit: 4e8118eb9e8d Files: 15 Total size: 23.8 KB Directory structure: gitextract_lpxhwkla/ ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── example_program_with_data/ │ ├── .cargo/ │ │ └── config │ ├── .gitignore │ ├── Cargo.toml │ ├── Makefile │ └── src/ │ └── main.rs ├── example_small_program/ │ ├── .cargo/ │ │ └── config │ ├── .gitignore │ ├── Cargo.toml │ ├── Makefile │ └── src/ │ └── main.rs └── src/ └── main.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /target ================================================ FILE: Cargo.toml ================================================ [package] name = "elfloader" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 gamozolabs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Summary `elfloader` is a super simple loader for ELF files that generates a flat in-memory representation of the ELF. Pair this with Rust and now you can write your shellcode in a proper, safe, high-level language. Any target that LLVM can target can be used, including custom target specifications for really exotic platforms and ABIs. Enjoy using things like `u64`s on 32-bit systems, bounds checked arrays, drop handling of allocations, etc :) It simply concatenates all `LOAD` sections together, using zero-padding if there are gaps, into one big flat file. This file includes zero-initialization of `.bss` sections, and thus can be used directly as a shellcode payload. If you don't want to waste time with fail-open linker scripts, this is probably a great way to go. This doesn't handle any relocations, it's on you to make sure the original ELF is based at the address you want it to be at. # Usage To use this tool, simply: ``` Usage: elfloader [--perms] [--binary] [--base=] --binary - Don't output a FELF, output the raw loaded image with no metadata --perms - Create a FELF0002 which includes permission data, overrides --binary --base= - Force the output to start at ``, zero padding from the base to the start of the first LOAD segment if needed. `` is default hex, can be overrided with `0d`, `0b`, `0x`, or `0o` prefixes. Warning: This does not _relocate_ to base, it simply starts the output at `` (adding zero bytes such that the output image can be loaded at `` instead of the original ELF base) - Path to input ELF - Path to output file ``` To install this tool run: `cargo install --path .` Now you can use `elfloader` from anywhere in your shell! # Dev This project was developed live here: https://www.youtube.com/watch?v=x0V-CEmXQCQ # Example There's an example in `example_small_program`, simply run `make` or `nmake` and this should generate an `example.bin` which is 8 bytes. ``` pleb@gamey ~/elfloader/example_small_program $ make cargo build --release Finished release [optimized] target(s) in 0.03s elfloader --binary target/aarch64-unknown-none/release/example_small_program example.bin pleb@gamey ~/elfloader/example_small_program $ ls -l ./example.bin -rw-r--r-- 1 pleb pleb 8 Nov 8 12:27 ./example.bin pleb@gamey ~/elfloader/example_small_program $ objdump -d target/aarch64-unknown-none/release/example_small_program target/aarch64-unknown-none/release/example_small_program: file format elf64-littleaarch64 Disassembly of section .text: 00000000133700b0 <_start>: 133700b0: 8b000020 add x0, x1, x0 133700b4: d65f03c0 ret ``` Now you can write your shellcode in Rust, and you don't have to worry about whether you emit `.data`, `.rodata`, `.bss`, etc. This will handle it all for you! There's also an example with `.bss` and `.rodata` ``` pleb@gamey ~/elfloader/example_program_with_data $ make cargo build --release Finished release [optimized] target(s) in 0.04s elfloader --binary target/aarch64-unknown-none/release/example_program_with_data example.bin pleb@gamey ~/elfloader/example_program_with_data $ ls -l ./example.bin -rw-r--r-- 1 pleb pleb 29 Nov 8 12:39 ./example.bin pleb@gamey ~/elfloader/example_program_with_data $ objdump -d target/aarch64-unknown-none/release/example_program_with_data target/aarch64-unknown-none/release/example_program_with_data: file format elf64-littleaarch64 Disassembly of section .text: 0000000013370124 <_start>: 13370124: 90000000 adrp x0, 13370000 <_start-0x124> 13370128: 90000008 adrp x8, 13370000 <_start-0x124> 1337012c: 52800029 mov w9, #0x1 // #1 13370130: 91048000 add x0, x0, #0x120 13370134: 3904f109 strb w9, [x8, #316] 13370138: d65f03c0 ret pleb@gamey ~/elfloader/example_program_with_data $ readelf -l target/aarch64-unknown-none/release/example_program_with_data Elf file type is EXEC (Executable file) Entry point 0x13370124 There are 4 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000120 0x0000000013370120 0x0000000013370120 0x0000000000000004 0x0000000000000004 R 0x1 LOAD 0x0000000000000124 0x0000000013370124 0x0000000013370124 0x0000000000000018 0x0000000000000018 R E 0x4 LOAD 0x000000000000013c 0x000000001337013c 0x000000001337013c 0x0000000000000000 0x0000000000000001 RW 0x4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x0 Section to Segment mapping: Segment Sections... 00 .rodata 01 .text 02 .bss 03 ``` # Internals This tool doesn't care about anything except for `LOAD` sections. It determines the endianness (little vs big) and bitness (32 vs 64) from the ELF header, and from there it creates a flat image based on program header virtual addresses (where it's loaded), file size (number of initialized bytes) and mem size (size of actual memory region). The bytes are initialized from the file based on the offset and file size, and this is then extended with zeros until mem size (or truncated if mem size is smaller than file size). These `LOAD` sections are then concatenated together with zero-byte padding for gaps. This is designed to be incredibly simple, and agnostic to the ELF input. It could be an executable, object file, shared object, core dump, etc, doesn't really care. It'll simply give you the flat representation of the memory, nothing more. This allows you to turn any ELF into shellcode, or a simpler file format that is easier to load in hard-to-reach areas, like embedded devices. Personally, I developed this for my MIPS NT 4.0 loader which allows me to run Rust code. # FELF0001 format This tool by default generates a FELF file format. This is a Falk ELF. This is a simple file format: ``` FELF0001 - Magic header entry - 64-bit little endian integer of the entry point address base - 64-bit little endian integer of the base address to load the image - Rest of the file is the raw image, to be loaded at `base` and jumped into at `entry` ``` # FELF0002 format (when --perms flag is used) This tool by default generates a FELF file format. This is a Falk ELF. This is a simple file format with permissions: ``` FELF0002 - Magic header entry - 64-bit little endian integer of the entry point address base - 64-bit little endian integer of the base address to load the image - Rest of the file is the raw image, to be loaded at `base` and jumped into at `entry` - Permissions, matching the bytes of where the byte contains the following flags bitwise or-ed together: 0x01 - Executable, 0x02 - Writable, 0x04 - Readable Padding bytes will be 0x00, and thus have no permissions for any access ``` ================================================ FILE: example_program_with_data/.cargo/config ================================================ [build] target = "aarch64-unknown-none" [target.aarch64-unknown-none] rustflags = ["-Clink-arg=--nmagic", "-Clink-arg=--image-base=0x13370000"] [unstable] build-std = ["core"] ================================================ FILE: example_program_with_data/.gitignore ================================================ /target /*.bin ================================================ FILE: example_program_with_data/Cargo.toml ================================================ [package] name = "example_program_with_data" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] [profile.dev] panic = "abort" [profile.release] opt-level = "z" panic = "abort" ================================================ FILE: example_program_with_data/Makefile ================================================ all: cargo build --release elfloader --binary target/aarch64-unknown-none/release/example_program_with_data example.bin ================================================ FILE: example_program_with_data/src/main.rs ================================================ #![no_std] #![no_main] use core::sync::atomic::{AtomicBool, Ordering}; #[panic_handler] fn panic(_panic_info: &core::panic::PanicInfo) -> ! { loop {} } // BSS example #[no_mangle] pub static BSSTHING: AtomicBool = AtomicBool::new(false); #[no_mangle] pub extern fn _start() -> *const u8 { BSSTHING.store(true, Ordering::Relaxed); "asdf".as_ptr() } ================================================ FILE: example_small_program/.cargo/config ================================================ [build] target = "aarch64-unknown-none" [target.aarch64-unknown-none] rustflags = ["-Clink-arg=--nmagic", "-Clink-arg=--image-base=0x13370000"] [unstable] build-std = ["core"] ================================================ FILE: example_small_program/.gitignore ================================================ /target /*.bin ================================================ FILE: example_small_program/Cargo.toml ================================================ [package] name = "example_small_program" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] [profile.dev] panic = "abort" [profile.release] opt-level = "z" panic = "abort" ================================================ FILE: example_small_program/Makefile ================================================ all: cargo build --release elfloader --binary target/aarch64-unknown-none/release/example_small_program example.bin ================================================ FILE: example_small_program/src/main.rs ================================================ #![no_std] #![no_main] #[panic_handler] fn panic(_panic_info: &core::panic::PanicInfo) -> ! { loop {} } #[no_mangle] pub extern fn _start(x: u64, y: u64) -> u64 { x.wrapping_add(y) } ================================================ FILE: src/main.rs ================================================ use std::fs::File; use std::io::{BufReader, Read, Seek, SeekFrom, BufWriter, Write}; use std::mem::size_of; use std::path::Path; /// Friendly wrapper around [`Error`] type Result = std::result::Result; /// Error types #[derive(Debug)] pub enum Error { /// Failed to open the ELF file Open(std::io::Error), /// Failed to consume a field from the input Consume(&'static str, std::io::Error), /// ELF magic was missing from file MissingMagic, /// ELF indiciated a bitness which was not a valid variant InvalidBitness(u8), /// ELF indiciated an endianness which was not a valid variant InvalidEndianness(u8), /// ELF version was unknown UnknownVersion(u8), /// Expected an executable but got a different ELF type ExpectedExecutable(u16), /// Seeking to the program headers failed SeekProgramHeaders(std::io::Error), /// Seeking the initialized data for a loaded segment failed LoadSeek(std::io::Error), /// Reading initialized bytes from file failed ReadInit(std::io::Error), /// Creating the FELF failed CreateFelf(std::io::Error), /// Writing the FELF failed WriteFelf(std::io::Error), /// Truncated integer for filesz IntegerTruncationFileSz, /// Truncated integer for memsz IntegerTruncationMemSz, /// Trucated integer for offset IntegerTruncationOffset, /// Integer overflow when computing current address IntegerOverflowCurrentAddress, /// Multiple sections overlap where they are loaded SectionOverlap, /// The ELF didn't supply any segments to load NoLoadSegments, /// The `--base` provided did not parse into a `u64` correctly InvalidBase(std::num::ParseIntError), } /// Consume bytes from a reader macro_rules! consume { // Consume a `u8` ($reader:expr, $field:expr) => {{ // Create buffer let mut tmp = [0u8; 1]; $reader.read_exact(&mut tmp).map(|_| { tmp[0] }).map_err(|x| Error::Consume($field, x)) }}; ($reader:expr, $ty:ty, $endian:expr, $field:expr) => {{ // Create buffer for type let mut tmp = [0u8; size_of::<$ty>()]; match $endian { Endianness::Little => { // Read the bytes and convert $reader.read_exact(&mut tmp).map(|_| { <$ty>::from_le_bytes(tmp) }).map_err(|x| Error::Consume($field, x)) } Endianness::Big => { // Read the bytes and convert $reader.read_exact(&mut tmp).map(|_| { <$ty>::from_be_bytes(tmp) }).map_err(|x| Error::Consume($field, x)) } } }}; ($reader:expr, $size:expr, $field:expr) => {{ // Create buffer for type let mut tmp = [0u8; $size]; // Read the bytes and convert $reader.read_exact(&mut tmp).map(|_| { tmp }).map_err(|x| Error::Consume($field, x)) }}; } macro_rules! consume_native { ($reader:expr, $bitness:ident, $endian:expr, $field:expr) => {{ match $bitness { Bitness::Bits32 => { consume!($reader, u32, $endian, $field).map(|x| x as u64) }, Bitness::Bits64 => consume!($reader, u64, $endian, $field), } }}; } /// Bitnesses for ELF files #[derive(Debug)] enum Bitness { /// 32-bit ELF Bits32, /// 64-bit ELF Bits64, } impl TryFrom for Bitness { type Error = Error; fn try_from(val: u8) -> Result { Ok(match val { 1 => Bitness::Bits32, 2 => Bitness::Bits64, _ => return Err(Error::InvalidBitness(val)), }) } } /// Endianness for ELF files #[derive(Debug)] enum Endianness { /// Little endian Little, /// Big endian Big, } impl TryFrom for Endianness { type Error = Error; fn try_from(val: u8) -> Result { Ok(match val { 1 => Endianness::Little, 2 => Endianness::Big, _ => return Err(Error::InvalidEndianness(val)), }) } } /// Loaded segment const PT_LOAD: u32 = 1; /// Executable segment const PF_X: u32 = 1 << 0; /// Writable segment const PF_W: u32 = 1 << 1; /// Readable segment const PF_R: u32 = 1 << 2; /// Load an ELF from disk /// /// Returns: /// /// `(entry virtual address, base address for flat map, flat map contents)` pub fn write_file(path: impl AsRef, base: Option, mut output: impl Write, binary: bool, save_perms: bool) -> Result<()> { // Open the file let mut reader = BufReader::new(File::open(path).map_err(Error::Open)?); // Check that this is an ELF if &consume!(reader, 4, "ELF magic")? != b"\x7fELF" { return Err(Error::MissingMagic); } // Get the bitness and endianness let bt = Bitness::try_from(consume!(reader, "bitness")?)?; let en = Endianness::try_from(consume!(reader, "endianness")?)?; // Make sure the ELF version matches let version = consume!(reader, "version")?; if version != 1 { return Err(Error::UnknownVersion(version)); } // We don't care about the ABI let _abi = consume!(reader, "abi")?; let _abiver = consume!(reader, "abi version")?; let _pad = consume!(reader, 7, "padding")?; let _objtyp = consume!(reader, u16, en, "type")?; let _machine = consume!(reader, u16, en, "machine")?; let _elfver = consume!(reader, u32, en, "ELF version")?; let entry = consume_native!(reader, bt, en, "entry point")?; let phoff = consume_native!(reader, bt, en, "program header offset")?; let _shoff = consume_native!(reader, bt, en, "section header offset")?; let _flags = consume!(reader, u32, en, "flags")?; let _ehsize = consume!(reader, u16, en, "ELF header size")?; let _phesz = consume!(reader, u16, en, "program header entry size")?; let phcnt = consume!(reader, u16, en, "program header entries")?; // Seek to the program headers reader.seek(SeekFrom::Start(phoff)) .map_err(Error::SeekProgramHeaders)?; // List of sections to load let mut load = Vec::new(); // Go through each program header entry for _ in 0..phcnt { // Get header type let typ = consume!(reader, u32, en, "PH type")?; // 64-bit has flags here let mut flags = if matches!(bt, Bitness::Bits64) { consume!(reader, u32, en, "PH flags")? } else { 0 }; // Parse program header let offset = consume_native!(reader, bt, en, "PH offset")?; let vaddr = consume_native!(reader, bt, en, "PH vaddr")?; let _paddr = consume_native!(reader, bt, en, "PH paddr")?; let filesz = consume_native!(reader, bt, en, "PH filesz")?; let memsz = consume_native!(reader, bt, en, "PH memsz")?; // 32-bit has flags here if matches!(bt, Bitness::Bits32) { flags = consume!(reader, u32, en, "PH flags")? } let _align = consume_native!(reader, bt, en, "PH align")?; if typ == PT_LOAD { // If the section is zero size, skip it entirely if memsz == 0 { continue; } // Read initialized bytes from file if needed let mut bytes = Vec::new(); if filesz > 0 { // Save the current position let stream_pos = reader.stream_position() .map_err(Error::LoadSeek)?; // Seek to the bytes in the file reader.seek(SeekFrom::Start(offset)) .map_err(Error::LoadSeek)?; // Resize buffer bytes.resize(filesz.try_into() .map_err(|_| Error::IntegerTruncationFileSz)?, 0u8); // Read initialized bytes from file reader.read_exact(&mut bytes).map_err(Error::ReadInit)?; // Seek back to where we were reader.seek(SeekFrom::Start(stream_pos)) .map_err(Error::LoadSeek)?; } // Pad out with zeros until memory size bytes.resize(memsz.try_into() .map_err(|_| Error::IntegerTruncationMemSz)?, 0u8); // Determine permissions for this segment let r = (flags & PF_R) != 0; let w = (flags & PF_W) != 0; let x = (flags & PF_X) != 0; // Save the address to load to and the bytes load.push((vaddr, bytes, r, w, x)); } } // Sort load sections by virtual address load.sort_by_key(|x| x.0); // Start load at the specified `base`, otherwise use the lowest address of // all the LOAD sections let lowest_addr = base.unwrap_or( load.get(0).ok_or(Error::NoLoadSegments)?.0); if !binary { // Write the FELF header if !save_perms { output.write_all(b"FELF0001").map_err(Error::WriteFelf)?; } else { output.write_all(b"FELF0002").map_err(Error::WriteFelf)?; } output .write_all(&entry.to_le_bytes()) .map_err(Error::WriteFelf)?; output .write_all(&lowest_addr.to_le_bytes()) .map_err(Error::WriteFelf)?; } // Permissions vector let mut perms = Vec::new(); // Write everything! let mut cur_addr = lowest_addr; for (vaddr, bytes, r, w, x) in load { // Get the offset from where we are let offset: usize = vaddr.checked_sub(cur_addr) .ok_or(Error::SectionOverlap)? .try_into() .map_err(|_| Error::IntegerTruncationOffset)?; // Pad out loaded representation until `vaddr` const ZERO_BUF: [u8; 1024 * 8] = [0u8; 1024 * 8]; let mut padding = offset; while padding > ZERO_BUF.len() { output.write_all(&ZERO_BUF).map_err(Error::WriteFelf)?; if save_perms { perms.write_all(&ZERO_BUF).map_err(Error::WriteFelf)?; } padding -= ZERO_BUF.len(); } output.write_all(&ZERO_BUF[..padding]) .map_err(Error::WriteFelf)?; if save_perms { perms.write_all(&ZERO_BUF[..padding]) .map_err(Error::WriteFelf)?; } // Place in the bytes output.write_all(&bytes).map_err(Error::WriteFelf)?; if save_perms { // Place in all the permission bytes let perm_flags = if r { PF_R } else { 0 } | if w { PF_W } else { 0 } | if x { PF_X } else { 0 }; perms.resize(perms.len() + bytes.len(), perm_flags as u8); } // Update current address cur_addr = vaddr .checked_add(bytes.len() as u64) .ok_or(Error::IntegerOverflowCurrentAddress)?; } if save_perms { // Add permissions to file output.write_all(&perms).map_err(Error::WriteFelf)?; } output.flush().map_err(Error::WriteFelf)?; Ok(()) } /// Entry point fn main() -> Result<()> { // Get the command line arguments let mut args = std::env::args().collect::>(); // Check if the `--binary` flag was specified let mut binary = false; args.retain(|x| if x == "--binary" { binary = true; false } else { true }); // Check if the `--perms` flag was specified let mut perms = false; args.retain(|x| if x == "--perms" { perms = true; false } else { true }); // Perms flag overrides the binary flag if perms { binary = false; } // Check if the `--base=` flag was specified let mut base = None; args.retain(|x| { if x.starts_with("--base=") { // Default to hex, skip over `--base=` let mut radix = 16; let mut x = &x[7..]; if x.starts_with("0x") { radix = 16; x = &x[2..]; } else if x.starts_with("0o") { radix = 8; x = &x[2..]; } else if x.starts_with("0b") { radix = 2; x = &x[2..]; } else if x.starts_with("0d") { radix = 10; x = &x[2..]; } // Convert to a `u64` base = Some(u64::from_str_radix(x, radix) .map_err(Error::InvalidBase)); // Don't keep this argument false } else { true } }); // Can't use `?` in closure let base = if let Some(base) = base { Some(base?) } else { None }; if args.len() != 3 { println!( r#"Usage: elfloader [--perms] [--binary] [--base=] --binary - Don't output a FELF, output the raw loaded image with no metadata --perms - Create a FELF0002 which includes permission data, overrides --binary --base= - Force the output to start at ``, zero padding from the base to the start of the first LOAD segment if needed. `` is default hex, can be overrided with `0d`, `0b`, `0x`, or `0o` prefixes. Warning: This does not _relocate_ to base, it simply starts the output at `` (adding zero bytes such that the output image can be loaded at `` instead of the original ELF base) - Path to input ELF - Path to output file"#); return Ok(()); } // Create the output file let mut output = BufWriter::new(File::create(&args[2]) .map_err(Error::CreateFelf)?); write_file(&args[1], base, &mut output, binary, perms)?; Ok(()) }