# Windows Compile

> Cross-compile Windows PE binaries (offensive C/C++ PoCs, DLLs, service abuse tools) on Linux with the MinGW-w64 cross toolchain — no Wine, no VM. Use when a target needs a .exe/.dll built from a Visual Studio / .vcxproj source tree (Perfusion, PrintSpoofer, potato variants, custom shellcode loaders). Encodes the Linux-vs-MSVC gotchas that break a naive build.

- Skill: `tokugero/windows-compile` (Agent Skill)
- Install (CLI): `npx skillmds@latest add tokugero/windows-compile`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tokugero/windows-compile/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tokugero (https://skillmd.com/u/tokugero)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tokugero/windows-compile

---


# Windows Cross-Compilation (MinGW-w64 on Linux)

Build native Windows PE32+/PE32 binaries from a Linux CTF room. NixOS ships
the whole toolchain under `pkgsCross.mingwW64` — reach for it before ever
considering a Windows VM or `cl.exe`.

```
x86_64-w64-mingw32-g++   # x64 target   (pkgsCross.mingwW64)
i686-w64-mingw32-g++     # x86 target   (pkgsCross.mingw32)
```

## When this applies vs. not

MinGW handles plain Win32 / COM / WMI / service C++ — which is **most**
privesc PoCs (Perfusion, PrintSpoofer, GodPotato/JuicyPotato variants, token
loaders, reflective DLLs). It does **not** handle MSVC-only source: `#import`
of COM typelibs, ATL/MFC, C++/CLI (`/clr`), or SEH `__try`/`__except`. If a
build dies on those, fall back to `msvc-wine` (real `cl.exe` under Wine) — but
try MinGW first; it almost always works for offensive tooling.

## The four Linux-vs-MSVC gotchas (fix all of these)

MSVC hides these; on Linux they surface as build errors. Encode the fixes and
they stop mattering:

1. **`mcfgthread/gthr.h: No such file`** — MinGW-w64's default thread model
   pulls mcfgthreads headers + static lib, not auto-wired. Add
   `pkgsCross.mingwW64.windows.mcfgthreads` (as a Nix `buildInput`, or `-I
   <dev>/include -L <out>/lib` for an ad-hoc shell). Needed the moment any
   source `#include`s `<iostream>`.

2. **`Windows.h: No such file`** — source writes `<Windows.h>` / `<Wbemidl.h>`
   / `<Shlwapi.h>`; MinGW ships them **lowercase** (`windows.h`, …). MSVC's
   filesystem is case-insensitive so it never mattered. Fix with a symlink
   **shim dir** (`Windows.h -> windows.h`) added via `-I` — see snippet below.
   Never patch the upstream source for this.

3. **`.def: syntax error` from `ld`** — binutils 2.46's `ld` won't accept a
   bare-`LIBRARY` module-definition file as a link input. If the source already
   has `extern "C" __declspec(dllexport)` on its exports (most do), **drop the
   `.def`** and add `-Wl,--kill-at` to strip the `@N` stdcall decoration so the
   export names stay clean. Verify with `objdump -p foo.dll | grep -i <name>`.

4. **`windres` can't find an embedded resource** — a `.rc` that references a
   payload with a **backslash** path (`..\Release\payload.dll`), or is UTF-16
   inside a `#ifdef WIN32/WIN64`, won't resolve under Linux `windres`.
   Regenerate a minimal UTF-8 `.rc` with a forward-slash path instead of
   fighting the original. Point `windres` at the dir with `-I`.

## Recipe A — ad-hoc build (no flake changes)

Fastest for one-off tools. Drop into a shell with the toolchain and go:

```bash
# x64 toolchain + resource compiler on PATH
nix shell nixpkgs#pkgsCross.mingwW64.buildPackages.gcc \
          nixpkgs#pkgsCross.mingwW64.buildPackages.binutils

CXX=x86_64-w64-mingw32-g++
MCF_INC=$(nix eval --raw nixpkgs#pkgsCross.mingwW64.windows.mcfgthreads.dev)/include
MCF_LIB=$(nix build --no-link --print-out-paths nixpkgs#pkgsCross.mingwW64.windows.mcfgthreads)/lib
SYSINC=$(nix build --no-link --print-out-paths nixpkgs#pkgsCross.mingwW64.windows.mingw_w64.dev)/include

# gotcha #2: case-fixup shim for every mixed-case <Header.h> in the source
mkdir -p shim
grep -rhoE '#include <[A-Za-z0-9_]+\.h>' . | sed -E 's/#include <(.*)>/\1/' | sort -u | \
while read -r h; do
  low=$(echo "$h" | tr '[:upper:]' '[:lower:]')
  [ "$h" != "$low" ] && [ -e "$SYSINC/$low" ] && ln -sf "$SYSINC/$low" "shim/$h"
done

# compile (add the win libs the source's #pragma comment(lib,...) named:
#   wbemuuid->-lwbemuuid, ole32->-lole32, oleaut32->-loleaut32,
#   Shlwapi->-lshlwapi, Rpcrt4->-lrpcrt4, ws2_32->-lws2_32, crypt32->-lcrypt32)
$CXX -O2 -municode -Ishim -I"$MCF_INC" -L"$MCF_LIB" \
  -o out.exe main.cpp \
  -l<libs...> -static -static-libgcc -static-libstdc++
# -municode ONLY if the entry point is wmain/wWinMain
```

Build a **DLL** with exported functions:

```bash
$CXX -O2 -shared -municode -Ishim -I"$MCF_INC" -L"$MCF_LIB" \
  -o payload.dll payload.cpp -Wl,--kill-at -l<libs...> \
  -static -static-libgcc -static-libstdc++
x86_64-w64-mingw32-objdump -p payload.dll | grep -iE '<ExportName>'   # confirm exports
```

**Embed a DLL into an EXE as a resource** (gotcha #4):

```bash
printf '#include "resource.h"\nIDR_RCDATA1 RCDATA "payload.dll"\n' > payload.rc
x86_64-w64-mingw32-windres -I . payload.rc -O coff -o res.o
# then add res.o as an input to the EXE link line above
```

For an **x86 (32-bit)** target, swap the package set to
`pkgsCross.mingw32` and the prefix to `i686-w64-mingw32-`.

## Recipe B — pin it into the room flake (reproducible, GC-safe)

When you want `nix build .#<tool>` to reproduce the binary. Add to `flake.nix`:

```nix
  # in the let block
  mingw     = pkgs.pkgsCross.mingwW64;
  mingwInc  = "${mingw.windows.mingw_w64.dev}/include";
  winPrefix = mingw.stdenv.cc.targetPrefix;          # "x86_64-w64-mingw32-"

  # a worked example — Perfusion (RpcEptMapper Performance-key -> SYSTEM).
  # Single-EXE exploit: the payload DLL is embedded as RCDATA and written to
  # disk by the EXE at runtime, so only Perfusion.exe ships to the target.
  perfusion = mingw.stdenv.mkDerivation {
    pname = "perfusion"; version = "0-unstable-2021-04-22";
    src = pkgs.fetchFromGitHub {
      owner = "itm4n"; repo = "Perfusion";
      rev  = "e2ab7f94e6cacb9755d328dc29f27c2a5bbab59f";
      hash = "sha256-gipWw0q6x4jeR1jqFusboO/iYRucHOT0anmWqIlKtyw=";
    };
    buildInputs = [ mingw.windows.mcfgthreads ];     # gotcha #1 (headers+static lib)
    dontConfigure = true;
    buildPhase = ''
      runHook preBuild
      mkdir -p shim                                  # gotcha #2 (header case)
      grep -rhoE '#include <[A-Za-z0-9_]+\.h>' Perfusion PerfusionDll \
        | sed -E 's/#include <(.*)>/\1/' | sort -u | while read -r h; do
          low=$(echo "$h" | tr '[:upper:]' '[:lower:]')
          [ "$h" != "$low" ] && [ -e "${mingwInc}/$low" ] \
            && ln -sf "${mingwInc}/$low" "shim/$h"
        done
      mkdir -p Release
      $CXX -O2 -shared -municode -Ishim \            # payload DLL (gotcha #3: __declspec, no .def)
        -o Release/PerfusionDll.dll PerfusionDll/PerfusionDll.cpp \
        -Wl,--kill-at -lshlwapi -lrpcrt4 -static -static-libgcc -static-libstdc++
      printf '#include "resource.h"\nIDR_RCDATA1 RCDATA "Release/PerfusionDll.dll"\n' > payload.rc
      ${winPrefix}windres -I Perfusion -I . payload.rc -O coff -o perf_res.o  # gotcha #4
      $CXX -O2 -municode -Ishim \                    # loader EXE (wmain -> -municode)
        -o Perfusion.exe Perfusion/Perfusion.cpp perf_res.o \
        -lwbemuuid -lole32 -loleaut32 -lshlwapi -static -static-libgcc -static-libstdc++
      runHook postBuild
    '';
    installPhase = ''
      runHook preInstall
      install -Dm755 Perfusion.exe Release/PerfusionDll.dll -t $out/bin
      runHook postInstall
    '';
  };
```

Expose it under the flake outputs: `packages.perfusion = perfusion;` and,
optionally, a `devShells.cross-win` carrying
`[ mingw.buildPackages.gcc mingw.buildPackages.binutils mingw.windows.mcfgthreads ]`
for interactive iteration.

## Verify before shipping to a target

```bash
file out.exe                                   # => PE32+ ... for MS Windows, x86-64
x86_64-w64-mingw32-objdump -p out.exe | grep 'DLL Name'   # imports resolvable on target?
```

`-static -static-libgcc -static-libstdc++` yields a self-contained binary — no
MinGW runtime DLLs needed on the box. Drop it via your existing SMB/HTTP/evil-winrm
upload path and run from the foothold shell.

