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.