The Teensy Executable Revisited
The Teensy Executable Revisited
(or, "Thunderclouds Gather on the Horizon")
On a couple of occasions, people have responded to my original essay<br>with the comment that what I've created by the end isn't really an ELF<br>executable. Rather, it is a file that the Linux kernel, in its current<br>incarnation, happens to mistake for an ELF executable.
It's a fair point. That 45-byte file clearly doesn't conform to<br>numerous requirements of the ELF specification. But can you blame me?<br>How could I have stopped at the point just before I tossed the ELF<br>specification out the window, knowing what might still be possible?
But to satisfy these purists, and the puritan side in all of us, I've<br>created this sequel.
So. We have an executable that we whittled down to 45 bytes. We now<br>want to bring it into rigid conformance with published standards,<br>while still keeping it as small as possible.
The point at which we strayed from straight and narrow path was when<br>we started fiddling with "unused" fields in the ELF header. So let's<br>back up to before that point:
BITS 32
org 0x08048000
ehdr: ; Elf32_Ehdr<br>db 0x7F, "ELF", 1, 1, 1 ; e_ident<br>times 9 db 0<br>dw 2 ; e_type<br>dw 3 ; e_machine<br>dd 1 ; e_version<br>dd _start ; e_entry<br>dd phdr - $$ ; e_phoff<br>dd 0 ; e_shoff<br>dd 0 ; e_flags<br>dw ehdrsz ; e_ehsize<br>dw phdrsz ; e_phentsize<br>dw 1 ; e_phnum<br>dw 0 ; e_shentsize<br>dw 0 ; e_shnum<br>dw 0 ; e_shstrndx<br>ehdrsz equ $ - ehdr
phdr: ; Elf32_Phdr<br>dd 1 ; p_type<br>dd 0 ; p_offset<br>dd $$ ; p_vaddr<br>dd $$ ; p_paddr<br>dd filesz ; p_filesz<br>dd filesz ; p_memsz<br>dd 5 ; p_flags<br>dd 0x1000 ; p_align<br>phdrsz equ $ - phdr
_start:<br>xor eax, eax<br>inc eax<br>mov bl, 42<br>int 0x80
filesz equ $ - $$
This was our ninety-one-byte version. So: are we stuck with this as<br>our best size? No, not quite. We violated no rules when we overlapped<br>the ELF header and the program header table by eight bytes. The ELF<br>specification explicitly permits overlap of different data structures<br>within the file. So let's do that here:
; tiny.asm
BITS 32
org 0x08048000
ehdr:<br>db 0x7F, "ELF", 1, 1, 1 ; e_ident<br>times 9 db 0<br>dw 2 ; e_type<br>dw 3 ; e_machine<br>dd 1 ; e_version<br>dd _start ; e_entry<br>dd phdr - $$ ; e_phoff<br>dd 0 ; e_shoff<br>dd 0 ; e_flags<br>dw ehdrsz ; e_ehsize<br>dw phdrsz ; e_phentsize<br>phdr: dd 1 ; e_phnum ; p_type<br>; e_shentsize<br>dd 0 ; e_shnum ; p_offset<br>; e_shstrndx<br>ehdrsz equ $ - ehdr<br>dd $$ ; p_vaddr<br>dd $$ ; p_paddr<br>dd filesz ; p_filesz<br>dd filesz ; p_memsz<br>dd 5 ; p_flags<br>dd 0x1000 ; p_align<br>phdrsz equ $ - phdr
_start: xor eax, eax<br>inc eax<br>mov bl, 42<br>int 0x80
filesz equ $ - $$
That gives us eighty-three bytes. What else can we do? Seems like<br>there isn't much. In desperation, we might turn back to the ELF<br>specification and read it over again, looking for something.
Are there any guarantees anything about the initial register values?<br>Only for one register: edx. And what is says is that it will contain<br>either zero, or the address of a final shutdown procedure. So, no<br>guarantees at all, really. Keep looking.
A-ha: The p_paddr field of the program header table structure! Every<br>other field of the headers which doesn't apply to to Intel<br>architecture, or doesn't apply to an executable file — or, at least,<br>not to our executable file — is required by the ELF specification to<br>be set to zero. But for the p_paddr field, the specification says the<br>field has unspecified contents. So we have four bytes that we can<br>play with, after all.
What can we do with them? Use it to hold part of our program,<br>naturally. Of course, we can't put the whole program there, so we'll<br>need to waste two of the four bytes on a jmp instruction, in order to<br>get to the rest of it. But that still leaves two bytes that we can<br>use, and the first instruction of our program is exactly two bytes<br>long.
; tiny.asm
BITS 32
org 0x08048000
ehdr:<br>db 0x7F, "ELF", 1, 1, 1 ; e_ident<br>times 9 db 0<br>dw 2 ; e_type<br>dw 3 ; e_machine<br>dd 1 ; e_version<br>dd _start ; e_entry<br>dd phdr - $$ ; e_phoff<br>dd 0 ; e_shoff<br>dd 0 ; e_flags<br>dw ehdrsz ; e_ehsize<br>dw phdrsz ; e_phentsize<br>phdr: dd 1 ; e_phnum ; p_type<br>; e_shentsize<br>dd 0 ; e_shnum ; p_offset<br>; e_shstrndx<br>ehdrsz equ $ - ehdr<br>dd $$ ; p_vaddr<br>_start: xor eax, eax ; p_paddr<br>jmp short part2<br>dd filesz ; p_filesz<br>dd filesz ; p_memsz<br>dd 5 ; p_flags<br>dd 0x1000 ; p_align<br>phdrsz equ $ - phdr
part2: inc eax<br>mov bl, 42<br>int 0x80
filesz equ $ - $$
So. Eighty-one bytes. Is that all?
The next field after the p_paddr field is the p_filesz field. If only<br>we could overlap the jmp instruction with that, we could squeeze<br>another instruction in there. But alas, the first byte of that field<br>is the size of the entire file, which would be an unwise jump to make.<br>And the remaining bytes are zeros. That approach doesn't look too<br>promising.
What about the field before p_paddr? That's the address the program is<br>to be loaded at. Well, we already know we don't have to use the<br>default value of 0x08048000. We do need to keep the address<br>page-aligned, at the very least, but we should be able to fit a<br>two-byte...