Modern PE Mangling


[:  :]

I’ve been wanting to learn more about Windows exploits and shellcode techniques for a bit, but a lot of resources out there (similarly to Linux), are based on 32 bit version of Windows, are very version specific, and simply don’t work on modern systems.

I decided to get started by learning more about 64 bit PEs. I really don’t know much about proper Windows API programming, so I wanted to see the bare minimum required to load and run code on Windows.

WARNING: I am not a Windows expert, I am barely a Windows user, so please excuse my noobishness and feel free to hit me up on twitter if you have any corrections or extra info for this.

Prior Work

Early attempts to create the smallest PE possible were based on 32 bit Windows versions. Prior to this, you could create beautifuly tiny COM files on DOS using 16 bit assembly.

The first writeup I read was this one, which determined the following limits on 32 bit Windows versions.

Unfortunately, the links to the files on this page are dead, so I could only see what was listed on the page.

The binary included here won’t run on modern Windows versions, so if we want to create a tiny binary to run on Windows 10, we’ll have to do some more research.

The consensus I found was that the limit for binary size in 64 bit Windows versions was 268 bytes. Thanks to the excellent Corkami docs (PE101 and PE102), I was able to explore the format a bit more in depth.

I located a few POCs, and examined how the binaries that were generated were structured.

The header overlay technique that was established in the first writeup seem to be employed by these POCs, but they don’t overlay section headers. The section headers define the .text segment as well as others, but it appears that they aren’t required for whatever reason.

First Attempts

I decided to keep the tradition of using NASM to write binaries by hand, because it’s easier to keep track of each byte individually and debug. My POC was based on the rcx/TinyPE repo’s smallest-pe.exe file. I created a list of all of the headers and their sizes / locations to keep track of what was already there.

TODO Insert Header Table

Anything marked with a * means that it is unused. Some of these might have some expected value ranges to respect, so keep that in mind when playing with them!

DOS Header

IndexSizeFree?Description
MA2e_magic
MB2ye_cblp
MC2ye_cp
MD2ye_crlc
ME2ye_cparhdr
MF2ye_minalloc
MG2ye_maxalloc
MH2ye_ss
MI2ye_sp
MJ2ye_csum
MK2ye_ip
ML2ye_cs
MM2ye_lsarlc
MN2ye_ovno
MO8ye_res
MP2ye_oemid
MQ2ye_oeminfo
MR20ye_res2
MS4e_lfanew PE Sig Addr

PE Header

IndexSizeFree?Description
PA4PE Signature
PB2Machine (Intel 386)
PC2NumberOfSections
PD4yTimeDateStamp
PE4yPointerToSymbolTable
PF4yNumberOfSymbols
PG2SizeOfOptionalHeader
PH2Characteristics (no relocs, executable, 32 bit)

Optional Header

IndexSizeFree?Description
OA2Magic (PE32)
OB1yMajorLinkerVersion
OC1yMinorLinkerVersion
OD4ySizeOfCode
OE4ySizeOfInitializedData
OF4ySizeOfUninitializedData
OG4AddressOfEntryPoint
OH4yBaseOfCode
OI4yBaseOfData
OJ4ImageBase
OK4SectionAlignment
OL4FileAlignment
OM2yMajorOperatingSystemVersion
ON2yMinorOperatingSystemVersion
OO2yMajorImageVersion
OP2yMinorImageVersion
OQ2MajorSubsystemVersion
OR2yMinorSubsystemVersion
OS4yWin32VersionValue
OT4SizeOfImage
OU4SizeOfHeaders
OV4yCheckSum
OW2Subsystem (Win32 GUI)
OX2yDllCharacteristics
OY4ySizeOfStackReserve
OZ4SizeOfStackCommit
O14SizeOfHeapReserve
O24ySizeOfHeapCommit
O34yLoaderFlags
O44yNumberOfRvaAndSizes

After I got it working, I needed a payload. I remembered the wonderful writeup by Iliya Dafchev, “Writing Windows Shellcode”, which details a more modern approach to Windows shellcode. It uses the technique of parsing the PEB structure to find the base address of kernel32.dll, and then calling WinExec using arguments on the stack and execute calc.exe.

As much as I love shellstorm, many of the payloads listed for Windows are based on much older versions. Some of them use hardcoded addresses which aren’t as portable, so I figured it’d be a lot nicer to take an existing payload and pack it into the binary.

The first step was loading payload into the binary and seeing if it worked as is. Fortunately, it did! I was able to just put the payload after the headers at 0x7C and execute. Here is what that binary looks like.

$ xxd tiny304.exe
                  MC-- MD-- ME-- MF-- MG-- MH--
        MA-- MB-- PA------- PB-- PC-- PD-------
00000000: 4d5a 0000 5045 0000 4c01 0000 0000 0000  MZ..PE..L.......
        MI-- MJ-- MK-- ML-- MM-- MN-- MO-------
        PE------- PF------- PG-- PH-- OA-- OBOC
00000010: 0000 0000 0000 0000 6000 0301 0b01 0000  ........`.......
        MO------- MP-- MQ-- MR-----------------
        OD------- OE------- OF------- OG-------
00000020: 0300 0000 0000 0000 0000 0000 7c00 0000  ............|...
        MR--------------------------- MS-------
        OH------- OI------- OJ------- OK-------
00000030: 0000 0000 0000 0000 0000 4000 0400 0000  ..........@.....
        OL------- OM-- ON-- OO-- OP-- OQ-- OR--
00000040: 0400 0000 0000 0000 0000 0000 0500 0000  ................
        OS------- OT------- OU------- OV-------
00000050: 0000 0000 8000 0000 7c00 0000 0000 0000  ........|.......
        OW-- OX-- OY------- OZ------- O1-------
00000060: 0200 0004 0000 1000 0010 0000 0000 1000  ................
        O2------- O3------- O4------- CK-------
00000070: 0010 0000 0000 0000 0000 0000 31f6 83ec  ............1...
00000080: 1856 6a63 6668 7865 6857 696e 4589 65fc  .VjcfhxehWinE.e.
00000090: 648b 5e30 8b5b 0c8b 5b14 8b1b 8b1b 8b5b  d.^0.[..[......[
000000a0: 1089 5df8 8b43 3c01 d88b 4078 01d8 8b48  ..]..C<...@x...H
000000b0: 2401 d989 4df4 8b78 2001 df89 7df0 8b50  $...M..x ...}..P
000000c0: 1c01 da89 55ec 8b58 1431 c08b 55f8 8b7d  ....U..X.1..U..}
000000d0: f08b 75fc 31c9 fc8b 3c87 01d7 6683 c108  ..u.1...<...f...
000000e0: f3a6 740a 4039 d872 e583 c426 eb41 8b4d  ..t.@9.r...&.A.M
000000f0: f489 d38b 55ec 668b 0441 8b04 8201 d831  ....U.f..A.....1
00000100: d252 682e 6578 6568 6361 6c63 686d 3332  .Rh.exehcalchm32
00000110: 5c68 7973 7465 6877 735c 5368 696e 646f  \hystehws\Shindo
00000120: 6843 3a5c 5789 e66a 0a56 ffd0 83c4 46c3  hC:\W..j.V....F.

Being shellcode, the payload has some extra instructions to preserve and restore registers, which help to clean up the execution if it was to be injected. We can safely take out those instructions and continue.

I did some other small optimizations to reduce the code size, but other than that it’s a pretty tight payload.

(Ab)using the Headers

I had looked through some of the previous documentation on aspects of the header that aren’t used. The headers are already overlayed, but within the overlay, there are still some portions of the headers that are unused. These areas can be used to store data or execute code without interfering with the binary’s execution.

The areas that I used are listed on the side here

StartEndLen
0x0C0x1812
0x1E0x2C14
0x440x4C8
0x4E0x546
0x5C0x604
0x700x788

When you jump around a header like this, you’ll have to remember a couple of things. First, for each cave
in the header, unless you are ending the code there, you must make sure you have enough room for your jumps to other areas.

I tend to allocate 2 bytes, because short jumps are generally the easiest to work with, but you can use whatever you like as long as you keep the space they take up in mind!

The other thing is, if you are jumping past the bounds of 127 bytes forward or 128 bytes back with a short jump, the size of the instruction increases.

Something else I noticed was that NumberOfRvaAndSizes is touchy, and it possibly prevents the binary from loading if you put something there. This and other header areas are certainly open for experimentation!

Another trick of mine for ELF binary mangling was to put the code entrypoint in the header itself. This is not as straightforward with 64 bit PEs.

Windows 8 established a restriction that the AddressOfEntryPoint can’t be smaller than SizeOfHeaders. The way around this is to set SizeOfHeaders to AddressOfEntryPoint. More info here.

I didn’t end up doing this trick, because the size of the binary was already very small, and I wanted to fill up each part of it until the very end.

Final Binary

So, armed with all of this information, this is the binary I ended up with.

  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
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
BITS 32
;--- Smallest possible Win10 binary that execs calc.exe --------------------\\--
;
; Compile:
;   nasm -f bin -o tiny268_64.exe tiny268_64.asm
; Notice: You might get an error like "Cannot be started 0xc000000005",
;         this is fine, just run it again.
; Versions:
;  Bypass TinyPE detections Date: 20200330
;   Size: 268 bytes (SHA1) c935b155c6cdeacc495d7b695e71f0229e9ce5fc
;  First version at 268 bytes Date: 20200329
;   Size: 268 bytes (SHA1) 60e2c89d391052cc00145d277883e7feb6b67dd0 
;  Original Version without optimization Date: 20200328
;   Size: 304 bytes (SHA1) bb59448a94acee171ea574e3a50dd6a2b75f4965 
;
; Breakdown of Sections - Listed in comments of the header 0x00:0x7C
;
;                     MC-- MD-- ME-- MF-- MG-- MH--
;           MA-- MB-- PA------- PB-- PC-- PD-------
; 00000000: 4d5a 0001 5045 0000 4c01 0000 31f6 83ec  MZ..PE..L...1...
;           MI-- MJ-- MK-- ML-- MM-- MN-- MO-------
;           PE------- PF------- PG-- PH-- OA-- OBOC
; 00000010: 1856 6a63 9090 eb06 6000 0301 0b01 6668  .Vjc....`.....fh
;           MO------- MP-- MQ-- MR-----------------
;           OD------- OE------- OF------- OG-------
; 00000020: 7865 6857 696e 4589 65fc eb22 7c00 0000  xehWinE.e.."|...
;           MR--------------------------- MS-------
;           OH------- OI------- OJ------- OK-------
; 00000030: 0000 0000 0000 0000 0000 4000 0400 0000  ..........@.....
;           OL------- OM-- ON-- OO-- OP-- OQ-- OR--
; 00000040: 0400 0000 8b5b 0c8b 5b14 eb10 0500 648b  .....[..[.....d.
;           OS------- OT------- OU------- OV-------
; 00000050: 5e30 ebf0 8000 0000 7c00 0000 8b1b eb10  ^0......|.......
;           OW-- OX-- OY------- OZ------- O1-------
; 00000060: 0200 0004 0000 1000 0010 0000 0000 1000  ................
;           O2------- O3------- O4------- CK-------
; 00000070: 8b1b 8b5b 10eb 07c3 0000 0000 eb8e 895d  ...[...........]
; 00000080: f88b 433c 01d8 8b40 7801 d88b 4824 01d9  ..C<...@x...H$..
; 00000090: 894d f48b 7820 01df 897d f08b 501c 01da  .M..x ...}..P...
; 000000a0: 8955 ec8b 5814 31c0 8b55 f88b 7df0 8b75  .U..X.1..U..}..u
; 000000b0: fc31 c9fc 8b3c 8701 d766 83c1 08f3 a674  .1...<...f.....t
; 000000c0: 0a40 39d8 72e5 83c4 26eb ac8b 4df4 89d3  .@9.r...&...M...
; 000000d0: 8b55 ec66 8b04 418b 0482 01d8 31d2 5268  .U.f..A.....1.Rh
; 000000e0: 2e65 7865 6863 616c 6368 6d33 325c 6879  .exehcalchm32\hy
; 000000f0: 7374 6568 7773 5c53 6869 6e64 6f68 433a  stehws\ShindohC:
; 00000100: 5c57 89e6 6a0a 56ff d083 c446            \W..j.V....F
; 
; Places code could be executed ------------------------------------------------
; Range       Len  Note
; 0x0C:0x18    12  jump0
; 0x1E:0x2C    14  jump1
; 0x44:0x4C     8  jump3
; 0x4E:0x54     6  jump2
; 0x5C:0x60     4  jump4 
; 0x70:0x78     8  jump5 + endy
;--- Start of MZ Header --------------------------------------------------------
mzhdr:
  dw "MZ"       ; 0x00 ; [MA] e_magic
  dw 0x100      ; 0x02 ; [MB] e_cblp This value will bypass TinyPE detections!
;--- Start of PE Header --------------------------------------------------------
pesig:
  dd "PE"       ; 0x04 ; [MC] e_cp [MD] e_crlc [PA] PE Signature
pehdr:
  dw 0x014C     ; 0x08 ; [ME] e_cparhdr [PB] Machine (Intel 386)
  dw 0          ; 0x0A ; [MF] e_minalloc [PC] NumberOfSections (0 haha)
jump0: ; WinExec Setup Part 1 --------------------------------------------------
  xor  esi,esi  ; 0x0C ; 31f6   ; Clear ESI [MG] e_maxalloc [PD] TimeDateStamp 
  sub  esp,0x18 ; 0x0E ; 83ec18 ; Make room for our bullshit [MH] e_ss [MI] e_sp
  push esi      ; 0x11 ; 56     ; Null   [PE] PointerToSymbolTable
  push 0x63     ; 0x12 ; 6a63   ; "c"    [MJ] e_csum 
  nop           ; 0x14 ; 90     ; spacer [MK] e_ip [PF] NumberOfSymbols 
  nop           ; 0x15 ; 90     ; spacer 
  jmp jump1     ; 0x16 ; eb06   ; [ML] e_cs
  dw 0x60       ; 0x18 ; [MM] e_lsarlc [PG] SizeOfOptionalHeader
  dw 0x103      ; 0x1A ; [MN] e_ovno [PH] Characteristics
;--- Start of Optional Header --------------------------------------------------
  dw 0x10B      ; 0x1C ; [MO] e_res [OA] Magic (PE32)
jump1: ; WinExec Setup Part 2 --------------------------------------------------
  push word 0x6578 ;0x1E; 66687865   ; "ex" [OB] MajorLinkerVersion 
                                     ; [OC] MinorLinkerVersion [OD] SizeOfCode
  push 0x456e6957  ;0x22; 6857696e45 ; "EniW"
                        ; [MP] e_oemid [MQ] e_oeminfo [OE] SizeOfInitializedData
  mov dword [ebp-4], esp ;0x27; 8965fc; Save our stack pointer addr for later 
                                      ; [MR] e_res2 [OF] SizeOfUninitializedData
  jmp jump2     ; 0x2A ; eb22 
  dd 0x7C       ; 0x2C ; [OG] AddressOfEntryPoint (Could make a label pointer)
  dd 0          ; 0x30 ; [OH] BaseOfCode
  dd 0          ; 0x34 ; [OI] BaseOfData
  dd 0x400000   ; 0x38 ; [OJ] ImageBase
  dd 4          ; 0x3C ; [MS] e_lfanew [OK] SectionAlignment
  dd 4          ; 0x40 ; [OL] FileAlignment
jump3: ; PEB Parse Part 2 ------------------------------------------------------
  mov  ebx, [ebx+0xc]  ; 0x44 ; 8b5b0c ; Get addr of PEB_LDR_DATA
                              ; [OM] MajorOperatingSystemVersion
                              ; [ON] MinorOperatingSystemVersion
  mov  ebx, [ebx+0x14] ; 0x47 ; 8b5b14 ; InMemoryOrderModuleList first entry
                              ; [OO] MajorImageVersion
  jmp  jump4           ; 0x4A ; eb10
                              ; [OP] MinorImageVersion
  dw 5          ; 0x4C ; [OQ] MajorSubsystemVersion
jump2: ; PEB Parser Part 1 -----------------------------------------------------
  mov  ebx, [fs:0x30+esi] ; 0x4E ; 648b5e30 ; Get PEB addr, FS holds TEB address
                                 ; [OR] MinorSubsystemVersion
                                 ; [OS] Win32VersionValue
  jmp jump3     ; 0x52 ; ebf0
  dd 0x80       ; 0x54 ; [OT] SizeOfImage
  dd 0x7C       ; 0x58 ; [OU] SizeOfHeaders
jump4: ; PEB Parser Part 3 -----------------------------------------------------
  mov  ebx, [ebx] ; 0x5C ; 8b1b ; Get address of ntdll.dll entry [OV] CheckSum
  jmp jump5       ; 0x5E ; eb10 
  dw 2          ; 0x60 ; [OW] Subsystem (Win32 GUI)
  dw 0x400      ; 0x62 ; [OX] DllCharacteristics   
  dd 0x100000   ; 0x64 ; [OY] SizeOfStackReserve   
  dd 0x1000     ; 0x68 ; [OZ] SizeOfStackCommit    
  dd 0x100000   ; 0x6C ; [O1] SizeOfHeapReserve    
jump5: ; PEB Parser Part 4 -----------------------------------------------------
  mov  ebx, [ebx]      ; 0x70 ; 8b1b   ; Get address of kernel32.dll list entry
                                       ; [O2] SizeOfHeapCommit 
  mov  ebx, [ebx+0x10] ; 0x72 ; 8b5b10 ; Get kernel32.dll base address 
                                       ; [O3] LoaderFlags 
  jmp jump6            ; 0x75 ; eb07
endy:
  ret                  ; 0x77 ; c3  ; Used to end the program    
  dd 0          ; 0x78 ; [O4] NumberOfRvaAndSizes  ; Note - this is touchy 
codesec:        ; 0x7C - Start of code -----------------------------------------
                       ; MachineCode ; Description 
  jmp jump0            ; eb8e        ; Jump back to header to begin execution
jump6:
;--- Grab kernel32.dll base addr -----------------------------------------------
; This piece of code grabs the Thread Environment Block structure's address from
; the FS segment register to locate the Process Environment Block structure 
; stored inside.
; Then it grabs the pointer to the PEB_LDR_DATA structure so it can grab the 
; InMemoryOrderModuleList, which tells us about DLLs in memory.
; Then it grabs the ntdll.dll entry in this list which helps us find the next
; entry, kernel32.dll. The base address of kernel32.dll is stored at 0x10 in 
; the entry.
; 
; [WORKFLOW]
; TEB
; @0x30 PEB
;   --> 0x0C PEB_LDR_DATA
;        --> 0x14 InMemoryOrderModuleList
;             --> 0x00 ntdll.dll entry address
;                  --> 0x00 kernel32.dll list entry address
;                       --> 0x10 kernel32.dll base address !!
; Note that most of this is done in the header, see jump2 - jump5
; PEB Parser Part 5 ------------------------------------------------------------
  mov  [ebp-0x8], ebx     ; 895df8   ; kernel32.dll base address
;--- Finding WinExec address
; This section weaves it's way through the headers of kernel32.dll. 
; Based on a non-fucky PE like this one, we can sort of rely on certain things
; being where we expect them.
; First, the Relative Virtual address of the PE signature is loaded from ebx.
; EAX then becomes the address that we're calculating from.
; 
; The addresses of our structures are calculated using the base address of the 
; PE signature in EAX + it's offset within that structure, and then added to the
; base address stored in EBX. These are then moved to the stack.
; 
  mov  eax,dword [ebx+0x3c] ; 8b433c  ; RVA of PE signature
  add  eax,ebx              ; 01d8    ; PE sig addr = base addr + RVA of PE sig
  mov  eax,dword [eax+0x78] ; 8b4078  ; RVA of Export Table
  add  eax,ebx              ; 01d8    ; Address of Export Table

  mov  ecx,dword [eax+0x24] ; 8b4824  ; RVA of Ordinal Table
  add  ecx,ebx              ; 01d9    ; Address of Ordinal Table
  mov  dword [ebp-0xc],ecx  ; 894df4  ; Put on the stack

  mov  edi,dword [eax+0x20] ; 8b7820  ; RVA of Name Pointer Table
  add  edi,ebx              ; 01df    ; Address of Name Pointer Table
  mov  dword [ebp-0x10],edi ; 897df0  ; Put on the stack

  mov  edx,dword [eax+0x1c] ; 8b501c  ; RVA of Address Table
  add  edx,ebx              ; 01da    ; Address of Address Table
  mov  dword [ebp-0x14],edx ; 8955ec  ; Put on the stack

  mov  ebx,dword [eax+0x14] ; 8b5814  ; Number of exported functions
;--- Using the Name Pointer Table
; This part loops through the Name Pointer Table and compares entries to what
; we're looking for: "WinExec".
; The number of entries is counted using EAX, and once the WinExec entry is 
; found, the entry in the ordinal table is found using the count. See 'locate'
  xor  eax,eax              ; 31c0    ; EAX will be our entry counter
  mov  edx, dword [ebp - 8] ; 8b55f8  ; EDX = kernel32.dll base address
loopy:
  mov  edi,dword [ebp-0x10] ; 8b7df0  ; edi = Address of Name Pointer Table
  mov  esi,dword [ebp-4]    ; 8b75fc  ; esi = "WinExec\x00"
  xor  ecx,ecx              ; 31c9    ; ECX = 0
  cld                       ; fc      ; Clear direction flag 
                                      ; Strings now go left->right
  mov  edi,dword [edi+eax*4] ; 8b3c87 ; Name Pointer Table entries are 4 bytes,
                                      ; edi (NPT addr) + eax (num entries) * 4
  add  edi,edx             ; 01d7     ; EDI = NPT addr + kernel32.ddl base addr
  add  cx,0x8              ; 6683c108 ; Length of "WinExec"
  repe cmpsb               ; f3a6     ; Compare the first 8 bytes in esi and edi
  jz locate                ; 740a     ; Jump if there's a match.

  inc  eax                 ; 40       ; Increment entry counter
  cmp  eax,ebx             ; 39d8     ; Check if the last function was reached
  jb   loopy               ; 72e5     ; If not the last one, continue
  add  esp,0x26            ; 83c426   ; Move stack away from our mess
  jmp  endy                ; eb41     ; If nothing found, return
;--- Executing our function
; Once we're here, we know the position of WinExec within the ordinal table
; of kernel32.dll, so now all that's left is to call the function.
; We use all of our saved addresses on the stack for this.
locate:
  mov  ecx, [ebp-0xc]       ; 8b4df4  ; ECX = Address of Ordinal Table
  mov  ebx, edx             ; 89d3    ; EBX = kernel32.dll base address
  mov  edx, [ebp-0x14]      ; 8b55ec  ; EDX = Address of Address Table
  mov  ax, [ecx+eax*2]      ; 668b0441; AX  = ordinal addr + (ordinal num * 2)
  mov  eax, [edx+eax*4]     ; 8b0482  ; EAX = Addr table addr + (ordinal * 4)
  add  eax,ebx              ; 01d8    ; EAX = WinExec Addr = 
                            ; = kernel32.dll base address + RVA of WinExec
  xor  edx,edx              ; 31d2       ; We need a 0...
  push edx                  ; 52         ; ...for the end of our string
  push 0x6578652e           ; 682e657865 ;
  push 0x636c6163           ; 6863616c63 ;
  push 0x5c32336d           ; 686d33325c ;
  push 0x65747379           ; 6879737465 ;
  push 0x535c7377           ; 6877735c53 ;
  push 0x6f646e69           ; 68696e646f ;
  push 0x575c3a43           ; 68433a5c57 ;
  mov  esi,esp              ; 89e6       ; ESI="C:\Windows\System32\calc.exe"
  push 0xa                  ; 6a0a       ; window state SW_SHOWDEFAULT
  push esi                  ; 56         ; "C:\Windows\System32\calc.exe"
  call eax                  ; ffd0       ; WinExec
  add  esp,0x46             ; 83c446     ; Clear the stack

The binary starts with a short jump back into the headers, and then leaps around to set up WinExec on the stack and begin to parse the PEB, before jumping back to the main code section and continuing execution there.

If WinExec isn’t found, it returns back into the headers to execute the final ret instruction.

The payload ends kind of abruptly. You could simply just not include the last instruction after call eax, but I didn’t have anything else to put there so I just left it to keep the size.

What do I use this for?

You can modify the last portion of code in the locate label to push whatever program you want to execute, in this case "C:\Windows\System32\calc.exe", onto the stack, and execute something else. You’ll have to make sure you allocate enough stack space for yourself and place arguments in the right place if they are required.

You can use powershell or some lolbins or whatever elite FUD 100% battle tested “plz don’t upload to VT or ur ded!!” binary loader to execute a base64 encoded version of this binary.

Check out my other small binaries on Github!

Download the POC binary here.

Payload generator for WinExec method

Shoutouts

Thanks to dnz, readme, xehle, secfarmer, remy, Ange Albertini, Iliya Dafchev, and everyone in the Binary Analysis and Exploit Dev chans I dump crap into.