skip to Main Content

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

  dw "MZ"                       ; e_magic
  dw 0                          ; e_cblp

; IMAGE_NT_HEADERS - lowest possible start is at 0x4
  dw 'PE',0                     ; Signature

  dw 0x14c                      ; Machine = IMAGE_FILE_MACHINE_I386
  dw 0                          ; NumberOfSections
  dd 'user'                     ; TimeDateStamp
  db '32',0,0                   ; PointerToSymbolTable
  dd 0                          ; NumberOfSymbols
  dw 0                          ; SizeOfOptionalHeader
  dw 2                          ; Characteristics = IMAGE_FILE_EXECUTABLE_IMAGE

  dw 0x10B                      ; Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC
  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

  dd 0                          ; VirtualAddress
  dd 0                          ; Size


  push  0                       ; = MB_OK - overlapps with IMAGE_DIRECTORY_ENTRY_IMPORT.Size
  push  world
  push  hello
  push  0
  call  [MessageBoxA]
  push  0
  call  [ExitProcess]

  dd impnameExitProcess - IMAGEBASE
  dd 0
  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

  dd impnameMessageBoxA - IMAGEBASE
  dd 0
  dd impnameMessageBoxA - IMAGEBASE
  dd 0

; IMAGE_IMPORT_DESCRIPTOR for kernel32.dll
  dd kernel32.dll_hintnames - IMAGEBASE ; OriginalFirstThunk / Characteristics
  db 'worl'                     ; TimeDateStamp
  db 'd!',0,0                   ; ForwarderChain
  dd kernel32.dll - IMAGEBASE   ; Name
  dd kernel32.dll_iat - IMAGEBASE ; FirstThunk

  dd user32.dll_hintnames - IMAGEBASE ; OriginalFirstThunk / Characteristics
  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
This Post Has 4 Comments
  1. 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).

    1. 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.

    2. 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 ... ­čśĽ

  2. 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):

Leave a Reply

Your email address will not be published. Required fields are marked *

Back To Top