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
Read other posts