Elf Binary Mangling Part 3 — Weaponization

Hey y’all, thanks for all the support ! I didn’t realize so many people would think this sort of coding was as cool as I do.

In the previous write up, the concept of binary golf was established, executing a binary in as few bytes as possible. There’s quite a lot of history in the realm of “size coding”, and extreme assembly optimization. The people who pioneered and later weaponized these approaches did some amazing work to really map out what the limitations of the processor actually are, and some wild ways of making things happen.

There are many resources regarding size coding, shellcode development, and other assembly tricks. In this write up, we are going to explore coding within the size boundary we established for ELF64, and actually make those 84 bytes actually do something.

Establishing Boundaries

Like everything in life, boundaries need to be established in order to understand processes and their effects. Within the boundaries of the ELF64 binary template, there are some key areas where code can safely exist. These are sections where the ABI that processes your binary doesn’t seem to mind a bunch of junk data. Due to us already overlaying ELF and program headers, there is a significant challenge to identifying these locations, and reusable structures that we can leverage.

Our main areas of focus today are:

0x04–0x0F : 12 bytes
0x3C-0x39 : 4 bytes 
0x44–0x47: 4 bytes 
0x4C-0x53: 8 bytes

bye.asm

This is the code for the program that we want to execute:
badcfe2143 .. mov edx, 0x4321fedc 
be69191228 .. mov esi, 0x28121969 
bfaddee1fe .. mov edi, 0xfee1dead 
b0a9 ........ mov al, 0xa9
0f05 ........ syscall

In a nutshell, this program executes the reboot syscall with the LINUX_REBOOT_CMD_POWER_OFF argument. This essentially executes the same syscall that your OS calls when you hold down the power off button, but without any of sync or other routines that will allow your system to shutdown gracefully. The syscall is executed by placing magic values and an argument for the specific type of reboot you want to do in the specified registers, and then calling the kernel.

The Process

So how do we load all of this into our 84 byte ELF binary? Let’s take a look at our code, and our binary, and see what we can do.

0x04–0x0F

The first two instructions move the constant values into two registers. These values are LINUX_REBOOT_MAGIC2 (0x28121969) into ESI, and LINUX_REBOOT_CMD_POWER_OFF (0x4321fedc) into EDX. Here, we are using the 32 bit forms of the registers. Moving a 32 bit value into the lower 32 bits of a given 64 bit register will zero out the top 32 bits, meaning we don’t need to xor or do anything else to ensure that the top 32 bits are 0. This is not true for moving to lower bits though, such as say, mov al, 8. This would keep the top values in RAX, and only change the bottom 8 bits to 00001000.

These two instructions are 5 bytes each, meaning that in our 12 byte boundary, we have two bytes left to use up here. This is the perfect amount of space to fit a short jump to the rest of the code!

0x4C-0x53

Now we jump down to the label reeb within the p_align section of the program header, which has 8 bytes for us to use. Here, we are moving another constant, LINUX_REBOOT_MAGIC1, necessary for our syscall into EDI, and jumping to the last location to execute. The mov and jmp instructions together are only 7 bytes, so I included a nop at the very end to keep the binary at a svelte 84 bytes. Without this, the binary wouldn’t execute. It also shows how much space you have to work with in this location.

When I initially released this binary, I didn’t put the mov al, 0xa9 and syscall instructions up in the program header, leading to 1 extra byte. To solve this, we do something that requires a bit of care to do properly.

0x3C-0x39 and 0x44–0x47

The final step is our jump to the label cya, starting at 0x3C. There are a few structures in this area that need to be addressed.

The p_filesz and p_memsz structures appear to need to be the same value in order to execute properly on most kernels. The other tricky aspect is that these are file sizes that have a size limit within the program header that needs to be investigated more. Since this is little endian when we write in nasm, it stays in the lower 4 bytes of the addresses. If you touch the first byte, it might put you over the available memory on the system, which will render the binary unusable. In my experience, using only 4 bytes in these locations is playing it safe, but you should definitely play around with these!

Knowing these limitations, we have enough space to do our final moves, loading RAX with our syscall value 0xa9, and executing a REBOOT.

A very interesting thing to note about the bytes at 0x3C-0x39 is that they are processed a total of three times by the kernel when this executes. First as the e_shnum and e_shstrndx structures in the ELF header, second as the p_filesz structure in the Program Header, and lastly as the code that finishes the execution of the binary.

Here is a handy one liner that will do this.

base64 -d <<< f0VMRrrc/iFDvmkZEijrPAIAPgABAAAABAAAAAEAAAAcAAAAAAAAAAAAAAAAAAAAAQAAAEAAOAABAAIAsKkPBQAAAACwqQ8FAAAAAL+t3uH+6+mQ > bye;chmod +x bye;sudo ./bye

Please read the next section before running this on any system.

Effects

On a desktop system, this binary will shut down your computer abruptly. There are some potential side effects from a shutdown like this, but personally I haven’t experienced any issues with it.

However, on a VPS, this specific syscall proves to be a bit of a problem. Since the virtual machine doesn’t actually have any of it’s own physical hardware (it’s either virtualized or shared with the host), the power button on a VPS isn’t really a thing. By executing a syscall the effectively “shuts off the power” to the operating system, this puts the VM in an unknown state.

So far, whenever this is run on a VPS, it seemingly wipes out the entire instance. A thread about this one liner (the 85 byte version) is here:

This is a far more destructive piece of code than rm -rf --no-preserve-root / or a fork bomb, because even in those situations, a VM could be recovered via snapshots or mitigated with access / resource controls.

.fini

So there is still quite a lot to explore in this space, and not enough people doing it! I encourage you to play around with these concepts, and see what you can do with it! The next write up in this series will have to do with some assembly optimization concepts, and some space saving tricks like reusing constants of the header (hint: 0x00–0x04), and their practical usage.

In case you missed it, here are links to Part 1 and Part 2 of this series.

greetz: hermit, blackout, jinn, dnz, phaith, readme, notpike, decoded and many others for encouraging me, nt for challenging me frequently (and publicly), and everyone who nuked their own VPS and VMs to test with me.

bye2: everyone who flashcards others with assembly, you really make chatting a joy and totally not toxic.

Check out our podcast @thugcrowd, and come hang in Discord to play with our bots and learn a thing or two.