packer

package
v0.134.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 12, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package packer is maldev's custom PE/ELF packer.

Phases shipped:

  • 1a — Pack / Unpack pipeline: AEAD cipher (AES-GCM default)
  • self-describing maldev-format blob (magic + version + cipher
  • compressor + sizes + nonce + ciphertext).
  • 1b — Windows x64 reflective loader stub (github.com/oioio-space/maldev/pe/packer/runtime).
  • 1c — Composability via PackPipeline / UnpackPipeline: stack PipelineOp steps (OpCipher / OpPermute / OpCompress / OpEntropyCover) in any order; each step's algorithm is wire-recorded but its key never is.
  • 1c.5 — Compression in pipeline (CompressorFlate, CompressorGzip via stdlib).
  • 1d — Anti-entropy under OpEntropyCover: EntropyCoverInterleave (low-entropy padding spliced between ciphertext chunks — drops real Shannon entropy proportional to padding ratio), EntropyCoverCarrier (PNG-shaped 32-byte header so first-bytes scanners don't fire), EntropyCoverHexAlphabet (each byte → 2 alphabet bytes, apparent entropy ≤ 4 bits/byte).
  • 1e (v0.61.0) — UPX-style in-place transform via PackBinary: encrypts the input binary's .text section with SGN polymorphic encoding (XOR/SUB/ADD rounds with register randomisation and junk insertion), appends a compact polymorphic decoder stub as a new R+W+X section (CALL+POP+ADD prologue for position-independent address recovery, N decoder loops, final JMP to original entry), and rewrites the entry-point field. Output is a single self-contained binary — no stage 2, no reflective loader. The kernel loads the output normally; the stub decrypts in place. Supports FormatWindowsExe (PE32+) and FormatLinuxELF (ELF64 static-PIE). Detection is Medium-High: UPX-like single-binary packer patterns are well-known to AV/EDR; stub bytes differ per pack (polymorphic) which defeats hash-based batch detection, but the RWX new section and entry-point rewrite are heuristically suspicious.
  • 3a (post-v0.61.0) — Anti-static-unpacker cover layer via AddCoverPE / AddCoverELF / ApplyDefaultCover: appends junk sections (PE) or junk PT_LOADs (ELF) with caller-chosen JunkFill strategy (JunkFillRandom for ~8 bits/byte entropy, JunkFillZero for flat-entropy padding, JunkFillPattern for machine-code-shaped histograms). The cover sections carry MEM_READ only — kernel maps them but never executes; runtime path is unchanged. Pair with PackBinary to inflate the static surface and frustrate fingerprints that match on exact section count + offset. DefaultCoverOptions picks 3 reasonable sections and is exposed via ApplyDefaultCover for one-liner integration. v0.63.0 extends the PE cover layer with fake imports via AddFakeImportsPE / DefaultFakeImports: a new `.idata2` section holds merged IMAGE_IMPORT_DESCRIPTOR entries for kernel32, user32, shell32, and ole32. The kernel resolves all entries at load time; DefaultCoverOptions / ApplyDefaultCover chain this step automatically for PE32+ inputs.

The full design (capability matrix, threat model, hard constraints, phase plan) is at docs/refactor-2026-doc/packer-design.md.

MITRE ATT&CK

  • T1027.002 — Obfuscated Files or Information: Software Packing
  • T1620 — Reflective Code Loading (Phase 1b onwards, when the reflective stub ships)

Detection level

moderate.

The pack-time pipeline (Phases 1a / 1c / 1d / 3a) is pure-Go offline byte manipulation — no syscalls, no network, no runtime artefacts. Detection of those layers is purely static: the blob's Magic prefix is fingerprintable when the blob is shipped raw, but in practice it travels inside a host binary that obscures it.

Phase 1e (PackBinary) emits a runnable PE/ELF whose CALL+POP+ADD prologue + new R+W+X section + entry-point rewrite is heuristically suspicious to AV/EDR static scans. Per-pack stub-byte uniqueness from the SGN engine defeats hash-based batch detection, but the structural shape is well-known. Pair with AddCoverPE / AddCoverELF / ApplyDefaultCover to inflate the static surface and frustrate fingerprints that match on exact section count + offset, plus EntropyCoverInterleave / EntropyCoverHexAlphabet in the PackPipeline path to drop apparent histogram entropy below 4 bits/byte.

Required privileges

unprivileged. The packer is pack-time only; runtime artefacts are produced by the loader (kernel for PackBinary outputs, github.com/oioio-space/maldev/pe/packer/runtime for reflective loads).

Platform

Cross-platform pack-time. PackBinary outputs are per-target (FormatWindowsExe PE32+ runs on Windows; FormatLinuxELF runs on Linux). Pack / PackPipeline outputs are architecture-neutral blobs. The reflective loader (github.com/oioio-space/maldev/pe/packer/runtime) ships for Windows x64 and Linux ELF64 (Phase 1f Stages A–E).

Example

See [Example] suite in [packer_example_test.go]: [ExamplePack], [ExamplePackBinary], [ExampleAddCoverPE], [ExampleApplyDefaultCover].

One-liner Phase 1e + cover for Linux ELF input:

out, _, err := packer.PackBinary(payload, packer.PackBinaryOptions{
    Format: packer.FormatLinuxELF, Stage1Rounds: 3, Seed: 1,
})
if err != nil { /* … */ }
if covered, err := packer.ApplyDefaultCover(out, 2); err == nil {
    out = covered
}

See also

Package packer is maldev's custom PE/ELF packer.

Pack / Unpack handle the encrypt-only pipeline (Phase 1c+). PackBinary is the operator-facing entry point added in Phase 1e (v0.61.x): it wraps a payload in a runnable host binary (Windows PE32+ via FormatWindowsExe or Linux ELF64 static-PIE via FormatLinuxELF) containing a polymorphic SGN-style stage-1 decoder and a reflective stage-2 loader. No go build or system toolchain is required at pack time.

Design + roadmap: docs/refactor-2026-doc/packer-design.md.

Index

Examples

Constants

View Source
const (
	BundleHKDFLabelMagic   = "maldev/bundle/magic"
	BundleHKDFLabelFooter  = "maldev/bundle/footer"
	BundleHKDFLabelVersion = "maldev/bundle/version"
	BundleHKDFLabelVaddr   = "maldev/bundle/vaddr"
)

BundleHKDFLabel* are the per-purpose HKDF-Expand labels used by DeriveBundleProfile to generate statistically independent bytes for each profile field. Wire-format-load-bearing — changing any of these strings invalidates every bundle ever produced against the matching label set, so they live here as named constants beside BundleMagic / BundleFooterMagic rather than as inline literals at the call site.

View Source
const (
	// BundleMagic is the four-byte ASCII tag at offset 0 — "MLDV".
	BundleMagic uint32 = 0x56444C4D

	// BundleVersion is the wire-format version surfaced in BundleHeader.
	BundleVersion uint16 = 0x0001

	// BundleHeaderSize, BundleFingerprintEntrySize, BundlePayloadEntrySize
	// are the on-disk sizes of each region's entry.
	BundleHeaderSize           = 32
	BundleFingerprintEntrySize = 48
	BundlePayloadEntrySize     = 32

	// BundleMaxPayloads is the practical upper bound on payload count.
	// Wire format allows uint16 (65 535); we cap at 255 per spec to keep
	// the fingerprint-loop stub size sane.
	BundleMaxPayloads = 255
)

Bundle wire format constants.

On-disk layout (all little-endian):

[BundleHeader               (32 bytes)]
[FingerprintEntry × Count   (48 bytes each)]
[PayloadEntry × Count       (32 bytes each)]
[EncryptedPayloadData × Count (variable, concatenated)]

The bundle header sits at the start of the bundle blob; the binary's entry point points into the bundle stub (which lives just past EncryptedPayloadData). All offsets in the header are RVAs relative to the bundle's first byte.

See docs/superpowers/specs/2026-05-08-packer-multi-target-bundle.md for the full design and threat model.

View Source
const (
	PTCPUIDVendor   uint8 = 1 << 0 // 12-byte CPUID EAX=0 vendor string check
	PTWinBuild      uint8 = 1 << 1 // PEB.OSBuildNumber range check
	PTCPUIDFeatures uint8 = 1 << 2 // CPUID EAX=1 ECX feature mask check
	PTMatchAll      uint8 = 1 << 3 // wildcard — matches any host
)

PredicateType bitmask flags for FingerprintPredicate.PredicateType.

Within a single FingerprintEntry, all enabled bits are ANDed: every active check must pass for the entry to match. Across entries, the first matching entry wins.

View Source
const (
	// CipherTypeXORRolling is the v0.61 default: payload bytes XORed
	// against a 16-byte key (PayloadEntry.Key) rolling modulo 16.
	// Size-preserving; one-line decrypt loop in the stub.
	CipherTypeXORRolling uint8 = 1
	// CipherTypeAESCTR is AES-128-CTR (Tier 🟡 #2.2). Key in
	// PayloadEntry.Key (16 B); the 16-byte IV/initial counter is
	// prepended to the encrypted payload data, and the 11 × 16-byte
	// expanded round keys (= 176 B, per [crypto.ExpandAESKey]) are
	// appended AFTER the ciphertext so the stub-side AES-NI decrypt
	// loop can `MOVDQU` them straight into XMM without an in-stub
	// expansion step. On-disk layout:
	//     [IV (16 B)] [AES-CTR ciphertext] [round keys (176 B)]
	// PayloadEntry.DataSize = 16 + len(ciphertext) + 176;
	// PlaintextSize = len(plaintext).
	//
	// Stub-side decrypt uses AES-NI — pack-time auto-injects the
	// AES bit (0x02000000) into the entry's PT_CPUID_FEATURES mask
	// + value so pre-AES-NI hosts fall through cleanly (no
	// crash, predicate just doesn't match). Operators can override
	// by pre-setting CPUIDFeatureMask/Value to include the AES bit
	// themselves; the auto-injection is a strict OR, never a
	// silent overwrite.
	CipherTypeAESCTR uint8 = 2

	// AESCTRRoundKeysSize is the byte size of the 11 expanded
	// AES-128 round keys appended after each CipherType=2 ciphertext.
	// Stub-side address: matched-entry data + 16 (IV) + plaintext_len.
	AESCTRRoundKeysSize = 176

	// CPUIDFeatureAES is the AES-NI feature bit in CPUID[1].ECX
	// (Intel SDM Vol. 2A). Pack-time auto-injects this bit into a
	// CipherType=2 entry's PT_CPUID_FEATURES mask + value so the
	// runtime predicate evaluator skips the entry on pre-AES-NI
	// hosts. Same bit since 2010 (Westmere / Bulldozer onwards).
	CPUIDFeatureAES uint32 = 0x02000000
)

CipherType values are written into PayloadEntry.CipherType and drive the pack-time encrypt + runtime decrypt path. The wire field is one byte; values not listed here are reserved.

View Source
const FormatVersion uint16 = 1

FormatVersion bumps when the on-wire blob layout changes in a non-backwards-compatible way. Unpack rejects unknown versions to fail loudly rather than misinterpret bytes.

View Source
const FormatVersionPipeline uint16 = 2

FormatVersionPipeline is the wire-format version emitted by PackPipeline. Distinct from FormatVersion (used by Pack) so old single-cipher blobs continue to unpack via the v1 path and new multi-step blobs route through UnpackPipeline.

View Source
const HeaderSize = 32

HeaderSize is the on-wire byte length of the fixed-size blob header. Constant across versions; format-version changes that extend the header live in a separate "extended header" trailer after the magic to preserve backward parsing.

Variables

View Source
var (
	VendorIntel = [12]byte{'G', 'e', 'n', 'u', 'i', 'n', 'e', 'I', 'n', 't', 'e', 'l'}
	VendorAMD   = [12]byte{'A', 'u', 't', 'h', 'e', 'n', 't', 'i', 'c', 'A', 'M', 'D'}
	VendorHygon = [12]byte{'H', 'y', 'g', 'o', 'n', 'G', 'e', 'n', 'u', 'i', 'n', 'e'}
)

Canonical CPUID vendor strings published as exported constants so callers don't re-spell them as `[12]byte{'G','e','n',...}` literals at every site. The values come from the Intel SDM Vol. 2A — every x86 CPU returns one of these (or a vendor-specific override).

Use in tests, CLI parser, and any operator-side fingerprint authoring path. Keeping them as `[12]byte` matches FingerprintPredicate.VendorString without conversion.

View Source
var (
	// ErrEmptyBundle fires when payloads is nil or has zero length.
	ErrEmptyBundle = errors.New("packer: empty bundle")
	// ErrBundleTooLarge fires when len(payloads) exceeds BundleMaxPayloads.
	ErrBundleTooLarge = errors.New("packer: bundle exceeds 255 payloads")
	// ErrBundleTruncated fires when a blob is shorter than the minimum
	// header. Surfaced by [InspectBundle] / [SelectPayload] / [UnpackBundle].
	ErrBundleTruncated = errors.New("packer: bundle truncated")
	// ErrBundleBadMagic fires when the magic dword does not match
	// [BundleMagic]. Surfaced by [InspectBundle].
	ErrBundleBadMagic = errors.New("packer: bundle bad magic")
	// ErrBundleOutOfRange fires when a declared offset / size escapes
	// the blob bounds. Surfaced by [InspectBundle].
	ErrBundleOutOfRange = errors.New("packer: bundle offset out of range")
	// ErrBundleBadKeyLen fires when [BundlePayload.Key] is non-nil
	// but its length isn't 16. Both XOR-rolling and AES-CTR ciphers
	// use 16-byte keys.
	ErrBundleBadKeyLen = errors.New("packer: BundlePayload.Key must be 16 bytes")
)

Sentinels surfaced by PackBinaryBundle.

View Source
var (
	// ErrUnsupportedEntropyCover fires when a step references an
	// EntropyCover constant this build doesn't implement.
	ErrUnsupportedEntropyCover = errors.New("packer: unsupported entropy-cover algo")

	// ErrEntropyCoverCorrupt fires when the wire-side metadata
	// (header byte / alphabet sentinel / carrier magic) doesn't
	// validate during reverse.
	ErrEntropyCoverCorrupt = errors.New("packer: entropy-cover blob is corrupt")
)

Sentinels for the entropy layer.

View Source
var (
	// ErrShortBlob fires when the input bytes are too small to
	// contain a maldev-packed header.
	ErrShortBlob = errors.New("packer: blob shorter than header")

	// ErrBadMagic fires when the first 4 bytes don't match [Magic].
	ErrBadMagic = errors.New("packer: bad magic")

	// ErrUnsupportedVersion fires when the blob's version field
	// doesn't match this build's [FormatVersion].
	ErrUnsupportedVersion = errors.New("packer: unsupported format version")

	// ErrUnsupportedCipher fires when the blob references a
	// Cipher constant this build doesn't know how to decrypt.
	ErrUnsupportedCipher = errors.New("packer: unsupported cipher")

	// ErrUnsupportedCompressor fires when the blob references a
	// Compressor constant this build doesn't know how to inflate.
	ErrUnsupportedCompressor = errors.New("packer: unsupported compressor")

	// ErrPayloadSizeMismatch fires when the header's PayloadSize
	// disagrees with the actual byte count after the header.
	ErrPayloadSizeMismatch = errors.New("packer: payload size mismatch")

	// ErrCorruptBlob fires when the blob's structural metadata is
	// internally inconsistent (e.g., pipeline table past the end of
	// the blob, zero-step pipeline, descriptor offsets that overlap).
	// Distinct from ErrBadMagic (wrong magic at offset 0) so callers
	// can differentiate "this isn't even a maldev blob" from
	// "this is a maldev blob whose internals are damaged".
	ErrCorruptBlob = errors.New("packer: blob structure corrupt")
)

Sentinel errors surfaced by the format / Unpack layer.

View Source
var (
	// ErrEmptyPipeline fires when [PackPipeline] is called with
	// an empty pipeline. Use the single-step [Pack] for the
	// AES-GCM-only convenience case.
	ErrEmptyPipeline = errors.New("packer: pipeline is empty")

	// ErrPipelineTooLong fires when the pipeline exceeds 255
	// steps. The wire format encodes step count as one byte;
	// 255 is more than any operator should ever stack.
	ErrPipelineTooLong = errors.New("packer: pipeline exceeds 255 steps")

	// ErrUnsupportedPermutation fires when a step references a
	// Permutation constant this build doesn't implement.
	ErrUnsupportedPermutation = errors.New("packer: unsupported permutation")

	// ErrPipelineKeysMismatch fires when [UnpackPipeline] is
	// called with a `keys` slice whose length disagrees with the
	// blob's recorded pipeline step count.
	ErrPipelineKeysMismatch = errors.New("packer: pipeline keys count mismatch")
)

Sentinels for the pipeline layer.

View Source
var BundleFooterMagic = [8]byte{'M', 'L', 'D', 'V', '-', 'E', 'N', 'D'}

BundleFooterMagic is the 8-byte sentinel an AppendBundle launcher writes at the very end of the wrapped binary so it can locate its own bundle blob without scanning. Reads as "MLDV-END" in ASCII.

View Source
var DefaultFakeImports = []FakeImport{
	{DLL: "kernel32.dll", Functions: []string{"Sleep", "GetCurrentThreadId"}},
	{DLL: "user32.dll", Functions: []string{"MessageBoxA", "GetCursorPos"}},
	{DLL: "shell32.dll", Functions: []string{"ShellExecuteA"}},
	{DLL: "ole32.dll", Functions: []string{"CoInitialize"}},
}

DefaultFakeImports is a ready-to-use list of real Windows 10 1809+ / Server 2019+ imports. All four DLLs ship in every supported Windows installation; all function names are stable exports verified against Microsoft public symbol tables.

View Source
var ErrCipherTypeFixedKey = errors.New("packer: CipherType requires random IV; cannot combine with FixedKey")

ErrCipherTypeFixedKey fires when a per-payload CipherType requires fresh randomness (e.g. AES-CTR's IV) but the bundle was packed with BundleOptions.FixedKey set — that path is test-determinism-only and can't coexist with cipher modes whose security relies on per-pack randomness.

View Source
var ErrCoverInvalidOptions = errors.New("packer/cover: invalid options")

ErrCoverInvalidOptions signals an empty / malformed CoverOptions.

View Source
var ErrCoverSectionTableFull = errors.New("packer/cover: section table full")

ErrCoverSectionTableFull signals the input PE has no remaining space between its phdr table and the first section's file offset for additional section headers. Real PEs almost always have slack; the error is for defensive synthetic-input rejection.

View Source
var ErrNotMinimalWrap = errors.New("packer: not a minimal-host shellcode wrap")

ErrNotMinimalWrap fires when UnwrapShellcode receives a binary that parses as PE/ELF but doesn't match the minimal-host shape PackShellcode produces (e.g. a real Go binary, or an encrypted PackBinary output). The .text section bytes are not the operator's raw shellcode in that case.

View Source
var ErrShellcodeEmpty = errors.New("packer: shellcode bytes empty")

ErrShellcodeEmpty fires on nil or zero-length shellcode input.

View Source
var ErrUnsupportedFormat = errors.New("packer: unsupported format")

ErrUnsupportedFormat fires when PackBinary's opts.Format does not match the magic-detected format of the input binary.

View Source
var Magic = [4]byte{'M', 'L', 'D', 'V'}

Magic identifies a maldev-packed blob. Four bytes at the start of every Pack output. Picked to avoid collision with common PE/ELF/script magics (MZ, ELF, #!, PK, …).

Functions

func AddCoverELF added in v0.61.1

func AddCoverELF(input []byte, opts CoverOptions) ([]byte, error)

AddCoverELF is the ELF64 mirror of AddCoverPE. Each JunkSection becomes a new PT_LOAD program-header entry with R only (no W, no X). The kernel maps each PT_LOAD as ordinary read-only data; runtime behaviour is unchanged.

ELF differs from PE in two relevant ways:

  • The Section header table (SHT) is optional at runtime — the kernel uses program headers (PHT). Cover layer adds PT_LOADs to the PHT; SHT entries are NOT added (a stripped binary stays stripped).
  • PT_LOAD entries must be sorted by p_vaddr ascending. Cover PT_LOADs are appended above the highest existing virtual end so the ordering is preserved.

The PHT is grown in place: cover layer writes new phdr slots after the last existing one. The input must therefore have at least len(JunkSections) phdr slots of slack between the PHT and the first PT_LOAD's file offset; real Go static-PIE binaries always do.

JunkSection.Name is ignored on ELF — sections are not part of the runtime path.

Returns ErrCoverInvalidOptions for empty options or non-ELF input; ErrCoverSectionTableFull when the PHT cannot grow.

func AddCoverPE added in v0.61.1

func AddCoverPE(input []byte, opts CoverOptions) ([]byte, error)

AddCoverPE appends junk sections to a packed PE32+ produced by PackBinary. The input is not modified; a new buffer is returned with the sections appended after the existing section table and SizeOfImage / NumberOfSections updated.

The added sections carry IMAGE_SCN_MEM_READ only (no W, no X). Loader maps them as ordinary read-only data; runtime behaviour is unchanged. Operators concerned about static analysis should pair JunkFillRandom with a vendor-realistic name (`.rsrc`, `.rdata2`); concerned about entropy heuristics should pair JunkFillZero with a benign name.

Returns ErrCoverInvalidOptions when JunkSections is empty, ErrCoverSectionTableFull when the section table cannot grow.

Example

ExampleAddCoverPE chains the cover layer after a PackBinary call to inflate the PE static surface with three junk sections of mixed entropy.

package main

import (
	"os"

	"github.com/oioio-space/maldev/pe/packer"
)

func main() {
	packed, err := os.ReadFile("packed.exe")
	if err != nil {
		return
	}
	covered, err := packer.AddCoverPE(packed, packer.CoverOptions{
		JunkSections: []packer.JunkSection{
			{Name: ".rsrc", Size: 0x4000, Fill: packer.JunkFillRandom},
			{Name: ".pdata", Size: 0x2000, Fill: packer.JunkFillPattern},
			{Name: ".tls", Size: 0x1000, Fill: packer.JunkFillZero},
		},
	})
	if err != nil {
		return
	}
	_ = os.WriteFile("covered.exe", covered, 0o755)
}

func AddFakeImportsPE added in v0.63.0

func AddFakeImportsPE(input []byte, fakes []FakeImport) ([]byte, error)

AddFakeImportsPE appends fake IMAGE_IMPORT_DESCRIPTOR entries to input (a PE32+ produced by PackBinary or AddCoverPE). The merged Import Directory — original entries followed by one entry per FakeImport, terminated by a zero descriptor — is placed in a new R-only section named ".idata2". DataDirectory[1] is patched to point at the new section.

Existing entries' FirstThunk RVAs are preserved verbatim — the loader patches those addresses, and the binary's code references them. OriginalFirstThunk is updated to point into the new section (the ILT body for existing entries is copied there) so the entire Import Directory is self-contained within the new section.

fakes must contain at least one entry; every DLL name and function name must be resolvable on the target OS or the kernel will reject the image at load time.

Returns ErrCoverInvalidOptions when fakes is empty or input is not a PE32+. Returns ErrCoverSectionTableFull when the section header table has no slack for an additional entry.

func AppendBundle added in v0.67.0

func AppendBundle(launcher []byte, bundle []byte) []byte

AppendBundle returns launcher bytes with `bundle` concatenated at the end, followed by an 8-byte little-endian offset of the bundle's first byte and the BundleFooterMagic sentinel:

[ launcher bytes        ]
[ bundle blob           ]
[ 8 BE: bundleStartOff  ]
[ 8 BE: BundleFooterMagic ]

Total 16-byte footer. The launcher reads its own binary at runtime, inspects the last 16 bytes, validates the magic, slices back to the bundle bytes, and proceeds with MatchBundleHost / UnpackBundle.

Returns a fresh slice; the input launcher slice is not modified.

func AppendBundleWith added in v0.73.0

func AppendBundleWith(launcher []byte, bundle []byte, profile BundleProfile) []byte

AppendBundleWith is the per-build-profile-aware variant of AppendBundle. The footer's 8-byte sentinel uses `profile.FooterMagic` instead of the canonical BundleFooterMagic. Operators wrapping with a custom BundleProfile (typically derived from `-secret` via DeriveBundleProfile) MUST use this variant; the matching launcher must know the same FooterMagic at runtime (typically injected via -ldflags -X). Caller-side parser is ExtractBundleWith.

func ApplyDefaultCover added in v0.61.1

func ApplyDefaultCover(input []byte, seed int64) ([]byte, error)

ApplyDefaultCover auto-detects whether input is a PE32+ or ELF64 and applies the DefaultCoverOptions cover layer via the matching AddCoverPE / AddCoverELF entry point.

Convenience wrapper for the common chain:

packed, _, _ := packer.PackBinary(payload, opts)
covered, _   := packer.ApplyDefaultCover(packed, time.Now().UnixNano())

Returns ErrCoverInvalidOptions when the input is neither a PE nor an ELF; underlying ELF-specific errors (ErrCoverSectionTableFull on Go static-PIE) propagate unchanged so operators can decide whether to bail or skip cover for that target.

Example

ExampleApplyDefaultCover is the one-liner cover layer. Auto-detects PE vs ELF and applies a 3-section default with randomized legit-looking names. Operators chain it after PackBinary; ELF static-PIE inputs return ErrCoverSectionTableFull and the operator falls back to the bare PackBinary output.

package main

import (
	"os"
	"time"

	"github.com/oioio-space/maldev/pe/packer"
)

func main() {
	packed, err := os.ReadFile("packed.bin")
	if err != nil {
		return
	}
	out := packed
	if covered, err := packer.ApplyDefaultCover(packed, time.Now().UnixNano()); err == nil {
		out = covered
	}
	_ = os.WriteFile("output.bin", out, 0o755)
}

func ExtractBundle added in v0.67.0

func ExtractBundle(wrapped []byte) ([]byte, error)

ExtractBundle is the inverse of AppendBundle: given the full bytes of an AppendBundle-wrapped launcher (typically read from `/proc/self/exe` or `os.Executable()`), it returns a slice over the embedded bundle. Errors when the footer magic is missing or the declared offset escapes the blob.

The returned slice references the input — caller must not mutate it while the bundle is in use.

func ExtractBundleWith added in v0.73.0

func ExtractBundleWith(wrapped []byte, profile BundleProfile) ([]byte, error)

ExtractBundleWith is the per-build-profile-aware variant of ExtractBundle. Validates the footer against `profile.FooterMagic` instead of the canonical BundleFooterMagic.

func HostCPUIDVendor added in v0.67.0

func HostCPUIDVendor() [12]byte

HostCPUIDVendor returns the 12-byte CPUID EAX=0 vendor string of the host CPU (e.g. {'G','e','n','u','i','n','e','I','n','t','e','l'}).

Implemented via antivm.CPUVendor, which calls Plan-9-asm `cpuidRaw(0, 0)` from the existing `recon/antivm` package — no mmap, no trampoline, no GC traps. The runtime stub-side asm (stage1.EmitCPUIDVendorRead) emits the same byte sequence inline for self-contained binaries that can't link to the recon package.

func MatchBundleHost added in v0.67.0

func MatchBundleHost(bundle []byte) (int, error)

MatchBundleHost is the operator-facing "would this payload fire on this host?" check. It reads the host's CPUID vendor (and on Windows, OSBuildNumber via RtlGetVersion — see bundle_host_windows.go), then calls SelectPayload against the supplied bundle.

Returns -1 if no entry matches. Errors flow from SelectPayload (truncation, bad magic).

On Linux the build number is reported as 0, so any entry with PT_WIN_BUILD + non-zero BuildMin will not match — which is the correct semantic since Linux bundles do not carry Windows build predicates.

func MatchBundleHostWith added in v0.73.0

func MatchBundleHostWith(bundle []byte, profile BundleProfile) (int, error)

MatchBundleHostWith is the per-build-profile-aware variant of MatchBundleHost. Validates the bundle's magic against `profile.Magic` (canonical default when zero) and dispatches via SelectPayloadWith.

func Pack

func Pack(data []byte, opts Options) (packed []byte, key []byte, err error)

Pack runs `data` through the configured AEAD cipher and emits a Magic-prefixed blob.

Returns the packed bytes + the AEAD key used (caller-supplied or freshly generated). The returned key is the only material needed to call Unpack later; the blob itself is opaque.

Example

ExamplePack is the Simple-tier round-trip via the blob pipeline (Phase 1a). Caller supplies a payload, gets back (blob, key); Unpack with the same key recovers the original.

package main

import (
	"github.com/oioio-space/maldev/pe/packer"
)

func main() {
	payload := []byte("hello packer")
	blob, key, err := packer.Pack(payload, packer.Options{
		Cipher: packer.CipherAESGCM,
	})
	if err != nil {
		return
	}
	got, err := packer.Unpack(blob, key)
	if err != nil {
		return
	}
	_ = got
}

func PackBinary added in v0.59.0

func PackBinary(input []byte, opts PackBinaryOptions) ([]byte, []byte, error)

PackBinary applies the UPX-style transform to a PE/ELF input binary: encrypts .text, appends a polymorphic decoder stub as a new section, rewrites the entry point. At runtime the kernel loads the modified binary normally; the stub decrypts .text and JMPs to the original OEP.

Pure Go: no go build, no system toolchain at pack-time.

Sentinels: ErrUnsupportedFormat, stubgen.ErrInvalidRounds, stubgen.ErrNoInput, plus transform sentinels (ErrNoTextSection, ErrOEPOutsideText, ErrTLSCallbacks, …).

Example

ExamplePackBinary shows the v0.61.0 UPX-style transform on a Linux ELF input. Output is single-binary; the kernel handles loading. Stage1Rounds=3 is the ship-tested baseline.

package main

import (
	"fmt"
	"os"
	"time"

	"github.com/oioio-space/maldev/pe/packer"
)

func main() {
	input, err := os.ReadFile("input.elf")
	if err != nil {
		return
	}
	out, key, err := packer.PackBinary(input, packer.PackBinaryOptions{
		Format:       packer.FormatLinuxELF,
		Stage1Rounds: 3,
		Seed:         time.Now().UnixNano(),
	})
	if err != nil {
		return
	}
	fmt.Printf("packed %d bytes (key %x...)\n", len(out), key[:8])
	_ = os.WriteFile("output.elf", out, 0o755)
}

func PackBinaryBundle added in v0.67.0

func PackBinaryBundle(payloads []BundlePayload, opts BundleOptions) ([]byte, error)

PackBinaryBundle packs N payload binaries into a single multi-target bundle blob. The bundle is a flat byte slice in spec layout, with each payload XOR-encrypted under an independent random 16-byte key. The runtime stub-side fingerprint evaluator and PE/ELF container injection live in `pe/packer/stubgen/stage1` and `pe/packer/transform` respectively (see [Limitations] for which pieces are shipping).

Returns the serialised bundle bytes. The caller is responsible for wrapping the bundle in a PE/ELF container — see PackBinary for the single-payload equivalent and the spec's §5 Stub Flow for the eventual multi-payload entry point.

Errors: ErrEmptyBundle, ErrBundleTooLarge, plus crypto/rand failures wrapping when FixedKey is nil.

func PackChainedProxyDLL added in v0.127.0

func PackChainedProxyDLL(input []byte, opts ChainedProxyDLLOptions) (proxy, payload, key []byte, err error)

PackChainedProxyDLL emits the **two-file DLL sideloading bundle** (Path A from docs/refactor-2026-doc/packer-exe-to-dll-plan.md):

  1. The EXE input is packed via PackBinary with ConvertEXEtoDLL=true → a payload DLL that runs the original EXE entry point on DLL_PROCESS_ATTACH.
  2. A separate proxy DLL is emitted via github.com/oioio-space/maldev/pe/dllproxy.GenerateExt mirroring the target legitimate DLL's exports + carrying a LoadLibraryA(opts.PayloadDLLName) call in its tiny DllMain.

Drop {proxy DLL, payload DLL} side-by-side in the victim's app directory. The host EXE LoadLibrary's the proxy (named like the legit target — e.g. version.dll); the proxy's DllMain LoadLibraryA's the payload, which decrypts and spawns a thread at the original EXE's entry. The proxy then forwards every export call back to the real target via the perfect-dll-proxy absolute path scheme.

Returns (proxyDLLBytes, payloadDLLBytes, key, err). Write each to disk under the right filename and ship both.

Operational drawback (vs. the future fused Path B in slice 6): two-file drop + the proxy DLL has an IAT entry on kernel32!LoadLibraryA — a detectable IOC for kits that fingerprint proxy DLLs by their import set.

func PackProxyDLL added in v0.129.0

func PackProxyDLL(input []byte, opts ProxyDLLOptions) (proxy, key []byte, err error)

PackProxyDLL emits the **single-file fused proxy** (Path B from docs/refactor-2026-doc/packer-exe-to-dll-plan.md slice 6). One PE that:

  1. Has IMAGE_FILE_DLL set + an export table mirroring the legitimate target's exports (each forwarded via the perfect-dll-proxy absolute path scheme by default).
  2. Carries the original EXE input encrypted in .text plus a DllMain stub appended as a new section. On DLL_PROCESS_ATTACH the stub decrypts .text once, resolves `kernel32!CreateThread` via PEB walk (no IAT entry on LoadLibraryA — that's the win over the chained Path A), spawns a thread on the original OEP, returns TRUE.

Drop the result under the legitimate target's filename next to a host EXE that imports from it. The host LoadLibrary's the proxy; DllMain runs the payload + every exported call is forwarded back to the real target.

Returns (proxyDLLBytes, key, err). Single drop, no LoadLibraryA IOC in the IAT (CreateThread resolved at runtime via PEB walk).

**OPSEC trade-off vs. PackChainedProxyDLL:** single-file drop + cleaner IAT (no LoadLibraryA), but the resulting DLL is bigger (carries both the encrypted EXE payload AND the export table forwarders).

func PackProxyDLLFromTarget added in v0.132.0

func PackProxyDLLFromTarget(payload, targetDLLBytes []byte, opts ProxyDLLOptions) (proxy, key []byte, err error)

PackProxyDLLFromTarget is a convenience wrapper around PackProxyDLL that infers the export list from a real target DLL supplied as bytes. The caller still owns [ProxyDLLOptions.TargetName] (the on-disk filename the proxy will impersonate) because the PE itself does not carry a reliable canonical name string.

Named exports are kept verbatim (Name + Ordinal). Ordinal-only entries are skipped — pe/dllproxy.Generate would forward them via "#N" strings, but the converted-DLL fused emitter currently constructs forwarder strings only from explicit names, so an ordinal-only loader call into the proxy would miss the table. Operators wanting ordinal coverage should call PackProxyDLL directly with a manually-built dllproxy.Export slice.

Returns the same (proxy, key) pair as PackProxyDLL. Errors when the target has no named exports or [ProxyDLLOptions.TargetName] is blank.

func PackShellcode added in v0.81.0

func PackShellcode(shellcode []byte, opts PackShellcodeOptions) ([]byte, []byte, error)

PackShellcode wraps `shellcode` in a minimal host PE/ELF and returns the runnable bytes. When opts.Encrypt is true, the result is also passed through PackBinary for stub-driven decryption.

Returns the binary bytes + the AEAD key (only non-nil when Encrypt is true and the operator did not supply opts.Key) + error.

Sentinels:

Example

ExamplePackShellcode shows the canonical operator flow for turning raw position-independent shellcode (msfvenom output, hand-rolled stage-1) into a runnable PE32+ or ELF64 binary.

Two modes:

  • Plain: smallest output (~400 B for 16-byte shellcode), no decryption stub. The shellcode bytes sit at the entry point in cleartext — trivially YARA-able. Use when stealth isn't the concern or the shellcode is pre-encrypted upstream.
  • Encrypted: ~8 KiB output, polymorphic SGN-style stub at the entry point decrypts the shellcode in place and JMPs to it. Same envelope the rest of the packer uses.

On Linux the encrypted path is end-to-end VM-validated via TestPackShellcode_E2E_EncryptedELFExits42.

package main

import (
	"fmt"
	"os"
	"time"

	"github.com/oioio-space/maldev/pe/packer"
)

func main() {
	// 17-byte Linux x86-64 exit_group(42).
	sc := []byte{
		0x48, 0xc7, 0xc0, 0xe7, 0x00, 0x00, 0x00, // mov rax, 231
		0x48, 0xc7, 0xc7, 0x2a, 0x00, 0x00, 0x00, // mov rdi, 42
		0x0f, 0x05, // syscall
	}

	// Plain wrap — runnable, shellcode at e_entry in cleartext.
	plain, _, err := packer.PackShellcode(sc, packer.PackShellcodeOptions{
		Format: packer.FormatLinuxELF,
	})
	if err != nil {
		return
	}
	_ = os.WriteFile("plain.elf", plain, 0o755)

	// Encrypted wrap — stub envelope, AEAD key returned for
	// out-of-band logging; the binary itself self-decrypts at run
	// time with the seed derived per pack.
	enc, key, err := packer.PackShellcode(sc, packer.PackShellcodeOptions{
		Format:       packer.FormatLinuxELF,
		Encrypt:      true,
		Stage1Rounds: 3,
		Seed:         time.Now().UnixNano(),
	})
	if err != nil {
		return
	}
	_ = os.WriteFile("enc.elf", enc, 0o755)
	fmt.Printf("plain=%d enc=%d key=%x...\n", len(plain), len(enc), key[:4])
}

func SelectPayload added in v0.67.0

func SelectPayload(bundle []byte, hostVendor [12]byte, hostBuild uint32) (int, error)

SelectPayload is the pure-Go reference implementation of the bundle stub's fingerprint-matching logic. Given a bundle blob and the host's CPUID vendor + Windows build number, it returns the index of the first FingerprintEntry whose predicate matches, or -1 if none does.

Matching logic per spec §3.4:

  • PT_MATCH_ALL (bit 3): always matches.
  • Otherwise, every set bit in PredicateType must pass:
  • PT_CPUID_VENDOR: VendorString == hostVendor (or all-zero wildcard)
  • PT_WIN_BUILD: BuildMin <= hostBuild <= BuildMax (zero on either bound means no bound on that side)
  • PT_CPUID_FEATURES: not consulted by SelectPayload — caller would supply the feature ECX value separately; deferred until needed.
  • Negate flag inverts the entire entry's match outcome.

On no match, the caller applies FallbackBehaviour from the header.

The runtime stub-side asm evaluator (in `pe/packer/stubgen/stage1`) mirrors this logic byte-for-byte (excepting the feature-mask branch not yet wired in either path).

func SelectPayloadWith added in v0.73.0

func SelectPayloadWith(bundle []byte, profile BundleProfile, hostVendor [12]byte, hostBuild uint32) (int, error)

SelectPayloadWith is the per-build-profile-aware variant of SelectPayload. Same matching semantics; only the magic-validation gate differs.

func Unpack

func Unpack(packed, key []byte) ([]byte, error)

Unpack reverses Pack given the original AEAD key. Returns the original `data` bytes the caller passed to Pack.

Sentinels: ErrBadMagic, ErrShortBlob, ErrUnsupportedVersion, ErrUnsupportedCipher, ErrUnsupportedCompressor, ErrPayloadSizeMismatch, plus the AEAD's own decryption errors when the key is wrong or the ciphertext was tampered with.

func UnpackBundle added in v0.67.0

func UnpackBundle(bundle []byte, idx int) ([]byte, error)

UnpackBundle is the host-side inverse of PackBinaryBundle: it parses a bundle blob, locates the payload at index `idx`, and decrypts it using the on-disk key.

This is a debugging / build-host helper. The runtime stub re-implements the same logic in asm and never exposes keys to memory unless its predicate matched.

func UnpackBundleWith added in v0.73.0

func UnpackBundleWith(bundle []byte, idx int, profile BundleProfile) ([]byte, error)

UnpackBundleWith is the per-build-profile-aware variant of UnpackBundle.

func UnpackPipeline added in v0.52.0

func UnpackPipeline(packed []byte, keys PipelineKeys) ([]byte, error)

UnpackPipeline reverses PackPipeline given the per-step keys returned by Pack.

func UnwrapShellcode added in v0.82.0

func UnwrapShellcode(wrapped []byte) ([]byte, error)

UnwrapShellcode is the symmetric reverse of PackShellcode for the PLAIN-wrap path (Encrypt=false). Given a runnable PE32+ or ELF64 produced by PackShellcode, it returns the raw shellcode bytes that sit at the entry point.

Defender utility: lets cmd/packerscope and analysts extract the shellcode payload from a minimal-host-wrapped binary without running it. Symmetric to transform.BuildMinimalPE32Plus / transform.BuildMinimalELF64WithSections:

exe, _, _ := packer.PackShellcode(sc, packer.PackShellcodeOptions{
    Format: packer.FormatLinuxELF,
})
got, _ := packer.UnwrapShellcode(exe)   // got == sc

Encrypted-wrap binaries (Encrypt=true) cannot be unwrapped here — the shellcode bytes are ciphertext at the entry point and the SGN-style stub does the runtime decryption with a per-pack key baked into the stub. Operators wanting that path use the symmetric `cmd/packerscope` flow with the AEAD key.

Returns:

  • shellcode bytes (slice into a copy — safe to retain after `wrapped` is freed)
  • ErrNotMinimalWrap when the input doesn't look like a PackShellcode plain output (size mismatch, magic mismatch, no .text section, or .text != entry-point body).
  • ErrUnsupportedFormat when the input is neither PE32+ nor ELF64.

func ValidateELF added in v0.57.0

func ValidateELF(elf []byte) error

ValidateELF returns nil when elf is a Go static-PIE binary the Linux runtime can load, or an error explaining the rejection reason. Operators should call this at pack time to catch unsupported inputs before deploy.

Thin wrapper around elfgate.CheckELFLoadable; lives on the packer package so CLI / SDK callers don't need to import an internal sub-package.

func WrapBundleAsExecutableLinux added in v0.69.0

func WrapBundleAsExecutableLinux(bundle []byte) ([]byte, error)

WrapBundleAsExecutableLinux composes a runnable Linux x86-64 ELF from a bundle blob. Layout:

[ELF Ehdr (64 B) | PT_LOAD Phdr (56 B) | stub asm (~160 B) | bundle blob]

Steps:

  1. Emit the vendor-aware stub (PIC trampoline + CPUID prologue + fingerprint scan loop + per-entry vendor compare + XOR decrypt + JMP to payload).
  2. Splice random Intel multi-byte NOPs at slot A (between PIC and CPUID prologue) for per-pack stub polymorphism — see [injectStubJunk].
  3. Patch the stub's `add r15, BUNDLE_OFF` immediate with the byte distance from the .pic label (5 bytes into the stub) to the bundle's first byte. Equivalent to `len(stub) - 5`.
  4. Concatenate stub + bundle.
  5. Wrap in transform.BuildMinimalELF64WithVaddr, using `profile.Vaddr` when set.

The result is a self-contained ELF — no PT_INTERP, no DT_NEEDED, no imports. The kernel maps it RWX and jumps to entry; the stub resolves the bundle base via call/pop PIC, walks the FingerprintEntry table dispatching on PT_MATCH_ALL or PT_CPUID_VENDOR with a 12-byte CPUID compare (all-zero VendorString = wildcard), XOR-decrypts the matched payload's data in place, and JMPs to it. The decrypted bytes must therefore be raw position-independent shellcode (NOT a packed PE/ELF — those need the cmd/bundle-launcher reflective path).

Today's gap: the asm evaluator does not honour the Negate flag yet (Go-side SelectPayload does); a future minor closes that. PT_WIN_BUILD is also Linux-stub-skipped since hostWinBuild=0 there.

func WrapBundleAsExecutableLinuxWith added in v0.74.0

func WrapBundleAsExecutableLinuxWith(bundle []byte, profile BundleProfile) ([]byte, error)

WrapBundleAsExecutableLinuxWith is the per-build-profile-aware variant of WrapBundleAsExecutableLinux. Validates the supplied bundle's magic against `profile.Magic` (canonical default when zero) before wrapping. The bundle stub asm itself reads only header offsets — count, fpTable, plTable — and is magic-agnostic, so per-build magic bytes pass through transparently.

Polymorphism: each call splices a fresh batch of Intel multi-byte NOPs into the stub (between the PIC trampoline and the CPUID prologue) so two packs of the same bundle produce distinct stub byte sequences — yara writers cannot signature the 160-byte stub across packs. The seed is drawn from crypto/rand. For deterministic pack output (testing, reproducible builds) use WrapBundleAsExecutableLinuxWithSeed.

func WrapBundleAsExecutableLinuxWithSeed added in v0.74.0

func WrapBundleAsExecutableLinuxWithSeed(bundle []byte, profile BundleProfile, seed int64) ([]byte, error)

WrapBundleAsExecutableLinuxWithSeed is the deterministic variant of WrapBundleAsExecutableLinuxWith: same seed → same stub junk pattern → byte-identical wrapped output (modulo the random per- payload XOR keys, which the caller controls via BundleOptions.FixedKey upstream). Use seed=0 for the canonical junk-free shape.

func WrapBundleAsExecutableWindows added in v0.85.0

func WrapBundleAsExecutableWindows(bundle []byte) ([]byte, error)

WrapBundleAsExecutableWindows composes a runnable Windows x86-64 PE32+ from a bundle blob. Windows symmetry of WrapBundleAsExecutableLinux.

Uses the V2NW Builder-driven scan stub ([bundleStubV2NegateWinBuildWindows], v0.88.0+) — honours PT_MATCH_ALL, PT_CPUID_VENDOR, PT_WIN_BUILD (via PEB.OSBuildNumber read), PT_CPUID_FEATURES, and the FingerprintPredicate.Negate flag. On no match, calls ntdll!RtlExitUserProcess(0) via the §2 PEB-walk primitive — silent clean exit, equivalent to BundleFallbackExit.

func WrapBundleAsExecutableWindowsWith added in v0.85.0

func WrapBundleAsExecutableWindowsWith(bundle []byte, profile BundleProfile) ([]byte, error)

WrapBundleAsExecutableWindowsWith is the per-build-profile-aware variant. Validates the bundle's magic against profile.Magic (canonical default when zero) before wrapping.

Per-build ImageBase derivation from profile.Vaddr is queued for PHASE B — today the canonical transform.MinimalPE32PlusImageBase (0x140000000) is used regardless of profile contents.

func WrapBundleAsExecutableWindowsWithSeed added in v0.85.0

func WrapBundleAsExecutableWindowsWithSeed(bundle []byte, profile BundleProfile, seed int64) ([]byte, error)

WrapBundleAsExecutableWindowsWithSeed is the deterministic variant. Same seed → same stub junk pattern → byte-identical wrapped output (modulo the random per-payload XOR keys from the upstream bundle pack). Use seed=0 for the canonical junk-free shape.

Types

type BundleEntryInfo added in v0.67.0

type BundleEntryInfo struct {
	// Fingerprint side.
	PredicateType     uint8
	Negate            bool
	VendorString      [12]byte
	BuildMin          uint32
	BuildMax          uint32
	CPUIDFeatureMask  uint32
	CPUIDFeatureValue uint32

	// Payload side.
	DataRVA       uint32
	DataSize      uint32
	PlaintextSize uint32
	CipherType    uint8
	Key           [16]byte
}

BundleEntryInfo is one parsed FingerprintEntry + PayloadEntry pair. Wire fields are decoded into typed Go fields; unrecognised PredicateType bits are preserved verbatim so callers can flag them.

type BundleFallbackBehaviour added in v0.67.0

type BundleFallbackBehaviour uint32

BundleFallbackBehaviour controls what the stub does when no FingerprintEntry matches the host.

const (
	// BundleFallbackExit silently calls ExitProcess(0) / exit(0). Default.
	BundleFallbackExit BundleFallbackBehaviour = 0
	// BundleFallbackCrash deliberately faults to surface a sandbox alert.
	BundleFallbackCrash BundleFallbackBehaviour = 1
	// BundleFallbackFirst selects payload 0 unconditionally. Operator
	// opt-in for dev/test only — defeats the per-host secrecy property.
	BundleFallbackFirst BundleFallbackBehaviour = 2
)

type BundleInfo added in v0.67.0

type BundleInfo struct {
	Magic              uint32
	Version            uint16
	Count              uint16
	FpTableOffset      uint32
	PayloadTableOffset uint32
	DataOffset         uint32
	FallbackBehaviour  BundleFallbackBehaviour
	Entries            []BundleEntryInfo
}

BundleInfo is the parsed-header view of a bundle blob, populated by InspectBundle. Fields mirror the spec §3 wire-format regions: a fixed BundleHeader followed by per-entry FingerprintEntry + PayloadEntry slices in matching order.

All offsets are RVAs from the start of the bundle blob. Sizes are measured in bytes. The Entries slice always has len(Entries) == Count.

func InspectBundle added in v0.67.0

func InspectBundle(bundle []byte) (BundleInfo, error)

InspectBundle parses a bundle blob's header and per-entry tables into a BundleInfo for inspection. It is the structured-output companion to the human-readable `cmd/packer bundle -inspect` flow and the preferred entrypoint for test assertions over the wire format.

Validates: magic, header length, that the declared region offsets stay inside the blob, and that each PayloadEntry's data range stays inside the blob. On any structural error it returns a wrapped error; callers can compare against ErrBundleTruncated / ErrBundleBadMagic / ErrBundleOutOfRange to differentiate.

func InspectBundleWith added in v0.73.0

func InspectBundleWith(bundle []byte, profile BundleProfile) (BundleInfo, error)

InspectBundleWith is the per-build-profile-aware variant of InspectBundle. Validates the magic against `profile.Magic` (canonical default when zero) instead of BundleMagic.

type BundleOptions added in v0.67.0

type BundleOptions struct {
	// FallbackBehaviour selects the action when no predicate matches.
	FallbackBehaviour BundleFallbackBehaviour
	// FixedKey, when non-nil, is the per-payload XOR key reused across
	// every payload — defeats the per-payload-secrecy property the spec
	// advertises and exists strictly for test determinism / reproducible
	// pack output. Production callers MUST leave this nil so each
	// payload gets a fresh random 16-byte key. Field is named to make
	// the call site self-explain its intent.
	FixedKey []byte
	// Profile carries the per-build IOC overrides (BundleMagic +
	// AppendBundle footer). Zero value = canonical wire-format
	// magics. Use [DeriveBundleProfile] to derive both from a
	// per-deployment secret string. Operators MUST set a fresh
	// secret per ship cycle to keep yara signatures from clustering
	// across deployments — Kerckhoffs in practice.
	Profile BundleProfile
}

BundleOptions parameterises PackBinaryBundle.

type BundlePayload added in v0.67.0

type BundlePayload struct {
	// Binary is the original PE/ELF bytes to embed.
	Binary []byte
	// Fingerprint is the host-matching rule for this payload.
	Fingerprint FingerprintPredicate
	// CipherType selects the encrypt-then-decrypt algorithm for
	// THIS payload. Zero value (and 1) → CipherTypeXORRolling (the
	// pre-#2.2 default); 2 → CipherTypeAESCTR. Mixing types within
	// one bundle is supported — each PayloadEntry carries its own
	// type byte so the stub dispatches per-entry.
	CipherType uint8
	// Key, when non-nil + 16 bytes, is the operator-supplied
	// encryption key for this payload. nil = pack-time generates a
	// fresh crypto-random 16-byte key (the default — preserves the
	// per-payload-secrecy property). Operator-supplied keys enable:
	//   - Reproducible packs across machines / runs (the same Key
	//     + Binary + IV always produces the same ciphertext for
	//     XOR-rolling; AES-CTR still differs due to random IV, but
	//     [crypto.ExpandAESKey] output stays identical).
	//   - HKDF-from-deployment-secret workflows where operators
	//     derive keys outside the pack pipeline.
	// Length MUST be exactly 16 — both CipherTypeXORRolling and
	// CipherTypeAESCTR use 16-byte keys. Rejected with
	// [ErrBundleBadKeyLen] otherwise. Mutually exclusive with
	// [BundleOptions.FixedKey] (the test-determinism mode):
	// FixedKey forces all payloads to share one key, BundlePayload.Key
	// is per-payload. If both are set, BundleOptions.FixedKey wins
	// (matching the pre-#2.4 behaviour).
	Key []byte
}

BundlePayload is one payload binary paired with its fingerprint predicate and the per-payload pack options.

type BundleProfile added in v0.73.0

type BundleProfile struct {
	Magic       uint32
	Version     uint16
	FooterMagic [8]byte

	// Vaddr is the per-build virtual base address the all-asm wrap
	// path's lone PT_LOAD lands at — randomises the canonical
	// 0x400000 yara surface ('tiny ELF at standard ld base'). Zero
	// = canonical [transform.MinimalELF64Vaddr]. Page-aligned
	// (4 KiB) under 0x800000_00000000 (kernel half).
	Vaddr uint64
}

BundleProfile groups the per-build IOCs an operator can override to randomise yara-able byte patterns across deployments. Per Kerckhoffs's principle: the wire format stays public; only the 4-byte Magic, the 2-byte Version, and the 8-byte AppendBundle FooterMagic are the per-build secrets. A defender can identify "this is a maldev bundle" only with the operator's secret in hand.

Use DeriveBundleProfile to get a deterministic profile from any secret string; all fields zero means "use the canonical bytes from the wire-format spec" (back-compat default).

func DeriveBundleProfile added in v0.73.0

func DeriveBundleProfile(secret []byte) BundleProfile

DeriveBundleProfile returns a BundleProfile derived from secret via HKDF-SHA256 (RFC 5869). Same secret → same profile. Empty / nil secret yields the canonical {BundleMagic, BundleFooterMagic} pair so a build with no -secret flag is wire-compatible with the public spec.

Why HKDF instead of plain SHA-256 slicing (changed in v0.83.0):

  • Each derived field gets its own HMAC-keyed expansion via a per-purpose label ("magic", "footer", "version", "vaddr"). Flipping bits in one field gives an attacker no algebraic handle on the other fields — they are statistically independent rather than slices of the same hash.
  • Standard practice. TLS 1.3, Signal, Noise all use HKDF for subkey derivation; defenders auditing this file recognise the construction immediately.

Wire-format consequence: bundles produced by v0.82.0 or earlier are NOT compatible with v0.83.0+ when a non-empty secret is set (the derived Magic / FooterMagic / Vaddr bytes differ). Operators re-pack their fleets at the migration boundary. The canonical (empty-secret) wire format is unchanged and remains the fallback.

A 16+ byte secret is recommended (operator's per-deployment GUID, build timestamp + nonce, etc.).

type ChainedProxyDLLOptions added in v0.127.0

type ChainedProxyDLLOptions struct {
	// PackOpts tunes the inner [PackBinary] call that converts the
	// EXE input into a payload DLL. ConvertEXEtoDLL is FORCED to
	// true regardless of the caller-supplied value (operator
	// intent is unambiguous when calling this entry point).
	PackOpts PackBinaryOptions

	// TargetName is the legitimate DLL name whose exports the
	// proxy mirrors (e.g. "version" for version.dll).
	TargetName string

	// Exports is the list of exports to forward back to the
	// legitimate target. Use [pe/parse.Exports] to extract them
	// from a real DLL on the operator host.
	Exports []dllproxy.Export

	// PayloadDLLName is the filename the proxy will pass to
	// LoadLibraryA on DLL_PROCESS_ATTACH. Defaults to
	// "payload.dll" when empty. Drop the proxy + payload DLLs
	// in the same directory under the file names this resolves
	// to and {TargetName.dll, PayloadDLLName} both load.
	PayloadDLLName string

	// ProxyOpts forwards additional dllproxy.Options knobs
	// (PathScheme, DOSStub, PatchCheckSum). Machine + PayloadDLL
	// are overridden by this entry point — operator-supplied
	// values for those are ignored.
	ProxyOpts dllproxy.Options
}

ChainedProxyDLLOptions parameterises PackChainedProxyDLL.

type Cipher

type Cipher uint8

Cipher selects the AEAD primitive used to encrypt the payload. AESGCM is the modern default; ChaCha20 wins on hosts without AES-NI; RC4 is legacy / shellcode-loader compatible only.

const (
	CipherAESGCM   Cipher = 0
	CipherChaCha20 Cipher = 1
	CipherRC4      Cipher = 2
)

func (Cipher) String

func (c Cipher) String() string

String returns the canonical lowercase cipher name.

type Compressor

type Compressor uint8

Compressor selects the compression pass run BEFORE encryption.

const (
	CompressorNone  Compressor = 0
	CompressorAPLib Compressor = 1 // reserved; not yet implemented
	CompressorLZMA  Compressor = 2 // reserved; not yet implemented
	CompressorZstd  Compressor = 3 // reserved; not yet implemented
	CompressorLZ4   Compressor = 4 // reserved; not yet implemented
	CompressorFlate Compressor = 5 // raw DEFLATE (compress/flate)
	CompressorGzip  Compressor = 6 // gzip-framed DEFLATE (compress/gzip)
)

func (Compressor) String

func (c Compressor) String() string

String returns the canonical lowercase compressor name.

type CoverOptions added in v0.61.1

type CoverOptions struct {
	// JunkSections is the ordered list of sections to append.
	// Each section contributes Size bytes of file growth (rounded
	// to FileAlignment).
	JunkSections []JunkSection

	// FakeImports is the list of DLL+function tuples to add as fake
	// IMAGE_IMPORT_DESCRIPTOR entries (PE only; ignored for ELF).
	// When non-nil, AddCoverPE chains AddFakeImportsPE after appending
	// junk sections. Every DLL name and function name must be a real
	// export on the target Windows version — the kernel rejects
	// unresolvable imports at load time.
	// Leave nil (the zero value) to skip the fake-imports step.
	FakeImports []FakeImport
}

CoverOptions bundles the cover-layer configuration.

func DefaultCoverOptions added in v0.61.1

func DefaultCoverOptions(seed int64) CoverOptions

DefaultCoverOptions returns a 3-section CoverOptions tuned for general-purpose anti-static-analysis cover. Seed controls the (deterministic) name + size + fill choice — operators pass time- or PID-derived seeds in production for per-build variance.

The defaults aim at a "looks like a normal compiled binary" histogram: one high-entropy section (~8 KiB, named after a common PE/ELF resource section), one machine-code-shaped section (~4 KiB, JunkFillPattern), one zero-padding section (~16 KiB, JunkFillZero). Total static surface increase ~28 KiB — small enough not to bloat the binary noticeably, large enough to defeat fingerprints that match on exact section counts and offsets.

Section names cycle through a pool of legitimate-looking candidates: ".rsrc", ".rdata2", ".pdata", ".tls", ".reloc2". Names are PE-specific; AddCoverELF ignores the Name field.

type EntropyCover added in v0.54.0

type EntropyCover uint8

EntropyCover enumerates the algorithms an OpEntropyCover step can pick. Algo numbers are wire-stable.

const (
	// EntropyCoverInterleave splits the input into fixed-size
	// chunks and inserts low-entropy padding between them. Drops
	// real Shannon entropy in proportion to padding ratio.
	EntropyCoverInterleave EntropyCover = 0

	// EntropyCoverCarrier prepends a PNG-shaped 32-byte header.
	// Doesn't change the bulk entropy but defeats heuristics that
	// flag "first 16 bytes look random" (common in droppers).
	EntropyCoverCarrier EntropyCover = 1

	// EntropyCoverHexAlphabet expands each byte to two bytes drawn
	// from a 16-element code-like alphabet. 2× size; apparent
	// histogram entropy drops to ~3-4 bits/byte. Real information
	// content is unchanged — useful only against histogram
	// scanners, not real cryptanalysis.
	EntropyCoverHexAlphabet EntropyCover = 2
)

func (EntropyCover) String added in v0.54.0

func (e EntropyCover) String() string

String returns the canonical lowercase cover name.

type FakeImport added in v0.63.0

type FakeImport struct {
	DLL       string   // e.g. "kernel32.dll"
	Functions []string // e.g. ["Sleep", "GetCurrentThreadId"]
}

FakeImport describes one DLL and its function list to add as a fake import entry. The DLL name and function names must be real exports on the target Windows version — the kernel rejects any name that cannot be resolved at load time.

type FingerprintPredicate added in v0.67.0

type FingerprintPredicate struct {
	PredicateType uint8

	// VendorString is the 12-byte CPUID vendor to match. Zero/empty means
	// wildcard (any vendor). Only consulted when PTCPUIDVendor is set.
	VendorString [12]byte

	// BuildMin and BuildMax form an inclusive Windows build-number range.
	// Zero on either end means "no bound on this side". Only consulted
	// when PTWinBuild is set.
	BuildMin uint32
	BuildMax uint32

	// CPUIDFeatureMask + CPUIDFeatureValue check
	// (CPUID[1].ECX & Mask) == Value. Mask=0 skips the check.
	CPUIDFeatureMask  uint32
	CPUIDFeatureValue uint32

	// Negate inverts the entire predicate match outcome.
	Negate bool
}

FingerprintPredicate encodes the host-matching logic for one payload.

PredicateType is a bitmask of PT* constants. Within one predicate all enabled checks are ANDed; across predicates the bundle stub picks the first matching entry.

type Format added in v0.59.0

type Format uint8

Format selects the host binary shape PackBinary emits.

const (
	FormatUnknown    Format = iota // zero value; rejected by PackBinary
	FormatWindowsExe               // Phase 1e (v0.61.x): PE32+ Windows executable
	FormatLinuxELF                 // Phase 1e (v0.61.x): ELF64 Linux static-PIE
	// FormatWindowsDLL — Phase 2-F-3-c follow-up (scoped in
	// docs/refactor-2026-doc/packer-dll-format-plan.md). The
	// Format constant is wired through here so PlanPE's DLL
	// rejection can route to the correct error message, but
	// the actual DLL stub implementation is a separate slice.
	// Selecting this format today still produces ErrIsDLL until
	// the stub work lands.
	FormatWindowsDLL
)

func (Format) String added in v0.59.0

func (f Format) String() string

String returns the canonical lowercase format name.

type JunkFill added in v0.61.1

type JunkFill uint8

JunkFill chooses how a junk section's body is generated. Each strategy aims at a different family of static-analysis heuristic.

const (
	// JunkFillRandom fills the section with cryptographic-quality
	// random bytes. Maxes out per-byte entropy (~8.0 bits) — good
	// for raising the file's average entropy and for hiding among
	// genuinely-encrypted .text sections of other packers. Bad if
	// the analyst flags "high-entropy non-RX section" as the
	// signal: the section reads RX-clear and shows up immediately.
	JunkFillRandom JunkFill = iota

	// JunkFillZero fills the section with zeros. Lowest-entropy
	// option; useful when stretching SizeOfImage without raising
	// the entropy curve (e.g., to push past a YARA rule that
	// triggers above a percentage threshold).
	JunkFillZero

	// JunkFillPattern fills the section with a repeating
	// frequency-ordered byte pattern that mimics machine code:
	// 0x00 (call/jmp displacement), 0x48 (REX.W), 0xC3 (RET),
	// 0xCC (INT3), 0x90 (NOP), 0xFF (CALL/JMP near opcode),
	// 0xE8 (CALL rel32), 0x55 (PUSH RBP). Result entropy ~3 bits
	// — looks like genuine .text under a casual entropy plot.
	JunkFillPattern
)

type JunkSection added in v0.61.1

type JunkSection struct {
	// Name is the 8-byte section name (truncated / NUL-padded).
	// Common cover names: `.rsrc`, `.rdata2`, `.pdata`, `.tls`.
	// Empty string defaults to `.rdata`.
	Name string

	// Size is the virtual+raw size of the section in bytes. Will
	// be rounded up to FileAlignment / SectionAlignment by the
	// PE/ELF emitter. Operator chooses based on how much padding
	// they want to add to the file.
	Size uint32

	// Fill picks the byte-pattern strategy. See [JunkFill].
	Fill JunkFill
}

JunkSection describes one cover section to append.

type Options

type Options struct {
	// Cipher selects the AEAD primitive. Only [CipherAESGCM] is
	// implemented today; [CipherChaCha20] and [CipherRC4] are
	// reserved constants and return [ErrUnsupportedCipher].
	Cipher Cipher

	// Compressor selects the compression pass run BEFORE
	// encryption. Only [CompressorNone] is implemented today;
	// other constants return [ErrUnsupportedCompressor].
	Compressor Compressor

	// Key, when non-nil, is the AEAD key. When nil, [Pack]
	// generates 32 random bytes via crypto.NewAESKey and
	// returns them as the second return value.
	Key []byte
}

Options tunes Pack. The zero value selects sensible defaults (AES-GCM, no compression, freshly-generated key).

type PackBinaryOptions added in v0.59.0

type PackBinaryOptions struct {
	// Format, when non-zero, is cross-checked against the magic bytes of
	// the input. FormatUnknown (zero) skips the cross-check and relies on
	// auto-detection.
	Format Format

	// Stage1Rounds is the number of SGN encoding rounds applied to the
	// encrypted .text section. Defaults to 3 when zero. Valid range: 1..10.
	Stage1Rounds int

	// Seed drives the poly engine. Zero means crypto-random.
	Seed int64

	// Key, when non-nil, is used as the XOR key for .text encryption.
	// When nil a fresh 32-byte key is generated.
	Key []byte

	// AntiDebug, when true, prepends a ~70-byte anti-debug prologue to the
	// Windows PE stub: three checks (PEB.BeingDebugged, PEB.NtGlobalFlag
	// mask 0x70, RDTSC delta around CPUID with threshold 1000 cycles).
	// Positive detection exits via RET — ntdll!RtlUserThreadStart's epilogue
	// calls ExitProcess(0), so the process exits cleanly without revealing
	// any SGN-decoded bytes. Default false (conservative). ELF stubs ignore
	// this flag.
	AntiDebug bool

	// Compress, when true, LZ4-compresses the .text section before SGN
	// encoding. The stub gains a 22-byte register-setup sequence plus the
	// 136-byte LZ4 block inflate decoder between the last SGN round and the
	// OEP JMP. Typical size reduction: 40–60 % for Go binaries. The packed
	// binary is self-contained — no external decompressor is needed at
	// runtime. Default false (conservative). See [stubgen.Options.Compress]
	// for the full in-place inflate layout.
	Compress bool

	// ConvertEXEtoDLL, when true, converts a PE32+ EXE input into a
	// PE32+ DLL output at pack time. Mutually exclusive with
	// FormatWindowsDLL. Rejected with [ErrUnsupportedFormat] when
	// the input isn't a PE32+ EXE.
	//
	// Operationally unlocks sideloading (drop the converted DLL
	// next to a signed legit EXE that LoadLibrary's it), classic
	// DLL injection, and LOLBAS rundll32 / regsvr32 chains.
	//
	// Slice 5 of docs/refactor-2026-doc/packer-exe-to-dll-plan.md.
	ConvertEXEtoDLL bool

	// DiagSkipConvertedPayload is a slice-5.5.y diagnostic flag.
	// When true alongside ConvertEXEtoDLL, the converted-DLL stub
	// omits SGN rounds + kernel32-resolver + CreateThread call —
	// emits only prologue + flag latch + return TRUE. Used to
	// bisect which stage causes ERROR_DLL_INIT_FAILED at LoadLibrary
	// time. Production code MUST leave this false.
	DiagSkipConvertedPayload bool

	// DiagSkipConvertedResolver and DiagSkipConvertedSpawn are the
	// finer-grained slice-5.5.y bisection flags forwarded to
	// [stubgen.Options]. Production code MUST leave both false.
	DiagSkipConvertedResolver bool
	DiagSkipConvertedSpawn    bool

	// ConvertEXEtoDLLDefaultArgs bakes a default command-line into
	// the converted-DLL stub. Ignored when ConvertEXEtoDLL is false.
	// Empty string preserves the prior behaviour where the payload
	// inherits the host process's GetCommandLineW result. See
	// [stubgen.Options.ConvertEXEtoDLLDefaultArgs] for the OPSEC
	// trade-off (the patch is permanent for the host process).
	ConvertEXEtoDLLDefaultArgs string

	// RandomizeStubSectionName, when true, names the appended PE
	// stub section with a fresh per-pack random label
	// (`.xxxxx\x00\x00`) instead of the hardcoded ".mldv". Defeats
	// YARA rules keyed on the literal default name. Default false
	// (conservative — packs reproducibly across runs).
	//
	// Phase 2-A of docs/refactor-2026-doc/packer-design.md.
	// PE only; ELF section names live in `.shstrtab` and aren't
	// load-relevant.
	RandomizeStubSectionName bool

	// RandomizeTimestamp, when true, overwrites the COFF File
	// Header's TimeDateStamp with a random epoch in the
	// `[now-5y, now]` window. Defeats temporal clustering by
	// threat-intel pivots that group samples by linker timestamp.
	// Per-pack uniqueness comes from a fresh-seeded RNG (seeded
	// from opts.Seed when non-zero, else crypto-random).
	//
	// Phase 2-B of docs/refactor-2026-doc/packer-design.md.
	// PE only — ELF doesn't carry an analogous build-timestamp
	// field the loader respects.
	RandomizeTimestamp bool

	// RandomizeLinkerVersion, when true, overwrites the Optional
	// Header's MajorLinkerVersion + MinorLinkerVersion bytes with
	// a random plausible MSVC pair (major ∈ [12, 15], minor ∈
	// [0, 99]). Defeats threat-intel pivots that cluster samples
	// by linker version ("all samples linked with VS2017 14.16").
	// Per-pack uniqueness comes from a fresh-seeded RNG.
	//
	// Phase 2-C of docs/refactor-2026-doc/packer-design.md.
	// PE only — ELF carries no analogous field.
	RandomizeLinkerVersion bool

	// RandomizeImageVersion, when true, overwrites the Optional
	// Header's MajorImageVersion + MinorImageVersion uint16
	// fields with a plausible "small in-house project" pair
	// (major ∈ [0, 9], minor ∈ [0, 99]). Defeats threat-intel
	// pivots that cluster samples by per-binary version stamp.
	// Per-pack uniqueness via fresh-seeded RNG.
	//
	// Phase 2-D of docs/refactor-2026-doc/packer-design.md.
	// PE only.
	RandomizeImageVersion bool

	// RandomizeExistingSectionNames, when true, overwrites every
	// section header's 8-byte Name slot with a fresh ".xxxxx\x00\x00"
	// label before the stub is appended. Section data, VAs, raw
	// offsets, sizes, characteristics, the DataDirectory, and the
	// relocation table are all untouched — Windows finds resources,
	// imports, exports, relocations via the Optional Header
	// DataDirectory (RVA-based), so renaming `.text` → `.xkqwz`
	// doesn't break the loader contract. Defeats name-pattern
	// heuristics ("section called .text is RWX — suspicious") and
	// YARA rules keyed on the host binary's original section labels.
	// Composes with RandomizeStubSectionName: the stub section is
	// appended *after* this rename so its name is controlled by
	// that flag.
	//
	// Phase 2-F-1 of docs/refactor-2026-doc/packer-design.md.
	// PE only.
	RandomizeExistingSectionNames bool

	// RandomizeJunkSections, when true, inserts a per-pack random
	// number of zero-byte "separator" sections between the host
	// PE's last existing section and the appended packer stub.
	// Each separator is uninitialised data (SizeOfRawData=0,
	// PointerToRawData=0, IMAGE_SCN_CNT_UNINITIALIZED_DATA |
	// IMAGE_SCN_MEM_READ) so the file size doesn't grow — only
	// SizeOfImage (the loader's RAM map) and NumberOfSections do.
	// Separators get random `.xxxxx` names; the stub's declared
	// VirtualAddress and OEP shift forward by count*SectionAlignment.
	//
	// Per-pack count drawn from [1, 5] using a fresh-seeded RNG
	// (deterministic given opts.Seed).
	//
	// Phase 2-F-2 of docs/refactor-2026-doc/packer-design.md.
	// Defeats heuristics keyed on "9 sections" or "stub is the
	// last header" patterns. PE only.
	RandomizeJunkSections bool

	// RandomizePEFileOrder, when true, permutes the FILE order of
	// host PE section bodies (not their VAs). PE/COFF allows the
	// file layout of section bodies to be in any order with
	// arbitrary FileAlignment-padded gaps; the loader maps each
	// section by its PointerToRawData / SizeOfRawData fields, not
	// by file ordering. So permuting the file order changes every
	// section body's on-disk offset without touching the runtime
	// image: VAs, relocations, the DataDirectory, OEP, and the
	// stub's RIP-relative addressing all stay byte-identical to a
	// vanilla pack.
	//
	// Defeats YARA rules anchored at file offsets ("file offset
	// 0x400 contains the decryption key bytes") with zero
	// loader-contract risk.
	//
	// The appended packer stub is exempt from the permutation
	// (skipLast=1), so the stub's file offset stays predictable
	// for any future stub-introspection work.
	//
	// Phase 2-F-3-b of docs/refactor-2026-doc/packer-design.md.
	// PE only.
	RandomizePEFileOrder bool

	// RandomizeImageBase, when true, overwrites the PE32+ Optional
	// Header's ImageBase (uint64 at +0x18) with a fresh random
	// value drawn from the canonical user-mode EXE range
	// `[0x140000000, 0x7FF000000000)` snapped to 64 KiB. Under
	// ASLR (which Go binaries enable by default via DYNAMIC_BASE),
	// the loader picks the actual load address regardless of this
	// value — so the only observable effect is a different
	// preferred-base byte sequence in the file image. Defeats
	// heuristics on canonical preferred-base values like the Go
	// linker's 0x140000000 default ("file's ImageBase = 0x140000000
	// → likely Go binary").
	//
	// Phase 2-F-3-c (lite) of docs/refactor-2026-doc/packer-design.md.
	// PE only.
	RandomizeImageBase bool

	// RandomizeImageVAShift, when true, shifts every section's
	// VirtualAddress forward by a random delta D = N×SectionAlignment
	// (N drawn from [1, 8] per pack, so D ∈ [4 KiB, 32 KiB] for the
	// PE32+ default 4 KiB SectionAlignment). The shift fixes up the
	// reloc table's absolute pointer values + each block's PageRVA,
	// every non-zero DataDirectory entry's RVA, the OEP, and
	// SizeOfImage. Section data is NOT moved — only metadata.
	//
	// Inter-section deltas are preserved, so RIP-relative
	// references between sections (which the linker bakes as raw
	// 32-bit displacements outside the reloc table) keep working
	// without re-encoding. This includes the SGN stub's reach into
	// .text, the central reason this transform exists in this
	// shape rather than per-section permutation.
	//
	// Defeats heuristics anchored at canonical VAs (".text starts
	// at VA 0x1000", "OEP is at 0x140001000"). Returns
	// [transform.ErrRelocsStripped] when the input PE has the
	// IMAGE_FILE_RELOCS_STRIPPED Characteristics bit set — such
	// images carry no relocation metadata and can't be safely
	// shifted; opt out for those binaries.
	//
	// Phase 2-F-3-c of docs/refactor-2026-doc/packer-design.md.
	// PE only.
	RandomizeImageVAShift bool

	// RandomizeAll, when true, ORs every individual Randomize*
	// flag above to true: stub section name, TimeDateStamp,
	// LinkerVersion, ImageVersion, existing section names. The
	// individual flags can still selectively turn additional
	// behaviour on; this is the "everything Phase 2 ships today"
	// shortcut.
	//
	// Phase 2-E of docs/refactor-2026-doc/packer-design.md.
	// PE only — opt-ins under the hood are PE-specific.
	RandomizeAll bool
}

PackBinaryOptions parameterizes PackBinary.

type PackShellcodeOptions added in v0.81.0

type PackShellcodeOptions struct {
	// Format selects the host binary format:
	//   - FormatWindowsExe: PE32+ AMD64
	//   - FormatLinuxELF:   ELF64 AMD64 ET_EXEC, RWX PT_LOAD, with
	//     SHT (so PackBinary can chew on it when Encrypt=true)
	// FormatUnknown is rejected; operators MUST pick a target OS.
	Format Format

	// Encrypt, when true, runs the wrapped host through [PackBinary]
	// for SGN-style stub-driven decryption. Default false (plain wrap).
	Encrypt bool

	// ImageBase / Vaddr override the canonical load address:
	//   - Windows: PE ImageBase (must be 64K aligned). Zero =
	//     transform.MinimalPE32PlusImageBase (0x140000000).
	//   - Linux: ELF PT_LOAD vaddr (must be page-aligned). Zero =
	//     transform.MinimalELF64Vaddr (0x400000).
	// Per-build-tunable values defeat 'tiny PE/ELF at standard base'
	// yara rules.
	ImageBase uint64

	// Stage1Rounds, Seed, Key, AntiDebug, Compress are forwarded to
	// [PackBinary] when Encrypt is true. Ignored otherwise. Field
	// types mirror [PackBinaryOptions] exactly.
	Stage1Rounds int
	Seed         int64
	Key          []byte
	AntiDebug    bool
	Compress     bool
}

PackShellcodeOptions configures PackShellcode.

type PadPattern added in v0.54.0

type PadPattern uint8

PadPattern selects the byte pattern EntropyCoverInterleave uses to fill its low-entropy padding spans. Mixed is the default — a deterministic interleave of NOP / int3 / zero that mimics aligned code-section padding.

const (
	PadPatternZeros    PadPattern = 0 // 0x00 only
	PadPatternInt3     PadPattern = 1 // 0xCC repeated (debug break, common in MSVC pad)
	PadPatternNOP      PadPattern = 2 // 0x90 repeated
	PadPatternMixedASM PadPattern = 3 // Cycle through a code-like alphabet
)

type Permutation added in v0.52.0

type Permutation uint8

Permutation enumerates the byte-permutation algorithms a `OpPermute` step can pick.

const (
	PermutationXOR        Permutation = 0 // XOR with repeating key
	PermutationArithShift Permutation = 1 // crypto.ArithShift (additive shift mod 256)
	PermutationSBox       Permutation = 2 // crypto.SubstituteBytes (key = 256+256-byte sbox+inverse pair)
)

func (Permutation) String added in v0.52.0

func (p Permutation) String() string

String returns the canonical lowercase permutation name.

type PipelineKeys added in v0.52.0

type PipelineKeys [][]byte

PipelineKeys is the per-step key material returned by PackPipeline. Index i carries the key produced (or echoed) for `Pipeline[i]`. Operators must transport both the blob AND the keys to the unpacker; the wire format only carries the Op + Algo of each step, never the key.

func PackPipeline added in v0.52.0

func PackPipeline(data []byte, pipeline []PipelineStep) ([]byte, PipelineKeys, error)

PackPipeline applies each step in `opts.Pipeline` to `data` in order and emits a maldev-format blob (FormatVersion 2). Returns the blob + per-step keys (auto-generated when the caller-supplied step.Key is nil).

The wire format records each step's Op + Algo so UnpackPipeline can reverse the chain, but the keys are NEVER stored in the blob — operators transport keys via a separate channel.

type PipelineOp added in v0.52.0

type PipelineOp uint8

PipelineOp identifies the kind of transformation a PipelineStep performs. Pack runs steps in slice order; Unpack runs them in reverse.

const (
	OpCipher  PipelineOp = 1 // AEAD or stream cipher (key-driven)
	OpPermute PipelineOp = 2 // byte permutation (S-Box, Matrix, ArithShift, XOR)
)
const OpCompress PipelineOp = 3

Add a compression step to the pipeline. The Algo byte selects which compressor (the same Compressor enum already declared in format.go).

Compression must run BEFORE encryption — encrypted data is near-uniform entropy and compresses to near-original size. Pack runs steps in order, so place the compression step EARLY in the pipeline.

pipeline := []packer.PipelineStep{
    {Op: packer.OpCompress, Algo: uint8(packer.CompressorFlate)},
    {Op: packer.OpCipher,   Algo: uint8(packer.CipherAESGCM)},
}
const OpEntropyCover PipelineOp = 4

OpEntropyCover is the pipeline op that lowers a blob's apparent Shannon entropy. It runs LATE in a pipeline — after compression and encryption — because its job is to undo the high-entropy signature those stages produce.

Three sub-algorithms ship: EntropyCoverInterleave (low-entropy padding spliced between ciphertext chunks — the only one that drops the actual histogram entropy), EntropyCoverCarrier (PNG-shaped header prefix so first-bytes scanners don't fire on randomness), and EntropyCoverHexAlphabet (each byte expanded to 2 bytes drawn from a low-entropy code-like alphabet — 2× size, apparent ~3-4 bits/byte).

None of these is a security primitive. They defeat byte- histogram heuristics, not adversaries with the wire format.

func (PipelineOp) String added in v0.52.0

func (o PipelineOp) String() string

String returns the canonical lowercase op name.

type PipelineStep added in v0.52.0

type PipelineStep struct {
	Op   PipelineOp
	Algo uint8
	Key  []byte
}

PipelineStep describes one transformation in the pipeline. Algo's meaning is Op-dependent: for OpCipher it's a Cipher; for OpPermute it's a Permutation. The Key bytes are the material the step needs to reverse — when nil at Pack time, the packer generates one and writes it into the returned PipelineKeys.

type ProxyDLLOptions added in v0.129.0

type ProxyDLLOptions struct {
	// PackOpts tunes the inner [PackBinary] call. ConvertEXEtoDLL
	// is FORCED to true regardless of the caller-supplied value.
	PackOpts PackBinaryOptions

	// TargetName is the legitimate DLL name whose exports the
	// fused proxy mirrors (e.g. "version" for version.dll).
	TargetName string

	// Exports is the list of exports to forward. Use
	// [pe/parse.Exports] to extract them from a real DLL on the
	// operator host.
	Exports []dllproxy.Export

	// PathScheme controls how forwarder strings address the
	// legitimate target DLL. Zero defaults to the
	// PerfectDLLProxy GLOBALROOT scheme.
	PathScheme dllproxy.PathScheme
}

ProxyDLLOptions parameterises PackProxyDLL — the fused (single-file) variant of the EXE→DLL sideloading pipeline. See ChainedProxyDLLOptions for the chained two-file variant.

Directories

Path Synopsis
internal
elfgate
Package elfgate implements the Z-scope pre-flight check for Go static-PIE ELF inputs: ET_DYN + .go.buildinfo present + no DT_NEEDED.
Package elfgate implements the Z-scope pre-flight check for Go static-PIE ELF inputs: ET_DYN + .go.buildinfo present + no DT_NEEDED.
Package runtime is the consumer side of pe/packer: takes a packed blob + key and reflectively loads the original PE into the current process's memory.
Package runtime is the consumer side of pe/packer: takes a packed blob + key and reflectively loads the original PE into the current process's memory.
Package stubgen drives the UPX-style transform pipeline for Phase 1e:
Package stubgen drives the UPX-style transform pipeline for Phase 1e:
amd64
Package amd64 wraps github.com/twitchyliquid64/golang-asm into a focused builder API for the polymorphic stage-1 decoder Phase 1e (v0.61.x) emits.
Package amd64 wraps github.com/twitchyliquid64/golang-asm into a focused builder API for the polymorphic stage-1 decoder Phase 1e (v0.61.x) emits.
poly
Package poly implements the SGN-style metamorphic engine the Phase 1e (v0.61.x) packer uses to generate polymorphic stage-1 decoders.
Package poly implements the SGN-style metamorphic engine the Phase 1e (v0.61.x) packer uses to generate polymorphic stage-1 decoders.
stage1
Package stage1 emits the polymorphic stub the UPX-style packer places in a new section of the modified host binary.
Package stage1 emits the polymorphic stub the UPX-style packer places in a new section of the modified host binary.
stage1/asmtrace command
Package main on non-Windows platforms is a stub.
Package main on non-Windows platforms is a stub.
Package transform implements UPX-style in-place modification of input PE/ELF binaries.
Package transform implements UPX-style in-place modification of input PE/ELF binaries.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL