Skip to content

Instantly share code, notes, and snippets.

@kajott
Created April 12, 2021 22:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kajott/7276f80e3d783f5cb90c51c011543ed2 to your computer and use it in GitHub Desktop.
Save kajott/7276f80e3d783f5cb90c51c011543ed2 to your computer and use it in GitHub Desktop.
minimal Win32 GUI application (648 bytes)
; Minimal Win32 application example, inspired by Dave Plummer:
; https://www.youtube.com/watch?v=b0zxIfJJLAY
; Based on initial reseach carried out here:
; https://keyj.emphy.de/win32-pe/
;
; (C) 2021 Martin J. Fiedler <keyj@emphy.de>
; Use at your own risk!
;
; Uses a few tricks to get there:
; - no linker, no sections -> minimal alignment
; - import by hash (costs us a bit of runtime performance,
; but saves *a lot* of space!)
; - collapses headers and puts data into unused header fields
; like there's no tomorrow
; - no error checking
; - uses a few technically unsafe, but pretty reliable assumptions about
; the NT loader (e.g. that kernel32.dll is always the third image loaded,
; after the executable itself and ntdll.dll)
;
; Assemble with YASM:
; yasm -f bin -o hellowin32_minimal.exe hellowin32_minimal.asm
; This creates a 648 byte .exe file, which isn't that bad,
; but I'm pretty sure it can be made a lot smaller still ...
bits 32
BASE equ 0x00400000
ALIGNMENT equ 4
SECTALIGN equ 4
%define RVA(obj) (obj - BASE)
org BASE
ClassName: ; overlap window class name with headers
; -> will be "MZkjPE"
mz_hdr:
dw "MZ" ; DOS magic
dw "kj" ; filler to align the PE header
pe_hdr:
dw "PE",0 ; PE magic + 2 padding bytes
dw 0x014c ; i386 architecture
dw 0 ; no sections
N_user32: db "user32.dll",0,0 ; 12 bytes of data collapsed into the header
;dd 0 ; - [UNUSED-12] timestamp
;dd 0 ; - [UNUSED] symbol table pointer
;dd 0 ; - [UNUSED] symbol count
dw 8 ; optional header size
dw 0x0102 ; characteristics: 32-bit, executable
opt_hdr:
dw 0x010b ; optional header magic
N_gdi32: db "gdi32.dll",0,0,0,0,0 ; 14 bytes of data collapsed into the header
;db 13,37 ; - [UNUSED-14] linker version
;dd RVA(the_end) ; - [UNUSED] code size
;dd RVA(the_end) ; - [UNUSED] size of initialized data
;dd 0 ; - [UNUSED] size of uninitialized data
dd RVA(main) ; entry point address
msg: ; overwrite the following with a variable later
dd RVA(main) ; - [UNUSED-8] base of code
dd RVA(main) ; - [UNUSED] base of data
dd BASE ; image base
dd SECTALIGN ; section alignment (collapsed with the
; PE header offset in the DOS header)
dd ALIGNMENT ; file alignment
dw 4,0 ; [UNUSED-4] OS version
dw 0,0 ; [UNUSED-4] image version
dw 4,0 ; subsystem version
kernel32base: ; overwrite the following with a variable later
dd 0 ; - [UNUSED-4] Win32 version
dd RVA(the_end) ; size of image
dd RVA(opt_hdr) ; size of headers (must be small enough
; so that entry point inside header is accepted)
user32base: ; overwrite the following with a variable later
dd 0 ; - [UNUSED-4] checksum
dw 2 ; subsystem = Windows GUI
dw 0 ; [UNUSED-2] DLL characteristics
dd 0x00100000 ; maximum stack size
dd 0x00001000 ; initial stack size
dd 0x00100000 ; maximum heap size
dd 0x00001000 ; initial heap size
gdi32base: ; overwrite the following with a variable later
dd 0 ; - [UNUSED-4] loader flags
dd 0 ; number of data directory entries (= none!)
OPT_HDR_SIZE equ $ - opt_hdr
ALL_HDR_SIZE equ $ - $$
wc: ; WNDCLASS structure
wc_style: dd 0x03 ; CS_HREDRAW | CS_VREDRAW
wc_lpfnWndProc: dd WndProc
wc_cbClsExtra: dd 0
wc_cbWndExtra: dd 0
wc_hInstance: dd 0
wc_hIcon: dd 0
wc_hCursor: dd 0
wc_hbrBackground: dd 0x11 ; COLOR_3DSHADOW + 1
wc_lpszMenuName: dd 0
wc_lpszClassName: dd ClassName
main:
mov eax, [fs:0x30] ; get PEB pointer from TEB
mov eax, [eax+0x0C] ; get PEB_LDR_DATA pointer from PEB
mov eax, [eax+0x14] ; go to first LDR_DATA_TABLE_ENTRY
mov eax, [eax] ; go to where ntdll.dll typically is
mov eax, [eax] ; go to where kernel32.dll typically is
mov ebx, [eax+0x10] ; load base address of the library
mov [kernel32base], ebx ; store kernel32's base address
; wc.hInstance = GetModuleHandleA(NULL)
push 0 ; lpModuleName = NULL
mov esi, 0xB15246B3 ; hash of "GetModuleHandleA"
call call_import ; call GetModuleHandleA
mov [wc_hInstance], eax ; wc.hInstance = result
; push the last two arguments for CreateWindowExA *right here*
; (because we still have hInstance in a register, we don't want to reload it!)
push 0 ; lpParam = NULL
push eax ; hInstance = hInstance
; user32base = LoadLibraryA("gdi32.dll")
mov esi, 0x01364564 ; hash of "LoadLibraryA"
push esi ; store hash for later
push N_gdi32 ; lpLibFileName = "gdi32.dll"
call call_import ; call LoadLibraryA
mov [gdi32base], eax ; store library base address
; user32base = LoadLibraryA("user32.dll")
pop esi ; restore hash of "LoadLibraryA"
push N_user32 ; lpLibFileName = "user32.dll"
call call_import ; call LoadLibraryA
mov [user32base], eax ; store library base address
mov ebx, eax ; use user32.dll for the next few calls
; wc.hCursor = LoadCursorA(NULL, IDC_ARROW)
push 0x7F00 ; lpCursorName = IDC_ARROW
push 0 ; hInstance = NULL
mov esi, 0x673ECB97 ; hash of "LoadCursorA"
call call_import ; call LoadCursorA
mov [wc_hCursor], eax ; wc.hCursor = result
; RegisterClassA(&wc)
push wc ; lpWndClass = &wc
mov esi, 0xD5793495 ; hash of "RegisterClassA"
call call_import ; call RegisterClassA
; hWnd = CreateWindowExA(0, ClassName, AppName, WS_OVERLAPPEDWINDOW + WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, 640, 480, NULL, NULL, hInstance, NULL)
; lpParam and hInstance are already on the stack
push 0 ; hMenu = NULL
push 0 ; hWndParent = NULL
push 0x1E0 ; nHeight = 480
push 0x280 ; nWidth = 640
mov eax, 0x80000000
push eax ; x = CW_USEDEFAULT = 0x80000000
push eax ; y = CW_USEDEFAULT = 0x80000000
push 0x10CF0000 ; dwStyle = WS_OVERLAPPEDWINDOW + WS_VISIBLE
push AppName ; lpWindowName = AppName
push ClassName ; lpClassName = ClassName
push 0 ; dwExStyle = 0
mov esi, 0xAFDFBED6 ; hash of "CreateWindowExA"
call call_import ; call CreateWindowExA
msgloop:
; prepare the stack for the GetMessage->TranslateMessage->DispatchMessage cascade
mov eax, msg ; temporarily store the &msg pointer
push eax ; lpMsg = &msg [DispatchMessageA]
push eax ; lpMsg = &msg [TranslateMessage]
push 0 ; wMsgFilterMax = 0 [GetMessageA]
push 0 ; wMsgFilterMin = 0 [GetMessageA]
push 0 ; hWnd = NULL [GetMessageA]
push eax ; lpMsg = &msg [GetMessageA]
mov esi, 0xB32289D2 ; hash of "GetMessageA"
call call_import ; call GetMessageA
or eax, eax ; if result == 0: goto exit
jz exit
mov esi, 0x18E22ECB ; hash of "TranslateMessage"
call call_import ; call TranslateMessage
mov esi, 0xE425D768 ; hash of "DispatchMessageA"
call call_import ; call DispatchMessageA
jmp msgloop
WndProc: ; FUNCTION that handles messages
push ebp ; create stack frame
mov ebp, esp
sub esp, 0x54 ; make space for temporary structures:
; ebp-0x54: PAINTSTRUCT ps
; ebp-0x14: RECT rect
push ebx ; save callee-preserved registers
push esi
push edi
; examine message ID: is it WM_DESTROY?
mov eax, [ebp+0x0C] ; load uMsg parameter
cmp eax, 2 ; compare with WM_DESTROY
je exit ; if equal, goto exit
mov ebx, [user32base] ; the following calls go into user32.dll
; is it WM_PAINT then?
cmp eax, 15 ; compare with WM_PAINT
jne uninteresting ; if not equal, call DefWindowProc
; push parameters for the EndPaint call later
lea eax, [ebp-0x54] ; load &ps
mov edx, [ebp+0x08] ; load hWnd
push eax ; lpPaint = &ps
push edx ; hWnd = hWnd
; HDC hDC = BeginPaint(hWnd, &ps);
push eax ; lpPaint = &ps
push edx ; hWnd = hWnd
mov esi, 0xD3EBA040 ; hash of "BeginPaint"
call call_import ; call BeginPaint
; set parameters for the DrawTextA() call later
lea edx, [ebp-0x14] ; load &rect
push 25h ; format = DT_SINGLELINE | DT_CENTER | DT_VCENTER
push edx ; lprc = &rect
push 15 ; cchText = strlen(AppName)
push AppName ; lpchText = AppName
push eax ; hdc = hDC
; set parameters for the GetClientRect() call later
push edx ; lpRect = &rect
push dword [ebp+0x08] ; hWnd = hWnd
; SetBkMode(hDC, TRANSPARENT)
push ebx ; save user32.dll base address
mov ebx, [gdi32base] ; switch to gdi32.dll
push 1 ; mode = TRANSPARENT
push eax ; hdc = hDC
mov esi, 0x702073EA ; hash of "SetBkMode"
call call_import ; call SetBkMode
pop ebx ; switch back to user32.dll
; call GetClientRect() with parameters set up above
mov esi, 0x65BA2A2F ; hash of "GetClientRect"
call call_import ; call GetClientRect
; call DrawTextA() with parameters set up above
mov esi, 0x5BBDFC08 ; hash of "DrawTextA"
call call_import ; call DrawTextA
; call EndPaint() with parameters set up above
mov esi, 0xE6288658 ; hash of "EndPaint"
call call_import ; call EndPaint
; return 0
xor eax, eax
jmp wndproc_end
uninteresting:
; return DefWindowProc(...)
push dword [ebp+0x14] ; copy the parameters
push dword [ebp+0x10]
push dword [ebp+0x0C]
push dword [ebp+0x08]
mov esi, 0xD9D37158 ; hash of "DefWindowProcA"
call call_import ; call into DefWindowProcA
wndproc_end:
pop edi ; restore registers
pop esi
pop ebx
mov esp, ebp ; unwind stack
pop ebp
ret 16 ; return with 16 additional stack bytes
exit: ; FUNCTION that exits the program
push 0
mov ebx, [kernel32base]
mov esi, 0x665640AC ; hash of "ExitProcess"
; fall-through into call_import
call_import: ; FUNCTION that calls procedure [esi] in library at base [ebx]
mov edx, [ebx+0x3c] ; get PE header pointer (w/ RVA translation)
add edx, ebx
mov edx, [edx+0x78] ; get export table pointer RVA (w/ RVA translation)
add edx, ebx
push edx ; store the export table address for later
mov ecx, [edx+0x18] ; ecx = number of named functions
mov edx, [edx+0x20] ; edx = address-of-names list (w/ RVA translation)
add edx, ebx
name_loop:
push esi ; store the desired function name's hash (we will clobber it)
mov edi, [edx] ; load function name (w/ RVA translation)
add edi, ebx
cmp_loop:
movzx eax, byte [edi] ; load a byte of the name ...
inc edi ; ... and advance the pointer
xor esi, eax ; apply xor-and-rotate
rol esi, 7
or eax, eax ; last byte?
jnz cmp_loop ; if not, process another byte
or esi, esi ; result hash match?
jnz next_name ; if not, this is not the correct name
; if we arrive here, we have a match!
pop esi ; restore the name pointer (though we don't use it any longer)
pop edx ; restore the export table address
sub ecx, [edx+0x18] ; turn the negative counter ECX into a positive one
neg ecx
mov eax, [edx+0x24] ; get address of ordinal table (w/ RVA translation)
add eax, ebx
movzx ecx, word [eax+ecx*2] ; load ordinal from table
;sub ecx, [edx+0x10] ; subtract ordinal base
mov eax, [edx+0x1C] ; get address of function address table (w/ RVA translation)
add eax, ebx
mov eax, [eax+ecx*4] ; load function address (w/ RVA translation)
add eax, ebx
jmp eax ; jump to the target function
next_name:
pop esi ; restore the name pointer
add edx, 4 ; advance to next list item
dec ecx ; decrease counter
jmp name_loop
AppName: db "KeyJ's Tiny App"
align ALIGNMENT, db 0
the_end:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment