Making our own executable packer

In this series, we'll attempt to understand how Linux executables are organized, how they are executed, and how to make a program that takes an executable fresh off the linker and compresses it - just because we can.

Read part 1

Series overview

1. What's in a Linux executable?

Executables have been fascinating to me ever since I discovered, as a kid, that they were just files. If you renamed a .exe to something else, you could open it in notepad! And if you renamed something else to a .exe, you'd get a neat error dialog.

Clearly, something was different about these files. Seen from notepad, they were mostly gibberish, but there to be order in that chaos. 12-year-old me knew that, although he didn't quite know how or where to dig to make sense of it all.

2. Running an executable without exec

In part 1, we've looked at three executables:

  • sample, an assembly program that prints "hi there" using the write system call.
  • entry_point, a C program that prints the address of main using printf
  • The /bin/true executable, probably also a C program (because it's part of GNU coreutils), and which just exits with code 0.

We noticed that when running through GDB, it always printed the same address. But when we ran it directly, it printed a different address on every run.

3. Position-independent code

In the last article, we found where code was hiding in our samples/hello executable, by disassembling the whole file and then looking for syscalls.

Later on, we learned how to inspect which memory ranges are mapped for a given PID (process identifier). We saw that memory areas weren't all equal: they can be readable, writable, and/or executable.

4. ELF relocations

The last article, Position-independent code, was a mess. But who could blame us? We looked at the world, and found it to be a chaotic and seemingly nonsensical place. So, in order to blend in, we had to let go of a little bit of sanity.

The time has come to reclaim it.

Short of faulty memory sticks, memory locations don't magically turn from 0x0 into valid addresses. Someone is doing the turning, and we're going to find out who, if it takes the rest of the series.

5. The simplest shared library

In our last article, we managed to load and execute a PIE (position-independent executable) compiled from the following code:

x86 assembly
; in `samples/hello-pie.asm`

        global _start

        section .text

_start: mov rdi, 1      ; stdout fd
        lea rsi, [rel msg]
        mov rdx, 9      ; 8 chars + newline
        mov rax, 1      ; write syscall
        

         ,     
         ,      
        

         

:     , 
6. Loading multiple ELF objects

Up until now, we've been loading a single ELF file, and there wasn't much structure to how we did it: everyhing just kinda happened in main, in no particular order.

But now that shared libraries are in the picture, we have to load multiple ELF files, with search paths, and keep them around so we can resolve symbols, and apply relocations across different objects.

7. Dynamic symbol resolution

Let's pick up where we left off: we had just taught elk to load not only an executable, but also its dependencies, and then their dependencies as well.

We discovered that ld-linux walked the dependency graph breadth-first, and so we did that too. Of course, it's a little bit overkill since we only have one dependency, but, nevertheless, elk happily loads our executable and its one dependency:

8. Dynamic linker speed and correctness

In the last article, we managed to load a program (hello-dl) that uses a single dynamic library (libmsg.so) containing a single exported symbol, msg.

Our program, hello-dl.asm, looked like this:

x86 assembly
        global _start
        extern msg

        section .text

_start:
        mov rdi, 1      ; stdout fd
        mov rsi, msg
        mov rdx,      
         ,       
        

         ,     
         ,      
        
9. GDB scripting and Indirect functions

In the last article, we cleaned up our dynamic linker a little. We even implemented the Dynamic relocation.

But it's still pretty far away from running real-world applications.

Let's try running a simple C application with it:

C code
// in `samples/puts.c`

#include <stdio.h>

int main() {
    puts("Hello from C");
    return 0;
}
Shell session
10. Safer memory-mapped structures

Welcome back to the "Making our own executable packer" series, where digressions are our bread and butter.

Last time, we implemented indirect functions in a no-libc C program. Of course, we got lost on the way and accidentally implemented a couple of useful elk-powered GDB functions - with only the minimal required amount of Python code.

The article got pretty long, and we could use a nice distraction. And I have just the thing! A little while ago, a member of the stumbled upon this series and gave me some feedback.

11. More ELF relocations

In our last installment of "Making our own executable packer", we did some code cleanups. We got rid of a bunch of unsafe code, and found a way to represent memory-mapped data structures safely.

But that article was merely a break in our otherwise colorful saga of "trying to get as many executables to run with our own dynamic loader". The last thing we got running was the program.

12. A no_std Rust binary

In Part 11, we spent some time clarifying mechanisms we had previously glossed over: how variables and functions from other ELF objects were accessed at runtime.

We saw that doing so "proper" required the cooperation of the compiler, the assembler, the linker, and the dynamic loader. We also learned that the mechanism for functions was actually quite complicated! And sorta clever!

13. Thread-local storage

Welcome back and thanks for joining us for the reads notes... the thirteenth installment of our series on ELF files, what they are, what they can do, what does the dynamic linker do to them, and how can we do it ourselves.

I've been pretty successfully avoiding talking about TLS so far (no, not that one) but I guess we've reached a point where it cannot be delayed any further, so.

14. In the bowels of glibc

Good morning, and welcome back to "how many executables can we run with our custom dynamic loader before things get really out of control".

In Part 13, we "implemented" thread-local storage. I'm using scare quotes because, well, we spent most of the article blabbering about Addressing Memory Through The Ages, And Other Fun Tidbits.

we
15. Between libcore and libstd

You're still here! Fantastic.

I have good news, and bad news. The good news is, we're actually going to make an executable packer now!

Bear

Hurray!

I know right? No lie, we're actually really going to start working on the final product from this point onwards.

Bear

What uhhh what about the previous fourteen parts?

Ah, yes, the previous fourteen parts. Well, we had fun, didn't we? And we learned a lot about ELF, how it's basically a database format that different tools look at in different ways, how it's mapped in memory (more or less), what we need to set up before starting up another executable, all that good stuff.

16. Everything but ELF

And we're back!

In the last article, we thanked our old code and bade it adieu, for it did not spark joy. And then we made a new, solid foundation, on which we planned to actually make an executable packer.

As part of this endeavor, we've made a crate called encore, which only depends on libcore, and provides some of the things libstd would give us, but which we cannot have, because we do not want to rely on a libc.

17. Running a self-relocatable ELF from memory

Welcome back!

In the last article, we did foundational work on minipak, our ELF packer.

It is now able to receive command-line arguments, environment variables, and auxiliary vectors. It can parse those command-line arguments into a set of options. It can make an ELF file smaller using the LZ4 compression algorithm, and pack it together with stage1, our launcher.

18. Fine, we'll relocate our own binary!

Welcome back to the eighteenth and final part of "Making our own executable packer".

In the last article, we had a lot of fun. We already had a "packer" executable, minipak, which joined together stage1 (a launcher), and a compressed version of whichever executable we wanted to pack.

What we added, was a whole bunch of abstractions to parse ELF headers using , which we used from to be able to launch the guest executable from memory, instead of writing it to a file and using on it.

This series is complete.