_ .-') _     ('-.   ('-.     _ .-') _        .-. .-')               .-') _     ('-.    .-')
( (  OO) )  _(  OO) ( OO ).-.( (  OO) )       \  ( OO )             (  OO) )  _(  OO)  ( OO ).
 \     .'_ (,------./ . --. / \     .'_        ;-----.\  ,--.   ,--./     '._(,------.(_)---\_)
 ,`'--..._) |  .---'| \-.  \  ,`'--..._)       | .-.  |   \  `.'  / |'--...__)|  .---'/    _ |
 |  |  \  ' |  |  .-'-'  |  | |  |  \  '       | '-' /_).-')     /  '--.  .--'|  |    \  :` `.
 |  |   ' |(|  '--.\| |_.'  | |  |   ' |       | .-. `.(OO  \   /      |  |  (|  '--.  '..`''.)
 |  |   / : |  .--' |  .-.  | |  |   / :       | |  \  ||   /  /\_     |  |   |  .--' .-._)   \
 |  '--'  / |  `---.|  | |  | |  '--'  /       | '--'  /`-./  /.__)    |  |   |  `---.\       /
 `-------'  `------'`--' `--' `-------'        `------'   `--'         `--'   `------' `-----'
                                                                                  ~ xcellerator

Ahoy, fellow ELF devotees! In this article, I want to introduce a small library I've been working
on called LibGolf. It started out as simply a vehicle for better understanding the ELF and program
headers, but has since spun into something reasonably practical. It makes is very easy to generate
a binary consisting of an ELF header, followed by a single program header, followed by a single
loadable segment. By default, all the fields in the headers are set to sane values, but there's a
simple way to play with these defaults - and that's what this article is all about! I'm going to
demonstrate how I used LibGolf to enumerate precisely which bytes are necessary and which are
ignored by the Linux loader. Fortunately, it turns out that the loader is one of the least picky
parsers among the standard Linux toolkit. Before we're through, we'll see several popular static
analysis tools crumble before our corrupted ELF, while the loader continues to merrily load and
jump to our chosen bytes.

|--[ Introducing LibGolf ]--|

A while back, I'd been playing with writing ELFs by hand in NASM. While this was fun for a while
(and certainly has it's benefits), I realised that I was missing out on all the fun that C structs
have to offer. In particular, as I'm sure many readers will no doubt be aware, <linux/elf.h>, is
packed full of fun things like `Elf64_Ehdr` and `Elf32_Phdr` ripe for declaring.

Not wanting such helpful headers to go to waste, I elected to take them, and put them to good use.
From these efforts, came libgolf.h, a libary that makes it easy to throw shellcode into a
functioning executable. I know what you're thinking - "this just sounds like a terrible linker!",
and you might be right. However, what's nice here is that you can easily modify the headers *before*
the binary is built.

Let's take a look at how this works. If you want to play along at home, you can find the source code
for all of this at [0]. You can find the code in this article under 'examples/01_dead_bytes'. The
basic setup needs two files; a C source file and a shellcode.h. As far as shellcode goes, I like to
go with the old faithful 'b0 3c 48 31 ff 0f 05', which disassembles to:

        mov al, 0x3c    @ b0 3c
        xor rdi, rdi    @ 48 31 ff
        syscall         @ 0f 05

(Yes - calling this "shellcode" is pushing things a bit!)

Essentially, it just calls exit(0). This is nice because we can easily check that these bytes
successfully executed with the shell expansion $?.

Throw this or some other shellcode (but make sure it's PIC - there's no support for relocatable
symbols yet!) into a buffer called buf[] in shellcode.h and jump back to the C file. If you just
wanted to get a binary that executes your shellcode, then this is all you need:

        #include "libgolf.h"
        #include "shellcode.h"

        int main(int argc, char **argv)

            return 0;

Compiling this and running the resulting executable will provide you with a .bin file - this is
your shiny new ELF! Pretty simple, right? Simplicity is often accompanied by the dull, as is the
case here, so let's do something more interesting!

Before going on, it's worth explaining what these two macros are doing behind the scenes. First off,
INIT_ELF() takes two arguments, the ISA and the architecture. Currently, LibGolf supports X86_64,
ARM32, and AARCH64 as valid ISAs and either 32 or 64 for the architecture. It first sets up some
internal bookkeeping structs, and decides whether to use the Elf32_* or Elf64_* objects for the
headers. It also automatically assigns pointers to the ELF and program headers, called ehdr and
phdr respectively. It is these that we will use to easily modify the fields. Aside from that, it
also copies the shellcode buffer over, and populates the ELF and program headers before calculating
a sane entry point. Now comes GEN_ELF(), which just prints some nice stats to stdout and then writes
the appropriate structs to the .bin file. The name of the .bin is determined by argv[0].

So, after we've used the INIT_ELF() macro, we have ehdr and phdr available to dereference. Suppose
we wanted to modify the e_version field of the ELF header. All we need to do is add a single line:

        #include "libgolf.h"
        #include "shellcode.h"

        int main(int argc, char **argv)

            // Set e_version to 12345678
            ehdr->e_version = 0x78563412;

            return 0;

Another quick compile and execute, and you'll have another .bin file waiting for you. Taking a look
at this file in xxd, hexyl, or your favourite bin-manipulator of choice, you'll see a pretty little
'12 34 56 78' peeking back at you starting at offset 0x14. Wasn't that easy?

To make things move a little faster, I like to use the following Makefile:

        .PHONY golf clean


        	@$(CC) -o $(PROG) $(PROG).c
        	@chmod +x $(PROG).bin

        	@rm $(PROG) $(PROG).bin

(This is the Makefile you'll find in the repo [0])

|--[ Falling At The First Hurdle ]--|

As many will already know, file parsers are awful things. While specifications usually have earnest
goals, they are rarely respected by those who should supposedly know better. Chief among such
blasphemers is the Linux ELF loader itself. LibGolf makes it easy to investigate the extent of these
crimes against elf.h.

A good place to begin is the beginning, which means the ELF header. At the start of any ELF file is
ofcourse, the familar 0x7f followed by ELF, known to its friends as EI_MAG0 through EI_MAG3.
Unsurprisingly, modifying any of these four bytes results in the Linux loader rejecting the file.
Thank goodness for that!

What about byte 0x5? Our trusty specification tells us that this is the EI_CLASS byte and denotes
the target architecture. Acceptable values are 0x01 and 0x02, for 32- and 64-bit respectively. I'll
say again: acceptable values are 0x01 and 0x02. What if we set it to 0x58 (or "X" for ASCII-
adherents)? We can do that by adding:

        (ehdr->e_ident)[EI_CLASS] = 0x58;

to our generating C file. (Why 0x58? It shows up clearly in xxd/hexyl output!)

Once we've got our .bin to play with, before trying to execute it, let's try a couple of other
familar ELF parsing tools in the search for further culprits. First on the list is gdb. Go on,
I'll wait. See what happens?

        "not in executable format: file format not recognized"

Likewise, objdump will give you a similar answer. It seems these parsers are doing their job
properly. Now, let's try to run the binary normally.

        <spoiler>It works perfectly.</spoiler>

If you're using my example shellcode, then a consultation with $? will regretably inform you that
the binary exited successfully. The same crimes are commited when setting EI_DATA and EI_VERSION to
illegal values too.

|--[ Turning The Corruption Up To 11 ]--|

So, how far can we go? Just how much of the ELF and program headers will the Linux loader ignore?
We've already covered EI_CLASS, EI_DATA and EI_VERSION, but it turns out that EI_OSABI is also
safely ignored. That takes us up to offset 0x8. According to the spec, next up is EI_ABIVERSION and
EI_PAD which, together, take us all the way to byte 0xf. No one cares about them it seems, so we
can set all of them to 0x58 without fear.

Marching ever further, we come across a field that appears to be resistant to being messed with:
e_type. Understandably, the Linux loader doesn't like it if we don't tell it what kind of ELF file
we're providing it with (it's nice to know that it does have *some* standards! - pun intended). We
need these two bytes to remain 0x0002 (or ET_EXEC to elf.h acolytes). Next up is another picky byte,
at the all-too-familiar 0x12 offset: e_machine, which designates the target ISA. As far as we're
concerned, by specifying X86_64 as the first argument to INIT_ELF(), this byte has already been
populated with 0x3e for us by LibGolf.

Suddenly, a wild e_version appeared! We're faced with another dissident, which supposedly should
always be the bytes 0x00000001. However, in practice, no one seems to be interested, so let's fill
it with 0x58585858 instead.

Following this string of heretics, we have a couple of important fields that appear to be resistant
to abuse; e_entry and e_phoff. I'm sure I needn't go into too much detail about e_entry; it's the
entry point of the binary, where execution is ultimately handed off to once the loadable sections
are in memory. While one might expect that the loader could manage without knowing what the offset
to the program header is, it appears that it isn't smart enough to figure it out without being
spoon-fed. Better leave these two alone.

LibGolf is yet to support section headers (and given its focus on producing *small* binaries, it is
probably unlikely to support them in the future). This means that, faced with any headers relating
to them, we can fiddle to our heart's content. That includes e_shoff, e_shentsize, eh_shnum and even
e_shstrndx. If we don't have any section headers, we can't be held accountable for corrupting them!

The remaining fields that are seemingly of some import to the Linux loader are e_ehsize, e_phentsize,
and e_phnum. Again, this isn't too surprising, seeing as they are concerned with loading the only
loadabale segment into memory before handing over control. If you need a refresher, e_ehsize is the
size of the ELF header (which is either 0x34 or 0x40 for 32- and 64-bit respectively), eh_phentsize
is the size of the upcoming program header (again, hardcoded to either 0x20 or 0x38 for 32- and
64-bit architectures). If the loader had been a little more picky about EI_CLASS, it wouldn't need
these two fields. Lastly, e_phnum is just the number of entries in the program header - for us it is
always 0x1. No doubt, this is used for some loop in the memory loading routines, but I haven't
investigated any further yet.

There is still one field left in the ELF header I haven't touched on, which is e_flags. The reason
is fairly simple, in that it's architecture dependent. For x86_64, it doesn't matter at all because
it's undefined (although it *is* important for some ARM platforms! Take a look at the arm32 example
at [0]).

That brings us to the end of the ELF header. For those not keeping count, just over 50% of the ELF
header is ignored by the loader. But what about the program header? It turns out that program headers
have a lot less wiggle room in them, but not for the reason one might expect. Indeed, *any* corruption
of the program header will not actually affect the Linux loader. We could fill the whole thing with
our trusty 0x58, and the loader won't care a jot. But beware, bold adventurer, fiddle with the wrong
byte and you'll be plunged into the dungeon of faulty segmentation!

So, is there anything at all susceptible to coercion in the program header? It transpires that there
are two fields that, by no fault of their own, simply aren't relevant anymore: p_paddr and p_align.
The former was important back in the heady days before virtual memory, where 4GB of RAM was nothing
more than a child's daydream and it was therefore important to inform the loader where in physical
memory the segment should be loaded.

Memory alignment is a funny one. Supposedly, p_vaddr is meant to equal p_offset modulo p_align.
"Proper" ELF files (at least those compiled with GCC) appear to just set p_offset equal to p_vaddr
and move on. This is also what LibGolf does by default and renders p_align totally superfluous!

All in all, not as fun as the ELF header, but still some small gains. The binary generating C file
now looks like this:

        #include "libgolf.h"
        #include "shellcode.h"

        int main(int argc, char **argv)

             * Breaks common static analysis tools like gdb and objdump
            (ehdr->e_ident)[EI_CLASS] = 0x58;   // Architecture
            (ehdr->e_ident)[EI_DATA] = 0x58;    // Endianness
            (ehdr->e_ident)[EI_VERSION] = 0x58; // Supposedly, always 0x01
            (ehdr->e_ident)[EI_OSABI] = 0x58;   // Target OS

            // Loop over the rest of e_ident
            int i;
            for ( i = 0 ; i < 0x10 ; i++ )
                (ehdr->e_ident)[i] = 0x58;

            ehdr->e_version = 0x58585858;       // Supposedly, always 0x00000001

            // Section headers? We don't need no stinkin' section headers!
            ehdr->e_shoff = 0x5858585858585858;
            ehdr->e_shentsize = 0x5858;
            ehdr->e_shnum = 0x5858;
            ehdr->e_shstrndx = 0x5858;

            ehdr->e_flags = 0x58585858;         // x86_64 has no defined flags

            phdr->p_paddr = 0x5858585858585858; // Physical address is ignored
            phdr->p_align = 0x5858585858585858; // p_vaddr = p_offset, so irrevelant

            return 0;

If you compile and run this program, you'll get the following binary:

        00000000: 7f45 4c46 5858 5858 5858 5858 5858 5858  .ELFXXXXXXXXXXXX
        00000010: 0200 3e00 5858 5858 7800 4000 0000 0000  ..>.XXXXx.@.....
        00000020: 4000 0000 0000 0000 5858 5858 5858 5858  @.......XXXXXXXX
        00000030: 5858 5858 4000 3800 0100 5858 5858 5858  XXXX@.8...XXXXXX
        00000040: 0100 0000 0500 0000 0000 0000 0000 0000  ................
        00000050: 0000 4000 0000 0000 5858 5858 5858 5858  ..@.....XXXXXXXX
        00000060: 0700 0000 0000 0000 0700 0000 0000 0000  ................
        00000070: 5858 5858 5858 5858 b03c 4831 ff0f 05    XXXXXXXX.<H1...

This file is 127 bytes in size, but we were able to replace a total of 50 bytes with 'X', meaning
just under 40% of this binary is ignored by the Linux ELF loader! Who knows what you could do with
50 bytes?

It turns out - quite a lot. Some amazing research from a couple of years ago by netspooky
demonstrated how you can pile up portions of the program header into the ELF header. Combined
with storing your shellcode inside one of these regions of dead bytes, and a few other neat tricks,
it's possible to shrink an ELF down to just 84 bytes - a 34% reduction on top of LibGolf's current
best efforts. I point you in the direction of his incredible "ELF Mangling" series at [1].

Another interesting aspect of these techniques is easily overlooked. Although the Linux loader seems
to care very little about the structure of an ELF beyond what it needs to just get to the machine
code, other tools are far more picky. We already looked at objdump and gdb, but a lot of AV solutions
also crumble when faced with a malformed ELF. In my research, the only product that (sorta) gets it
right is ClamAV, with a positive result for "Heuristics.Broken.Executable". Of course, dynamic
analysis is still anyone's bet.

|--[ Going Forwards ]--|

x86_64 isn't the only ISA supported by LibGolf! You can also use it to build tiny executables for
ARM32 and AARCH64 platforms too. In the repo on GitHub [0], you'll find some examples for both ARM
platforms (including the dead bytes one from this article).

But examples be damned! Hopefully most of you that have made it this far want to take a look at
libgolf.h itself. As I mentioned at the start, this whole thing started off as a learning exercise,
so I paid special attention to commenting things in as much detail as I could.

|--[ A Note on Reproducibility ]--|

Throughout this research, I mainly tested on Ubuntu 20.04 with kernel 5.4.0-65-generic, but also
verified that the same results could be obtained on 5.11.11-arch1-1. I've heard that strange things
can sometimes happen on the WSL kernels, but I've not investigated them - maybe you can!

|--[ Callouts ]--|

A special "ahoy" to everyone at Thugcrowd, Symbolcrash, and the Mental ELF Support Group!

|--[ References ]--|

[0] https://www.github.com/xcellerator/libgolf
[1] https://n0.lol/ebm/1.html