Mathieu GAILLARD

Load and execute an ELF in user mode in Linux

In my Operating System class at Purdue University, I was assigned a lab, in which I needed to implement an ELF loader for an embedded operating system: Xinu.

I decided to first experiment with Ubuntu to get a better grasp of ELF in general. Luckily, I found a sample of code on Stack Overflow: loading ELF file in C in user space, but unfortunately it seems outdated and it didn’t run on my lab computer. I investigated and found the problems that prevented it from succesfully running. So, here is the updated version that works for me.

// File: loader.c

#include <stdio.h>
#include <string.h>
#include <elf.h>
#include <sys/mman.h>
#include <dlfcn.h>
#include <assert.h>
#include <stdbool.h>

int is_image_valid(Elf32_Ehdr *hdr)
{
    // Check that the file starts with the magic ELF number
    // 0x7F followed by ELF(45 4c 46) in ASCII
    assert(hdr->e_ident[EI_MAG0] == 0x7F);
    assert(hdr->e_ident[EI_MAG1] == 0x45);
    assert(hdr->e_ident[EI_MAG2] == 0x4c);
    assert(hdr->e_ident[EI_MAG3] == 0x46);

    return 1;
}

void* resolve(const char* sym)
{
    static void *handle = NULL;

    if (handle == NULL)
    {
        handle = dlopen("libc.so.6", RTLD_NOW);
    }

    assert(handle != NULL);

    void* resolved_sym = dlsym(handle, sym);

    // assert(resolved_sym != NULL);

    return resolved_sym;
}

void relocate(Elf32_Shdr* shdr, const Elf32_Sym* syms, const char* strings, const char* src, char* dst)
{
    Elf32_Rel* rel = (Elf32_Rel*)(src + shdr->sh_offset);

    for(int j = 0; j < shdr->sh_size / sizeof(Elf32_Rel); j += 1)
    {
        const char* sym = strings + syms[ELF32_R_SYM(rel[j].r_info)].st_name;
        
        switch (ELF32_R_TYPE(rel[j].r_info))
        {
            case R_386_JMP_SLOT:
            case R_386_GLOB_DAT:
                *(Elf32_Word*)(dst + rel[j].r_offset) = (Elf32_Word)resolve(sym);
                break;
        }
    }
}

int find_global_symbol_table(Elf32_Ehdr* hdr, Elf32_Shdr* shdr)
{
    for (int i = 0; i < hdr->e_shnum; i++)
    {
        if (shdr[i].sh_type == SHT_DYNSYM)
        {
            return i;
            break;
        }
    }

    return -1;
}

int find_symbol_table(Elf32_Ehdr* hdr, Elf32_Shdr* shdr)
{
    for (int i = 0; i < hdr->e_shnum; i++)
    {
        if (shdr[i].sh_type == SHT_SYMTAB)
        {
            return i;
            break;
        }
    }

    return -1;
}

void* find_sym(const char* name, Elf32_Shdr* shdr, Elf32_Shdr* shdr_sym, const char* src, char* dst)
{
    Elf32_Sym* syms = (Elf32_Sym*)(src + shdr_sym->sh_offset);
    const char* strings = src + shdr[shdr_sym->sh_link].sh_offset;
    
    for (int i = 0; i < shdr_sym->sh_size / sizeof(Elf32_Sym); i += 1)
    {
        if (strcmp(name, strings + syms[i].st_name) == 0)
        {
            return dst + syms[i].st_value;
        }
    }

    return NULL;
}

void* image_load(char *elf_start, unsigned int size)
{
    Elf32_Ehdr      *hdr     = NULL;
    Elf32_Phdr      *phdr    = NULL;
    Elf32_Shdr      *shdr    = NULL;
    char            *start   = NULL;
    char            *taddr   = NULL;
    void            *entry   = NULL;
    int i = 0;
    char *exec = NULL;

    hdr = (Elf32_Ehdr *) elf_start;
    
    if (!is_image_valid(hdr))
    {
        printf("Invalid ELF image\n");
        return 0;
    }

    exec = mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);

    if (!exec)
    {
        printf("Error allocating memory\n");
        return 0;
    }

    // Start with clean memory.
    memset(exec, 0x0, size);

    // Entries in the program header table
    phdr = (Elf32_Phdr *)(elf_start + hdr->e_phoff);

    // Go over all the entries in the ELF
    for (i = 0; i < hdr->e_phnum; ++i)
    {
        if (phdr[i].p_type != PT_LOAD)
        {
            continue;
        }

        if (phdr[i].p_filesz > phdr[i].p_memsz)
        {
            printf("image_load:: p_filesz > p_memsz\n");
            munmap(exec, size);
            return 0;
        }

        if (!phdr[i].p_filesz)
        {
            continue;
        }

        // p_filesz can be smaller than p_memsz,
        // the difference is zeroe'd out.
        start = elf_start + phdr[i].p_offset;
        taddr = phdr[i].p_vaddr + exec;
        memmove(taddr, start, phdr[i].p_filesz);

        if (!(phdr[i].p_flags & PF_W))
        {
            // Read-only.
            mprotect((unsigned char *) taddr, phdr[i].p_memsz, PROT_READ);
        }

        if (phdr[i].p_flags & PF_X)
        {
            // Executable.
            mprotect((unsigned char *) taddr, phdr[i].p_memsz, PROT_EXEC);
        }
    }

    // Section table
    shdr = (Elf32_Shdr *)(elf_start + hdr->e_shoff);

    // Find the global symbol table
    int global_symbol_table_index = find_global_symbol_table(hdr, shdr);
    // Symbols and names of the dynamic symbols (for relocation)
    Elf32_Sym* global_syms = (Elf32_Sym*)(elf_start + shdr[global_symbol_table_index].sh_offset);
    char* global_strings = elf_start + shdr[shdr[global_symbol_table_index].sh_link].sh_offset;
    
    // Relocate global dynamic symbols
    for (i = 0; i < hdr->e_shnum; ++i)
    {
        if (shdr[i].sh_type == SHT_REL)
        {
            relocate(shdr + i, global_syms, global_strings, elf_start, exec);
        }
    }

    // Find the main function in the symbol table
    int symbol_table_index = find_symbol_table(hdr, shdr);
    entry = find_sym("main", shdr, shdr + symbol_table_index, elf_start, exec);

   return entry;
}

int main(int argc, char** argv, char** envp)
{
    char buf[1048576]; // Allocate 1MB for the program
    memset(buf, 0x0, sizeof(buf));

    FILE* elf = fopen(argv[1], "rb");

    if (elf != NULL)
    {
        int (*ptr)(int, char **, char**);

        fread(buf, sizeof(buf), 1, elf);
        ptr = image_load(buf, sizeof(buf));

        if (ptr != NULL)
        {
            printf("Run the loaded program:\n");

            // Run the main function of the loaded program
            ptr(argc, argv, envp);
        }
        else
        {
            printf("Loading unsuccessful...\n");
        }

        fclose(elf);

        return 0;
    }
    
    return 1;
}
// File: elf.c

#include <stdio.h>

int square(int x)
{
    return x*x;
}

int main(int argc, char** argv)
{
    fprintf(stdout, "Hello world!\n");
    fprintf(stdout, "fprintf=%p, stdout=%p\n", fprintf, stdout);
    fprintf(stdout, "square(10) = %d\n", square(10));

    return 0;
}

Building and running the code

The Makefile to build the code:

all: loader elf

loader: loader.c
	gcc -m32 -g -Wall -o loader loader.c -ldl

elf: elf.c
	gcc -m32 -pie -fPIE -o elf elf.c

Running the code and result:

$ make
$ ./loader elf
# Run the loaded program:
# Hello world!
# fprintf=0xf7da5410, stdout=0xf7f2cd80
# square(10) = 100

For reference the code runs successfully on this system:

$ lsb_release -a
# LSB Version:    core-9.20170808ubuntu1-noarch:security-9.20170808ubuntu1-noarch
# Distributor ID: Ubuntu
# Description:    Ubuntu 18.04.5 LTS
# Release:        18.04
# Codename:       bionic

Some useful function for debugging:

To display a header and check that it is valid.

void print_header(Elf32_Ehdr *hdr)
{
    printf("ELF Header:\n");
    printf("Magic: %02x %02x %02x %02x\n", hdr->e_ident[EI_MAG0],
                                           hdr->e_ident[EI_MAG1],
                                           hdr->e_ident[EI_MAG2],
                                           hdr->e_ident[EI_MAG3]);
    printf("Class \t\t\t\t\t%d\n", hdr->e_ident[EI_CLASS]);
    printf("Data \t\t\t\t\t%d\n", hdr->e_ident[EI_DATA]);
    printf("Version: \t\t\t\t0x%x\n", hdr->e_ident[EI_VERSION]);
    printf("OS/ABI: \t\t\t\t0x%x\n", hdr->e_ident[EI_OSABI]);
    printf("ABI Version: \t\t\t\t0x%x\n", hdr->e_ident[EI_ABIVERSION]);
    printf("Type \t\t\t\t\t%d\n", hdr->e_type);
    printf("Machine: \t\t\t\t%d\n", hdr->e_machine);
    printf("Version: \t\t\t\t0x%x\n", hdr->e_version);
    printf("Entry point address: \t\t\t0x%x\n", hdr->e_entry);
    printf("Start of program headers: \t\t0x%x (bytes into file)\n", hdr->e_phoff);
    printf("Start of section headers: \t\t0x%x (bytes into file)\n", hdr->e_shoff);
    printf("Flags: \t\t\t\t\t0x%x\n", hdr->e_flags);
    printf("Size of this header: \t\t\t%d (bytes)\n", hdr->e_ehsize);
    printf("Size of program headers: \t\t%d (bytes)\n", hdr->e_phentsize);
    printf("Number of program headers: \t\t%d\n", hdr->e_phnum);
    printf("Size of section headers: \t\t%d (bytes)\n", hdr->e_shentsize);
    printf("Number of section headers: \t\t%d\n", hdr->e_shnum);
    printf("Section header string table index: \t%d\n", hdr->e_shstrndx);
    printf("\n\n");
}

To dump the content of a buffer containing the ELF program:

void hex_dump(char* buf, int size)
{
    int line_position = 0;

    for (int i = 0; i < size; i++)
    {
        // If at the beginning of the line
        if (line_position == 0)
        {
            printf("%08x  ", i);
        }

        // Display the char in hexadecimal
        printf("%02X ", (unsigned char)buf[i]);
        line_position++;

        // Additional space after 8 char
        if (line_position == 8)
        {
            printf(" ");
        }
        
        // New line after 16 char
        if (line_position == 16)
        {
            printf("\n");

            // Reset the position for next line
            line_position = 0;
        }
    }
    printf("\n");
}

To find the beginning of a function in the binary program. It’s especially useful to check if the entry point returned by the loader is correct. Also, it can be used to check that the portion of memory where the program resides is executable:

void* find_program(char* buf, int size)
{
    // Binary program to search in the buffer
    const char program[] = {0x8d, 0x4c, 0x24, 0x04,
                            0x83, 0xe4, 0xf0,
                            0xff, 0x71, 0xfc,
                            0x55, 
                            0x89, 0xe5,
                            0x53,
                            0x51};

    // Size of the binary program
    const int program_size = sizeof(program);

    // Address of the program in the buffer, if found
    void* address = NULL;

    // Search for the pattern in the buffer
    for (int i = 0; i < size - program_size; i++)
    {
        bool found = true;
        for (int j = 0; j < program_size && found; j++)
        {
            if (buf[i + j] != program[j])
            {
                found = false;
            }
        }

        if (found)
        {
            address = (void*)(buf + i);
        }
    }

    return address;
}