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.
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
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.
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.
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
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!
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
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.
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.
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.
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.