One of the annoying things of my Windows Internals/Security research is when every single component and mechanism I’ve looked at in the last six months has ultimately resulted in me finding very interesting design bugs, which I must now wait on Microsoft to fix before being able to talk further about them. As such, I have to take a smaller break from kernel-specific research (although I hope to lift the veil over at least one issue at the No Such Conference in Paris this year). And so, in the next following few blog posts, probably inspired by having spent too much time talking with my friend Ange Albertini, I’ll be going over some neat PE tricks.
Write a portable executable (PE/EXE) file which can be spawned through a standard CreateProcess call and will result in STATUS_SUCCESS being returned as well as a valid Process Handle, but will not
- Contain any actual x86/x64 assembly code section (i.e.: the whole PE should be read-only, no +X section)
- Run a single instruction of what could be construed as x86 assembly code, which is part of the file itself (i.e.: random R/O data should not somehow be forced into being executed as machine code)
- Crash or make any sort of interactive/visible notice to the user, event log entry, or other error condition.
Interesting, this was actually a real-world situation that I was asked to provide a solution for — not a mere mental exercise. The idea was being able to prove, in the court of law, that no “foreign” machine code had executed as a result of this executable file having been launched (i.e.: obviously the kernel ran some code, and the loader ran too, but all this is pre-existing Microsoft OS code). Yet, the PE file had to not only be valid, but to also return a valid process handle to the caller.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
HEADER:00000000 ; IMAGE_DOS_HEADER HEADER:00000000 HEADER:00000000 .686p HEADER:00000000 .mmx HEADER:00000000 .model flat HEADER:00000000 HEADER:00000000 ; Segment type: Pure data HEADER:00000000 HEADER segment page public 'DATA' use32 HEADER:00000000 assume cs:HEADER HEADER:00000000 __ImageBase dw 5A4Dh ; PE magic number HEADER:00000002 dw 0 ; Bytes on last page of file HEADER:00000004 ; IMAGE_NT_HEADERS HEADER:00000004 dd 4550h ; Signature HEADER:00000008 ; IMAGE_FILE_HEADER HEADER:00000008 dw 14Ch ; Machine HEADER:0000000A dw 0 ; Number of sections HEADER:0000000C dd 0 ; Time stamp HEADER:00000010 dd 0 ; Pointer to symbol table HEADER:00000014 dd 0 ; Number of symbols HEADER:00000018 dw 0 ; Size of optional header HEADER:0000001A dw 2 ; Characteristics HEADER:0000001C ; IMAGE_OPTIONAL_HEADER HEADER:0000001C dw 10Bh ; Magic number HEADER:0000001E db 0 ; Major linker version HEADER:0000001F db 0 ; Minor linker version HEADER:00000020 dd 0 ; Size of code HEADER:00000024 dd 0 ; Size of initialized data HEADER:00000028 dd 0 ; Size of uninitialized data HEADER:0000002C dd 7FBE02F8h ; Address of entry point HEADER:00000030 dd 0 ; Base of code HEADER:00000034 dd 0 ; Base of data HEADER:00000038 dd 400000h ; Image base HEADER:0000003C dd 4 ; Section alignment HEADER:00000040 dd 4 ; File alignment HEADER:00000044 dw 0 ; Major operating system version HEADER:00000046 dw 0 ; Minor operating system version HEADER:00000048 dw 0 ; Major image version HEADER:0000004A dw 0 ; Minor image version HEADER:0000004C dw 4 ; Major subsystem version HEADER:0000004E dw 0 ; Minor subsystem version HEADER:00000050 dd 0 ; Reserved 1 HEADER:00000054 dd 40h ; Size of image HEADER:00000058 dd 0 ; Size of headers HEADER:0000005C dd 0 ; Checksum HEADER:00000060 dw 2 ; Subsystem HEADER:00000062 dw 0 ; Dll characteristics HEADER:00000064 dd 0 ; Size of stack reserve HEADER:00000068 dd 0 ; Size of stack commit HEADER:0000006C dd 0 ; Size of heap reserve HEADER:00000070 dd 0 ; Size of heap commit HEADER:00000074 dd 0 ; Loader flag HEADER:00000078 dd 0 ; Number of data directories HEADER:0000007C HEADER ends HEADER:0000007C end
As per Corkami, in Windows 7 and higher, you’ll want to make sure that the PE is at least 252 bytes on x86, or 268 bytes on x64.
Here’s a 64 byte Base64 representation of a .gz file containing the 64-bit compatible (268 byte) executable:
There is one non-standard machine configuration in which this code will actually still crash (but still return STATUS_SUCCESS in CreateProcess, however). This is left as an exercise to the reader.
The application executes and exits successfully. But as you can see, no code is present in the binary. How does it work? Do you have any other solutions which satisfy the challenge?