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. AddpkgsCross.mingwW64.windows.mcfgthreads(as a NixbuildInput, or-I <dev>/include -L <out>/libfor 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 errorfromld— binutils 2.46'sldwon't accept a bare-LIBRARYmodule-definition file as a link input. If the source already hasextern "C" __declspec(dllexport)on its exports (most do), drop the.defand add-Wl,--kill-atto strip the@Nstdcall decoration so the export names stay clean. Verify withobjdump -p foo.dll | grep -i <name>.windrescan't find an embedded resource — a.rcthat references a payload with a backslash path (..\Release\payload.dll), or is UTF-16 inside a#ifdef WIN32/WIN64, won't resolve under Linuxwindres. Regenerate a minimal UTF-8.rcwith a forward-slash path instead of fighting the original. Pointwindresat 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:
# 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:
$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):
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:
# 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 # => 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.