Created
April 12, 2021 22:36
-
-
Save kajott/7276f80e3d783f5cb90c51c011543ed2 to your computer and use it in GitHub Desktop.
minimal Win32 GUI application (648 bytes)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; 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 <[email protected]> | |
; 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