剑庭安
首页
黑客安全首页全部安全栏目 黑客技术黑客技术、渗透测试、漏洞利用工具与教程 安全软件安全工具、扫描器、检测与审计软件 病毒分析恶意软件分析、样本、逆向与沙箱 CTF思路CTF 题目、writeup、解题思路与工具 防护思路防御技术、加固、检测响应与蓝队 红蓝攻防红队攻击模拟、蓝队对抗、攻防演练 漏洞知识库漏洞、CVE、PoC、EXP 与安全研究
逆向|反编译
PHP源码PHP 网站源码、程序、系统 APP源码安卓/iOS APP 源码 ASP源码ASP 网站源码与程序 NET源码.NET 网站源码与系统 小程序源码微信/支付宝小程序源码 Dedecms风格模板Dedecms 织梦风格模板 Empirecms风格模板EmpireCMS 帝国风格模板 Discuz商业模板/插件Discuz 商业模板与插件 WordPress主题/插件WordPress 主题与插件 其它源码、模板其它各类源码与模板 游戏源码游戏源码与引擎项目 加密货币源码加密货币、区块链、钱包、USDT/比特币相关源码与工具 手游源码手机游戏源码、服务端、客户端与引擎项目 传奇源码热血传奇、传奇世界、复古传奇等客户端/服务端/引擎源码 页游源码网页游戏、H5 游戏、浏览器游戏源码 端游源码电脑单机游戏、PC 网游客户端/服务端源码
最新万能免驱动支持所有型号打印机下载程序 日历天气老黄历 · 每天同步真实日期与天气
站长工具站长工具 自助开通会员浏览该分类 其他资源qtziy SEO工具SEO工具 站群源码站群源码 泛程序泛程序 蜘蛛模板蜘蛛模板 蜘蛛池蜘蛛池 外链工具外链工具
搜索 自助开通会员🔓 破甲 API
登录 注册
首页 分类列表 CTF思路 PolyEngine - PolyEngine是一款专为…
PolyEngine - PolyEngine是一款专为CT设计的回避式PE封隔器 CTF思路 点击放大
作者:admin 发布:2026-10-05

PolyEngine - PolyEngine是一款专为CT设计的回避式PE封隔器

项目说明PolyEngine — Polymorphic PE Packer 📦PolyEngine is a research-grade, e PE packer designed for CTF challenges and low-le security education. It focu…
资源可下载 免费下载 0 条讨论
会员资源
开通会员后即可下载全部资源。
登录后开通会员
会员方案
347浏览量
0讨论
2下载链接
可下载资源状态
内容可验证
更新 2026-10-05
人工审核发布
评论区答疑

PolyEngine - PolyEngine is an evasive PE packer designed for CT

PolyEngine - PolyEngine is an evasive PE packer designed for CT

项目说明

PolyEngine — Polymorphic PE Packer 📦

PolyEngine is a research-grade, e PE packer designed for CTF challenges and low-le security education. It focuses on bypassing EDR and A through a layered stack of in-memory execution and obfuscation techniques.

This is a side project I've been working on for some time. I have used Claude Code to implement and correct some of techniques I wanted to implement in my own PE packer. There's a lot of comments on functions and what they do, since it was a big learning experience for me and Claude does this flawlessly (I'm bad at it). I hope it helps some people to learn some Windows internals or with bypassing some AVs and static detections from more ad solutions when you tackle those ProLabs 🏯.

🔥 Big thank you to MalDe for all the materials to create it and inspiration.

🌩 Thanks to for inspiration via some goofy tweet with silly cat.

> Disclaimer: This tool is intended exclusively for authorized security testing, CTF competitions and educational use. Usage against systems without explicit permission is prohibited. The author assumes no liability for misuse.

---

Usage

Build order: Stub first, then Builder. Stub Release|x64 emits stub_v0.bin..stub_v3.bin; Builder embeds one into .rsrc.

Ensure stub_v0.bin..stub_v3.bin are in the working directory (or pass --stub).

---

Examples

Worked examples grouped by scenario. Every flag is opt-out (e is fully ON by default), so the simplest in already gets the full stack.

<details>
<summary><b>Basic packing - EXE / DLL / shellcode</b></summary>

Pack an unmanaged EXE. Builder auto-detects the MZ header and routes through the RunPE path:

Pack raw position-independent shellcode (Cobalt Strike .bin, msfvenom -f raw, etc.). No MZ → direct call into the decompressed buffer:

Pack a DLL and call its default DllMain only (no export):

Use a stub from a non-default location:

</details>

<details>
<summary><b>DLL payloads with exports - Havoc / Sliver / custom beacons</b></summary>

Call a named export after DllMain returns. Most C2 implants ship as a DLL with a single entry export (e.g. Ha: Start, Sliver: RunSliver):

Pass a string argument to the export (max 127 chars). Useful for payloads that take a config string, URL, or shell command:

--keep-alive is required for any payload that spawns its own threads - without it the loader calls ExitProcess and kills the beacon.

</details>

<details>
<summary><b>Long-running implants (C2 beacons)</b></summary>

Cobalt Strike / Sliver / Havoc all spawn a beacon thread and return. The loader thread must terminate without taking the process down:

</details>

<details>
<summary><b>Module stomping presets - picking a host DLL</b></summary>

The decryptor stub is hidden inside the .text section of a benign Windows DLL. Pick the preset whose loaded modules look most legitimate for the target context:

PRINT (default) - xpsservices.dll, msi.dll, dbghelp.dll. Common across most workstations.
NETWORK - winhttp.dll, wtsapi32.dll, wlanapi.dll. Fits a payload that already needs network APIs loaded.
WEB - mshtml.dll (idx 10, ~17 MB .text) with msi.dll/dbghelp.dll fallbacks. Required for payloads whose compressed blob exceeds the ~2.5 MB ceiling of indices 0-9 (e.g. Go-based implants such as Sliver).
RANDOM - three random indices from indices 0-9 (including bcrypt.dll); mshtml.dll (idx 10) is reachable only via --preset WEB.

Switch from LoadLibraryW stomping to NtCreateSection+NtMap o (DLL ne PEB.Ldr):

</details>

<details>
<summary><b>EDR userland unhooking</b></summary>

Restore clean .text bytes for ntdll, kernel32, kernelbase from \KnownDlls\ over any EDR inline hooks. HellsHall already bypasses sensitive Nt* hooks; --unhook is needed only when the payload itself calls hooked Win32 APIs (e.g. LoadLibrary, CreateProcess):

</details>

<details>
<summary><b>PEB spoofing - masquerading the process</b></summary>

O the auto-picked spoof name. Pick something that fits the parent process / launch context (Office macro → RuntimeBroker.exe looks weird, WmiPrvSE.exe blends in better):

The spoof name is just an ASCII filename - Stub prepends C:\Windows\System32\ at runtime. Anything not in the default 9-process pool works too.

</details>

<details>
<summary><b>Tuning e thresholds</b></summary>

Longer hammer delay against patient sandboxes that run for tens of seconds before judging:

Higher uptime threshold - only run if the box has been up for at least 30 minutes (most sandboxes spin a fresh VM per sample):

Tighter sleep-forwarding detection (shorter sleep, harder to fast-forward without observable error):

Custom semaphore name for the exec-control check (avoids clashing with another sample using the default wuauctl):

Stack e for a paranoid profile:

</details>

<details>
<summary><b>Disabling features for debugging / lab work</b></summary>

When iterating in a debugger, the TLS callback and debugger checks fire immediately. Disable both to attach freely:

Skip every sandbox check (still keeps OPSEC features on - ETW patch, PEB spoof, call-stack spoof):

Same thing, shorter:

Disable specific OPSEC features (e.g. when the target en doesn't need ETW patching, or PEB spoof breaks a payload that walks its own PEB):

--disable all ORs every token (OPSEC + sandbox/debug checks) and sets the E fast-exit shortcut. To keep OPSEC features on while skipping only the checks, list the check tokens explicitly instead of all.

</details>

<details>
<summary><b>Identity cloning — , icon, Authenticode cert</b></summary>

Copy the cosmetic identity of any donor PE into the packed output. The output binary's Explorer properties, taskbar icon, and Digital Signatures tab all reflect the donor:

What gets cloned:

  • (RT_) — Explorer "Properties → Details" tab: company, product name, file , copyright. All language IDs present in the donor are copied.

  • Icon (RT_GROUP_ICON + RT_ICON) — the lowest-ID icon group (the one Explorer uses for the shell icon). Taskbar, alt-tab, and file browser all show the donor's icon.

  • Authenticode cert directory — the raw WIN_CERTIFICATE PKCS#7 blob is appended at 8-byte-aligned EOF. Explorer "Properties → Digital Signatures" shows the donor's signer (e.g. Microsoft Windows). Get-AuthenticodeSignature returns Status = HashMismatch — the signature is structurally valid but the hash covers the donor's bytes, not ours. Defeats casual inspection; any real (signtool , Win, A) detects the mismatch.
  • OPSEC — OriginalFilename: the donor's embeds OriginalFilename (e.g. notepad.exe). Some signature checkers and Defender heuristics flag a mismatch between OriginalFilename and the actual file name on disk. Name the output to match:

    Combination with --pfx: when both flags are present, Phase 11 (clone) runs before Phase 12 (sign). The real signature o the cloned cert directory; and icon are preserved. Get-AuthenticodeSignature shows your certificate as valid, not the donor's hash-mismatched cert:

    :

    </details>

    <details>
    <summary><b>UAC ele — requireAdministrator manifest</b></summary>

    Embed a requestedExecutionLevel="requireAdministrator" manifest so the output PE triggers a UAC prompt on launch and receives a high-integrity token if the user approves:

    The manifest is an RT_MANIFEST resource (resource ID 1 — CREATEPROCESS_MANIFEST_RESOURCE_ID), the same slot the Windows loader checks for application compatibility and pri manifests. The output is otherwise identical to a build without --uac; no changes to the Stub or payload path.

    Combination with --clone-meta and --pfx: Phase 10.5 (manifest) runs before Phase 11 (clone) and Phase 12 (sign). The Authenticode signature computed in Phase 12 covers all embedded resources including the manifest — the hash is valid over the final binary. The UAC dialog shows the publisher name from the signing cert:

    OPSEC: a UAC prompt is a , user-facing event. The "Do you want to allow this app to make changes to your device?" dialog displays the on-disk filename and the Authenticode publisher (or "Unknown publisher" if unsigned). Combine --uac with --clone-meta to show a familiar icon and --pfx to show a credible publisher. For unattended execution that already starts from an elevated process (e.g. service, WMI lateral mo, local admin shell), --uac is unnecessary.

    </details>

    <details>
    <summary><b>Authenticode signing (PFX, no signtool)</b></summary>

    Sign the packed output with a PFX certificate. Builder talks to mssign32!SignerSignEx2 directly — no signtool.exe on the operator workstation, no Windows SDK signing tool required:

    PFX with no password (omit the --pfx-pass flag entirely):

    Add an RFC 3161 timestamp so the signature stays valid after the cert expires or is revoked. Note that the timestamp authority logs the build IP and the exact signing moment — see the OPSEC note below:

    OPSEC — when to skip the timestamp: --ts-url is opt-in. Each request to a public TSA puts the operator's build host into the TSA's HTTP logs alongside the exact second the signature was produced — a strong forensic correlation if the sample later surfaces in incident response. Embedding the timestamp also stamps that same moment into the signature blob inside the packed PE, where any later analyst can read it. When no timestamp is required (typical for self-signed certs or short-lived ops where signature beyond cert expiry doesn't matter), omit the flag and signing stays fully air-gappable.

    OPSEC — when to keep the timestamp: stolen or short-li certs that will be revoked benefit from a TSA countersignature — Windows accepts the signature post-re as long as the timestamp predates the re entry. In that case, sign from an isolated a proxy / Tor, and treat the TSA's logs as a deliberate (but contained) exposure.

    The private key is ne: PFXImportCertStore is called with PKCS12_NO_PERSIST_KEY, so no key container appears under %APPDATA%\Microsoft\Crypto. SHA-256 digest, CNG-preferred KSP for compatibility with PFXs produced by New-SelfSignedCertificate and OpenSSL ≥3.x.

    </details>

    <details>
    <summary><b>Realistic combinations</b></summary>

    Cobalt Strike beacon DLL, network-themed host, full e, custom mutex name:

    Ha shellcode, o instead of stomping, longer hammer delay:

    Stage-2 EXE for a CTF where you control the trigger and don't need anti-debug:

    Lateral-mo helper DLL with a path argument:

    Long-term implant disguised as a common Microsoft binary, full e stack:

    Packed sample with a real self-signed cert + matching identity:

    </details>

    ---

    Build

    Studio 2022 (MSVC v143), Release|x64 only. Open PolyEngine.sln.

    Build order matters:
    1. Stub project (Release|x64) → MSBuild fan-out emits x64/Release/stub_v0.bin … stub_v3.bin (POLY_=0..3)
    2. Builder project → produces Builder/x64/Release/Builder.exe (or solution OutDir)

    Each stub_v*.bin is a PE with linker entry EntryPoint only (no CRT). differ in OPSEC phase order and decoy/island layout; HellsHall/Moonwalk stay shared. Builder picks one at random from the CWD (or via --stub), runs StubMorph, then embeds the payload under a per-build RT_RCDATA ID.

    MASM: Stub. → HellsHall.asm; Builder → Engine/DecryptorStub.asm. No CMake, no Makefile.

    The solution also builds PolyEngine.GUI (nati + OpenGL3 + Dear ImGui, in-tree) and its test runner. Both land in x64/Release/ next to Builder.exe and stub_v*.bin — the GUI resol/stub paths from its own directory, so it works straight after the build with zero configuration. Run x64/Release/PolyEngine.GUI.exe; run x64/Release/PolyEngine.GUI.Tests.exe to execute the unit tests (38 tests, no external framework). GUI projects are x64-only and are skipped in Release|x86 solution builds.

    !GUI-1

    !GUI-2

    !GUI-3

    To add a new API hash: compute Djb2HashA("ApiName") (same algorithm as in ApiHashing.cpp), add a g_Hash_* global in ApiHashing.h, initialize it in ApiHashing_InitHashes().

    ---

    Architecture O

    Three components implementing a pack → encrypt → inject pipeline: Builder packs the input PE/shellcode, Engine (shared lib) provides crypto/compression/mutation primitives, Stub is the runtime executor embedded in the output PE.

    <details>
    <summary>Detailed pipeline + Stub execution flow</summary>

    </details>

    ---

    E Techniques

    <details>
    <summary><b>Loader — <code>stub_v0.bin</code>..<code>stub_v3.bin</code></b></summary>

    Stub Release|x64 builds four loaders (POLY_=0..3, separate IntDir). Each binary has a different OPSEC phase order and per- decoy/island sizes (PolyIslands.c), so static hashes diverge. Shared and never : HellsHall.asm, SilentMoonwalk data layout (g_SpoofSyntheticStack), TLS/ResID marker tags.

    | | Loader_OpsecPhase order |
    |---|---|
    | V0 | Unhook → Spoof → ETW → AMSI → XTEA → PEB |
    | V1 | Spoof → Unhook → XTEA → PEB → ETW → AMSI |
    | V2 | Unhook → Spoof → PEB → XTEA → ETW → AMSI |
    | V3 | Spoof → ETW → AMSI → Unhook → XTEA → PEB |

    Builder with no --stub picks randomly among stub_v0.bin..stub_v3.bin in the CWD (CryptGenRandom). --stub <path> forces one file.

    </details>

    <details>
    <summary><b>Pack-time StubMorph</b></summary>

    After TLS/ResID marker patches and before writing the output PE, StubMorph_Apply (Engine/StubMorph.c) mutates the chosen loader image in-place:

  • plausible TimeDateStamp drawn from [now−5y, now] — a fully random DWORD can land in the future, which is a heuristic flag

  • toolchain-profile section names (MSVC / MinGW / Delphi / NSIS style, picked per build; skips .rsrc, .reloc, .tls, .CRT) — random 8-char names would trip UPX-style packer heuristics

  • Rich header strip when a non-MS is drawn (DanS..Rich+key zeroed — a Microsoft toolchain signature would contradict the rename story; MS keeps it for coherence)

  • clear IMAGE_DIRECTORY_ENTRY_DEBUG + PE checksum (a real checksum is written back at the final Step 13 on every build)

  • rewrite POLY island pads (50 4C 59 A0 … AF markers in PolyIslands.c, incl. the g_PolyDecoy island) with random bytes of the same length, then randomize the tag markers themselves — no PLY pattern or fixed decoy content survives into the output PE; no PE growth, no reloc fixups
  • Does not touch HellsHall, spoof globals, or marker tags used by TLS/ResID patching.

    </details>

    <details>
    <summary><b>Indirect Syscalls — HellsHall</b></summary>

    All sensitive NT operations (NtProtect, NtAllocate, etc.) go through indirect syscalls rather than the hooked user-mode stubs in the process ntdll:

    1. At startup, Syscalls_Init() parses process ntdll's Export Directory and collects every Zw* function's RVA into a flat table.
    2. SSNs are derived by R (HellsGate/FreshyCalls ): all Zw* RVAs are sorted; sorted index == SSN. Hook-agnostic by design — works even if EDR hooks ha function prologues, because hooks don't reorder exports.
    3. A syscall; ret (0F 05 C3) trampoline is located inside process ntdll's .text section. The 3-byte sequence is the standard tail of every Nt* stub. EDR userland inline hooks target the *entry point* of exported Nt* functions (first 5–15 bytes) — never the syscall instruction at the stub's end, because patching the middle of a stub would break its semantics. The bytes at any matching site are therefore unmodified, identical to a clean \KnownDlls\ mapping, and live in MEM_IMAGE memory backed by C:\Windows\System32\ntdll.dll on disk. g_CleanTrampoline points there directly — no secondary ntdll mapping, no MEM_PRIVATE copy.
    4. All syscalls jump to g_CleanTrampoline — EDR hooks at exported Nt* entry points are bypassed. From an ETW kernel-side stack walker's perspective, the leaf frame already lands inside ntdll.dll (no "unbacked syscall" IOC).

    </details>

    <details>
    <summary><b>Userland Unhooking — <code>--unhook</code></b></summary>

    Optional pass that runs once after Syscalls_Init() and before any payload-relevant syscall. For each of ntdll, kernel32, kernelbase:

    1. NtOpenSection(\KnownDlls\<dll>) + NtMap — clean image bytes (same shared section the loader originally mapped from, before any EDR could install hooks).
    2. Page-by-page memcmp of the live .text against the clean copy.
    3. Where bytes differ (= EDR inline hook): NtProtect RX→RW, memcpy clean bytes over the hook, NtProtect RW→RX.
    4. Unmap and close.

    This restores normal ntdll.dll/kernel32.dll/kernelbase.dll semantics for any subsequent Win32 call (PEB walks, LoadLibrary, etc.). Skipped when --unhook is omitted — HellsHall on its own already bypasses all sensitive Nt* hooks, so unhooking is opt-in (it's a heavier action with a small risk of breaking unusual hook layouts).

    </details>

    <details>
    <summary><b>Call Stack Spoofing — SilentMoonwalk RSP Pivot</b></summary>

    EDRs monitor the thread call stack at the moment a syscall fires to the caller chain looks legitimate. PolyEngine defeats this with a SilentMoonwalk-style RSP pivot — no hardware breakpoints, no , no exceptions:

    1. StackSpoof_Init() scans ntdll's .text (and any other IMAGE_SCN_MEM_EXECUTE section) for two gadgets:
    - Gadget 1 — add rsp, imm8; ret (48 83 C4 XX C3) inside a function whose UNWIND_INFO ad a matching imm8 alloc delta. Constrained to imm8 < 0x20 so the chain doesn't collide with forwarded stack args.
    - Gadget 2 — jmp rbx (FF E3), located in any executable section by raw byte scan, but only accepted if the matching site sits inside a RUNTIME_FUNCTION whose UNWIND_INFO parses cleanly. The function's allocDelta determines where RtlUserThreadStart is planted on the synthetic stack, so a gadget without a matching runtime function would break the EDR- chain. Jumping into the middle of a longer instruction is fine — the CPU decodes FF E3 from the gadget address regardless of the preceding byte.
    2. A static g_SpoofSyntheticStack[32] is laid out so that the trampoline's ret walks gadget1 → gadget2 → back to the loader's continuation point, with RtlUserThreadStart planted further down as the apparent thread root.
    3. Every call to HellsHallSyscall (when spoofing is enabled) does push rbx; lea rbx, AfterJmpPoint; mo], rsp; lea rsp, g_SpoofSyntheticStack; jmp r11. The kernel sees an RSP pointing into the synthetic stack, anchored in legitimate ntdll code.
    4. Stack-based syscall arguments (5..10) are forwarded from the caller's frame to the synthetic stack at offsets 0x28..0x50 *before* the pivot — without this, the kernel would read gadget addresses as arguments and return STATUS_ACCESS_.
    5. After the syscall, gadget2's jmp rbx lands on AfterJmpPoint, which restores RSP from g_SpoofSa, pops rbx, and returns to the loader.
    6. StackSpoof_Cleanup() clears g_SpoofEnabled so the payload's own threads see real return addresses.

    </details>

    <details>
    <summary><b>Module Stomping / Module O</b></summary>

    Instead of (RWX), the polymorphic decryptor executes inside the .text section of a legitimately loaded Windows DLL. No RWX memory is e.

    Stomping (default — LoadLibraryW, DLL appears in PEB LDR):

    1. ModuleStomp_Alloc() iterates the three DLLs selected by --preset. DLL indices are stored in .rsrc and resolved from g_DllPool at runtime.
    2. The first DLL with an executable section large enough for the decryptor stub is chosen.
    3. Original .text bytes are saved to a private RW buffer before touching the section.
    4. The decryptor stub (only) is copied into the stomped region. The payload blob stays in a separate RW allocation (pEncryptedPayload).
    5. NtProtect RW → RX: the stomped region becomes executable. The payload allocation remains RW — decryptor receives its address in RCX (Windows x64 ABI first argument).
    6. Decryptor runs, decrypts pEncryptedPayload in-place. NtProtect RX → RW immediately after return.
    7. Region is wiped, original bytes are restored, section is set back to PAGE_EXECUTE_READ.

    O (--o — NtCreateSection(SEC_IMAGE) + NtMap, NOT in PEB LDR):

  • Same sa pattern and no-RWX in as stomping.

  • DLL is mapped directly from the raw file handle — ne in PEB.Ldr, defeating tools that enumerate loaded modules.

  • After use: NtUnmap discards COW private pages, removing all e of the write.
  • Result in both cases: memory region is MEM_IMAGE backed by the DLL's file on disk — memory scanners see a legitimate image mapping, not an anonymous region.

    </details>

    <details>
    <summary><b>Polymorphic Decryptor — MutationEngine</b></summary>

    Each build produces a unique 34-byte x64 ASM decryptor stub, ne to any prior build:

  • NOP / junk insertion — random NOP/junk instructions between functional instructions, drawn from a 22-entry pool spanning RBX/R10/R11/R12/R13 (PUSH/POP, XCHG, TEST, MO)

  • Register swapping — functional registers randomly reassigned across equi sets

  • Instruction substitution — each cipher step emitted in one of three semantically equi (e.g. xor al, k / sub al, ~k+1 / not al; xor al, ~k)

  • Loop counter — inc r9 randomized between inc r9, add r9,1, lea r9,[r9+1]; comparison swapped between cmp rdx,r9 and cmp r9,rdx

  • Block permutation — 4 independent setup blocks (RCX/RDX/R10/R11 zeroing) reordered shuffle (24 possible orderings)

  • XOR key order swap — random xorSwapped flag flips outer key application order; encryptor + decryptor stay in sync bit
  • The CompoundEncrypt cipher (XOR→ROL→ADD→XOR on each byte) maps cleanly to the four-instruction decryptor template. The MutationEngine emits a different combination for every step, making static signature matching of the decryptor loop infeasible.

    </details>

    <details>
    <summary><b>Encryption Stack</b></summary>

    | Layer | Algorithm | Key source |
    |---|---|---|
    | Outer | XTEA-CTR (128-bit) | Runtime-derived base XOR per-build CryptGenRandom salt |
    | Inner | CompoundEncrypt (XOR+ROL+ADD+XOR) | Per-build __rdtsc-seeded compound key, embedded in decryptor stub |

    Outer XTEA key deri (Xtea_DeriveKey): the 128-bit key is built at runtime from arithmetic on irrational-number constants (φ, √2, √3, √5, √10 scaled to 32 bits). All five seed constants are themsel into XOR pairs (A ^ B) so no plaintext irrational byte sequence appears in .rdata — recovery requires running the deri. No contiguous 16-byte key blob exists in the binary. The final key is deri XOR key_salt, where key_salt is 16 bytes of CryptGenRandom output stored in .rsrc — every build produces a unique keystream.

    Dynamic magic (no static YARA anchor): the .rsrc metadata block ends with magic = key_salt[0]^key_salt[1]^key_salt[2]^key_salt[3]. The Stub locates the block by scanning backwards and this in — no 0xDEADBEEF or other fixed constant exists for a YARA rule to anchor on.

    </details>

    <details>
    <summary><b>TLS Callback Anti-Debug</b></summary>

    The Windows Loader invokes TLS callbacks from .CRT$XLB before AddressOfEntryPoint recei — before any payload is in memory. At that moment the en is probed with zero external API dependencies (only CPU intrinsics and direct PEB reads):

    | Check | PEB / Heap field | Detection condition |
    |---|---|---|
    | BeingDebugged | PEB+0x002 | any Win32 debugger attached |
    | NtGlobalFlag | PEB+0xBC (x64) | bits 0x70 set by ntdll under a debugger |
    | Heap Flags | ProcessHeap+0x70 (x64) | value != 2 (HEAP_GROWABLE) |
    | Heap ForceFlags | ProcessHeap+0x74 (x64) | value != 0 |

    On detection: __fastfail(FAST_FAIL_FATAL_APP_EXIT) — bypasses all user-mode exception handlers (VEH, SEH, UnhandledExceptionFilter). WER records STATUS_STACK_BUFFER_O (c0000409), indistinguishable from a legitimate memory-safety crash.

    Can be disabled at build time with --disable tls. Builder patches a 5-byte marker in the loader stub to neutralize the callback before embedding.

    </details>

    <details>
    <summary><b>ETW Patching</b></summary>

    Opsec_PatchEtw() writes a 3-byte no-op to the first bytes of EtwE in the process ntdll:

    Applied through NtProtect ( + Moonwalk RSP pivot). A separate pPage holds the base address for NtProtect (the kernel may round it to a page boundary); pEtw is preserved for the actual byte write.

    </details>

    <details>
    <summary><b>AMSI Patching</b></summary>

    Opsec_PatchAmsi() writes a 6-byte stub over AmsiScanBuffer in the in-process amsi.dll:

    Callers treat E_IN as "bad input", not "malicious" — no alert path fires. Applied through the same HellsHall NtProtect RX→RW→RX pattern as the ETW patch, immediately after it in every POLY_ order.

    Patch only if already mapped: amsi.dll is loaded into the process by A before EntryPoint runs. If it is *not* present, the patch is skipped entirely — the loader ne LoadLibrary("amsi.dll"), because an on-demand load would leave a fresh, timestamped IOC in the PEB LDR list. Soft-fail: a failed patch ne payload delivery. Disable with --disable amsi.

    </details>

    <details>
    <summary><b>Block non-Microsoft DLLs — blockdlls</b></summary>

    Right after payload extraction (before e checks), Loader_BlockDlls() sets SetProcessMitigationPolicy(ProcessSignaturePolicy, MicrosoftSignedOnly). From that point the OS refuses to load any DLL that is not Microsoft-signed into the loader process — EDR userland agents injected via LoadLibrary (in-process scanners, hooking DLLs) are turned away. The policy is one-way and survives for the process lifetime; it covers both the module-stomp DLLs (all System32, MS-signed) and anything the payload later loads.

    Two implementation notes:

  • The API is resolved from kernelbase.dll, ne — kernel32 exposes SetProcessMitigationPolicy only as a *forwarder string* (api-ms-win-core-...), so a nai walk returns a pointer to ASCII text (crash at call time).

  • DynamicCodePolicy is deliberately not set — it would kill the loader's own module-stomping writes.
  • Tradeoff: payloads that LoadLibrary non-Microsoft-signed DLLs (custom plugin DLLs dropped next to the output, third-party dependencies) will fail those loads. Disable with --disable blockdlls for such payloads. Get-ProcessMitigation does not display runtime-set signature policy (cmdlet blind spot) — via the API return value or by attempting an unsigned LoadLibrary.

    </details>

    <details>
    <summary><b>Entropy shaping — nibble-to-text payload resource</b></summary>

    A raw XTEA blob sits at ~7.99 bits/byte — the classic *high-entropy resource* heuristic that static scanners flag instantly. Engine/EntropyCodec.c maps every byte to two symbols of a 16-letter lowercase ASCII alphabet between the XTEA pass (Builder Phase 8.5) and the .rsrc write. The alphabet is per-build, not a compile-time constant: EntropyDeri draws an ordered 16-of-26 subset from a-z Fisher-Yates, seeded by key_salt (a fixed alphabet would be a cross-sample fingerprint — one charset-regex retro-hunt would match every build):

  • .rsrc payload entropy drops from ~7.8 to ~4.0 bits/byte, and statistically indistinguishable from text/config data

  • cost: 2x resource size

  • metadata.blobSize keeps the decoded size; the resource physically holds blobSize*2 encoded bytes immediately before the metadata block

  • Stub derives the identical alphabet from key_salt in the metadata and decodes in GetPayloadFromResource (EntropyDecode, 256-byte reverse table on stack) into the same RW buffer XTEA then decrypts in place — no extra allocation, no crypto change
  • </details>

    <details>
    <summary><b>Decoy import directory — synthetic .idata</b></summary>

    The CRT-free stub has an empty import directory — and a GUI-subsystem exe with zero imports is as much of a packer tell as a suspicious one. (RtlAddFunctionTable, AllocConsole, GetConsoleWindow, ShowWindow are all lazy-resolved via GetProcAddressH at their RunPE call sites, so the old "console-hider trio + SEH registration" import set is gone.)

    Engine/DecoyImports.c (Builder Phase 11.5) appends a synthetic .idata section to the packed output: one kernel32.dll descriptor with 5-8 randomly picked innocuous exports (partial Fisher-Yates over a 12-name pool: GetTickCount64, QueryPerformanceCounter, GetSystemTimeAsFileTime, ...), INT+IAT thunk arrays, and NumberOfSections / SizeOfImage / DataDirectory[IMPORT] / [IAT] updates. The result looks like a minimal MSVC GUI binary's import table. The decoys are ne; the loader fills the IAT with real addresses at load time as usual.

    Pipeline position matters: Phase 11.5 runs *after* the last BeginUpdateResource/EndUpdateResource cycle (Phases 10/10.5/11) because UpdateResource relocates sections on .rsrc growth and fixes up only the RESOURCE and BASERELOC directories — an import directory planted earlier ends up with a stale RVA. It runs *before* signing (Phase 12) so the signature covers it, and is skipped when a cert table already exists (--clone-meta path — appending past WIN_CERTIFICATE would in the cloned cert).

    </details>

    <details>
    <summary><b>PEB Spoofing</b></summary>

    Opsec_SpoofPeb() rewrites:

  • PEB.ImageBaseFileName — process name shown by Process Hacker, etc.

  • PEB.ImagePathName and PEB.CommandLine — full path in process listings

  • PEB.BeingDebugged = 0, PEB.NtGlobalFlag = 0 — anti-debug flags

  • ProcessHeap.Flags = 2, ProcessHeap.ForceFlags = 0 — heap debug flags
  • The spoof filename is set via --spoof-name. If omitted, Builder picks randomly from a pool of 9 common System32 processes (RuntimeBroker.exe, SgrmBroker.exe, WmiPrvSE.exe, SearchIndexer.exe, taskhostw.exe, spoolsv.exe, wlrmdr.exe, WMPDMC.exe, hvix64.exe) using CryptGenRandom.

    </details>

    <details>
    <summary><b>Sandbox & Anti-Analysis Checks</b></summary>

    E() runs before any syscall initialization and uses only the Win32 API layer. Checks are indi toggleable via --disable.

    Hard checks — a single positi immediate exit:

    | Check | Method | What it detects |
    |---|---|---|
    | debugger | PEB.BeingDebugged, PEB.NtGlobalFlag, ProcessHeap flags, NtQueryInformationProcess(ProcessDebugPort) | Win32 debugger attached |
    | api-emu | RtlComputeCrc32(seed, NULL, 0) — must equal seed | API emulation that returns wrong results |
    | exec-ctrl | Named semaphore (wuauctl by default, configurable) — ERROR_ALREADY_EXISTS | Second execution of the sample |
    | sleep-fwd | Sleep(ms) + GetTickCount64 delta, threshold = 90% of ms | Sandbox that fast-forwards Sleep calls |

    Soft checks — 2 or more positi to exit (reduces false positives):

    | Check | Threshold | What it detects |
    |---|---|---|
    | uptime | system uptime < N minutes (default: 2) | Freshly spawned sandbox VM |
    | cpu | logical processor count < 2 | Low-spec sandbox |
    | screen | screen width ≤ 1024 px | 800×600 / 1024×768 sandbox resolutions |
    | files | HKCU\...\Explorer\RecentDocs subkey count < 5 | Clean / fake user profile |

    Timing delay:

    E() burns real wall-clock time via / pairs timed by GetTickCount64. The duration is configurable via --hammer-s (default: 3 seconds). Sandbox time-accelerators cannot fast-forward allocator round-trips, making this effecti sleep-fast-forward e that bypasses the sleep-fwd check.

    </details>

    <details>
    <summary><b>API Hashing — Djb2</b></summary>

    All Windows API names are replaced at compile time with precomputed Djb2 hashes stored as g_Hash_* globals. GetProcAddressH() walks the export directory and hashes each exported name until a match is found — no plaintext API string appears in the import table or .data.

    Forwarded exports: on Win10/11 many kernel32 exports are forwarders (InitializeCriticalSection → NTDLL.RtlInitializeCriticalSection, ExitThread → NTDLL.RtlExitUserThread) — their EAT entries point at MODULE.Function ASCII strings inside the export directory, not code. Both GetProcAddressH and RunPE's RunPE_GetExport detect forwarder RVAs (inside the export directory range) and follow them to the target module's EAT, so payloads (e.g. MinGW CRT init calling EnterCriticalSection) never end up executing a string as code.

    </details>

    <details>
    <summary><b>Identity cloning — <code>--clone-meta</code></b></summary>

    Optional Phase 11 post-build step that copies three cosmetic attributes from a donor PE into the already-built output. Runs after BuildInfectedPE (which rewrites .rsrc entirely — anything written earlier would be lost) and before SignPeWithPfx (which o the cert directory with a real signature if --pfx is also given).

    CloneMeta_CopyResources loads the donor as a flat data file (LOAD_LIBRARY_AS_DATAFILE — deliberately *without* LOAD_LIBRARY_AS_IMAGE_RESOURCE, which activates MUI resource redirection on system files and routes RT_GROUP_ICON lookups to a language satellite .mui that contains no icons). RT_ is enumerated via EnumResourceLanguagesA to collect all language IDs; each is written with UpdateResourceA. For RT_GROUP_ICON, the lowest integer-ID group is selected (the one Explorer uses for the shell icon by con). FindResourceA is not used for the actual data lookup — it fails with ERROR_RESOURCE_NAME_NOT_FOUND (1813) on LOAD_LIBRARY_AS_DATAFILE handles even for resources that EnumResourceNamesA just found, because the LANG_NEUTRAL fallback path is broken in datafile mode. Instead, EnumResourceLanguagesA extracts the exact LANGID stored in the donor, then FindResourceExA uses it directly. All UpdateResourceA calls open the output PE with BeginUpdateResourceA(FALSE) — the merge flag preserves the existing .rsrc payload entry from Phase 10.

    CloneMeta_CopyCertDirectory reads the donor into a flat buffer via ReadFileToBuffer, parses the DOS → NT headers (supporting both x86 and x64 donors via OptionalHeader.Magic dispatch), and extracts DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]. That directory entry uses a file offset (not an RVA) — it is the only PE data directory where is a raw byte offset into the file. The cert blob is appended at 8-byte-aligned EOF (WIN_CERTIFICATE alignment requirement), the target's SECURITY directory entry is patched in-place, then MapFileAndCheckSumA (from imagehlp.lib) recomputes the PE checksum. The write handle is closed before calling MapFileAndCheckSumA — MapFileAndCheckSumA opens its own internal handle and would fail with a sharing if the caller holds an exclusi handle — then re-opened briefly to write just the 4-byte checksum at the computed file offset.

    </details>

    <details>
    <summary><b>Authenticode signing — <code>--pfx</code></b></summary>

    Optional post-build step. When --pfx is supplied, Builder signs the packed output via mssign32!SignerSignEx2 (resolved at runtime — no link-time mssign32 dependency, no signtool.exe on the operator workstation). Signing runs as Phase 9 after BuildInfectedPE returns, because writing the signature rewrites IMAGE_DIRECTORY_ENTRY_SECURITY and recalculates the PE checksum; any later resource edit would in the signature.

    The PFX is imported with PKCS12_NO_PERSIST_KEY | PKCS12_PREFER_CNG_KSP | PKCS12_INCLUDE_EXTENDED_PROPERTIES. NO_PERSIST_KEY keeps the private key resident in memory only — no key container file written under %APPDATA%\Microsoft\Crypto, which would otherwise tie the operator's workstation to the signed sample. PREFER_CNG_KSP is required for modern PFXs (PowerShell New-SelfSignedCertificate, OpenSSL ≥3.x); without it SignerSignEx2 fails with NTE_BAD_TYPE (0x8009000A). Digest is SHA-256.

    --ts-url is opt-in. RFC 3161 timestamping sends an HTTP request to the timestamp authority, which logs the requester IP plus the signing moment, and embeds that moment into the signature blob inside the PE. Skip the flag for fully air-gappable signing; use it only when the signature must outlive the cert's lifetime (e.g. stolen / short-li certs that will be revoked).

    </details>

    ---

    .rsrc Metadata Block Layout

    Resource payload is not fixed at ID 101. At pack time Builder:

    1. Draws a WORD RT_RCDATA ID via CryptGenRandom in range 0x0100..0x7EFF
    2. Patches LE bytes at g_PayloadResIdMarker[4..5] in the loader stub (tag {0xB1,0x0B,0x1D,0xE0} in Payload.c; unpatched default = 101)
    3. Runs StubMorph_Apply on the stub image
    4. Writes UpdateResource(RT_RCDATA, id) = [EntropyCodec-shaped XTEA blob (blobSize*2 bytes) | PAYLOAD_METADATA (280 bytes)]

    At runtime Stub reads the marker and calls FindResourceW with that ID. The metadata block is located by scanning backwards from the end of the resource (up to 128 bytes, tolerating UpdateResource alignment padding) and magic == XOR(key_salt[0..3]).

    <details>
    <summary>Field-by-field layout</summary>

    flags bits (see Engine/OpsecFlags.h): OPSEC 0–5, e/unhook 6–16, PAYLOAD_FLAG_IS_SHELLCODE (17), OPSEC_FLAG_NO_AMSI (18), OPSEC_FLAG_NO_BLOCKDLLS (19).

    No fixed value exists anywhere in the block — every field is either random (key_salt, magic) or build-specific. The RT_RCDATA ID is also per-build. YARA cannot anchor on a static byte sequence or a fixed resource ID.

    Loader exit codes (Release): all loader failure paths exit with code 0 via LOADER_EXIT (Stub/Common.h). Debug builds keep distinct codes for step diagnosis. E detections already exit 0.

    </details>

    ---

    Module Stomping DLL Pool

    Builder --preset selects 3 DLL indices stored in .rsrc. Stub resol from g_DllPool at runtime — no DLL name appears in the payload or metadata.

    <details>
    <summary>DLL pool table</summary>

    | Index | DLL | Group |
    |---|---|---|
    | 0 | xpsservices.dll | PRINT |
    | 1 | msi.dll | PRINT |
    | 2 | dbghelp.dll | PRINT |
    | 3 | winmm.dll | MEDIA |
    | 4 | dxgi.dll | MEDIA |
    | 5 | oleaut32.dll | MEDIA |
    | 6 | winhttp.dll | NETWORK |
    | 7 | wtsapi32.dll | NETWORK |
    | 8 | wlanapi.dll | NETWORK |
    | 9 | bcrypt.dll | (RANDOM-only) |
    | 10 | mshtml.dll | WEB (~17 MB .text — fits payloads > 2.5 MB) |

    Index 9 (bcrypt.dll) is reachable only via --preset RANDOM; index 10 (mshtml.dll) only via --preset WEB. Builder at build time that at least one of the three selected DLLs has an executable section large enough for the payload blob, and warns if none qualify. Index 10 (mshtml.dll) is there to allow big payloads to run after being packed.

    </details>

    ---

    Project Structure

    <details>
    <summary>File tree</summary>

    </details>

    ---

    Dependencies

  • Windows 10/11 x64 target

  • Studio 2022 (MSVC v143) — no external libraries

  • Stub: zero CRT dependency (/NODEFAULTLIB), no malloc/free, no <string.h>
  • ---

    Maintained by: Razz | Built for opsec-conscious security research and fun

    *Assistance in implementation done with Claude Code and Kimi K3*

    ---

    License

    MIT — authorized use only. See LICENSE for the full disclaimer.

    项目信息

    开发语言:C++

    Star 数:167

    技术要点

    该项目属于CTF思路方向的开源资源,代码结构清晰,适合安全研究人员学习参考。项目围绕PolyEngine is an evasive PE packer designed for CTF challenges and low-level Windows security education. It focuses on bypassing EDR and AV heuristics through a layered stack of in-memory execution and obfuscation techniques.展开,提供了可运行的实现,对理解相关安全机制、工具原理与攻防技术具有较高的研究价值。

    从技术实现角度看,此类项目的核心在于对底层原理的深入理解与工程化落地。开发者在研究过程中可以结合官方文档与社区讨论,梳理项目的初始化流程、核心模块职责与数据流转,逐步掌握其中的技术细节与设计思路。

    使用与学习建议

    1. 下载源码压缩包,解压到本地环境。

    2. 根据项目类型准备对应的运行环境与依赖。

    3. 按照 README 中的步骤完成配置与部署。

    4. 在隔离的测试环境中运行验证,避免影响生产系统。

    免责声明

    本资源来源于开源社区,仅供安全研究、技术学习与合法授权范围内的攻防演练使用,请勿用于任何非法或侵害他人权益的用途。使用本项目产生的一切后果由使用者自行承担。

    资源标题PolyEngine - PolyEngine是一款专为CT设计的回避式PE封隔器
    所属分类CTF思路
    发布作者admin
    发布时间2026-10-05 23:48:25
    下载权限 免费下载
    附件数量2 个下载链接
    浏览次数347
    讨论数量0
    主题状态正常讨论中

    本资源免费,登录后即可在本页「下载链接」区域获取全部附件。

    请在评论区留言说明具体链接,或联系作者补档。楼主更新后本页会同步最新的下载地址。

    请先阅读正文中的授权说明;未注明的默认仅限站内学习交流,禁止直接搬运到其他站点二次售卖。
    还没有讨论,快来抢沙发~
    登录 后才能参与讨论
    下载链接
    会员才能下载
    登录后开通会员
    同分类推荐
    暂无同类资源
    免费登录即可下载
    查看会员
    放大预览

    © 2026 剑庭安 - 网站开发定制 SEO 优化蜘蛛池代做 在线看手相算命. 保留所有权利。

    Powered by 酷像SEOv3.0

    网站地图 Sitemap.xml
    首页 全部分类 黑客安全 逆向|反编译 精品源码 站长工具自助开通会员其他资源SEO工具站群源码泛程序蜘蛛模板蜘蛛池外链工具创意乐园 搜索 会员服务 登录 注册