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

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

Leave a Reply

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

Back To Top