mirror of
https://github.com/JLospinoso/gargoyle
synced 2026-06-06 15:54:31 +00:00
feat(native): complete Gargoyle refresh backlog
Complete the native build/runtime refresh while preserving the Win32 proof-of-concept baseline. - add the GargoyleX64 sibling prototype with x64 NASM PIC and timer reentry parity - consolidate Visual C++ and NASM behavior into shared MSBuild props/targets - expand just recipes and the acceptance harness across x86/x64 Debug/Release builds - harden native quality gates with MSVC warnings-as-errors, code analysis, and ASan build coverage - refresh README and MkDocs architecture, validation, responsible-use, references, and future-work docs Validation: - just ci - just native-check - Debug and Release live acceptance - Debug x64 smoke run Closes #2. Closes #13. Closes #14. Closes #15. Closes #16. Closes #17. Closes #18. Closes #19.
This commit is contained in:
@@ -271,4 +271,5 @@ coverage.xml
|
||||
htmlcov/
|
||||
site/
|
||||
dist/
|
||||
asan/
|
||||
|
||||
|
||||
+14
-2
@@ -1,20 +1,32 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 14
|
||||
VisualStudioVersion = 14.0.25420.1
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.0.11102.4
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Gargoyle", "Gargoyle.vcxproj", "{76CDDD0D-EA56-4AA9-9B94-41390A34AB23}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GargoyleX64", "GargoyleX64\GargoyleX64.vcxproj", "{D7F13E1F-6B38-4E38-8B11-30E8B9AF4601}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x86 = Debug|x86
|
||||
Debug|x64 = Debug|x64
|
||||
Release|x86 = Release|x86
|
||||
Release|x64 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{76CDDD0D-EA56-4AA9-9B94-41390A34AB23}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{76CDDD0D-EA56-4AA9-9B94-41390A34AB23}.Debug|x86.Build.0 = Debug|Win32
|
||||
{76CDDD0D-EA56-4AA9-9B94-41390A34AB23}.Debug|x64.ActiveCfg = Debug|Win32
|
||||
{76CDDD0D-EA56-4AA9-9B94-41390A34AB23}.Release|x86.ActiveCfg = Release|Win32
|
||||
{76CDDD0D-EA56-4AA9-9B94-41390A34AB23}.Release|x86.Build.0 = Release|Win32
|
||||
{76CDDD0D-EA56-4AA9-9B94-41390A34AB23}.Release|x64.ActiveCfg = Release|Win32
|
||||
{D7F13E1F-6B38-4E38-8B11-30E8B9AF4601}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{D7F13E1F-6B38-4E38-8B11-30E8B9AF4601}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{D7F13E1F-6B38-4E38-8B11-30E8B9AF4601}.Debug|x64.Build.0 = Debug|x64
|
||||
{D7F13E1F-6B38-4E38-8B11-30E8B9AF4601}.Release|x86.ActiveCfg = Release|x64
|
||||
{D7F13E1F-6B38-4E38-8B11-30E8B9AF4601}.Release|x64.ActiveCfg = Release|x64
|
||||
{D7F13E1F-6B38-4E38-8B11-30E8B9AF4601}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
+5
-51
@@ -17,34 +17,14 @@
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="build\Gargoyle.Configuration.props" />
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<Import Project="build\Gargoyle.Cpp.props" />
|
||||
<ItemGroup>
|
||||
<CustomBuild Include="setup.nasm">
|
||||
<FileType>Document</FileType>
|
||||
<Message>Assembling setup.nasm</Message>
|
||||
<Command>nasm -f bin setup.nasm -o $(Configuration)\setup.pic</Command>
|
||||
<Outputs>$(Configuration)\setup.pic</Outputs>
|
||||
</CustomBuild>
|
||||
<CustomBuild Include="gadget.nasm">
|
||||
<FileType>Document</FileType>
|
||||
<Message>Assembling gadget.nasm</Message>
|
||||
<Command>nasm -f bin gadget.nasm -o $(Configuration)\gadget.pic</Command>
|
||||
<Outputs>$(Configuration)\gadget.pic</Outputs>
|
||||
</CustomBuild>
|
||||
<NasmPic Include="setup.nasm" />
|
||||
<NasmPic Include="gadget.nasm" />
|
||||
</ItemGroup>
|
||||
<Import Project="build\NasmPic.targets" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
@@ -56,45 +36,19 @@
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
<LocalDebuggerWorkingDirectory>$(OutDir)</LocalDebuggerWorkingDirectory>
|
||||
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
<LocalDebuggerWorkingDirectory>$(OutDir)</LocalDebuggerWorkingDirectory>
|
||||
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
<PrecompiledHeader>
|
||||
</PrecompiledHeader>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>DbgHelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<PrecompiledHeader>
|
||||
</PrecompiledHeader>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>DbgHelp.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<CustomBuild Include="setup.nasm" />
|
||||
<CustomBuild Include="gadget.nasm" />
|
||||
<NasmPic Include="setup.nasm">
|
||||
<Filter>Source Files</Filter>
|
||||
</NasmPic>
|
||||
<NasmPic Include="gadget.nasm">
|
||||
<Filter>Source Files</Filter>
|
||||
</NasmPic>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" 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">
|
||||
<ProjectGuid>{D7F13E1F-6B38-4E38-8B11-30E8B9AF4601}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>GargoyleX64</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="..\build\Gargoyle.Configuration.props" />
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<Import Project="..\build\Gargoyle.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="main_x64.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<NasmPic Include="setup_x64.nasm" />
|
||||
<NasmPic Include="reentry_x64.nasm" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\build\NasmPic.targets" />
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx;nasm</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="main_x64.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<NasmPic Include="setup_x64.nasm">
|
||||
<Filter>Source Files</Filter>
|
||||
</NasmPic>
|
||||
<NasmPic Include="reentry_x64.nasm">
|
||||
<Filter>Source Files</Filter>
|
||||
</NasmPic>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,77 @@
|
||||
# Gargoyle x64 Example
|
||||
|
||||
This directory contains a sibling x64 example for issue #2. It does not replace
|
||||
the historical Win32 proof of concept in the repository root.
|
||||
|
||||
The example is intentionally small and benign:
|
||||
|
||||
- `GargoyleX64.vcxproj` builds only `Platform=x64` through the root
|
||||
`Gargoyle.sln`.
|
||||
- `setup_x64.nasm` is the main raw x64 PIC entry point.
|
||||
- `reentry_x64.nasm` is the executable wait/APC re-entry PIC.
|
||||
- `main_x64.cpp` loads both PIC blobs, prepares a pointer-sized configuration
|
||||
block, resolves the Windows APIs the PIC needs, and invokes the setup PIC.
|
||||
- The payload is a repeating `MessageBoxA` with the caption/text
|
||||
`gargoyle x64`.
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```powershell
|
||||
$nasmDir = Join-Path $env:LOCALAPPDATA 'bin\NASM'
|
||||
if (Test-Path (Join-Path $nasmDir 'nasm.exe')) { $env:Path = "$nasmDir;$env:Path" }
|
||||
MSBuild.exe Gargoyle.sln /p:Configuration=Debug /p:Platform=x64 /m
|
||||
MSBuild.exe Gargoyle.sln /p:Configuration=Release /p:Platform=x64 /m
|
||||
```
|
||||
|
||||
With the current Visual Studio defaults, the root solution emits the x64 outputs
|
||||
under `x64\Debug\` or `x64\Release\`. Run the executable from its output
|
||||
directory so it can find `setup_x64.pic` and `reentry_x64.pic`:
|
||||
|
||||
```powershell
|
||||
Push-Location x64\Debug
|
||||
.\GargoyleX64.exe
|
||||
Pop-Location
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
The x64 path is a sibling design rather than a direct port of `setup.nasm`. The
|
||||
Win32 PIC depends on stack arguments and an `esp`-based stack pivot. The x64
|
||||
example instead uses two PIC blobs:
|
||||
|
||||
- `setup_x64.pic` owns the setup state and benign payload loop.
|
||||
- `reentry_x64.pic` remains executable while `setup_x64.pic` is parked
|
||||
read-only during alertable waits.
|
||||
|
||||
The setup PIC creates a waitable timer, registers the callback entry inside the
|
||||
re-entry PIC, displays the benign MessageBox payload, and then calls the re-entry
|
||||
PIC's wait entry. The wait entry marks `setup_x64.pic` `PAGE_READONLY` and enters
|
||||
`WaitForSingleObjectEx(..., TRUE)`. Timer re-entry restores
|
||||
`setup_x64.pic` to `PAGE_EXECUTE_READ`, returns to the setup loop, and displays
|
||||
the benign payload again.
|
||||
|
||||
The implementation follows the Win64 ABI requirements that must be correct
|
||||
before larger designs are considered:
|
||||
|
||||
- the configuration block uses pointer-sized fields;
|
||||
- the setup PIC receives its configuration pointer in `rcx`;
|
||||
- API calls place the first four arguments in `rcx`, `rdx`, `r8`, and `r9`;
|
||||
- every call reserves the required 32-byte shadow space;
|
||||
- the stack is kept 16-byte aligned across calls;
|
||||
- fifth and sixth arguments are passed on the stack; and
|
||||
- nonvolatile registers used by the PIC are preserved.
|
||||
|
||||
## Validation
|
||||
|
||||
Run the platform-aware acceptance harness from a Windows desktop session:
|
||||
|
||||
```powershell
|
||||
uv run --all-groups gargoyle-acceptance --configuration Debug --platform x64
|
||||
uv run --all-groups gargoyle-acceptance --configuration Release --platform x64
|
||||
```
|
||||
|
||||
The harness validates the x64 setup banner and closes two benign `gargoyle x64`
|
||||
MessageBox rounds. The first window proves the initial PIC handoff; the second
|
||||
window proves timer/APC re-entry.
|
||||
@@ -0,0 +1,161 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
using PicEntry = void (*)(void*);
|
||||
constexpr DWORD invocation_interval_ms = 15 * 1000;
|
||||
constexpr size_t reentry_callback_offset = 16;
|
||||
|
||||
struct X64Configuration {
|
||||
uint64_t initialized;
|
||||
void* setup_address;
|
||||
uint64_t setup_length;
|
||||
void* reentry_wait;
|
||||
void* reentry_callback;
|
||||
void* VirtualProtectEx;
|
||||
void* WaitForSingleObjectEx;
|
||||
void* CreateWaitableTimerW;
|
||||
void* SetWaitableTimer;
|
||||
void* MessageBoxA;
|
||||
void* sleep_handle;
|
||||
int64_t due_time;
|
||||
uint32_t interval;
|
||||
uint32_t old_protection;
|
||||
};
|
||||
|
||||
static_assert(sizeof(void*) == 8, "GargoyleX64 must be built as a 64-bit target.");
|
||||
static_assert(offsetof(X64Configuration, initialized) == 0x00);
|
||||
static_assert(offsetof(X64Configuration, setup_address) == 0x08);
|
||||
static_assert(offsetof(X64Configuration, setup_length) == 0x10);
|
||||
static_assert(offsetof(X64Configuration, reentry_wait) == 0x18);
|
||||
static_assert(offsetof(X64Configuration, reentry_callback) == 0x20);
|
||||
static_assert(offsetof(X64Configuration, VirtualProtectEx) == 0x28);
|
||||
static_assert(offsetof(X64Configuration, WaitForSingleObjectEx) == 0x30);
|
||||
static_assert(offsetof(X64Configuration, CreateWaitableTimerW) == 0x38);
|
||||
static_assert(offsetof(X64Configuration, SetWaitableTimer) == 0x40);
|
||||
static_assert(offsetof(X64Configuration, MessageBoxA) == 0x48);
|
||||
static_assert(offsetof(X64Configuration, sleep_handle) == 0x50);
|
||||
static_assert(offsetof(X64Configuration, due_time) == 0x58);
|
||||
static_assert(offsetof(X64Configuration, interval) == 0x60);
|
||||
static_assert(offsetof(X64Configuration, old_protection) == 0x64);
|
||||
|
||||
vector<uint8_t> read_binary(const string& filename) {
|
||||
fstream stream{ filename, fstream::in | fstream::ate | fstream::binary };
|
||||
if (!stream) {
|
||||
throw runtime_error("[-] Couldn't open \"" + filename + "\".");
|
||||
}
|
||||
|
||||
const auto size = static_cast<size_t>(stream.tellg());
|
||||
stream.seekg(0, fstream::beg);
|
||||
|
||||
auto result = vector<uint8_t>(size);
|
||||
stream.read(reinterpret_cast<char*>(result.data()), result.size());
|
||||
if (!stream) {
|
||||
throw runtime_error("[-] Couldn't read \"" + filename + "\".");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void* resolve_export(const wchar_t* module_name, const char* export_name) {
|
||||
auto module = GetModuleHandleW(module_name);
|
||||
if (!module) {
|
||||
module = LoadLibraryW(module_name);
|
||||
}
|
||||
if (!module) {
|
||||
throw runtime_error("[-] Couldn't load module.");
|
||||
}
|
||||
|
||||
const auto address = GetProcAddress(module, export_name);
|
||||
if (!address) {
|
||||
throw runtime_error("[-] Couldn't GetProcAddress.");
|
||||
}
|
||||
return reinterpret_cast<void*>(address);
|
||||
}
|
||||
|
||||
void* allocate_pic(const string& filename, size_t& pic_size) {
|
||||
const auto bytes = read_binary(filename);
|
||||
pic_size = bytes.size();
|
||||
|
||||
const auto memory = VirtualAlloc(nullptr, pic_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if (!memory) {
|
||||
throw runtime_error("[-] Couldn't VirtualAlloc.");
|
||||
}
|
||||
|
||||
memcpy(memory, bytes.data(), bytes.size());
|
||||
|
||||
DWORD old_protection;
|
||||
const auto protected_memory = VirtualProtect(memory, pic_size, PAGE_EXECUTE_READ, &old_protection);
|
||||
if (!protected_memory) {
|
||||
throw runtime_error("[-] Couldn't VirtualProtect.");
|
||||
}
|
||||
return memory;
|
||||
}
|
||||
}
|
||||
|
||||
void launch(const string& setup_pic_path) {
|
||||
printf("[ ] Loading x64 setup PIC from \"%s\".\n", setup_pic_path.c_str());
|
||||
size_t setup_size;
|
||||
const auto setup_memory = allocate_pic(setup_pic_path, setup_size);
|
||||
printf("[+] Loaded %zu bytes of x64 PIC.\n", setup_size);
|
||||
|
||||
const auto reentry_pic_path = "reentry_x64.pic";
|
||||
printf("[ ] Loading x64 re-entry PIC from \"%s\".\n", reentry_pic_path);
|
||||
size_t reentry_size;
|
||||
const auto reentry_memory = allocate_pic(reentry_pic_path, reentry_size);
|
||||
const auto reentry_callback =
|
||||
static_cast<void*>(static_cast<uint8_t*>(reentry_memory) + reentry_callback_offset);
|
||||
printf("[+] Loaded %zu bytes of x64 re-entry PIC.\n", reentry_size);
|
||||
|
||||
auto config = X64Configuration{};
|
||||
config.setup_address = setup_memory;
|
||||
config.setup_length = setup_size;
|
||||
config.reentry_wait = reentry_memory;
|
||||
config.reentry_callback = reentry_callback;
|
||||
config.VirtualProtectEx = resolve_export(L"kernel32.dll", "VirtualProtectEx");
|
||||
config.WaitForSingleObjectEx = resolve_export(L"kernel32.dll", "WaitForSingleObjectEx");
|
||||
config.CreateWaitableTimerW = resolve_export(L"kernel32.dll", "CreateWaitableTimerW");
|
||||
config.SetWaitableTimer = resolve_export(L"kernel32.dll", "SetWaitableTimer");
|
||||
config.MessageBoxA = resolve_export(L"user32.dll", "MessageBoxA");
|
||||
config.due_time = -static_cast<int64_t>(invocation_interval_ms) * 10000;
|
||||
config.interval = invocation_interval_ms;
|
||||
|
||||
printf("[+] x64 timer/APC prototype configured.\n");
|
||||
printf(" ================================\n");
|
||||
printf(" Gargoyle x64 PIC @ ----> 0x%p\n", setup_memory);
|
||||
printf(" x64 re-entry PIC @ ----> 0x%p\n", reentry_memory);
|
||||
printf(" x64 APC callback @ ---> 0x%p\n", reentry_callback);
|
||||
printf(" Configuration @ -------> 0x%p\n", &config);
|
||||
printf(" VirtualProtectEx @ ----> 0x%p\n", config.VirtualProtectEx);
|
||||
printf(" WaitForSingleObjectEx @ 0x%p\n", config.WaitForSingleObjectEx);
|
||||
printf(" CreateWaitableTimerW @ 0x%p\n", config.CreateWaitableTimerW);
|
||||
printf(" SetWaitableTimer @ ---> 0x%p\n", config.SetWaitableTimer);
|
||||
printf(" MessageBoxA @ --------> 0x%p\n", config.MessageBoxA);
|
||||
printf(" Timer period @ -------> %lu ms\n", invocation_interval_ms);
|
||||
printf("[ ] Entering benign x64 PIC payload loop.\n");
|
||||
|
||||
reinterpret_cast<PicEntry>(setup_memory)(&config);
|
||||
|
||||
printf("[-] x64 PIC returned unexpectedly.\n");
|
||||
}
|
||||
|
||||
int main() {
|
||||
setvbuf(stdout, nullptr, _IONBF, 0);
|
||||
try {
|
||||
launch("setup_x64.pic");
|
||||
} catch (const exception& e) {
|
||||
printf("%s\n", e.what());
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
BITS 64
|
||||
DEFAULT REL
|
||||
|
||||
STRUC X64Configuration
|
||||
.initialized: RESQ 1
|
||||
.setup_addr: RESQ 1
|
||||
.setup_length: RESQ 1
|
||||
.reentry_wait: RESQ 1
|
||||
.reentry_callback: RESQ 1
|
||||
.VirtualProtectEx: RESQ 1
|
||||
.WaitForSingleObjectEx: RESQ 1
|
||||
.CreateWaitableTimerW: RESQ 1
|
||||
.SetWaitableTimer: RESQ 1
|
||||
.MessageBoxA: RESQ 1
|
||||
.sleep_handle: RESQ 1
|
||||
.due_time: RESQ 1
|
||||
.interval: RESD 1
|
||||
.old_protection: RESD 1
|
||||
ENDSTRUC
|
||||
|
||||
wait_entry:
|
||||
jmp wait_body
|
||||
times 16 - ($ - wait_entry) db 0x90
|
||||
|
||||
callback_entry:
|
||||
jmp callback_body
|
||||
times 16 - ($ - callback_entry) db 0x90
|
||||
|
||||
; Called from setup_x64 as void (*wait_entry)(X64Configuration*).
|
||||
; This code remains executable while setup_x64.pic is parked PAGE_READONLY.
|
||||
wait_body:
|
||||
push rbx
|
||||
mov rbx, rcx
|
||||
sub rsp, 48
|
||||
|
||||
; VirtualProtectEx(GetCurrentProcess(), setup_addr, setup_length,
|
||||
; PAGE_READONLY, &old_protection)
|
||||
mov rcx, -1
|
||||
mov rdx, [rbx + X64Configuration.setup_addr]
|
||||
mov r8, [rbx + X64Configuration.setup_length]
|
||||
mov r9d, 0x02
|
||||
lea rax, [rbx + X64Configuration.old_protection]
|
||||
mov [rsp + 32], rax
|
||||
call [rbx + X64Configuration.VirtualProtectEx]
|
||||
|
||||
; WaitForSingleObjectEx(timer, INFINITE, TRUE)
|
||||
mov rcx, [rbx + X64Configuration.sleep_handle]
|
||||
mov edx, 0xFFFFFFFF
|
||||
mov r8d, 1
|
||||
call [rbx + X64Configuration.WaitForSingleObjectEx]
|
||||
|
||||
; Returning to setup_x64.pic is only safe after its page is executable again.
|
||||
; This is idempotent when the APC callback already restored PAGE_EXECUTE_READ.
|
||||
mov rcx, -1
|
||||
mov rdx, [rbx + X64Configuration.setup_addr]
|
||||
mov r8, [rbx + X64Configuration.setup_length]
|
||||
mov r9d, 0x20
|
||||
lea rax, [rbx + X64Configuration.old_protection]
|
||||
mov [rsp + 32], rax
|
||||
call [rbx + X64Configuration.VirtualProtectEx]
|
||||
|
||||
add rsp, 48
|
||||
pop rbx
|
||||
ret
|
||||
|
||||
; Called by the waitable timer APC as:
|
||||
; void CALLBACK TimerAPCProc(config, timer_low, timer_high).
|
||||
callback_body:
|
||||
push rbx
|
||||
mov rbx, rcx
|
||||
sub rsp, 48
|
||||
|
||||
; VirtualProtectEx(GetCurrentProcess(), setup_addr, setup_length,
|
||||
; PAGE_EXECUTE_READ, &old_protection)
|
||||
mov rcx, -1
|
||||
mov rdx, [rbx + X64Configuration.setup_addr]
|
||||
mov r8, [rbx + X64Configuration.setup_length]
|
||||
mov r9d, 0x20
|
||||
lea rax, [rbx + X64Configuration.old_protection]
|
||||
mov [rsp + 32], rax
|
||||
call [rbx + X64Configuration.VirtualProtectEx]
|
||||
|
||||
add rsp, 48
|
||||
pop rbx
|
||||
ret
|
||||
@@ -0,0 +1,61 @@
|
||||
BITS 64
|
||||
DEFAULT REL
|
||||
|
||||
STRUC X64Configuration
|
||||
.initialized: RESQ 1
|
||||
.setup_addr: RESQ 1
|
||||
.setup_length: RESQ 1
|
||||
.reentry_wait: RESQ 1
|
||||
.reentry_callback: RESQ 1
|
||||
.VirtualProtectEx: RESQ 1
|
||||
.WaitForSingleObjectEx: RESQ 1
|
||||
.CreateWaitableTimerW: RESQ 1
|
||||
.SetWaitableTimer: RESQ 1
|
||||
.MessageBoxA: RESQ 1
|
||||
.sleep_handle: RESQ 1
|
||||
.due_time: RESQ 1
|
||||
.interval: RESD 1
|
||||
.old_protection: RESD 1
|
||||
ENDSTRUC
|
||||
|
||||
; Call me like void (*entry)(void* configuration).
|
||||
; RCX holds the configuration pointer on Win64.
|
||||
|
||||
push rbx
|
||||
mov rbx, rcx
|
||||
sub rsp, 64 ; 32-byte shadow space, two stack arguments, 16-byte alignment.
|
||||
|
||||
cmp qword [rbx + X64Configuration.initialized], 0
|
||||
jne payload
|
||||
|
||||
; CreateWaitableTimerW(NULL, FALSE, NULL)
|
||||
xor ecx, ecx
|
||||
xor edx, edx
|
||||
xor r8d, r8d
|
||||
call [rbx + X64Configuration.CreateWaitableTimerW]
|
||||
mov [rbx + X64Configuration.sleep_handle], rax
|
||||
|
||||
; SetWaitableTimer(timer, &due_time, interval, reentry_callback, config, FALSE)
|
||||
mov rcx, rax
|
||||
lea rdx, [rbx + X64Configuration.due_time]
|
||||
mov r8d, [rbx + X64Configuration.interval]
|
||||
mov r9, [rbx + X64Configuration.reentry_callback]
|
||||
mov [rsp + 32], rbx
|
||||
mov qword [rsp + 40], 0
|
||||
call [rbx + X64Configuration.SetWaitableTimer]
|
||||
|
||||
mov qword [rbx + X64Configuration.initialized], 1
|
||||
|
||||
payload:
|
||||
xor ecx, ecx
|
||||
lea rdx, [gargoyle_text]
|
||||
lea r8, [gargoyle_text]
|
||||
mov r9d, 0x40
|
||||
call [rbx + X64Configuration.MessageBoxA]
|
||||
|
||||
mov rcx, rbx
|
||||
call [rbx + X64Configuration.reentry_wait]
|
||||
jmp payload
|
||||
|
||||
gargoyle_text:
|
||||
db 'gargoyle x64', 0
|
||||
@@ -4,7 +4,13 @@
|
||||
|
||||
# Building gargoyle
|
||||
|
||||
*gargoyle* is only implemented for 32-bit Windows (64-bit Windows on Windows is fine). The current build-only baseline is tested with:
|
||||
*gargoyle* is a Windows research proof of concept. The original 32-bit
|
||||
implementation remains the reference baseline (64-bit Windows on Windows is
|
||||
fine). This refresh also includes a sibling x64 example in `GargoyleX64\` that
|
||||
uses pointer-sized configuration, Win64 ABI PIC calls, and a benign timer/APC
|
||||
re-entry loop without replacing the Win32 design.
|
||||
|
||||
The current baseline is tested with:
|
||||
|
||||
* [Visual Studio](https://visualstudio.microsoft.com/downloads/): Visual Studio 18 with MSVC toolset `v145`, or a compatible retargeted Visual Studio C++ toolchain.
|
||||
* Windows 10 SDK. The project uses `WindowsTargetPlatformVersion` `10.0` so MSBuild selects the latest installed Windows 10 SDK.
|
||||
@@ -22,24 +28,63 @@ Clone *gargoyle*:
|
||||
git clone https://github.com/JLospinoso/gargoyle.git
|
||||
```
|
||||
|
||||
Open `Gargoyle.sln` and build the `Debug|x86` or `Release|x86` configuration. You can also build from PowerShell:
|
||||
Open `Gargoyle.sln` and build the `Debug|x86` or `Release|x86` configuration
|
||||
for the Win32 proof of concept. The same solution also contains the
|
||||
`GargoyleX64` example under `Debug|x64` and `Release|x64`. You can build the
|
||||
native examples from PowerShell:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' Gargoyle.sln /p:Configuration=Debug /p:Platform=x86 /m
|
||||
& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' Gargoyle.sln /p:Configuration=Release /p:Platform=x86 /m
|
||||
& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' Gargoyle.sln /p:Configuration=Debug /p:Platform=x64 /m
|
||||
& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' Gargoyle.sln /p:Configuration=Release /p:Platform=x64 /m
|
||||
```
|
||||
|
||||
The executable loads `setup.pic` and `gadget.pic` relative to the current working directory. The Visual Studio debugger is configured to run from the output directory so F5 can find those files. If you launch manually, run from `Debug\` or `Release\`.
|
||||
|
||||
Use the `justfile` recipes for the full refreshed build and validation path:
|
||||
|
||||
```powershell
|
||||
uv sync --all-groups
|
||||
just ci
|
||||
```
|
||||
|
||||
You can run the Python acceptance harness with [uv](https://docs.astral.sh/uv/):
|
||||
|
||||
```powershell
|
||||
uv sync --all-groups
|
||||
uv run gargoyle-acceptance --configuration Debug
|
||||
uv run gargoyle-acceptance --configuration Release
|
||||
uv run gargoyle-acceptance --configuration Debug --platform x64
|
||||
```
|
||||
|
||||
The harness builds the requested configuration, launches `Gargoyle.exe` from the output directory, validates the setup banner, and closes two benign `gargoyle` MessageBox windows to confirm initial PIC execution and timer re-entry. Use `uv run gargoyle-acceptance --help` for options.
|
||||
The harness builds the requested configuration and platform, launches the
|
||||
executable from the output directory, validates the setup banner, and closes two
|
||||
benign MessageBox windows to confirm initial PIC execution and timer re-entry.
|
||||
Use `uv run gargoyle-acceptance --help` for options.
|
||||
|
||||
The native quality gate is MSVC-based:
|
||||
|
||||
```powershell
|
||||
just native-check
|
||||
```
|
||||
|
||||
That recipe runs MSVC code analysis for Debug/Release on x86 and x64, then builds
|
||||
Debug AddressSanitizer variants for both platforms under the ignored `asan\`
|
||||
directory. The normal `just ci` gate includes this native quality pass.
|
||||
|
||||
The x64 example builds through the root solution:
|
||||
|
||||
```powershell
|
||||
just build-x64-all
|
||||
```
|
||||
|
||||
Run `GargoyleX64.exe` from its configuration output directory so it can find
|
||||
`setup_x64.pic` and `reentry_x64.pic`. With the current Visual Studio defaults,
|
||||
the root solution emits the x64 executable and PIC files under `x64\Debug\` or
|
||||
`x64\Release\`. The x64 example uses a separate executable re-entry PIC to park
|
||||
`setup_x64.pic` as read-only during alertable waits, restore execute permission
|
||||
on timer re-entry, and show the benign `gargoyle x64` MessageBox again.
|
||||
|
||||
There is some harness code in `main.cpp` that configures the following three components:
|
||||
|
||||
@@ -55,6 +100,18 @@ auto gadget_memory = get_gadget(use_mshtml, gadget_pic_path);
|
||||
|
||||
Every 15 seconds, gargoyle will pop up a message box. When you click ok, gargoyle sets up the tail calls to mark itself non-executable and to wait for the timer. For fun, use [Sysinternals's excellent VMMap tool](https://technet.microsoft.com/en-us/sysinternals/vmmap.aspx) to examine when *gargoyle*'s PIC is executable. If a message box is active, *gargoyle* will be executable. If it is not, *gargoyle* should not be executable. The PIC's address is printed to `stdout` just before the harness calls into the PIC.
|
||||
|
||||
## Documentation
|
||||
|
||||
The refreshed documentation includes:
|
||||
|
||||
* `docs/acceptance.md` for the Python acceptance harness.
|
||||
* `docs/validation-checklist.md` for automated and manual runtime checks.
|
||||
* `docs/win32-architecture.md` for the current Win32 configuration, stack, timer/APC, gadget, and protection-cycle layout.
|
||||
* `docs/x64-architecture.md` for the sibling x64 setup PIC and re-entry PIC layout.
|
||||
* `docs/responsible-use.md` for limitations, detection visibility, and responsible-use boundaries.
|
||||
* `docs/future-work.md` for x64 and post-x64 directions that should not bloat the core proof of concept.
|
||||
* `docs/references.md` for the original article, detection work, forensics research, and later related techniques.
|
||||
|
||||
# More information
|
||||
See the blog post [available at lospi.net](https://jlospinoso.github.io/security/assembly/c/cpp/developing/software/2017/03/04/gargoyle-memory-analysis-evasion.html) for more information.
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<LocalDebuggerWorkingDirectory>$(OutDir)</LocalDebuggerWorkingDirectory>
|
||||
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
|
||||
<CodeAnalysisRuleSet>NativeRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(EnableASAN)'=='true'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<TreatWarningAsError>true</TreatWarningAsError>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<AdditionalOptions>/FS /Zc:__cplusplus /Zc:preprocessor %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(EnableASAN)'=='true'">
|
||||
<ClCompile>
|
||||
<BasicRuntimeChecks>Default</BasicRuntimeChecks>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<PrecompiledHeader>
|
||||
</PrecompiledHeader>
|
||||
<Optimization>Disabled</Optimization>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<PrecompiledHeader>
|
||||
</PrecompiledHeader>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<CustomBuild Include="@(NasmPic)">
|
||||
<FileType>Document</FileType>
|
||||
<Message>Assembling %(Filename)%(Extension)</Message>
|
||||
<Command>nasm -f bin "%(FullPath)" -o "$(OutDir)%(Filename).pic"</Command>
|
||||
<Outputs>$(OutDir)%(Filename).pic</Outputs>
|
||||
</CustomBuild>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+26
-7
@@ -1,12 +1,14 @@
|
||||
# Acceptance Harness
|
||||
|
||||
The acceptance harness is a Python CLI that builds and launches the existing
|
||||
Win32 Gargoyle binary, validates the setup banner, and closes the benign
|
||||
MessageBox payload windows.
|
||||
Gargoyle binary, validates the setup banner, and closes the benign MessageBox
|
||||
payload windows. It defaults to the Win32 baseline and can also validate the
|
||||
x64 sibling example.
|
||||
|
||||
```powershell
|
||||
uv run gargoyle-acceptance --configuration Debug
|
||||
uv run gargoyle-acceptance --configuration Release
|
||||
uv run gargoyle-acceptance --configuration Debug --platform x64
|
||||
```
|
||||
|
||||
By default the harness:
|
||||
@@ -14,12 +16,29 @@ By default the harness:
|
||||
- requires Windows;
|
||||
- verifies `nasm.exe` is available on `PATH`;
|
||||
- discovers MSBuild or uses the `--msbuild` path;
|
||||
- builds `Gargoyle.sln` for `x86`;
|
||||
- checks that `Gargoyle.exe`, `setup.pic`, and `gadget.pic` exist;
|
||||
- builds `Gargoyle.sln` for the requested platform;
|
||||
- checks that the expected executable and PIC files exist;
|
||||
- launches from the configuration output directory;
|
||||
- waits for a complete setup banner with non-zero addresses;
|
||||
- closes two `gargoyle` MessageBox windows to confirm the first payload run and
|
||||
the timer/APC re-entry.
|
||||
- waits for a complete platform-specific setup banner with non-zero addresses;
|
||||
- closes two platform-specific MessageBox windows to confirm the first payload
|
||||
run and timer/APC re-entry.
|
||||
|
||||
Use `--rounds 1` for a shorter check that only confirms the initial PIC handoff.
|
||||
Use `--skip-build` when you want to validate already-built outputs.
|
||||
|
||||
## Platforms
|
||||
|
||||
`--platform x86` validates the original Win32 path:
|
||||
|
||||
- builds `Debug|x86` or `Release|x86`;
|
||||
- expects `Gargoyle.exe`, `setup.pic`, and `gadget.pic`;
|
||||
- parses the Win32 PIC, gadget, configuration, stack, and trampoline addresses;
|
||||
- closes MessageBox windows titled `gargoyle`.
|
||||
|
||||
`--platform x64` validates the sibling x64 path:
|
||||
|
||||
- builds `Debug|x64` or `Release|x64`;
|
||||
- expects `GargoyleX64.exe`, `setup_x64.pic`, and `reentry_x64.pic`;
|
||||
- parses the setup PIC, re-entry PIC, APC callback, configuration, and API
|
||||
addresses;
|
||||
- closes MessageBox windows titled `gargoyle x64`.
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# Future Work And Post-X64 Directions
|
||||
|
||||
Future Gargoyle work should preserve the tiny, understandable Win32 proof of
|
||||
concept while making research directions explicit. New experiments should be
|
||||
small, reviewable, and easy to separate from the baseline demo.
|
||||
|
||||
## Near-Term Direction
|
||||
|
||||
- Keep the Win32 path as the reference implementation. Do not replace it with a
|
||||
generalized framework or hide the original technique behind large abstraction
|
||||
layers.
|
||||
|
||||
- Keep the x64 example documented as a sibling architecture rather than a
|
||||
transparent port. The x64 calling convention, stack alignment, nonvolatile
|
||||
registers, unwind metadata, CFG, and CET all change the design constraints.
|
||||
|
||||
- Prefer visible, benign payload evidence for every architecture. The MessageBox
|
||||
pattern is intentionally simple because it makes runtime validation easy and
|
||||
keeps the repository in research territory.
|
||||
|
||||
- Keep defender observations next to offensive-mechanics explanations. Any
|
||||
future runtime change should update [Responsible Use, Limitations, And
|
||||
Detection](responsible-use.md) and [Validation Checklist](validation-checklist.md).
|
||||
|
||||
## X86 To X64 Contrast To Document
|
||||
|
||||
- Stack pivoting: the existing x86 implementation pivots through `esp`; x64
|
||||
work must account for `rsp` alignment, shadow space, and the Windows x64
|
||||
register argument convention.
|
||||
|
||||
- Callback shape: the current waitable timer path is compact, but x64 designs
|
||||
may choose waitable timers, timer queues, `NtContinue`-style context
|
||||
restoration, or another documented callback path. Each choice has different
|
||||
defensive visibility.
|
||||
|
||||
- Gadget assumptions: x86 `pop reg; pop esp; ret`-style pivots are not a direct
|
||||
recipe for x64. The checked-in x64 example uses a separate re-entry PIC; any
|
||||
future gadget or context-restoration design should explain provenance and
|
||||
failure modes without encouraging broad gadget harvesting.
|
||||
|
||||
- Mitigation interaction: CFG, CET, shadow stacks, and continuation-target
|
||||
validation can turn historical ROP assumptions into crashes or detections.
|
||||
Document those outcomes rather than treating them as blockers to bypass.
|
||||
|
||||
- Validation evidence: x64 work needs the same evidence standard as Win32:
|
||||
build output, setup banner, benign payload, protection transition, timer
|
||||
re-entry, and clear desktop-only checks.
|
||||
|
||||
## Possible Companion Detector
|
||||
|
||||
A small defensive companion could make the project more useful without changing
|
||||
the PoC:
|
||||
|
||||
- Read a process memory map and highlight private committed regions whose
|
||||
protections change between executable and non-executable states.
|
||||
|
||||
- Correlate the setup banner addresses with VMMap or debugger observations.
|
||||
|
||||
- Emit a short report suitable for the validation checklist.
|
||||
|
||||
- Stay local and observational. Do not inject into unrelated processes, bypass
|
||||
product protections, or collect private data.
|
||||
|
||||
## Experiment Placement
|
||||
|
||||
- Keep the default branch focused on the buildable baseline and documented
|
||||
validation workflow.
|
||||
|
||||
- Put alternate x64 re-entry work in small branches or clearly named docs until
|
||||
it has a stable build and a benign acceptance story.
|
||||
|
||||
- Document experiments that are interesting but too large, too brittle, or too
|
||||
operational for this repository instead of merging them into the core.
|
||||
|
||||
- Prefer separate PRs when changes have different review surfaces: docs,
|
||||
Win32 hardening, x64 implementation, and detector experiments should not be
|
||||
tangled unless the plan explicitly calls for integration.
|
||||
|
||||
## Research Questions
|
||||
|
||||
- How does the original waitable-timer/APC design compare with later
|
||||
timer-queue and `NtContinue` sleep-obfuscation families from a defender's
|
||||
point of view?
|
||||
|
||||
- Which observations are stable across Windows versions: memory protections,
|
||||
VAD state, timer objects, APC delivery, stack shape, and callback targets?
|
||||
|
||||
- Can a tiny detector demonstrate the defensive visibility without requiring
|
||||
kernel components or product-specific telemetry?
|
||||
|
||||
- Which x64 design choices are educational enough to include here, and which
|
||||
belong only as references to external work such as YouMayPasser, Cronos, or
|
||||
ShellcodeFluctuation?
|
||||
|
||||
- The refresh audit mentioned "Mirage" as later related work. What exact public
|
||||
source should be cited, and does it belong in the same sleep-obfuscation
|
||||
family tree?
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No credential theft, persistence, lateral movement, autonomous exploitation,
|
||||
staged payload deployment, or C2-oriented features.
|
||||
|
||||
- No attempt to keep pace with commercial evasion frameworks.
|
||||
|
||||
- No broad payload loader, plugin system, or operator workflow.
|
||||
|
||||
- No mitigation-bypass arms race. Platform failures and detections should be
|
||||
documented as useful research outcomes.
|
||||
|
||||
## Issue Coverage
|
||||
|
||||
This page contributes to:
|
||||
|
||||
- #13 by keeping the refresh scope explicit.
|
||||
- #19 by recording post-x64 directions that do not bloat the core PoC.
|
||||
- #17 by carrying responsible-use boundaries into future experiments.
|
||||
- #18 by requiring a validation story for future architecture work.
|
||||
+5
-3
@@ -3,7 +3,8 @@
|
||||
Gargoyle is a small Windows proof of concept for memory-scanner evasion.
|
||||
|
||||
The 2026 refresh keeps the original Win32 artifact intact while adding modern
|
||||
build tooling, acceptance validation, and documentation.
|
||||
build tooling, acceptance validation, documentation, and a sibling x64
|
||||
timer/APC example.
|
||||
|
||||
## Developer Commands
|
||||
|
||||
@@ -11,7 +12,8 @@ build tooling, acceptance validation, and documentation.
|
||||
uv sync --all-groups
|
||||
just check
|
||||
uv run gargoyle-acceptance --configuration Debug
|
||||
uv run gargoyle-acceptance --configuration Debug --platform x64
|
||||
```
|
||||
|
||||
Runtime validation is Windows-only because it launches `Gargoyle.exe` and
|
||||
interacts with its benign MessageBox payload.
|
||||
Runtime validation is Windows-only because it launches the native executable and
|
||||
interacts with benign MessageBox payloads.
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# Reference Map
|
||||
|
||||
This page places Gargoyle in the public research lineage around memory-scanner
|
||||
evasion, process-memory forensics, and later sleep-obfuscation work. It is a
|
||||
curated map, not an endorsement of every linked project or an implementation
|
||||
guide. The repository should remain a small, benign research artifact.
|
||||
|
||||
Access dates below use 2026-05-08.
|
||||
|
||||
## Original Work
|
||||
|
||||
- [Gargoyle: A memory scanning evasion technique](https://lospi.net/security/assembly/c/cpp/developing/software/2017/03/04/gargoyle-memory-analysis-evasion.html)
|
||||
is the original March 2017 article that explains the waitable-timer, APC,
|
||||
stack-pivot, and protection-flipping proof of concept. It is still the best
|
||||
starting point for understanding what this repository is intended to show.
|
||||
Accessed 2026-05-08.
|
||||
|
||||
- [JLospinoso/gargoyle](https://github.com/JLospinoso/gargoyle) is the
|
||||
original public proof-of-concept repository. The current refresh preserves the
|
||||
Win32 implementation while adding build, validation, and documentation
|
||||
scaffolding around it. Accessed 2026-05-08.
|
||||
|
||||
## Detection And Forensics
|
||||
|
||||
- [Hunting for Gargoyle Memory Scanning Evasion](https://blog.f-secure.com/hunting-for-gargoyle-memory-scanning-evasion/)
|
||||
documents F-Secure Countercept's defensive analysis and summarizes the core
|
||||
detection idea: code may be non-executable when memory scanners run, but the
|
||||
timer/APC and memory-layout artifacts remain observable. Accessed 2026-05-08.
|
||||
|
||||
- [WithSecureLabs volatility-plugins: gargoyle.py](https://github.com/WithSecureLabs/volatility-plugins/blob/master/gargoyle.py)
|
||||
is the public Volatility plugin associated with that detection work. It is a
|
||||
useful reference for Gargoyle-specific memory-forensics heuristics. Accessed
|
||||
2026-05-08.
|
||||
|
||||
- [Hiding Process Memory Via Anti-Forensic Techniques](https://www.sciencedirect.com/science/article/pii/S2666281720302614)
|
||||
is the DFRWS USA 2020 open-access paper that compares Gargoyle with stronger
|
||||
memory-hiding approaches and notes that Gargoyle changes scanner visibility
|
||||
rather than truly removing the memory range from the process map. Accessed
|
||||
2026-05-08.
|
||||
|
||||
- [Black Hat USA 2020 slides: Hiding Process Memory Via Anti-Forensic Techniques](https://i.blackhat.com/USA-20/Wednesday/us-20-Block-Hiding-Process-Memory-Via-Anti-Forensic-Techniques.pdf)
|
||||
are the conference slides for the same research thread and cite Gargoyle as
|
||||
prior work. Accessed 2026-05-08.
|
||||
|
||||
- [DFRWS-memory-subversion/DFRWS-USA-2020](https://github.com/DFRWS-memory-subversion/DFRWS-USA-2020)
|
||||
contains the companion materials for the DFRWS 2020 paper, including detection
|
||||
and subversion research artifacts. Accessed 2026-05-08.
|
||||
|
||||
- [Windows Memory Forensics: Detecting Unintentionally Hidden Injected Code by
|
||||
Examining Page Table Entries](https://dfrws.org/wp-content/uploads/2019/06/2019_USA_paper-windows_memory_forensics_detecting_unintentionally_hidden_injected_code_by_examining_page_table_entries.pdf)
|
||||
gives related page-table and memory-forensics context for injected code that
|
||||
ordinary virtual-address-descriptor views may miss or misclassify. Accessed
|
||||
2026-05-08.
|
||||
|
||||
## Direct Derivatives And Adjacent Experiments
|
||||
|
||||
- [Bypassing Memory Scanners with Cobalt Strike and Gargoyle](https://labs.withsecure.com/publications/experimenting-bypassing-memory-scanners-with-cobalt-strike-and-gargoyle)
|
||||
is an MWR/WithSecure experiment that applied the Gargoyle idea to a larger
|
||||
payload. It is useful here mainly because it made defender-visible artifacts
|
||||
explicit and helped motivate the later detection work. Accessed 2026-05-08.
|
||||
|
||||
- [waldo-irc/YouMayPasser](https://github.com/waldo-irc/YouMayPasser) describes
|
||||
itself as an x64 Gargoyle implementation and documents several indicators of
|
||||
compromise in its README. Treat it as lineage and design context rather than
|
||||
a target architecture for this repository. Accessed 2026-05-08.
|
||||
|
||||
- [mgeeky/ShellcodeFluctuation](https://github.com/mgeeky/ShellcodeFluctuation)
|
||||
cites Gargoyle as background for cyclically changing memory protections and
|
||||
optionally encrypting content while dormant. It is part of the broader family
|
||||
of memory-state fluctuation ideas that followed Gargoyle. Accessed
|
||||
2026-05-08.
|
||||
|
||||
## Later Sleep-Obfuscation Family
|
||||
|
||||
- [Idov31/Cronos](https://github.com/Idov31/Cronos) is a waitable-timer based
|
||||
sleep-obfuscation proof of concept that draws from the Ekko family and uses
|
||||
repeated protection and encryption state changes during idle periods. It is a
|
||||
useful comparison point because it keeps timers central while moving beyond
|
||||
Gargoyle's tiny Win32 shape. Accessed 2026-05-08.
|
||||
|
||||
- [Cronos Sleep Obfuscation](https://idov31.github.io/posts/cronos-sleep-obfuscation)
|
||||
is the companion write-up for Cronos and discusses how later sleep
|
||||
obfuscators evolved from simple memory-protection toggling into timer-driven
|
||||
encryption and re-entry strategies. Accessed 2026-05-08.
|
||||
|
||||
- [Understanding Sleep Obfuscation](https://binarydefense.com/resources/blog/understanding-sleep-obfuscation/)
|
||||
gives a defender-oriented comparison of Ekko, Cronos, Foliage, and related
|
||||
approaches. It is especially useful for thinking about detection categories
|
||||
rather than any single implementation. Accessed 2026-05-08.
|
||||
|
||||
- [Hunting for timer-queue timers](https://labs.withsecure.com/publications/hunting-for-timer-queue-timers)
|
||||
covers detection work for timer-queue based sleep obfuscation and contrasts
|
||||
that family with waitable-timer variants such as Cronos. Accessed 2026-05-08.
|
||||
|
||||
## Windows API And Tool References
|
||||
|
||||
- [VirtualProtectEx](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotectex)
|
||||
is the Win32 API Gargoyle uses to toggle the setup PIC between executable and
|
||||
non-executable protections. Accessed 2026-05-08.
|
||||
|
||||
- [SetWaitableTimer](https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-setwaitabletimer)
|
||||
documents the waitable timer and completion routine behavior that Gargoyle
|
||||
uses for re-entry. Accessed 2026-05-08.
|
||||
|
||||
- [WaitForSingleObjectEx](https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobjectex)
|
||||
documents alertable waits, which are important because queued APC completion
|
||||
routines only run when the relevant thread enters an alertable state. Accessed
|
||||
2026-05-08.
|
||||
|
||||
- [Using Waitable Timers with an Asynchronous Procedure Call](https://learn.microsoft.com/en-us/windows/win32/sync/using-a-waitable-timer-with-an-asynchronous-procedure-call)
|
||||
is Microsoft's conceptual example for waitable timers with APC completion
|
||||
routines. Accessed 2026-05-08.
|
||||
|
||||
- [VMMap](https://learn.microsoft.com/en-us/sysinternals/downloads/vmmap) is the
|
||||
Sysinternals process memory viewer used in the original demo instructions and
|
||||
in the refreshed manual validation checklist. Accessed 2026-05-08.
|
||||
|
||||
## Open Follow-Ups
|
||||
|
||||
- The initial refresh audit mentioned "Mirage" among later related work, but no
|
||||
reliable public source was verified during this docs pass. Keep it as a
|
||||
follow-up research item before adding it as a citation.
|
||||
|
||||
- The x64 lineage deserves an implementation-focused comparison now that
|
||||
Gargoyle has a sibling x64 timer/APC example. This page intentionally records
|
||||
public references without prescribing a broader evasion design.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Responsible Use, Limitations, And Detection
|
||||
|
||||
Gargoyle is a historical Windows research proof of concept. Its refreshed
|
||||
documentation should help maintainers, defenders, and researchers understand the
|
||||
technique, reproduce the benign demo, and recognize the artifacts it leaves
|
||||
behind. It should not evolve into a general offensive framework.
|
||||
|
||||
## Responsible-Use Boundaries
|
||||
|
||||
- Keep demo payloads benign. The repository's live runtime demonstration should
|
||||
remain the `gargoyle` MessageBox payload or an equally harmless local
|
||||
observation.
|
||||
|
||||
- Run the proof of concept only in systems you own or have explicit permission
|
||||
to use. Prefer disposable Windows VMs or lab machines where interactive window
|
||||
automation is acceptable.
|
||||
|
||||
- Do not add credential theft, persistence, lateral movement, autonomous
|
||||
exploitation, payload deployment, or operational misuse guidance.
|
||||
|
||||
- Treat references to larger payload experiments as historical and defensive
|
||||
context. They explain why the technique was studied and how defenders looked
|
||||
for it; they are not acceptance criteria for this repository.
|
||||
|
||||
- Preserve the small proof-of-concept shape. A maintainable research artifact is
|
||||
more valuable here than a broader evasion platform.
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- Architecture: the Win32/x86 path remains the reference implementation. The
|
||||
checked-in x64 sibling demonstrates timer/APC re-entry with a separate
|
||||
re-entry PIC, but it is not a transparent port of the Win32 stack-pivot chain.
|
||||
|
||||
- Calling convention: `setup.nasm` relies on 32-bit stack calling conventions,
|
||||
`esp`/`ebp` manipulation, and a compact `pop reg; pop esp; ret`-style pivot.
|
||||
The x64 sibling uses register arguments, shadow space, stack alignment, and a
|
||||
separate re-entry surface instead of copying those assumptions.
|
||||
|
||||
- Runtime environment: the acceptance harness needs a Windows desktop session
|
||||
because it closes visible `gargoyle` MessageBox windows. Headless CI can build
|
||||
and run Python checks, but the live MessageBox validation is local and
|
||||
interactive by design.
|
||||
|
||||
- Gadget source: the demo first tries to locate a compatible pivot gadget in a
|
||||
system DLL and falls back to the tiny `gadget.pic` artifact if needed. System
|
||||
DLL layout, mitigation policy, and Windows version differences can affect this
|
||||
path.
|
||||
|
||||
- Visibility: Gargoyle does not make memory disappear. The core idea is to make
|
||||
the PIC non-executable while idle so simplistic executable-page scans are less
|
||||
likely to classify it as code. Process memory maps, timer state, private
|
||||
allocations, and behavioral telemetry can still expose the demo.
|
||||
|
||||
- Mitigations and platform changes: Control Flow Guard, CET/shadow-stack
|
||||
behavior, stricter callback validation, endpoint telemetry, and debugger or
|
||||
sandbox instrumentation can all affect ROP, callback, and context-restoration
|
||||
assumptions. Treat failures as research data, not as reasons to add bypasses.
|
||||
|
||||
- Timing: the default cadence is intentionally obvious: a MessageBox appears
|
||||
about every 15 seconds. Scanner timing, scheduler behavior, and manual
|
||||
interaction can make observations slightly noisy.
|
||||
|
||||
## Defensive Visibility
|
||||
|
||||
Defenders and maintainers can reason about Gargoyle through several observable
|
||||
categories:
|
||||
|
||||
- Memory protection transitions: the setup PIC is executable while the payload
|
||||
runs and non-executable while the demo waits. VMMap, debuggers, ETW providers,
|
||||
or memory forensics can observe the region changing state.
|
||||
|
||||
- Private code-adjacent memory: the setup PIC, fallback gadget, scratch stack,
|
||||
and trampoline are allocated at runtime rather than loaded as ordinary module
|
||||
code. Even when non-executable, those regions remain part of the process
|
||||
address space.
|
||||
|
||||
- Timer and APC behavior: Gargoyle uses a waitable timer and an alertable wait
|
||||
path to re-enter the code. Auditing timer completion routines, APC delivery,
|
||||
and alertable waits can expose behavior that simple page scans miss.
|
||||
|
||||
- ROP and stack-pivot shape: the pivot gadget and stack trampoline are small but
|
||||
distinctive. Defensive work can look for unusual callback targets, stack
|
||||
movement into private allocations, and return paths that do not resemble
|
||||
ordinary compiler output.
|
||||
|
||||
- Console and UI artifacts: the refreshed demo prints platform-specific PIC,
|
||||
configuration, callback, and API addresses, then opens benign MessageBox
|
||||
windows titled `gargoyle` or `gargoyle x64`. Those are acceptance evidence,
|
||||
not stealth features.
|
||||
|
||||
- Forensics clues: Volatility-style plugins, page-table inspection, VAD review,
|
||||
and memory-map comparisons can all provide evidence even when an executable
|
||||
page scanner misses the idle PIC.
|
||||
|
||||
## Safe Defender Exercises
|
||||
|
||||
- Build the Debug and Release configurations and run the acceptance harness
|
||||
against the benign MessageBox payload.
|
||||
|
||||
- Record the setup banner addresses and inspect the Gargoyle process with VMMap
|
||||
before dismissing the MessageBox, after dismissing it, and after the next
|
||||
timer re-entry.
|
||||
|
||||
- Compare what user-mode memory-map tools show with what a memory-forensics
|
||||
workflow sees. Focus on whether the region exists, what its protection is, and
|
||||
whether timers or callback targets explain re-entry.
|
||||
|
||||
- Use the public references in [Reference Map](references.md) to compare
|
||||
Gargoyle-specific heuristics with broader sleep-obfuscation detection ideas.
|
||||
|
||||
## Issue Coverage
|
||||
|
||||
This page contributes to:
|
||||
|
||||
- #13 by framing the 2026 refresh as a research artifact.
|
||||
- #17 by documenting responsible use, limitations, and defender visibility.
|
||||
- #18 by naming the observations that the validation checklist should capture.
|
||||
- #19 by setting boundaries for future work.
|
||||
@@ -0,0 +1,153 @@
|
||||
# Validation Checklist
|
||||
|
||||
Use this checklist alongside the automated acceptance harness in
|
||||
[Acceptance Harness](acceptance.md). It is intentionally short and benign: the
|
||||
goal is to confirm that the Win32 proof of concept still demonstrates the
|
||||
documented memory-state transition without turning runtime validation into an
|
||||
operational playbook.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Windows desktop session where visible MessageBox windows are acceptable.
|
||||
- Current Visual Studio C++ toolchain and Windows 10 SDK.
|
||||
- NASM available on `PATH`.
|
||||
- `uv` and Python 3.13 available for the acceptance harness.
|
||||
- Optional: [Sysinternals VMMap](https://learn.microsoft.com/en-us/sysinternals/downloads/vmmap)
|
||||
for process memory inspection.
|
||||
- Optional: debugger or tracing tool for local research observations.
|
||||
|
||||
## Automated Checks
|
||||
|
||||
Run these before manual runtime validation:
|
||||
|
||||
```powershell
|
||||
uv sync --all-groups
|
||||
just build-all
|
||||
just native-check
|
||||
just check
|
||||
```
|
||||
|
||||
For the live acceptance path, run:
|
||||
|
||||
```powershell
|
||||
uv run --all-groups gargoyle-acceptance --configuration Debug
|
||||
uv run --all-groups gargoyle-acceptance --configuration Release
|
||||
uv run --all-groups gargoyle-acceptance --configuration Debug --platform x64
|
||||
uv run --all-groups gargoyle-acceptance --configuration Release --platform x64
|
||||
```
|
||||
|
||||
Expected automated evidence:
|
||||
|
||||
- `Gargoyle.exe`, `setup.pic`, and `gadget.pic` exist in each configuration
|
||||
output directory.
|
||||
- `GargoyleX64.exe`, `setup_x64.pic`, and `reentry_x64.pic` exist in each x64
|
||||
output directory.
|
||||
- MSVC code analysis completes for Debug/Release on x86 and x64 with warnings as
|
||||
errors.
|
||||
- AddressSanitizer Debug builds complete for x86 and x64 under `asan\`.
|
||||
- The setup banner includes non-zero addresses for the Gargoyle PIC, ROP gadget,
|
||||
configuration, stack bounds, and stack trampoline.
|
||||
- The x64 setup banner includes non-zero addresses for the setup PIC, re-entry
|
||||
PIC, APC callback, configuration, and imported APIs.
|
||||
- The harness closes at least two benign `gargoyle` MessageBox rounds when run
|
||||
with its default settings, confirming the initial handoff and one timer/APC
|
||||
re-entry.
|
||||
- The x64 harness closes at least two benign `gargoyle x64` MessageBox rounds,
|
||||
confirming the initial handoff and one timer/APC re-entry.
|
||||
|
||||
## Manual Runtime Checklist
|
||||
|
||||
1. Build `Debug|x86` or `Release|x86`.
|
||||
|
||||
2. Start `Gargoyle.exe` from the matching output directory so `setup.pic` and
|
||||
`gadget.pic` are beside the executable.
|
||||
|
||||
3. Save the console banner. Confirm the printed addresses are non-zero and that
|
||||
the PIC, configuration, stack, and trampoline are distinct regions.
|
||||
|
||||
4. When the first `gargoyle` MessageBox appears, inspect the process in VMMap or
|
||||
a comparable local tool. The setup PIC address from the banner should be in a
|
||||
committed region that is executable while the payload is active.
|
||||
|
||||
5. Dismiss the MessageBox. During the idle interval, refresh the memory view. The
|
||||
setup PIC region should remain committed but should no longer be executable.
|
||||
|
||||
6. Wait for the next MessageBox. The default interval is approximately 15
|
||||
seconds. A second window confirms timer/APC re-entry.
|
||||
|
||||
7. Dismiss the second MessageBox and close the process after collecting evidence.
|
||||
The demo should not create files, network connections, persistence, or
|
||||
non-benign payload effects.
|
||||
|
||||
## x64 Manual Runtime Checklist
|
||||
|
||||
1. Build `Debug|x64` or `Release|x64`.
|
||||
|
||||
2. Start `GargoyleX64.exe` from the matching output directory so `setup_x64.pic`
|
||||
and `reentry_x64.pic` are beside the executable.
|
||||
|
||||
3. Save the console banner. Confirm the setup PIC, re-entry PIC, APC callback,
|
||||
configuration, and imported API addresses are non-zero.
|
||||
|
||||
4. When the first `gargoyle x64` MessageBox appears, inspect the setup PIC
|
||||
address from the banner. It should be executable while the payload is active.
|
||||
|
||||
5. Dismiss the MessageBox. During the idle interval, the setup PIC should be
|
||||
parked as read-only while the separate re-entry PIC remains executable.
|
||||
|
||||
6. Wait for the next `gargoyle x64` MessageBox. The default interval is
|
||||
approximately 15 seconds. A second window confirms timer/APC re-entry.
|
||||
|
||||
7. Dismiss the second MessageBox and close the process after collecting
|
||||
evidence.
|
||||
|
||||
## Optional Diagnostic Observations
|
||||
|
||||
- Watch for `VirtualProtectEx` calls that make the setup PIC executable before
|
||||
payload execution and non-executable afterward.
|
||||
|
||||
- Watch for `SetWaitableTimer` and alertable `WaitForSingleObjectEx` behavior
|
||||
that explains why the timer completion routine runs on re-entry.
|
||||
|
||||
- Compare the fallback `gadget.pic` path with the system-DLL gadget path when
|
||||
`mshtml.dll` is absent or lacks a compatible pivot sequence.
|
||||
|
||||
- Record Windows version, Visual Studio toolset, NASM version, configuration,
|
||||
and whether the process ran under WOW64.
|
||||
|
||||
## CI-Safe Versus Desktop-Only
|
||||
|
||||
CI-safe:
|
||||
|
||||
- NASM assembly and Visual Studio compilation.
|
||||
- Python formatting, linting, typing, unit tests, and documentation builds.
|
||||
- Static checks that do not launch the interactive demo.
|
||||
|
||||
Desktop-only:
|
||||
|
||||
- MessageBox automation.
|
||||
- VMMap inspection.
|
||||
- Debugger or live process tracing.
|
||||
- Any observation that depends on a visible interactive session.
|
||||
|
||||
## Failure Clues
|
||||
|
||||
- Missing `setup.pic` or `gadget.pic`: launch from the configuration output
|
||||
directory or rebuild the project.
|
||||
|
||||
- Setup banner timeout: inspect the console output, artifact paths, and whether
|
||||
the process started from the expected working directory.
|
||||
|
||||
- Exit before the second MessageBox: suspect gadget selection, stack pivot,
|
||||
timer setup, or protection-restoration behavior.
|
||||
|
||||
- Memory remains executable while idle: inspect the `VirtualProtectEx` tail-call
|
||||
path and confirm the observed region matches the banner's PIC address.
|
||||
|
||||
## Issue Coverage
|
||||
|
||||
This page contributes to:
|
||||
|
||||
- #13 by making the refreshed artifact easier to verify.
|
||||
- #18 by capturing reproducible automated and manual validation steps.
|
||||
- #17 by keeping live validation tied to benign, observable behavior.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Win32 Architecture
|
||||
|
||||
Gargoyle's current runtime is a small 32-bit Windows proof of concept. The
|
||||
supported path is the Win32 build produced by `Gargoyle.sln` with `Platform=x86`,
|
||||
NASM on `PATH`, and a current Visual Studio C++ toolchain targeting Windows SDK
|
||||
`10.0`.
|
||||
|
||||
The demo intentionally keeps the payload benign: each invocation displays a
|
||||
`gargoyle` MessageBox. The interesting behavior is the setup around that payload,
|
||||
not the payload itself.
|
||||
|
||||
## Runtime Layout
|
||||
|
||||
`main.cpp` allocates three executable or mutable regions:
|
||||
|
||||
- `setup.pic`, assembled from `setup.nasm`, contains the position-independent
|
||||
setup and re-entry code.
|
||||
- A ROP gadget is selected from an executable section of `mshtml.dll` when
|
||||
possible. If that search fails, `gadget.pic` provides a tiny fallback gadget.
|
||||
- A read/write workspace contains the configuration block, scratch stack, and
|
||||
trampoline frame consumed by the setup PIC.
|
||||
|
||||
The C++ `SetupConfiguration` and `StackTrampoline` structures mirror the NASM
|
||||
layout in `setup.nasm`. Keep those layouts paired: the setup PIC reads fields by
|
||||
offset rather than by symbol names.
|
||||
|
||||
## Control Flow
|
||||
|
||||
1. `main.cpp` loads `setup.pic` as executable memory, then searches for a
|
||||
stack-pivot gadget.
|
||||
2. The workspace is zeroed and populated with Win32 API addresses, the timer
|
||||
interval, the selected gadget, the scratch stack, and the trampoline frame.
|
||||
3. The setup PIC receives the configuration pointer, pivots to the scratch stack,
|
||||
creates a waitable timer, and registers the gadget as the APC completion
|
||||
callback. The callback argument points at the trampoline frame.
|
||||
4. Before waiting, the setup PIC tail-calls `VirtualProtectEx` to mark the PIC
|
||||
region `PAGE_READONLY`, then enters an alertable wait through
|
||||
`WaitForSingleObjectEx`.
|
||||
5. On timer delivery, the APC callback enters the gadget. The gadget pivots the
|
||||
stack to the trampoline frame, which restores the PIC to `PAGE_EXECUTE_READ`
|
||||
and returns to `setup.pic`.
|
||||
6. The benign MessageBox payload runs again, and the cycle repeats.
|
||||
|
||||
The console banner logs the selected gadget source, timer/APC setup, stack-pivot
|
||||
target, and protection cycle so a failing run can be diagnosed from stdout before
|
||||
using a debugger.
|
||||
|
||||
## Validation Notes
|
||||
|
||||
The acceptance harness can validate the observable behavior in an interactive
|
||||
Windows desktop session:
|
||||
|
||||
```powershell
|
||||
uv run --all-groups gargoyle-acceptance --configuration Debug
|
||||
uv run --all-groups gargoyle-acceptance --configuration Release
|
||||
```
|
||||
|
||||
A healthy run prints non-zero addresses for the PIC, gadget, configuration,
|
||||
scratch stack, and trampoline. It then shows and closes two benign `gargoyle`
|
||||
MessageBox windows: the initial handoff and the timer/APC re-entry.
|
||||
|
||||
Manual validation can additionally confirm:
|
||||
|
||||
- the selected gadget source is either `system DLL: mshtml.dll` or
|
||||
`allocated fallback PIC: gadget.pic`;
|
||||
- the timer period is 15000 ms;
|
||||
- the logged protection cycle is `PAGE_EXECUTE_READ -> PAGE_READONLY ->
|
||||
PAGE_EXECUTE_READ`;
|
||||
- a debugger or VMMap-style view agrees that the PIC is not left writable after
|
||||
setup.
|
||||
|
||||
The old Windows 7 x64 NULL-jump report is treated as unsupported legacy behavior
|
||||
for this Win32 refresh. The current issue to watch is whether the fallback gadget
|
||||
is selected on a supported Win32 run; that path is logged explicitly so it can be
|
||||
reproduced and debugged without broadening the proof of concept.
|
||||
@@ -0,0 +1,60 @@
|
||||
# x64 Architecture
|
||||
|
||||
`GargoyleX64` is a sibling Windows x64 example, not a transparent port of the
|
||||
Win32 stack-pivot design. It keeps the payload benign and visible while proving
|
||||
the x64-specific mechanics needed for a timer/APC re-entry loop.
|
||||
|
||||
## Runtime Layout
|
||||
|
||||
The x64 build emits three files beside each other in the Visual Studio output
|
||||
directory:
|
||||
|
||||
- `GargoyleX64.exe`, the C++ harness that loads raw PIC blobs and prepares the
|
||||
configuration block.
|
||||
- `setup_x64.pic`, the main x64 setup and payload loop.
|
||||
- `reentry_x64.pic`, a small executable wait/re-entry surface used while
|
||||
`setup_x64.pic` is parked read-only.
|
||||
|
||||
The C++ `X64Configuration` layout mirrors the NASM layout consumed by both PIC
|
||||
files. It stores pointer-sized fields for the setup PIC, re-entry wait function,
|
||||
APC callback, imported Windows APIs, timer handle, relative due time, timer
|
||||
period, and saved protection value.
|
||||
|
||||
## Control Flow
|
||||
|
||||
1. `main_x64.cpp` loads `setup_x64.pic` and `reentry_x64.pic` as executable raw
|
||||
PIC and fills the shared configuration block.
|
||||
2. `setup_x64.pic` receives the configuration pointer in `rcx`, creates a
|
||||
waitable timer, and arms it with the callback entry inside `reentry_x64.pic`.
|
||||
3. The benign payload displays a `gargoyle x64` MessageBox.
|
||||
4. After the MessageBox closes, `setup_x64.pic` calls the wait entry in
|
||||
`reentry_x64.pic`.
|
||||
5. The wait entry marks `setup_x64.pic` `PAGE_READONLY` and enters an alertable
|
||||
`WaitForSingleObjectEx`.
|
||||
6. Timer re-entry restores `setup_x64.pic` to `PAGE_EXECUTE_READ` through the
|
||||
re-entry PIC, then control returns to the setup loop and the benign payload
|
||||
appears again.
|
||||
|
||||
The wait path also reapplies `PAGE_EXECUTE_READ` before returning to the setup
|
||||
PIC. That restore is idempotent when the APC callback has already run, and it
|
||||
keeps unexpected waitable-timer return modes from jumping back into a read-only
|
||||
setup page.
|
||||
|
||||
## Validation Notes
|
||||
|
||||
Run the x64 acceptance harness from a Windows desktop session:
|
||||
|
||||
```powershell
|
||||
uv run --all-groups gargoyle-acceptance --configuration Debug --platform x64
|
||||
uv run --all-groups gargoyle-acceptance --configuration Release --platform x64
|
||||
```
|
||||
|
||||
A healthy run prints non-zero addresses for the setup PIC, re-entry PIC, APC
|
||||
callback, configuration block, and imported APIs. The harness then closes two
|
||||
`gargoyle x64` MessageBox windows: the initial handoff and one timer/APC
|
||||
re-entry.
|
||||
|
||||
The x64 example intentionally avoids the Win32 `pop reg; pop esp; ret` gadget
|
||||
assumption. The separate re-entry PIC is the stable executable surface for the
|
||||
x64 demonstration, while the setup PIC is the region whose idle protection state
|
||||
is meant to be inspected.
|
||||
@@ -48,12 +48,58 @@ build-debug:
|
||||
build-release:
|
||||
just build Release
|
||||
|
||||
build-x64 configuration:
|
||||
$nasmDir = Join-Path $env:LOCALAPPDATA 'bin\NASM'; if (Test-Path (Join-Path $nasmDir 'nasm.exe')) { $env:Path = "$nasmDir;$env:Path" }; $msbuild = if ($env:MSBUILD) { $env:MSBUILD } elseif (Test-Path 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe') { 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' } else { 'MSBuild.exe' }; & $msbuild Gargoyle.sln /p:Configuration={{configuration}} /p:Platform=x64 /m
|
||||
|
||||
build-x64-debug:
|
||||
just build-x64 Debug
|
||||
|
||||
build-x64-release:
|
||||
just build-x64 Release
|
||||
|
||||
build-x64-all:
|
||||
just build-x64-debug
|
||||
just build-x64-release
|
||||
|
||||
build-all:
|
||||
just build-debug
|
||||
just build-release
|
||||
just build-x64-all
|
||||
|
||||
acceptance configuration="Debug":
|
||||
uv run --all-groups gargoyle-acceptance --configuration {{configuration}}
|
||||
native-analyze configuration="Debug" platform="x86":
|
||||
$nasmDir = Join-Path $env:LOCALAPPDATA 'bin\NASM'; if (Test-Path (Join-Path $nasmDir 'nasm.exe')) { $env:Path = "$nasmDir;$env:Path" }; $msbuild = if ($env:MSBUILD) { $env:MSBUILD } elseif (Test-Path 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe') { 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' } else { 'MSBuild.exe' }; & $msbuild Gargoyle.sln /p:Configuration={{configuration}} /p:Platform={{platform}} /p:RunCodeAnalysis=true /m:1
|
||||
|
||||
native-analyze-all:
|
||||
just native-analyze Debug x86
|
||||
just native-analyze Release x86
|
||||
just native-analyze Debug x64
|
||||
just native-analyze Release x64
|
||||
|
||||
native-asan configuration="Debug" platform="x86":
|
||||
$nasmDir = Join-Path $env:LOCALAPPDATA 'bin\NASM'; if (Test-Path (Join-Path $nasmDir 'nasm.exe')) { $env:Path = "$nasmDir;$env:Path" }; $msbuild = if ($env:MSBUILD) { $env:MSBUILD } elseif (Test-Path 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe') { 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' } else { 'MSBuild.exe' }; $outDir = Join-Path $PWD 'asan\{{platform}}\{{configuration}}'; $intDir = Join-Path $PWD 'asan\obj\{{platform}}\{{configuration}}'; New-Item -ItemType Directory -Force $outDir, $intDir | Out-Null; & $msbuild Gargoyle.sln /p:Configuration={{configuration}} /p:Platform={{platform}} /p:EnableASAN=true "/p:OutDir=$outDir\" "/p:IntDir=$intDir\" /m:1
|
||||
|
||||
native-asan-all:
|
||||
just native-asan Debug x86
|
||||
just native-asan Debug x64
|
||||
|
||||
native-check:
|
||||
just native-analyze-all
|
||||
just native-asan-all
|
||||
|
||||
acceptance configuration="Debug" platform="x86":
|
||||
uv run --all-groups gargoyle-acceptance --configuration {{configuration}} --platform {{platform}}
|
||||
|
||||
acceptance-x86 configuration="Debug":
|
||||
just acceptance {{configuration}} x86
|
||||
|
||||
acceptance-x64 configuration="Debug":
|
||||
just acceptance {{configuration}} x64
|
||||
|
||||
acceptance-all:
|
||||
just acceptance-x86 Debug
|
||||
just acceptance-x86 Release
|
||||
just acceptance-x64 Debug
|
||||
just acceptance-x64 Release
|
||||
|
||||
check:
|
||||
just format-check
|
||||
@@ -67,4 +113,5 @@ ci:
|
||||
just sync
|
||||
just lock-check
|
||||
just build-all
|
||||
just native-check
|
||||
just check
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
@@ -16,10 +21,10 @@ namespace {
|
||||
constexpr DWORD invocation_interval_ms = 15 * 1000;
|
||||
constexpr size_t stack_size = 0x10000;
|
||||
|
||||
vector<vector<uint8_t>> rop_gadget_candidates = {
|
||||
constexpr array<array<uint8_t, 3>, 2> rop_gadget_candidates{ {
|
||||
{ 0x59, 0x5C, 0xC3 }, // pop ecx; pop esp; ret
|
||||
{ 0x58, 0x5C, 0xC3 } // pop eax; pop esp; ret
|
||||
};
|
||||
} };
|
||||
|
||||
/// Mirrors the NASM Configuration layout consumed by setup.nasm.
|
||||
struct SetupConfiguration {
|
||||
@@ -57,12 +62,58 @@ namespace {
|
||||
uint8_t stack[stack_size];
|
||||
StackTrampoline tramp;
|
||||
};
|
||||
|
||||
struct GadgetSelection {
|
||||
void* address;
|
||||
string source;
|
||||
};
|
||||
|
||||
string win32_error(const string& operation, DWORD error = GetLastError()) {
|
||||
LPSTR message = nullptr;
|
||||
auto chars = FormatMessageA(
|
||||
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr,
|
||||
error,
|
||||
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||
reinterpret_cast<LPSTR>(&message),
|
||||
0,
|
||||
nullptr
|
||||
);
|
||||
|
||||
string result = operation + " failed (GetLastError=" + to_string(error) + ")";
|
||||
if (chars && message) {
|
||||
string formatted{ message, chars };
|
||||
while (!formatted.empty() && (formatted.back() == '\r' || formatted.back() == '\n')) {
|
||||
formatted.pop_back();
|
||||
}
|
||||
result += ": " + formatted;
|
||||
}
|
||||
if (message) {
|
||||
LocalFree(message);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* page_protection_name(DWORD protection) {
|
||||
switch (protection) {
|
||||
case PAGE_EXECUTE_READ:
|
||||
return "PAGE_EXECUTE_READ";
|
||||
case PAGE_EXECUTE_READWRITE:
|
||||
return "PAGE_EXECUTE_READWRITE";
|
||||
case PAGE_READONLY:
|
||||
return "PAGE_READONLY";
|
||||
case PAGE_READWRITE:
|
||||
return "PAGE_READWRITE";
|
||||
default:
|
||||
return "unknown protection";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocates the mutable workspace shared with the PIC.
|
||||
Workspace& allocate_workspace() {
|
||||
auto result = VirtualAllocEx(GetCurrentProcess(), nullptr, sizeof(Workspace), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if (!result) throw runtime_error("[-] Couldn't VirtualAllocEx: " + GetLastError());
|
||||
if (!result) throw runtime_error("[-] " + win32_error("VirtualAllocEx workspace allocation"));
|
||||
RtlSecureZeroMemory(result, sizeof(Workspace));
|
||||
return *static_cast<Workspace*>(result);
|
||||
}
|
||||
@@ -74,12 +125,17 @@ MyTuple allocate_pic(const string& filename) {
|
||||
auto pic_size = static_cast<size_t>(file_stream.tellg());
|
||||
file_stream.seekg(0, fstream::beg);
|
||||
auto pic = VirtualAllocEx(GetCurrentProcess(), nullptr, pic_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||
if (!pic) throw runtime_error("[-] Couldn't VirtualAllocEx: " + GetLastError());
|
||||
if (!pic) throw runtime_error("[-] " + win32_error("VirtualAllocEx PIC allocation"));
|
||||
file_stream.read(static_cast<char*>(pic), pic_size);
|
||||
if (!file_stream) throw runtime_error("[-] Couldn't read complete PIC from \"" + filename + "\".");
|
||||
file_stream.close();
|
||||
DWORD old_protection;
|
||||
auto prot_result = VirtualProtectEx(GetCurrentProcess(), pic, pic_size, PAGE_EXECUTE_READ, &old_protection);
|
||||
if (!prot_result) throw runtime_error("[-] Couldn't VirtualProtectEx: " + GetLastError());
|
||||
if (!prot_result) throw runtime_error("[-] " + win32_error("VirtualProtectEx PIC protection"));
|
||||
printf("[ ] Protected \"%s\" as %s (old protection 0x%08lx).\n",
|
||||
filename.c_str(),
|
||||
page_protection_name(PAGE_EXECUTE_READ),
|
||||
old_protection);
|
||||
return MyTuple(pic, pic_size);
|
||||
}
|
||||
|
||||
@@ -87,37 +143,43 @@ MyTuple allocate_pic(const string& filename) {
|
||||
void* get_system_dll_gadget(const string& system_dll_filename) {
|
||||
printf("[ ] Loading \"%s\" system DLL.\n", system_dll_filename.c_str());
|
||||
auto dll_base = reinterpret_cast<uint8_t*>(LoadLibraryA(system_dll_filename.c_str()));
|
||||
if (!dll_base) throw runtime_error("[-] Couldn't LoadLibrary: " + GetLastError());
|
||||
if (!dll_base) throw runtime_error("[-] " + win32_error("LoadLibraryA " + system_dll_filename));
|
||||
|
||||
printf("[+] Loaded \"%s\" at 0x%p.\n", system_dll_filename.c_str(), dll_base);
|
||||
printf("[+] Loaded \"%s\" at 0x%p.\n", system_dll_filename.c_str(), static_cast<void*>(dll_base));
|
||||
|
||||
auto pe_header = ImageNtHeader(dll_base);
|
||||
if (!pe_header) throw runtime_error("[-] Couldn't ImageNtHeader: " + GetLastError());
|
||||
if (!pe_header) throw runtime_error("[-] ImageNtHeader returned null for \"" + system_dll_filename + "\".");
|
||||
|
||||
auto filtered_section_headers = vector<PIMAGE_SECTION_HEADER>();
|
||||
auto section_header = reinterpret_cast<PIMAGE_SECTION_HEADER>(pe_header + 1);
|
||||
auto executable_section_headers = vector<PIMAGE_SECTION_HEADER>();
|
||||
auto current_section_header = reinterpret_cast<PIMAGE_SECTION_HEADER>(pe_header + 1);
|
||||
for (int i = 0; i < pe_header->FileHeader.NumberOfSections; ++i)
|
||||
{
|
||||
if (section_header->Characteristics & IMAGE_SCN_MEM_EXECUTE) {
|
||||
filtered_section_headers.push_back(section_header);
|
||||
printf("[ ] Found executable section \"%s\" at 0x%p.\n", section_header->Name, dll_base + section_header->VirtualAddress);
|
||||
if (current_section_header->Characteristics & IMAGE_SCN_MEM_EXECUTE) {
|
||||
executable_section_headers.push_back(current_section_header);
|
||||
printf("[ ] Found executable section \"%.*s\" at 0x%p (%lu bytes).\n",
|
||||
IMAGE_SIZEOF_SHORT_NAME,
|
||||
reinterpret_cast<const char*>(current_section_header->Name),
|
||||
static_cast<void*>(dll_base + current_section_header->VirtualAddress),
|
||||
static_cast<unsigned long>(current_section_header->Misc.VirtualSize));
|
||||
}
|
||||
current_section_header++;
|
||||
}
|
||||
section_header++;
|
||||
};
|
||||
|
||||
for (auto section_header : filtered_section_headers)
|
||||
for (auto candidate_section_header : executable_section_headers)
|
||||
{
|
||||
for (auto rop_gadget : rop_gadget_candidates)
|
||||
for (const auto& rop_gadget : rop_gadget_candidates)
|
||||
{
|
||||
auto section_base = dll_base + section_header->VirtualAddress;
|
||||
vector<uint8_t> section_content(section_base, section_base + section_header->Misc.VirtualSize);
|
||||
auto search_result = search(begin(section_content), end(section_content), begin(rop_gadget), end(rop_gadget));
|
||||
if (search_result == end(section_content))
|
||||
auto section_base = dll_base + candidate_section_header->VirtualAddress;
|
||||
auto section_end = section_base + candidate_section_header->Misc.VirtualSize;
|
||||
auto search_result = search(section_base, section_end, begin(rop_gadget), end(rop_gadget));
|
||||
if (search_result == section_end)
|
||||
continue;
|
||||
|
||||
auto rop_gadget_offset = section_base + (search_result - begin(section_content));
|
||||
printf("[+] Found ROP gadget in section \"%s\" at 0x%p.\n", section_header->Name, rop_gadget_offset);
|
||||
return rop_gadget_offset;
|
||||
printf("[+] Found ROP gadget in section \"%.*s\" at 0x%p.\n",
|
||||
IMAGE_SIZEOF_SHORT_NAME,
|
||||
reinterpret_cast<const char*>(candidate_section_header->Name),
|
||||
static_cast<void*>(search_result));
|
||||
return search_result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,18 +188,24 @@ void* get_system_dll_gadget(const string& system_dll_filename) {
|
||||
}
|
||||
|
||||
/// Selects either a system-DLL gadget or the tiny fallback gadget PIC.
|
||||
void* get_gadget(bool use_system_dll, const string& gadget_system_dll_filename, const string& gadget_pic_path) {
|
||||
void* memory;
|
||||
GadgetSelection get_gadget(bool use_system_dll, const string& gadget_system_dll_filename, const string& gadget_pic_path) {
|
||||
void* memory = nullptr;
|
||||
if (use_system_dll) {
|
||||
printf("[ ] Gadget source candidate: system DLL search in \"%s\".\n", gadget_system_dll_filename.c_str());
|
||||
memory = get_system_dll_gadget(gadget_system_dll_filename);
|
||||
if (memory) {
|
||||
return GadgetSelection{ memory, "system DLL: " + gadget_system_dll_filename };
|
||||
}
|
||||
}
|
||||
if (!use_system_dll || !memory) {
|
||||
printf("[ ] Gadget source candidate: allocated fallback gadget PIC.\n");
|
||||
printf("[ ] Allocating executable memory for \"%s\".\n", gadget_pic_path.c_str());
|
||||
size_t size;
|
||||
tie(memory, size) = allocate_pic(gadget_pic_path);
|
||||
printf("[+] Allocated %u bytes for gadget PIC.\n", size);
|
||||
printf("[+] Allocated %zu bytes for gadget PIC.\n", size);
|
||||
return GadgetSelection{ memory, "allocated fallback PIC: " + gadget_pic_path };
|
||||
}
|
||||
return memory;
|
||||
return GadgetSelection{ memory, "unknown" };
|
||||
}
|
||||
|
||||
/// Wires together the PIC, gadget, trampoline, scratch stack, and demo payload.
|
||||
@@ -145,18 +213,20 @@ void launch(const string& setup_pic_path, const string& gadget_system_dll_filena
|
||||
printf("[ ] Allocating executable memory for \"%s\".\n", setup_pic_path.c_str());
|
||||
void* setup_memory; size_t setup_size;
|
||||
tie(setup_memory, setup_size) = allocate_pic(setup_pic_path);
|
||||
printf("[+] Allocated %d bytes for PIC.\n", setup_size);
|
||||
printf("[+] Allocated %zu bytes for PIC.\n", setup_size);
|
||||
|
||||
auto use_system_dll{ true };
|
||||
printf("[ ] Configuring ROP gadget.\n");
|
||||
auto gadget_memory = get_gadget(use_system_dll, gadget_system_dll_filename, gadget_pic_path);
|
||||
auto gadget = get_gadget(use_system_dll, gadget_system_dll_filename, gadget_pic_path);
|
||||
auto gadget_memory = gadget.address;
|
||||
printf("[ ] ROP gadget source: %s at 0x%p.\n", gadget.source.c_str(), gadget_memory);
|
||||
printf("[+] ROP gadget configured.\n");
|
||||
|
||||
printf("[ ] Allocating read/write memory for config, stack, and trampoline.\n");
|
||||
auto& scratch_memory = allocate_workspace();
|
||||
auto& config = scratch_memory.config;
|
||||
auto& tramp = scratch_memory.tramp;
|
||||
printf("[+] Allocated %u bytes for scratch memory.\n", sizeof(scratch_memory));
|
||||
printf("[+] Allocated %zu bytes for scratch memory.\n", sizeof(scratch_memory));
|
||||
|
||||
printf("[ ] Building stack trampoline.\n");
|
||||
tramp.old_protections_ptr = &tramp.old_protections;
|
||||
@@ -167,6 +237,12 @@ void launch(const string& setup_pic_path, const string& gadget_system_dll_filena
|
||||
tramp.address = setup_memory;
|
||||
tramp.return_address = setup_memory;
|
||||
tramp.setup_config = &config;
|
||||
printf("[ ] Stack pivot target: APC gadget pivots to trampoline 0x%p.\n", static_cast<void*>(&tramp));
|
||||
printf("[ ] APC re-entry protection restore: VirtualProtectEx(0x%p, %zu, %s).\n",
|
||||
setup_memory,
|
||||
setup_size,
|
||||
page_protection_name(tramp.protections));
|
||||
printf("[ ] Stack trampoline returns to setup PIC at 0x%p after restoring execute permissions.\n", setup_memory);
|
||||
printf("[+] Stack trampoline built.\n");
|
||||
|
||||
printf("[ ] Building configuration.\n");
|
||||
@@ -180,16 +256,29 @@ void launch(const string& setup_pic_path, const string& gadget_system_dll_filena
|
||||
config.tramp_addr = &tramp;
|
||||
config.interval = invocation_interval_ms;
|
||||
config.target = gadget_memory;
|
||||
printf("[ ] Timer setup APIs: CreateWaitableTimerW=0x%p, SetWaitableTimer=0x%p.\n",
|
||||
config.CreateWaitableTimer,
|
||||
config.SetWaitableTimer);
|
||||
printf("[ ] APC setup: callback gadget=0x%p, callback argument trampoline=0x%p.\n",
|
||||
config.target,
|
||||
config.tramp_addr);
|
||||
printf("[ ] Alertable wait: WaitForSingleObjectEx=0x%p, timer period=%lu ms.\n",
|
||||
config.WaitForSingleObjectEx,
|
||||
invocation_interval_ms);
|
||||
printf("[ ] PIC protection cycle: %s -> %s while waiting -> %s on APC re-entry.\n",
|
||||
page_protection_name(PAGE_EXECUTE_READ),
|
||||
page_protection_name(PAGE_READONLY),
|
||||
page_protection_name(PAGE_EXECUTE_READ));
|
||||
printf("[+] Configuration built.\n");
|
||||
|
||||
printf("[+] Success!\n");
|
||||
printf(" ================================\n");
|
||||
printf(" Gargoyle PIC @ -----> 0x%p\n", setup_memory);
|
||||
printf(" ROP gadget @ -------> 0x%p\n", gadget_memory);
|
||||
printf(" Configuration @ ----> 0x%p\n", &scratch_memory.config);
|
||||
printf(" Top of stack @ -----> 0x%p\n", &scratch_memory.stack);
|
||||
printf(" Bottom of stack @ --> 0x%p\n", &scratch_memory.stack[stack_size-1]);
|
||||
printf(" Stack trampoline @ -> 0x%p\n", &scratch_memory.tramp);
|
||||
printf(" Configuration @ ----> 0x%p\n", static_cast<void*>(&scratch_memory.config));
|
||||
printf(" Top of stack @ -----> 0x%p\n", static_cast<void*>(&scratch_memory.stack));
|
||||
printf(" Bottom of stack @ --> 0x%p\n", static_cast<void*>(&scratch_memory.stack[stack_size-1]));
|
||||
printf(" Stack trampoline @ -> 0x%p\n", static_cast<void*>(&scratch_memory.tramp));
|
||||
|
||||
reinterpret_cast<callable>(setup_memory)(&config);
|
||||
}
|
||||
@@ -200,5 +289,7 @@ int main() {
|
||||
launch("setup.pic", "mshtml.dll", "gadget.pic");
|
||||
} catch (exception& e) {
|
||||
printf("%s\n", e.what());
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -34,4 +34,10 @@ markdown_extensions:
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Acceptance Harness: acceptance.md
|
||||
- Validation Checklist: validation-checklist.md
|
||||
- Win32 Architecture: win32-architecture.md
|
||||
- x64 Architecture: x64-architecture.md
|
||||
- Responsible Use: responsible-use.md
|
||||
- Future Work: future-work.md
|
||||
- References: references.md
|
||||
- Python API: api.md
|
||||
|
||||
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from os import environ, pathsep
|
||||
from pathlib import Path
|
||||
|
||||
from gargoyle_acceptance.environment import Configuration, Toolchain
|
||||
from gargoyle_acceptance.environment import Configuration, Platform, Toolchain
|
||||
from gargoyle_acceptance.errors import AcceptanceError
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ class BuildResult:
|
||||
def build_solution(
|
||||
repo_root: Path,
|
||||
configuration: Configuration,
|
||||
platform: Platform,
|
||||
toolchain: Toolchain,
|
||||
*,
|
||||
timeout_seconds: float = 120.0,
|
||||
@@ -36,6 +37,7 @@ def build_solution(
|
||||
Args:
|
||||
repo_root: Repository root containing `Gargoyle.sln`.
|
||||
configuration: Visual Studio configuration to build.
|
||||
platform: Visual Studio solution platform to build.
|
||||
toolchain: Resolved toolchain paths.
|
||||
timeout_seconds: Maximum time allowed for MSBuild.
|
||||
|
||||
@@ -49,7 +51,7 @@ def build_solution(
|
||||
str(toolchain.msbuild),
|
||||
"Gargoyle.sln",
|
||||
f"/p:Configuration={configuration}",
|
||||
"/p:Platform=x86",
|
||||
f"/p:Platform={platform}",
|
||||
"/m",
|
||||
)
|
||||
try:
|
||||
|
||||
@@ -10,7 +10,11 @@ from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
|
||||
from gargoyle_acceptance.environment import coerce_optional_path, parse_configuration
|
||||
from gargoyle_acceptance.environment import (
|
||||
coerce_optional_path,
|
||||
parse_configuration,
|
||||
parse_platform,
|
||||
)
|
||||
from gargoyle_acceptance.errors import AcceptanceError
|
||||
from gargoyle_acceptance.harness import AcceptanceReport, run_acceptance
|
||||
|
||||
@@ -39,6 +43,14 @@ def acceptance(
|
||||
help="Repository root. Defaults to upward discovery from the current directory.",
|
||||
),
|
||||
] = None,
|
||||
platform: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--platform",
|
||||
"-p",
|
||||
help="Visual Studio solution platform to build and run.",
|
||||
),
|
||||
] = "x86",
|
||||
msbuild: Annotated[
|
||||
Path | None,
|
||||
typer.Option("--msbuild", help="Optional full path to MSBuild.exe."),
|
||||
@@ -64,6 +76,7 @@ def acceptance(
|
||||
Args:
|
||||
configuration: Visual Studio configuration to build and run.
|
||||
repo_root: Optional repository root.
|
||||
platform: Visual Studio solution platform to build and run.
|
||||
msbuild: Optional MSBuild path.
|
||||
skip_build: Whether to skip the build step.
|
||||
rounds: Number of MessageBox rounds to validate.
|
||||
@@ -75,6 +88,7 @@ def acceptance(
|
||||
try:
|
||||
report = run_acceptance(
|
||||
configuration=parse_configuration(configuration),
|
||||
platform=parse_platform(platform),
|
||||
repo_root=coerce_optional_path(repo_root),
|
||||
msbuild=coerce_optional_path(msbuild),
|
||||
skip_build=skip_build,
|
||||
@@ -109,6 +123,7 @@ def _render_success(report: AcceptanceReport) -> None:
|
||||
table.add_column("Check", style="bold")
|
||||
table.add_column("Value")
|
||||
table.add_row("Configuration", report.artifacts.configuration)
|
||||
table.add_row("Platform", report.artifacts.platform)
|
||||
table.add_row("Executable", str(report.artifacts.executable))
|
||||
table.add_row("MessageBox rounds", str(report.message_box_rounds))
|
||||
table.add_row("Setup lines", str(len(report.setup.lines)))
|
||||
|
||||
@@ -14,27 +14,33 @@ from gargoyle_acceptance.errors import AcceptanceError
|
||||
|
||||
Configuration = Literal["Debug", "Release"]
|
||||
VALID_CONFIGURATIONS: tuple[Configuration, ...] = ("Debug", "Release")
|
||||
Platform = Literal["x86", "x64"]
|
||||
VALID_PLATFORMS: tuple[Platform, ...] = ("x86", "x64")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GargoyleArtifacts:
|
||||
"""Paths produced by a Gargoyle Win32 build.
|
||||
"""Paths produced by a Gargoyle native build.
|
||||
|
||||
Attributes:
|
||||
repo_root: Repository root containing `Gargoyle.sln`.
|
||||
configuration: Visual Studio configuration name.
|
||||
platform: Visual Studio solution platform.
|
||||
output_dir: Directory containing the executable and PIC blobs.
|
||||
executable: Built `Gargoyle.exe`.
|
||||
setup_pic: Built `setup.pic`.
|
||||
gadget_pic: Built `gadget.pic`.
|
||||
executable: Built executable.
|
||||
setup_pic: Built setup PIC.
|
||||
gadget_pic: Optional Win32 stack-pivot gadget PIC.
|
||||
reentry_pic: Optional x64 wait/re-entry PIC.
|
||||
"""
|
||||
|
||||
repo_root: Path
|
||||
configuration: Configuration
|
||||
platform: Platform
|
||||
output_dir: Path
|
||||
executable: Path
|
||||
setup_pic: Path
|
||||
gadget_pic: Path
|
||||
gadget_pic: Path | None = None
|
||||
reentry_pic: Path | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -73,6 +79,29 @@ def parse_configuration(value: str) -> Configuration:
|
||||
)
|
||||
|
||||
|
||||
def parse_platform(value: str) -> Platform:
|
||||
"""Normalize a Visual Studio solution platform option.
|
||||
|
||||
Args:
|
||||
value: User-provided platform name.
|
||||
|
||||
Returns:
|
||||
A supported platform literal.
|
||||
|
||||
Raises:
|
||||
AcceptanceError: If the platform is not supported.
|
||||
"""
|
||||
normalized = value.strip()
|
||||
for platform_name in VALID_PLATFORMS:
|
||||
if normalized.lower() == platform_name.lower():
|
||||
return platform_name
|
||||
raise AcceptanceError(
|
||||
"Unsupported platform",
|
||||
f"Expected one of {', '.join(VALID_PLATFORMS)}, got {value!r}.",
|
||||
"Use --platform x86 or --platform x64.",
|
||||
)
|
||||
|
||||
|
||||
def require_windows(system_name: str | None = None) -> None:
|
||||
"""Require the current platform to be Windows.
|
||||
|
||||
@@ -116,25 +145,26 @@ def resolve_repo_root(start: Path | None = None) -> Path:
|
||||
)
|
||||
|
||||
|
||||
def artifacts_for(repo_root: Path, configuration: Configuration) -> GargoyleArtifacts:
|
||||
def artifacts_for(
|
||||
repo_root: Path,
|
||||
configuration: Configuration,
|
||||
platform_name: Platform = "x86",
|
||||
) -> GargoyleArtifacts:
|
||||
"""Compute expected build outputs for a configuration.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root.
|
||||
configuration: Visual Studio configuration.
|
||||
platform_name: Visual Studio solution platform.
|
||||
|
||||
Returns:
|
||||
Expected artifact paths.
|
||||
"""
|
||||
output_dir = repo_root / configuration
|
||||
return GargoyleArtifacts(
|
||||
repo_root=repo_root,
|
||||
configuration=configuration,
|
||||
output_dir=output_dir,
|
||||
executable=output_dir / "Gargoyle.exe",
|
||||
setup_pic=output_dir / "setup.pic",
|
||||
gadget_pic=output_dir / "gadget.pic",
|
||||
)
|
||||
candidates = _artifact_candidates(repo_root, configuration, platform_name)
|
||||
for candidate in candidates:
|
||||
if all(path.is_file() for path in _required_artifact_paths(candidate)):
|
||||
return candidate
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def verify_artifacts(artifacts: GargoyleArtifacts) -> None:
|
||||
@@ -146,20 +176,112 @@ def verify_artifacts(artifacts: GargoyleArtifacts) -> None:
|
||||
Raises:
|
||||
AcceptanceError: If any expected artifact is missing.
|
||||
"""
|
||||
missing = [
|
||||
path
|
||||
for path in (artifacts.executable, artifacts.setup_pic, artifacts.gadget_pic)
|
||||
if not path.is_file()
|
||||
]
|
||||
missing = [path for path in _required_artifact_paths(artifacts) if not path.is_file()]
|
||||
if missing:
|
||||
formatted = "\n".join(f"- {path}" for path in missing)
|
||||
raise AcceptanceError(
|
||||
"Build artifacts missing",
|
||||
f"The {artifacts.configuration}|x86 output is incomplete:\n{formatted}",
|
||||
(
|
||||
f"The {artifacts.configuration}|{artifacts.platform} output is incomplete:\n"
|
||||
f"{formatted}"
|
||||
),
|
||||
"Build the configuration first or run without --skip-build.",
|
||||
)
|
||||
|
||||
|
||||
def _artifact_candidates(
|
||||
repo_root: Path,
|
||||
configuration: Configuration,
|
||||
platform_name: Platform,
|
||||
) -> tuple[GargoyleArtifacts, ...]:
|
||||
"""Return likely Visual Studio output locations.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root.
|
||||
configuration: Visual Studio configuration.
|
||||
platform_name: Visual Studio solution platform.
|
||||
|
||||
Returns:
|
||||
Candidate artifact layouts in preference order.
|
||||
"""
|
||||
if platform_name == "x86":
|
||||
return (
|
||||
_x86_artifacts(repo_root, configuration, repo_root / configuration),
|
||||
_x86_artifacts(repo_root, configuration, repo_root / "Win32" / configuration),
|
||||
_x86_artifacts(repo_root, configuration, repo_root / "x86" / configuration),
|
||||
)
|
||||
return (
|
||||
_x64_artifacts(repo_root, configuration, repo_root / "x64" / configuration),
|
||||
_x64_artifacts(repo_root, configuration, repo_root / "GargoyleX64" / "x64" / configuration),
|
||||
_x64_artifacts(repo_root, configuration, repo_root / "GargoyleX64" / configuration),
|
||||
)
|
||||
|
||||
|
||||
def _x86_artifacts(
|
||||
repo_root: Path, configuration: Configuration, output_dir: Path
|
||||
) -> GargoyleArtifacts:
|
||||
"""Create a Win32 artifact layout.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root.
|
||||
configuration: Visual Studio configuration.
|
||||
output_dir: Candidate output directory.
|
||||
|
||||
Returns:
|
||||
Win32 artifact paths.
|
||||
"""
|
||||
return GargoyleArtifacts(
|
||||
repo_root=repo_root,
|
||||
configuration=configuration,
|
||||
platform="x86",
|
||||
output_dir=output_dir,
|
||||
executable=output_dir / "Gargoyle.exe",
|
||||
setup_pic=output_dir / "setup.pic",
|
||||
gadget_pic=output_dir / "gadget.pic",
|
||||
)
|
||||
|
||||
|
||||
def _x64_artifacts(
|
||||
repo_root: Path, configuration: Configuration, output_dir: Path
|
||||
) -> GargoyleArtifacts:
|
||||
"""Create an x64 artifact layout.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root.
|
||||
configuration: Visual Studio configuration.
|
||||
output_dir: Candidate output directory.
|
||||
|
||||
Returns:
|
||||
x64 artifact paths.
|
||||
"""
|
||||
return GargoyleArtifacts(
|
||||
repo_root=repo_root,
|
||||
configuration=configuration,
|
||||
platform="x64",
|
||||
output_dir=output_dir,
|
||||
executable=output_dir / "GargoyleX64.exe",
|
||||
setup_pic=output_dir / "setup_x64.pic",
|
||||
reentry_pic=output_dir / "reentry_x64.pic",
|
||||
)
|
||||
|
||||
|
||||
def _required_artifact_paths(artifacts: GargoyleArtifacts) -> tuple[Path, ...]:
|
||||
"""Return non-optional artifact paths for a build.
|
||||
|
||||
Args:
|
||||
artifacts: Candidate artifact layout.
|
||||
|
||||
Returns:
|
||||
Required paths.
|
||||
"""
|
||||
paths = [artifacts.executable, artifacts.setup_pic]
|
||||
if artifacts.gadget_pic is not None:
|
||||
paths.append(artifacts.gadget_pic)
|
||||
if artifacts.reentry_pic is not None:
|
||||
paths.append(artifacts.reentry_pic)
|
||||
return tuple(paths)
|
||||
|
||||
|
||||
def resolve_msbuild(explicit: Path | None = None) -> Path:
|
||||
"""Resolve MSBuild from an explicit path, PATH, or common Visual Studio installs.
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from gargoyle_acceptance.build import BuildResult, build_solution
|
||||
from gargoyle_acceptance.environment import (
|
||||
Configuration,
|
||||
GargoyleArtifacts,
|
||||
Platform,
|
||||
artifacts_for,
|
||||
require_windows,
|
||||
resolve_repo_root,
|
||||
@@ -38,6 +39,25 @@ ADDRESS_LABELS = (
|
||||
"Bottom of stack",
|
||||
"Stack trampoline",
|
||||
)
|
||||
X64_EXPECTED_MARKERS = (
|
||||
"[+] x64 timer/APC prototype configured.",
|
||||
"[ ] Entering benign x64 PIC payload loop.",
|
||||
)
|
||||
X64_ADDRESS_LABELS = (
|
||||
"Gargoyle x64 PIC",
|
||||
"x64 re-entry PIC",
|
||||
"x64 APC callback",
|
||||
"Configuration",
|
||||
"VirtualProtectEx",
|
||||
"WaitForSingleObjectEx",
|
||||
"CreateWaitableTimerW",
|
||||
"SetWaitableTimer",
|
||||
"MessageBoxA",
|
||||
)
|
||||
MESSAGE_BOX_TITLES: dict[Platform, str] = {
|
||||
"x86": "gargoyle",
|
||||
"x64": "gargoyle x64",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -108,6 +128,7 @@ class LineReader:
|
||||
def run_acceptance(
|
||||
*,
|
||||
configuration: Configuration,
|
||||
platform: Platform = "x86",
|
||||
repo_root: Path | None = None,
|
||||
msbuild: Path | None = None,
|
||||
skip_build: bool = False,
|
||||
@@ -118,6 +139,7 @@ def run_acceptance(
|
||||
|
||||
Args:
|
||||
configuration: Visual Studio configuration to build and run.
|
||||
platform: Visual Studio solution platform to build and run.
|
||||
repo_root: Optional repository root. Defaults to upward discovery.
|
||||
msbuild: Optional explicit MSBuild path.
|
||||
skip_build: Whether to skip the MSBuild step.
|
||||
@@ -140,18 +162,19 @@ def run_acceptance(
|
||||
|
||||
root = resolve_repo_root(repo_root)
|
||||
toolchain = resolve_toolchain(msbuild)
|
||||
build = None if skip_build else build_solution(root, configuration, toolchain)
|
||||
artifacts = artifacts_for(root, configuration)
|
||||
build = None if skip_build else build_solution(root, configuration, platform, toolchain)
|
||||
artifacts = artifacts_for(root, configuration, platform)
|
||||
verify_artifacts(artifacts)
|
||||
|
||||
process = _start_gargoyle(artifacts)
|
||||
controller = MessageBoxController()
|
||||
try:
|
||||
setup = _wait_for_setup(process, timeout_seconds=timeout_seconds)
|
||||
setup = _wait_for_setup(process, platform=platform, timeout_seconds=timeout_seconds)
|
||||
closed_rounds = _close_message_boxes(
|
||||
process=process,
|
||||
controller=controller,
|
||||
rounds=rounds,
|
||||
title=MESSAGE_BOX_TITLES[platform],
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
finally:
|
||||
@@ -164,11 +187,15 @@ def run_acceptance(
|
||||
)
|
||||
|
||||
|
||||
def parse_setup_output(lines: list[str] | tuple[str, ...]) -> SetupObservation:
|
||||
def parse_setup_output(
|
||||
lines: list[str] | tuple[str, ...],
|
||||
platform: Platform = "x86",
|
||||
) -> SetupObservation:
|
||||
"""Parse and validate Gargoyle's setup banner.
|
||||
|
||||
Args:
|
||||
lines: Captured process output.
|
||||
platform: Platform-specific banner format.
|
||||
|
||||
Returns:
|
||||
Parsed setup evidence.
|
||||
@@ -176,9 +203,11 @@ def parse_setup_output(lines: list[str] | tuple[str, ...]) -> SetupObservation:
|
||||
Raises:
|
||||
AcceptanceError: If required markers or addresses are missing.
|
||||
"""
|
||||
missing_markers = [marker for marker in EXPECTED_MARKERS if marker not in lines]
|
||||
addresses = _parse_addresses(lines)
|
||||
missing_addresses = [label for label in ADDRESS_LABELS if label not in addresses]
|
||||
expected_markers = X64_EXPECTED_MARKERS if platform == "x64" else EXPECTED_MARKERS
|
||||
address_labels = X64_ADDRESS_LABELS if platform == "x64" else ADDRESS_LABELS
|
||||
missing_markers = [marker for marker in expected_markers if marker not in lines]
|
||||
addresses = _parse_addresses(lines, address_labels)
|
||||
missing_addresses = [label for label in address_labels if label not in addresses]
|
||||
if missing_markers or missing_addresses:
|
||||
problems = []
|
||||
if missing_markers:
|
||||
@@ -200,17 +229,18 @@ def parse_setup_output(lines: list[str] | tuple[str, ...]) -> SetupObservation:
|
||||
return SetupObservation(lines=tuple(lines), addresses=addresses)
|
||||
|
||||
|
||||
def setup_banner_complete(lines: list[str]) -> bool:
|
||||
def setup_banner_complete(lines: list[str], platform: Platform = "x86") -> bool:
|
||||
"""Return whether captured lines contain a complete setup banner.
|
||||
|
||||
Args:
|
||||
lines: Captured process output.
|
||||
platform: Platform-specific banner format.
|
||||
|
||||
Returns:
|
||||
`True` when the setup banner can be parsed successfully.
|
||||
"""
|
||||
try:
|
||||
parse_setup_output(lines)
|
||||
parse_setup_output(lines, platform)
|
||||
except AcceptanceError:
|
||||
return False
|
||||
return True
|
||||
@@ -250,12 +280,14 @@ def _start_gargoyle(artifacts: GargoyleArtifacts) -> subprocess.Popen[str]:
|
||||
def _wait_for_setup(
|
||||
process: subprocess.Popen[str],
|
||||
*,
|
||||
platform: Platform,
|
||||
timeout_seconds: float,
|
||||
) -> SetupObservation:
|
||||
"""Wait for Gargoyle to print a complete setup banner.
|
||||
|
||||
Args:
|
||||
process: Running Gargoyle process.
|
||||
platform: Platform-specific banner format.
|
||||
timeout_seconds: Maximum time to wait.
|
||||
|
||||
Returns:
|
||||
@@ -288,8 +320,8 @@ def _wait_for_setup(
|
||||
if line is None:
|
||||
continue
|
||||
lines.append(line)
|
||||
if setup_banner_complete(lines):
|
||||
return parse_setup_output(lines)
|
||||
if setup_banner_complete(lines, platform):
|
||||
return parse_setup_output(lines, platform)
|
||||
raise AcceptanceError(
|
||||
"Setup timed out",
|
||||
f"Gargoyle did not print a complete setup banner within {timeout_seconds:.0f} seconds.\n"
|
||||
@@ -303,6 +335,7 @@ def _close_message_boxes(
|
||||
process: subprocess.Popen[str],
|
||||
controller: MessageBoxController,
|
||||
rounds: int,
|
||||
title: str,
|
||||
timeout_seconds: float,
|
||||
) -> int:
|
||||
"""Close the benign MessageBox payload for a number of rounds.
|
||||
@@ -311,6 +344,7 @@ def _close_message_boxes(
|
||||
process: Running Gargoyle process.
|
||||
controller: Window controller used to find and close windows.
|
||||
rounds: Number of payload windows to close.
|
||||
title: MessageBox title to wait for.
|
||||
timeout_seconds: Maximum wait per round.
|
||||
|
||||
Returns:
|
||||
@@ -332,7 +366,7 @@ def _close_message_boxes(
|
||||
)
|
||||
hwnd = controller.wait_for_message_box(
|
||||
pid=process.pid,
|
||||
title="gargoyle",
|
||||
title=title,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
controller.close_window(hwnd)
|
||||
@@ -357,18 +391,22 @@ def _terminate_process(process: subprocess.Popen[str]) -> None:
|
||||
process.wait(timeout=5)
|
||||
|
||||
|
||||
def _parse_addresses(lines: list[str] | tuple[str, ...]) -> dict[str, int]:
|
||||
def _parse_addresses(
|
||||
lines: list[str] | tuple[str, ...],
|
||||
address_labels: tuple[str, ...],
|
||||
) -> dict[str, int]:
|
||||
"""Parse address lines from Gargoyle's setup banner.
|
||||
|
||||
Args:
|
||||
lines: Captured process output.
|
||||
address_labels: Address labels expected for the platform.
|
||||
|
||||
Returns:
|
||||
Address values keyed by banner label.
|
||||
"""
|
||||
addresses: dict[str, int] = {}
|
||||
for line in lines:
|
||||
for label in ADDRESS_LABELS:
|
||||
for label in address_labels:
|
||||
prefix = f"{label} @"
|
||||
if line.strip().startswith(prefix) and "0x" in line:
|
||||
raw_address = line.rsplit("0x", maxsplit=1)[-1].strip()
|
||||
|
||||
+4
-4
@@ -16,7 +16,7 @@ def test_build_solution_uses_expected_msbuild_command(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The build command stays narrow: configuration, x86 platform, and parallel build."""
|
||||
"""The build command stays narrow: configuration, platform, and parallel build."""
|
||||
seen: dict[str, object] = {}
|
||||
|
||||
def fake_run(command: tuple[str, ...], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
||||
@@ -27,7 +27,7 @@ def test_build_solution_uses_expected_msbuild_command(
|
||||
monkeypatch.setattr(build.subprocess, "run", fake_run)
|
||||
toolchain = Toolchain(msbuild=Path("MSBuild.exe"), nasm=Path("nasm.exe"))
|
||||
|
||||
result = build.build_solution(tmp_path, "Debug", toolchain)
|
||||
result = build.build_solution(tmp_path, "Debug", "x86", toolchain)
|
||||
|
||||
assert result.command == (
|
||||
"MSBuild.exe",
|
||||
@@ -64,7 +64,7 @@ def test_build_solution_raises_on_failure(
|
||||
toolchain = Toolchain(msbuild=Path("MSBuild.exe"), nasm=Path("nasm.exe"))
|
||||
|
||||
with pytest.raises(AcceptanceError, match="Build failed"):
|
||||
build.build_solution(tmp_path, "Release", toolchain)
|
||||
build.build_solution(tmp_path, "Release", "x64", toolchain)
|
||||
|
||||
|
||||
def test_build_solution_raises_on_timeout(
|
||||
@@ -80,4 +80,4 @@ def test_build_solution_raises_on_timeout(
|
||||
toolchain = Toolchain(msbuild=Path("MSBuild.exe"), nasm=Path("nasm.exe"))
|
||||
|
||||
with pytest.raises(AcceptanceError, match="Build timed out"):
|
||||
build.build_solution(tmp_path, "Debug", toolchain, timeout_seconds=1)
|
||||
build.build_solution(tmp_path, "Debug", "x86", toolchain, timeout_seconds=1)
|
||||
|
||||
@@ -16,6 +16,7 @@ def _report(tmp_path: Path) -> AcceptanceReport:
|
||||
artifacts = GargoyleArtifacts(
|
||||
repo_root=tmp_path,
|
||||
configuration="Debug",
|
||||
platform="x86",
|
||||
output_dir=tmp_path / "Debug",
|
||||
executable=tmp_path / "Debug" / "Gargoyle.exe",
|
||||
setup_pic=tmp_path / "Debug" / "setup.pic",
|
||||
|
||||
@@ -11,6 +11,7 @@ from gargoyle_acceptance.environment import (
|
||||
artifacts_for,
|
||||
coerce_optional_path,
|
||||
parse_configuration,
|
||||
parse_platform,
|
||||
require_windows,
|
||||
resolve_msbuild,
|
||||
resolve_nasm,
|
||||
@@ -33,6 +34,18 @@ def test_parse_configuration_rejects_unknown_value() -> None:
|
||||
parse_configuration("x64")
|
||||
|
||||
|
||||
def test_parse_platform_accepts_supported_values() -> None:
|
||||
"""Platform parsing accepts the two supported Visual Studio solution platforms."""
|
||||
assert parse_platform("x86") == "x86"
|
||||
assert parse_platform("X64") == "x64"
|
||||
|
||||
|
||||
def test_parse_platform_rejects_unknown_value() -> None:
|
||||
"""Platform parsing gives a clear error for unsupported names."""
|
||||
with pytest.raises(AcceptanceError, match="Unsupported platform"):
|
||||
parse_platform("arm64")
|
||||
|
||||
|
||||
def test_require_windows_rejects_non_windows() -> None:
|
||||
"""Platform validation rejects non-Windows systems."""
|
||||
with pytest.raises(AcceptanceError, match="Windows required"):
|
||||
@@ -94,9 +107,34 @@ def test_artifacts_for_uses_configuration_output_directory(tmp_path: Path) -> No
|
||||
artifacts = artifacts_for(tmp_path, "Debug")
|
||||
|
||||
assert artifacts.output_dir == tmp_path / "Debug"
|
||||
assert artifacts.platform == "x86"
|
||||
assert artifacts.executable == tmp_path / "Debug" / "Gargoyle.exe"
|
||||
|
||||
|
||||
def test_artifacts_for_uses_x64_default_output_directory(tmp_path: Path) -> None:
|
||||
"""x64 artifact paths match the root solution's Visual Studio default output."""
|
||||
artifacts = artifacts_for(tmp_path, "Release", "x64")
|
||||
|
||||
assert artifacts.output_dir == tmp_path / "x64" / "Release"
|
||||
assert artifacts.platform == "x64"
|
||||
assert artifacts.executable == tmp_path / "x64" / "Release" / "GargoyleX64.exe"
|
||||
assert artifacts.setup_pic == tmp_path / "x64" / "Release" / "setup_x64.pic"
|
||||
assert artifacts.reentry_pic == tmp_path / "x64" / "Release" / "reentry_x64.pic"
|
||||
|
||||
|
||||
def test_artifacts_for_selects_existing_candidate(tmp_path: Path) -> None:
|
||||
"""Artifact discovery accepts an alternate VS output candidate when files exist there."""
|
||||
output_dir = tmp_path / "GargoyleX64" / "Debug"
|
||||
output_dir.mkdir(parents=True)
|
||||
(output_dir / "GargoyleX64.exe").write_text("")
|
||||
(output_dir / "setup_x64.pic").write_text("")
|
||||
(output_dir / "reentry_x64.pic").write_text("")
|
||||
|
||||
artifacts = artifacts_for(tmp_path, "Debug", "x64")
|
||||
|
||||
assert artifacts.output_dir == output_dir
|
||||
|
||||
|
||||
def test_resolve_msbuild_accepts_explicit_path(tmp_path: Path) -> None:
|
||||
"""MSBuild resolution accepts an explicit executable path."""
|
||||
msbuild = tmp_path / "MSBuild.exe"
|
||||
|
||||
+40
-7
@@ -28,6 +28,24 @@ VALID_OUTPUT = [
|
||||
" Bottom of stack @ --> 0x00A80037",
|
||||
" Stack trampoline @ -> 0x00A80038",
|
||||
]
|
||||
VALID_X64_OUTPUT = [
|
||||
'[ ] Loading x64 setup PIC from "setup_x64.pic".',
|
||||
"[+] Loaded 113 bytes of x64 PIC.",
|
||||
'[ ] Loading x64 re-entry PIC from "reentry_x64.pic".',
|
||||
"[+] Loaded 144 bytes of x64 re-entry PIC.",
|
||||
"[+] x64 timer/APC prototype configured.",
|
||||
" Gargoyle x64 PIC @ ----> 0x000001827D9B0000",
|
||||
" x64 re-entry PIC @ ----> 0x000001827D9C0000",
|
||||
" x64 APC callback @ ---> 0x000001827D9C0010",
|
||||
" Configuration @ -------> 0x00000057D1DAF658",
|
||||
" VirtualProtectEx @ ----> 0x00007FFD49A72340",
|
||||
" WaitForSingleObjectEx @ 0x00007FFD49A71230",
|
||||
" CreateWaitableTimerW @ 0x00007FFD49A70000",
|
||||
" SetWaitableTimer @ ---> 0x00007FFD49A71111",
|
||||
" MessageBoxA @ --------> 0x00007FFD4B30CAC0",
|
||||
" Timer period @ -------> 15000 ms",
|
||||
"[ ] Entering benign x64 PIC payload loop.",
|
||||
]
|
||||
|
||||
|
||||
class FakeProcess:
|
||||
@@ -76,6 +94,14 @@ def test_parse_setup_output_accepts_complete_banner() -> None:
|
||||
assert parsed.addresses["ROP gadget"] == 0x6AE93472
|
||||
|
||||
|
||||
def test_parse_setup_output_accepts_x64_complete_banner() -> None:
|
||||
"""Setup parsing accepts the x64 timer/APC banner."""
|
||||
parsed = harness.parse_setup_output(VALID_X64_OUTPUT, "x64")
|
||||
|
||||
assert parsed.addresses["Gargoyle x64 PIC"] == 0x000001827D9B0000
|
||||
assert parsed.addresses["x64 APC callback"] == 0x000001827D9C0010
|
||||
|
||||
|
||||
def test_parse_setup_output_rejects_missing_marker() -> None:
|
||||
"""Setup parsing identifies an incomplete setup chain."""
|
||||
with pytest.raises(AcceptanceError, match="Setup banner incomplete"):
|
||||
@@ -112,6 +138,7 @@ def test_run_acceptance_orchestrates_build_and_runtime(
|
||||
artifacts = GargoyleArtifacts(
|
||||
repo_root=tmp_path,
|
||||
configuration="Debug",
|
||||
platform="x86",
|
||||
output_dir=tmp_path / "Debug",
|
||||
executable=tmp_path / "Debug" / "Gargoyle.exe",
|
||||
setup_pic=tmp_path / "Debug" / "setup.pic",
|
||||
@@ -130,16 +157,19 @@ def test_run_acceptance_orchestrates_build_and_runtime(
|
||||
monkeypatch.setattr(
|
||||
harness,
|
||||
"build_solution",
|
||||
lambda root, configuration, toolchain: BuildResult(command=("MSBuild.exe",), output="ok"),
|
||||
lambda root, configuration, platform, toolchain: BuildResult(
|
||||
command=("MSBuild.exe",),
|
||||
output="ok",
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(harness, "artifacts_for", lambda root, configuration: artifacts)
|
||||
monkeypatch.setattr(harness, "artifacts_for", lambda root, configuration, platform: artifacts)
|
||||
monkeypatch.setattr(harness, "verify_artifacts", lambda value: calls.append("artifacts"))
|
||||
monkeypatch.setattr(harness, "_start_gargoyle", lambda value: process)
|
||||
monkeypatch.setattr(harness, "MessageBoxController", object)
|
||||
monkeypatch.setattr(
|
||||
harness,
|
||||
"_wait_for_setup",
|
||||
lambda proc, timeout_seconds: harness.parse_setup_output(VALID_OUTPUT),
|
||||
lambda proc, platform, timeout_seconds: harness.parse_setup_output(VALID_OUTPUT),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
harness,
|
||||
@@ -167,7 +197,7 @@ def test_wait_for_setup_reads_complete_banner() -> None:
|
||||
"""The setup waiter parses complete stdout from a live process."""
|
||||
process = FakeLiveProcess(StringIO("\n".join(VALID_OUTPUT) + "\n"))
|
||||
|
||||
setup = harness._wait_for_setup(process, timeout_seconds=1) # type: ignore[arg-type]
|
||||
setup = harness._wait_for_setup(process, platform="x86", timeout_seconds=1) # type: ignore[arg-type]
|
||||
|
||||
assert setup.addresses["Stack trampoline"] == 0x00A80038
|
||||
|
||||
@@ -178,7 +208,7 @@ def test_wait_for_setup_reports_early_exit() -> None:
|
||||
process.returncode = 1
|
||||
|
||||
with pytest.raises(AcceptanceError, match="exited early"):
|
||||
harness._wait_for_setup(process, timeout_seconds=1) # type: ignore[arg-type]
|
||||
harness._wait_for_setup(process, platform="x86", timeout_seconds=1) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_wait_for_setup_reports_missing_stdout() -> None:
|
||||
@@ -187,7 +217,7 @@ def test_wait_for_setup_reports_missing_stdout() -> None:
|
||||
process.stdout = None
|
||||
|
||||
with pytest.raises(AcceptanceError, match="stdout unavailable"):
|
||||
harness._wait_for_setup(process, timeout_seconds=1) # type: ignore[arg-type]
|
||||
harness._wait_for_setup(process, platform="x86", timeout_seconds=1) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_wait_for_setup_reports_timeout() -> None:
|
||||
@@ -195,7 +225,7 @@ def test_wait_for_setup_reports_timeout() -> None:
|
||||
process = FakeLiveProcess(StringIO("partial\n"))
|
||||
|
||||
with pytest.raises(AcceptanceError, match="Setup timed out"):
|
||||
harness._wait_for_setup(process, timeout_seconds=0.01) # type: ignore[arg-type]
|
||||
harness._wait_for_setup(process, platform="x86", timeout_seconds=0.01) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_start_gargoyle_reports_launch_failure(
|
||||
@@ -206,6 +236,7 @@ def test_start_gargoyle_reports_launch_failure(
|
||||
artifacts = GargoyleArtifacts(
|
||||
repo_root=tmp_path,
|
||||
configuration="Debug",
|
||||
platform="x86",
|
||||
output_dir=tmp_path,
|
||||
executable=tmp_path / "Gargoyle.exe",
|
||||
setup_pic=tmp_path / "setup.pic",
|
||||
@@ -247,6 +278,7 @@ def test_close_message_boxes_closes_requested_rounds() -> None:
|
||||
process=FakeProcess(), # type: ignore[arg-type]
|
||||
controller=controller, # type: ignore[arg-type]
|
||||
rounds=2,
|
||||
title="gargoyle",
|
||||
timeout_seconds=1,
|
||||
)
|
||||
|
||||
@@ -298,5 +330,6 @@ def test_close_message_boxes_reports_early_exit() -> None:
|
||||
process=process, # type: ignore[arg-type]
|
||||
controller=object(), # type: ignore[arg-type]
|
||||
rounds=1,
|
||||
title="gargoyle",
|
||||
timeout_seconds=1,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user