Thinking that Bovik’s flags might be hidden in plain sight, you find on your minimap of the Inner Sanctum that there’s a golf course tucked into the northeast corner. Before other people catch on to the idea, you sneak off towards the rolling grassy hills of the golf course.

As you walk up to the entrance, a small man with pointy ears pops up out of the ground.

“Would you like to play? Right now almost nobody is here, so it’ll only be a thousand checkers to play a round.“

You wave your hand and send the man the required amount. It’s not a lot of money, but your balance dwindles to a paltry sum.

“Thanks! Enjoy your game.“

A golf club appears in your hand along with a ball that looks suspiciously too large. The start of the first hole beckons, and you stride over to tee off.

On the website we see a scoreboard with team names and file sizes. There is an “Upload” button which shows us the rules of the game:

Upload a 64-bit ELF shared object of size at most 1024 bytes. It should spawn a shell (execute execve("/bin/sh", ["/bin/sh"], ...)) when used like

LD_PRELOAD=<upload> /bin/true

Ok, we have to write a tiny ELF file which spawns a shell when LD_PRELOADed. Unfortunately a simple C program produces a 16kB large .so file, which is clearly too big.

ELF: Dynamic Shared Objects

First of all we need to know how ELF files work.

Every 64bit ELF file starts with an Elf64_Ehdr struct. It contains a magic word, as well as general information about the file like the CPU architecture, entry point, and endianess. It also has pointers to two other structures, the Program Headers and Section Headers.

Program Headers (Elf64_Phdr) describe which segments are stored in the ELF file. There are two important types of segments: PT_LOAD segments and PT_DYNAMIC segments. Only PT_LOAD segments are loaded into memory. They usually contain code, data, and other information that has to be available at runtime. A PT_DYNAMIC segment has to overlap with a PT_LOAD segment, and it holds information for the dynamic linker.

Section Headers (Elf64_Shdr) describe sections in the ELF file, like e.g. the .text section. They contain additional information about which data is stored where within a segment. A single segment can contain multiple sections.

Since we want to create a library, we need both code and a PT_DYNAMIC segment. We will see that sections are not important at all.

ELF: The DYNAMIC Segment

The DYNAMIC segment consists of a list of Elf64_Dyn entries, which contain a tag and a value. According to some webpage a few entries are mandatory and all other entries are optional.

One entry is particularly interesting, the DT_INIT entry. It has a pointer to an initializer function which is executed after the library is loaded. It is supposed to initialize the library. We will use this function to spawn the shell.

Writing An ELF File By Hand

With this information, we can start to write an ELF file by hand. To start “small”, we write a fully compliant ELF file with all necessary information.

The shellcode is rather simple, it’s an off the shelf msfvenom shellcode.

First Attempt: golf.c

Running this program produces a which is 821 bytes large. We can upload this to the website, but it tells us that we have to shrink the file in order to get the flag.


Making It Smaller

Now let’s remove all unnecessary things. First of all the section headers are totally irrelevant, we can just remove them. With the section headers removed, we can also remove the strings for the string tables, since they were only referenced from section headers.

We can also remove most of the segments and only leave a single PT_LOAD segment which holds the code and the DYNAMIC data. And we can remove most of the Elf64_Dyn entries, since they are not needed.

Now we only have two Program Headers:

  • PT_LOAD which loads the whole file to address 0
  • PT_DYNAMIC which points to the Elf64_Dyn list

We still have a few Elf64_Dyn entries:

  • DT_INIT for our initializer

But this is still too large.

We can start to overlap data. The last Program Header doesn’t need a value in p_memsz since it is not a PT_LOAD segment. We could start our Elf64_Dyn list in this field, such that it overlaps with the Program Header.

We can save 6 more bytes by overlapping the first Program Header with the end of the ELF header, since the last 3 fields in the ELF header are not used.

We cannot remove any more Elf64_Dyn records, since removing any of them makes the linker crash.

Seccond Attempt: golf.c

We now have 229 bytes, but it is still too large.

Making It Even Smaller

Running /bin/true with preloaded in a debugger reveals that our initializer is called via call rax, so rax has a pointer to the shellcode. Now it’s time to write some custom shellcode which is smaller. Since we have the address to our shellcode, we can store the /bin/sh string in some unused ELF header field and directly reference it. Since the offset to the Section Headers e_shoff is not used, we can just store the string in there.

The shellcode now looks like this:

401000:       48 8d b8 5e ff ff ff    lea    rdi,[rax-0xa2]
401007:       31 c0                   xor    eax,eax
401009:       50                      push   rax
40100a:       57                      push   rdi
40100b:       48 89 e6                mov    rsi,rsp
40100e:       50                      push   rax
40100f:       48 89 e2                mov    rdx,rsp
401012:       b0 3b                   mov    al,0x3b
401014:       0f 05                   syscall

Third Attempt: golf.c

This generates a 224 byte large, but that’s still too big. We have to reach 192 bytes!

Creating A Tiny But Totally Broken ELF File

Shrinking the file even further is no longer trivial. We now have to overlap everything, and completely get rid of the shellcode section.

There are a few pointers within the ELF header as well as within Program Headers that are never used. We can overwrite them with arbitrary values and the linker won’t care. We can use this to split the shellcode and embed it in those pointers.

Unfortunately the first command (lea rdi,[rax-0xa2]) not only depends on the address of the initializer function, it also consists of 7 bytes, so we cannot add a jump afterwards. Luckily there are some pointers where the exact value of the next field doesn’t matter that much, so we can enter something slightly too big. More specifically, if we put this command into the p_addr field of the DYNAMIC segment, we can put the offset for a jmp into the p_filesz field. The DT_INIT has to point to the p_addr field. The lea is executed, and then the jump is taken.

The next shellcode fragment can be placed in the e_entry field of the ELF header. We can fit 4 shellcode instructions in there, but then we only have one byte left for the next jump. This time we cannot change the next byte, but since the next field is the pointer to the Program Headers and the Program Headers start at offset 0x3A, we conveniently jump to address 0x5A. This is right into the p_filesz field of the PT_LOAD header. We put another jump there, back to the p_addr field of the same Program Header. In this field we can finally put the remaining commands of the shellcode.

Since we don’t need the shellcode at the end of the file anymore, the last Elf64_Dyn entry ends with a NULL value. We don’t have to store that in our ELF file, since everything past the file end is implicitly zero. We don’t even have to store the whole d_tag of the last Elf64_Dyn record, since only the first byte is nonzero.

With those optimizations, the final ELF file is only 187 bytes large.

Final Solution: golf.c

Interestingly enough, IDA Pro refuses to load this ELF file.

With this file, we finally get our flags: