Creating the smallest possible Windows executable using assembly language
Using nasm, we can build the smallest possible native exe (without using a packer, dropper or anything like that) file that will work on all Windows versions. This is what one of the possible solution binary looks like:
The code for this little cutie:
IMAGEBASE equ 400000h
BITS 32
ORG IMAGEBASE
; IMAGE_DOS_HEADER
dw "MZ" ; e_magic
dw 0 ; e_cblp
; IMAGE_NT_HEADERS - lowest possible start is at 0x4
Signature:
dw 'PE',0 ; Signature
; IMAGE_FILE_HEADER
dw 0x14c ; Machine = IMAGE_FILE_MACHINE_I386
dw 0 ; NumberOfSections
user32.dll:
dd 'user' ; TimeDateStamp
db '32',0,0 ; PointerToSymbolTable
dd 0 ; NumberOfSymbols
dw 0 ; SizeOfOptionalHeader
dw 2 ; Characteristics = IMAGE_FILE_EXECUTABLE_IMAGE
; IMAGE_OPTIONAL_HEADER32
dw 0x10B ; Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC
kernel32.dll:
db 'k' ; MajorLinkerVersion
db 'e' ; MinorLinkerVersion
dd 'rnel' ; SizeOfCode
db '32',0,0 ; SizeOfInitializedData
dd 0 ; SizeOfUninitializedData
dd Start - IMAGEBASE ; AddressOfEntryPoint
dd 0 ; BaseOfCode
dd 0 ; BaseOfData
dd IMAGEBASE ; ImageBase
dd 4 ; SectionAlignment - overlapping address with IMAGE_DOS_HEADER.e_lfanew
dd 4 ; FileAlignment
dw 0 ; MajorOperatingSystemVersion
dw 0 ; MinorOperatingSystemVersion
dw 0 ; MajorImageVersion
dw 0 ; MinorImageVersion
dw 4 ; MajorSubsystemVersion
dw 0 ; MinorSubsystemVersion
dd 0 ; Win32VersionValue
dd 0x40 ; SizeOfImage
dd 0 ; SizeOfHeaders
dd 0 ; CheckSum
dw 2 ; Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI
dw 0 ; DllCharacteristics
dd 0 ; SizeOfStackReserve
dd 0 ; SizeOfStackCommit
dd 0 ; SizeOfHeapReserve
dd 0 ; SizeOfHeapCommit
dd 0 ; LoaderFlags
dd 2 ; NumberOfRvaAndSizes
; IMAGE_DIRECTORY_ENTRY_EXPORT
dd 0 ; VirtualAddress
dd 0 ; Size
; IMAGE_DIRECTORY_ENTRY_IMPORT
dd IMAGE_IMPORT_DESCRIPTOR - IMAGEBASE ; VirtualAddress
Start:
push 0 ; = MB_OK - overlapps with IMAGE_DIRECTORY_ENTRY_IMPORT.Size
push world
push hello
push 0
call [MessageBoxA]
push 0
call [ExitProcess]
kernel32.dll_iat:
ExitProcess:
dd impnameExitProcess - IMAGEBASE
dd 0
kernel32.dll_hintnames:
dd impnameExitProcess - IMAGEBASE
dw 0
impnameExitProcess: ; IMAGE_IMPORT_BY_NAME
dw 0 ; Hint, terminate list before
db 'ExitProcess' ; Name
impnameMessageBoxA: ; IMAGE_IMPORT_BY_NAME
dw 0 ; Hint, terminate string before
db 'MessageBoxA', 0 ; Name
user32.dll_iat:
MessageBoxA:
dd impnameMessageBoxA - IMAGEBASE
dd 0
user32.dll_hintnames:
dd impnameMessageBoxA - IMAGEBASE
dd 0
IMAGE_IMPORT_DESCRIPTOR:
; IMAGE_IMPORT_DESCRIPTOR for kernel32.dll
dd kernel32.dll_hintnames - IMAGEBASE ; OriginalFirstThunk / Characteristics
world:
db 'worl' ; TimeDateStamp
db 'd!',0,0 ; ForwarderChain
dd kernel32.dll - IMAGEBASE ; Name
dd kernel32.dll_iat - IMAGEBASE ; FirstThunk
; IMAGE_IMPORT_DESCRIPTOR for user32.dll
dd user32.dll_hintnames - IMAGEBASE ; OriginalFirstThunk / Characteristics
hello:
db 'Hell' ; TimeDateStamp
db 'o',0,0,0 ; ForwarderChain
dd user32.dll - IMAGEBASE ; Name
dd user32.dll_iat - IMAGEBASE ; FirstThunk
; IMAGE_IMPORT_DESCRIPTOR empty one to terminate the list all bytes after the end will be zero in memory
times 7 db 0 ; fill up exe to be 268 byte, smallest working exe for win7 64bit
Save the file as tinyexe.asm and assemble it with:
nasm -f bin -o tinyexe.exe tinyexe.asm
Some short facts about this binary:
- As Ange Albertini found out, the smallest possible universal exe that works for all Windows version up to Windows 7 64 bit (Still needs to be tested on Windows 8 tho) is 268 byte
There is still room for optimization in this code (like moving code into header, using smaller opcodes for it or exiting the program without the call to ExitProcess), but the resulting binary can’t be smaller anyway - Some fields in the header can be abused to store code or data, I use them to store the 2 imported dll names. Peter Ferrie did some nice work on figuring the details out of what fields can be reused
- Some lists like the import descriptor one use an empty entry to mark the end of the list, so we can reuse the extra length definition of this list for other data if the value inside this field is high enough to point after the end of such lists
- The imported dlls can be imported without using the .dll at the end of the string
- We don’t need a linker for this project, even the assembler does not have to do much work beside resolving symbolic names and calculating the memory locations and translating the push and call instruction to opcode
- The binary works when run with Wine, whether the exe works on Win 9x and Win 2k I still need to verify
Interesting piece of code! Though you mention “smallest executable”, you don’t mention what the actual size is of the resulting executable (you show a screenshot of the dump of its contents, but I can’t count the bytes). And just for testing the mentioned Win7, Win8, Win10 scenarios, it would be nice if you offer the resulting binary it for download (I don’t have nasm, though I guess I could compile it myself, of course).
He mentions the size of the resulting executable right here:
“As Ange Albertini found out, the smallest possible universal exe that works for all Windows version up to Windows 7 64 bit (Still needs to be tested on Windows 8 tho) is 268 byte”
And having assembled the program myself I can verify that this number is correct.
I have to admit that since this executable displays a MessageBox with the caption/text “hello”/”world” an empty executable that simply returned 0x0 to the OS (in EAX) would indeed be (quite) smaller … 😕
Impressive header overlapping and other byte squeezing tricks!
I wasn’t able to make the 268-byte .exe work on Windows XP SP3. (It works for me in Wine 1.6.2.) This inspired me to create a version which works on Windows XP SP3. It ended up being 404 bytes large, mostly because of the 4 * 40 bytes of IMAGE_SECTION_HEADER Windows XP SP3 requires. You can see it here (filename hh2.nasm): https://github.com/pts/pts-tinype