| name | windows-compile |
| description | 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.
|
| argument-hint | [repo-url-or-path] [x64|x86] |
| user-invocable | true |
| allowed-tools | Bash, Read, Write, Edit, Grep, Glob, WebFetch |
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:
-
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 #includes <iostream>.
-
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.
-
.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>.
-
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:
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
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
$CXX -O2 -municode -Ishim -I"$MCF_INC" -L"$MCF_LIB" \
-o out.exe main.cpp \
-l<libs...> -static -static-libgcc -static-libstdc++
Build a DLL with exported functions:
$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>'
Embed a DLL into an EXE as a resource (gotcha #4):
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
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:
# 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
file out.exe
x86_64-w64-mingw32-objdump -p out.exe | grep 'DLL Name'
-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.