Skip to content
λmaldev wiki/
pagesPE to Shellcode
T1620WindowsC / C++ShellcodePE

PE to Shellcode

updated 2026-09-049 min readthehackersbrain
Authorized use only. This material is published for detection engineering, malware analysis and authorized red-team engagements. Running these techniques against systems you do not own or have written permission to test is illegal.

Overview

PE-to-shellcode is the conversion of a portable executable into a self-contained shellcode blob. The PE’s bytes are embedded as a C array, and a small position-independent preamble is written that, when it runs, finds free memory, copies the embedded image in, applies the relocations and import fixups a loader would normally do, and jumps to the entry point. After that jump, you are executing the original PE in memory

  • no file on disk, no module the OS registered.

This is the bridge between two things people often conflate. On one side is raw shellcode, which is just bytes that do one thing. On the other is a full PE with sections, an import table and an entry point. PE-to-shellcode makes the second look like the first: the thing you inject is a blob of machine code, but what that blob boots is a complete image. Tools like Donut exist precisely to generate this blob for you so you do not have to hand-write the preamble.

Note.

The preamble must be position-independent. It runs at an unknown address before it has set anything up, so it cannot use absolute references to itself. See position independent code for why this is the single most common source of “works in x64dbg, crashes in the real target” bugs.

The call chain

  1. 1
    Embed the PE as a byte array
    The PE file becomes an unsigned char payload[] inside the shellcode.
  2. 2
    Find a free region
    Locate space for the image via the PEB Ldr table or a free-range search.
  3. 3
    Copy the PE bytes in place
    Write the embedded payload into the reserved region; the shellcode must be relocatable.
  4. 4
    Run the manual-map fixups
    Apply relocations and bind the IAT on the copied image, as a loader would.
  5. 5
    Jump to the entry point
    Transfer control to the mapped entry point; the shellcode's job is done.

Reference implementation

The first step is mechanical: turn the PE into a byte array.

embed.sh
# turn the PE into a C array the shellcode can carry
xxd -i implant.dll > implant_bytes.h

What the generated shellcode then does, conceptually. This is the anatomy of the blob, not a complete implementation - a generator such as Donut emits the real thing.

shellcode_anatomy.cC (shellcode)
// position-independent preamble, abridged
extern const unsigned char payload[];   // the embedded PE (from xxd -i)
extern const unsigned long  payload_len;

void __fastcall shellcode_entry() {
  // 1. find a free region large enough for the image
  //    - walk the PEB Ldr data table for a free range, or
  //    - VirtualAlloc a fresh block sized to SizeOfImage
  // 2. copy the embedded bytes in place  <-- the image now exists in memory
  memcpy(region, payload, payload_len);

  // 3. apply relocations if region != PreferredBase, then bind the IAT
  //    (identical steps to a manual map, on the copied bytes)
  fixup_relocations(region);
  fixup_imports(region);

  // 4. jump to the entry point - control never returns here
  ((void(*)())region)();
}
Caution.

The shellcode and the embedded PE are two different position-independent problems. The preamble must be PIC before it runs; the PE must be rebased after it is copied in. Tools that generate the blob handle both, but a hand-rolled preamble that forgets the second step will crash on the PE’s first import call.

Verifying in the lab

Generate the blob, drop it into a target, and watch the PE appear in memory. WinDbg will show an MZ region that is not in the module list, and the process executing the implant’s code.

windbg
0:000> !process 0 0
...
0:000> lm
(no module for the implant)  <-- injected as shellcode, not a module

0:000> dd <region> L2
<region>  4d5a 9000 0300 0000   ^ the embedded PE, now running

Detection

The detection surface is the same as manual mapping, plus the fact that a full PE landed inside what should have been a small shellcode blob.

MEMORY SCAN
An RWX private region holding an MZ blob with no backing file and no image-load event.
SYSMON EID 8
A remote thread whose start address points into a private region, not a loaded module.
SYSMON EID 5
A process writing a full PE into another process's private memory with no new module.
STATIC
Shellcode carrying an embedded MZ header plus relocation and import fixup logic.

Rank them: the static scan for an embedded MZ plus fixup logic catches the blob before it runs and is the cheapest control. The EID 8 remote-thread-with-private-start check catches the injection at runtime. The memory scan is the highest-fidelity but periodic catch-all.

Was this page useful?edit this page ↗