ELF Binary Mangling Pt. 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 loader 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 that we want to execute:

1
2
3
4
5
mov edx, 0x4321fedc ; badcfe2143  
mov esi, 0x28121969 ; be69191228  
mov edi, 0xfee1dead ; bfaddee1fe  
mov al, 0xa9        ; b0a9        
syscall             ; 0f05        

In a nutshell, this program executes the reboot syscall with the argument LINUX_REBOOT_CMD_POWER_OFF. 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.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
; 84 byte LINUX_REBOOT_CMD_POWER_OFF Binary Golf
BITS 64
  org 0x100000000
;---------------------+------+------------+------------------------------------------+-----------------------------+----------+
; CODE LISTING        | OFFS | ASSEMBLY   | CODE COMMENT                             | ELF HEADER STRUCT           | PHDR     | 
;---------------------+------+------------+------------------------------------------+-----------------------------+----------+
  db 0x7F, "ELF"      ; 0x0  |   7f454c46 | PROTIP: Can use magic as a constant ;)   | ELF Magic                   |          |
_start:               ;------|------------|------------------------------------------|-----------------------------|----------|
  mov edx, 0x4321fedc ; 0x04 | badcfe2143 | Moving magic values...                   | ei_class,ei_data,ei_version |          |
  mov esi, 0x28121969 ; 0x09 | be69191228 | into their respective places             | unused                      |          |
  jmp short reeb      ; 0x0E |       eb3c | Short jump down to @x4c                  | unused                      |          |
  dw 2                ; 0x10 |       0200 |                                          | e_type                      |          |
  dw 0x3e             ; 0x12 |       3e00 |                                          | e_machine                   |          |
  dd 1                ; 0x14 |   01000000 |                                          | e_version                   |          |
  dd _start - $$      ; 0x18 |   04000000 |                                          | e_entry                     |          |
phdr:                 ;------|------------|------------------------------------------|-----------------------------|----------|
  dd 1                ; 0x1C |   01000000 |                                          | e_entry                     | p_type   |
  dd phdr - $$        ; 0x20 |   1c000000 |                                          | e_phoff                     | p_flags  |
  dd 0                ; 0x24 |   00000000 |                                          | e_phoff                     | p_offset |
  dd 0                ; 0x28 |   00000000 |                                          | e_shoff                     | p_offset |
  dq $$               ; 0x2C |   00000000 |                                          | e_shoff                     | p_vaddr  |
                      ; 0x30 |   01000000 |                                          | e_flags                     | p_vaddr  |
  dw 0x40             ; 0x34 |       4000 |                                          | e_shsize                    | p_addr   |
  dw 0x38             ; 0x36 |       3800 |                                          | e_phentsize                 | p_addr   |
  dw 1                ; 0x38 |       0100 |                                          | e_phnum                     | p_addr   |
  dw 2                ; 0x3A |       0200 |                                          | e_shentsize                 | p_addr   |
cya:                  ;------|------------|------------------------------------------|-----------------------------|----------|
  mov al, 0xa9        ; 0x3C |       b0a9 | Load syscall                             | e_shnum                     | p_filesz |
  syscall             ; 0x3E |       0f05 | Execute syscall                          | e_shstrndx                  | p_filesz |
  dd 0                ; 0x40 |   00000000 | Filler, should try to keep as all 0's    |                             | p_filesz |
  mov al, 0xa9        ; 0x44 |       b0a9 | Load syscall                             |                             | p_memsz  |
  syscall             ; 0x46 |       0f05 | Execute syscall                          |                             | p_memsz  |
  dd  0               ; 0x48 |   00000000 | Filler, should try to keep as all 0's    |                             | p_memsz  |
reeb:                 ;------|------------|------------------------------------------|-----------------------------|----------|
  mov edi, 0xfee1dead ; 0x4C | bfaddee1fe | Load magic "LINUX_REBOOT_CMD_POWER_OFF"  |                             | p_align  |
  jmp short cya       ; 0x51 |       ebe9 | Short jmp back to e_shnum/p_filesz @0x3C |                             | p_align  |
  nop                 ; 0x53 |         90 | Filler, could use this byte for code.    |                             | p_align  |
;---------------------+------+------------+------------------------------------------+-----------------------------+----------+
; Note that we are overlaying the ELF Header with the program headers.
; You have 12 bytes minus your short jump from 0x4-0x10 to store code
; Then you have 8 bytes within the program headers at 0x4c for more 
; code, plus e_shentsize and the lower bytes of p_filesz + p_memsz for
; storage and code if you stay within the bounds - still testing.
;
;      LINUX_REBOOT_CMD_POWER_OFF
;             (RB_POWER_OFF, 0x4321fedc; since Linux 2.1.30).  The message
;             "Power down." is printed, the system is stopped, and all power
;             is removed from the system, if possible.  If not preceded by a
;             sync(2), data will be lost.
; [ Compile ]
; nasm -f bin -o bye bye.nasm
;
; One Liner 
; base64 -d <<< f0VMRrrc/iFDvmkZEijrPAIAPgABAAAABAAAAAEAAAAcAAAAAAAAAAAAAAAAAAAAAQAAAEAAOAABAAIAsKkPBQAAAACwqQ8FAAAAAL+t3uH+6+mQ > bye;chmod +x bye;sudo ./bye
;
; Syscall reference: http://man7.org/linux/man-pages/man2/reboot.2.html

; [ Full breakdown ]
; --- Elf Header 
; Offset  #  Value             Purpose
; 0-3     A  7f454c46          Magic number - 0x7F, then 'ELF' in ASCII
; 4       B  ba                1 = 32 bit, 2 = 64 bit
; 5       C  dc                1 = little endian, 2 = big endian
; 6       D  fe                ELF Version
; 7       E  21                OS ABI - usually 0 for System V
; 8-F     F  43be69191228eb3c  Unused/padding 
; 10-11   G  0200              1 = relocatable, 2 = executable, 3 = shared, 4 = core
; 12-13   H  3e00              Instruction set
; 14-17   I  01000000          ELF Version
; 18-1F   J  0400000001000000  Program entry position
; 20-27   K  1c00000000000000  Program header table position - This is actually in the middle of J.
; 28-2f   L  0000000000000000  Section header table position (Don't have one here so whatev)
; 30-33   M  01000000          Flags - architecture dependent
; 34-35   N  4000              Header size
; 36-37   O  3800              Size of an entry in the program header table
; 38-39   P  0100              Number of entries in the program header table
; 3A-3B   Q  0200              Size of an entry in the section header table
; 3C-3D   R  b0a9              Number of entries in the section header table [holds mov al, 0xa9 load syscall]
; 3E-3F   S  0f05              Index in section header table with the section name [holds syscall opcodes]
;
; --- Program Header
; OFFSET  #   Value            Purpose 
; 1C-1F   PA  01000000         Type of segment
;                                0 = null - ignore the entry
;                                1 = load - clear p_memsz bytes at p_vaddr to 0, then copy p_filesz bytes from p_offset to p_vaddr 
;                                2 = dynamic - requires dynamic linking
;                                3 = interp - contains a file path to an executable to use as an interpreter for the following segment
;                                4 = note section
; 20-23   PB  1c000000         Flags 
;                                1 = PROT_READ  readable
;                                2 = PROT_WRITE writable
;                                4 = PROT_EXEC  executable
;                                In this case the flags are 1c which is 00011100
;                                The ABI only pays attention to the lowest three bits, meaning this is marked "PROT_EXEC"
; 24-2B   PC 0000000000000000   The offset in the file that the data for this segment can be found (p_offset)
; 2C-33   PD 0000000001000000   Where you should start to put this segment in virtual memory (p_vaddr)
; 34-3B   PE 4000380001000200   Physical Address 
; 3C-43   PF b0a90f0500000000   Size of the segment in the file (p_filesz) | NOTE: Can store string here and p_memsz as long as they
; 44-4B   PG b0a90f0500000000   Size of the segment in memory (p_memsz)    | are equal and not over 0xffff - holds mov al, 0xa9 and syscall 
; 4C-43   PH bfaddee1feebe990   The required alignment for this section (must be a power of 2)  Well... supposedly, because you can write code here.
; 
; Breakdown of the hex dump according to the above data
;           A---------- B- C- D- E-  F----------------------
; 00000000  7f 45 4c 46 ba dc fe 21  43 be 69 19 12 28 eb 3c  |.ELF...!C.i..(.<|
;                                                PA---------
;           G---- H---- I----------  J----------------------
; 00000010  02 00 3e 00 01 00 00 00  04 00 00 00 01 00 00 00  |..>.............|
;           PB--------- PC---------------------- PD---------
;           K----------------------  L----------------------
; 00000020  1c 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
;           PD--------- PE---------------------- PF---------
;           M---------- N---- O----  P---- Q---- R---- S----
; 00000030  01 00 00 00 40 00 38 00  01 00 02 00 b0 a9 0f 05  |....@.8.........|
;           PF--------- PG---------------------- PH---------
; 00000040  00 00 00 00 b0 a9 0f 05  00 00 00 00 bf ad de e1  |................|
;           PH---------
; 00000050  fe eb e9 90                                       |....|

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/iFDvmkZEijrPAIAPgABAAAABAAAAAEAAAAcAAAAAAAAAAAAAAAAAAA \ 
AAQAAAEAAOAABAAIAsKkPBQAAAACwqQ8FAAAAAL+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 can put 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:

https://twitter.com/netspooky/status/1061010829666017280

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.

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.

facebook emote dog saying bye