Jump to entry point of ELF from loader
Asked Answered
I

1

4

Thanks to the help in this question, the loader can now map a statically compiled hello world into memory and jump somewhere in that memory region. The problem I'm facing now is I seem not to jump to the right address or I'm calling the function in the wrong way (or wrong function?).

Below is the code to try; I can't find in glibc where the loader calls the entry point of the program to verify I'm doing the right thing. I tried:

  • Calling _start(void)
  • Calling _start(int, char**, char**)
  • Calling __libc_start_main with the last parameters set to NULL (found here)
  • Calling directly main
  • Calling some addresses in (found with gdb in rdi: 400B4D, r8: 4018E0, rcx: 401840)

The entry point is 0x400a30, some instructions below __libc_start_main. With some of them it SIGSEGV, SIGABRT or prints:

hello, world! haswell xeon_phi ../csu/libc-start.c FATAL: kernel too old
   __ehdr_start.e_phentsize == sizeof *GL(dl_phdr) unexpected reloc type in static binary  FATAL: cannot determine kernel version
 __libc_start_main /dev/full /dev/null   cannot set %fs base address for thread-local storage :  %s%s%s:%u: %s%sAssertion `%s' failed.
%n        Unexpected error.

and some hundred of junk lines.

int main(int argc, char* argv[argc+1]) {
    FILE *fp = fopen(argv[1], "r");
    if (!fp) {
        fprintf(stderr, "cannot open file %s", argv[1]);
        return 1;
    }

    fseek(fp, 0L, SEEK_END);
    size_t sz = ftell(fp) + 1;
    rewind(fp);

    char *region = mmap(NULL, sz, PROT_READ, MAP_PRIVATE, fileno(fp), 0);
    if (region == MAP_FAILED) {
        fprintf(stderr, "could not mmap");
        return 1;
    }

    Elf64_Ehdr elf64Ehdr;
    memmove(&elf64Ehdr, region, sizeof(elf64Ehdr));

    size_t offset;
    Elf64_Phdr elf64Phdr;
    for (uint16_t i = 0; i != elf64Ehdr.e_phnum; ++i) {
        offset = elf64Ehdr.e_phoff + i * elf64Ehdr.e_phentsize;
        memmove(&elf64Phdr, region + offset, sizeof(elf64Phdr));
        switch (elf64Phdr.p_type) {
            case PT_NULL:
                break;
            case PT_LOAD:
                if (load(&elf64Phdr, region + elf64Phdr.p_offset)) {
                    exit(EXIT_FAILURE);
                }
                break;
            default:
                break;
        }
    }
    printf("jumping to: 0x%x\n", elf64Ehdr.e_entry);

    char *argv1[] = {"", NULL};

    int ret = ((int (*)(int, char **, char **)) elf64Ehdr.e_entry)(1, argv1, argv1);
    return ret;
}

int load(const Elf64_Phdr *phdr, const void *elf_bytes_for_phdr) {
    const size_t pagesize = getpagesize();
    const size_t unaligned_bytes = phdr->p_vaddr % pagesize;

    void *base_addr = phdr->p_vaddr - unaligned_bytes;
    size_t total_bytes = phdr->p_memsz + unaligned_bytes;

    void *region = mmap(base_addr, total_bytes,
            phdr->p_flags | PROT_WRITE, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
    if (region != MAP_FAILED) {
        memset(region, 0, unaligned_bytes);
        return memcpy(region + unaligned_bytes, elf_bytes_for_phdr, phdr->p_filesz) != region + unaligned_bytes && 
        ! mprotect(region, total_bytes, phdr->p_flags);
    }
    return 1;
}
Incapacious answered 7/4, 2019 at 23:42 Comment(1)
Am I missing something; it looks like you're calling elf64Ehdr.e_entry directly, i.e. without offsetting by the base address (region)...?Zebapda
V
4

The problem I'm facing now is I seem not to jump to the right address or I'm calling the function in the wrong way

Your problem is neither of the above (although "calling the wrong way" is not too far).

For a statically-linked executable, Elf64_Ehdr.e_entry is the correct address to call (it points to _start), and _start takes no arguments.

The problem is that it is the job of _start to

  1. initialize libc, and
  2. to find correct values of argc, argv and envp, and finally
  3. to call main(argc, argv, envp).

The question then is: how can _start accomplish step 2?

The answer: there is a protocol that Linux kernel implements and _start uses to accomplish step 2.

In particular, the kernel copies the actual (string) values of argv[0], argv[1], ... envp[0], envp[1], etc. to the stack, then the pointers to these strings. There is also something called auxilliary vector.

The _start expects to find all of this info on the stack, and will misbehave when it doesn't find it. I believe that is the root cause of your current problem.

Here is an article which explains expected setup with references to Linux kernel source code. Another article.

Voncile answered 8/4, 2019 at 0:6 Comment(10)
Said more simply, the entry point is not a C function. The contract for entry requires specific initial values for certain registers, and the main argument is a pointer to the vector of argc, argv entries, environment entries, aux, that arrives in the stack pointer.Ignatzia
I like those two articles, link more! Back to my question: is my only option to push to the stack argc and the addresses of argv and env using inline asm (or plain linked asm)? Calling _start with (argc, argv, env) in fact does not help. What is Elf64_Ehdr.e_start? I see no ref. to it. + @R..Incapacious
@Incapacious Sorry, I meant .e_entry (you are already doing it). And yes, you must make the stack look exactly the way kernel prepares it and _start expects.Voncile
@bicup: "Calling" _start (rather e_entry) without asm is impossible in general because it's not a function. For some archs, it may be possible to write a function call where the ABI constraints happen to guarantee the right properties for the ELF entry point, but that's even worse than (and equally arch-specific as) writing asm.Ignatzia
@R.. I wrote a simple function in x64 that pushes 1 (argc), and 3 times <pointer to string "hi", NULL> (argv, env, aux) and it crashes at 0x0. Could it depend on _start not reading from the stack but registers in x64?Incapacious
@bicup: You're pushing in the wrong order (backwards), and "hi" is not a valid environment string (it lacks an '=' character) nor a valid auxv entry (they are not strings but pairs of (usually small) integer key and integer or pointer value.Ignatzia
@R.. the stack now looks good with gdb, but it still gets killed. Before jumping to e_entry: $rdi: 0x400a30 and $rsp -> 1 (argc); jmp rdi goes to a xor ebp, ebp, then push rsp kills it. Is it missing something in the stack or registers? From the articles above, it looks fineIncapacious
(I have also tried pushing 16 pairs of integer, integer pointers + the null one)Incapacious
Still looking for help.Incapacious
I just came across bitwagon.com/jumpstart/jumpstart.html which very likely contains the answer (at least for i386).Voncile

© 2022 - 2024 — McMap. All rights reserved.