diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..c1dbd15
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,53 @@
+# Phase 1 — Solution & Shared Library (RDPWrap.Common)
+Create src-csharp/ solution with four projects: RDPWrap.Common (class lib), RDPWInst (console), RDPConf (WinForms), RDPCheck (WinForms)
+NativeMethods.cs — all P/Invoke declarations: kernel32 (GetNativeSystemInfo, LoadLibraryEx, FindResource, Wow64Disable/RevertFsRedirection, CreateProcess, OpenProcess, TerminateProcess, CreateToolhelp32Snapshot, Thread32First/Next, OpenThread, SuspendThread/ResumeThread), advapi32 (all SCM + ACL + token functions), winsta.dll (WinStationEnumerateW, WinStationFreeMemory)
+RegistryHelper.cs — HKLM read/write helpers with KEY_WOW64_64KEY flag support for 64-bit hosts
+ServiceHelper.cs — OpenSCManager/OpenService/QueryServiceConfig/QueryServiceStatusEx/ChangeServiceConfig/StartService wrappers (or wrap System.ServiceProcess.ServiceController where sufficient)
+ArchHelper.cs — GetNativeSystemInfo-based arch detection + Wow64DisableWow64FsRedirection/RevertWow64FsRedirection helpers
+FileVersionHelper.cs — GetFileVersion via LoadLibraryEx + manual VS_VERSIONINFO parsing (or FileVersionInfo.GetVersionInfo())
+ProcessHelper.cs — ExecWait (hidden Process.Start + WaitForExit), KillProcess
+HttpHelper.cs — replace WinInet with HttpClient: DownloadStringAsync (for INI content), DownloadFileAsync (for binary assets)
+ResourceHelper.cs — Assembly.GetManifestResourceStream → extract to file path
+IniHelper.cs — INIHasSection(path, section) string search
+SecurityHelper.cs — ConvertStringSidToSid + SetEntriesInAcl + SetNamedSecurityInfo (grant SID full access), AddPrivilege (token privilege adjustment)
+
+# Phase 2 — RDPWInst (Console Installer)
+Argument parsing + main dispatch (/install, /uninstall, /update, /wraponly)
+CheckInstall() — verify TermService ImagePath (svchost) and ServiceDll (not third-party)
+CheckTermsrvProcess() — EnumServicesStatusEx loop to find TermService PID + co-hosted services; auto-start if PID=0
+CheckTermsrvDependencies() — ensure CertPropSvc and SessionEnv are not disabled
+CheckTermsrvVersion() — read termsrv.dll version, classify as unsupported / partial / full using built-in INI
+TSConfigRegistry(enable) — write fDenyTSConnections, EnableConcurrentSessions, AllowMultipleTSSessions, AllowRemoteRPC, EnableLinkedConnections
+ExtractFiles() — pull rdpw32/rdpw64, rdpclip, rfxvmt, config out of embedded resources; create install dir; set ACLs for S-1-5-18 and S-1-5-6
+SetWrapperDll() / ResetServiceDll() — write/restore ServiceDll registry value (REG_EXPAND_SZ); reg.exe workaround for Vista
+DeleteFiles() — remove rdpwrap.ini, rdpwrap.dll, install folder on uninstall
+GitINIFile() / DownloadFileToDisk() — HttpClient-based downloads from releases/latest/download/rdpwrap.ini
+TryAutoGenerateOffsets() — download RDPWrapOffsetFinder_x64/x86.exe + Zydis_x64/x86.dll, run via cmd.exe /c "... >> rdpwrap.ini", clean up temp files
+AddPrivilege() / KillProcess() / full install/uninstall/update orchestration wiring
+Embed binary resources (rdpw32.dll, rdpw64.dll, rdpclip*, rfxvmt*, rdpwrap.ini) into the .csproj as EmbeddedResource
+Add UAC app manifest: requestedExecutionLevel = requireAdministrator
+
+# Phase 3 — RDPConf (WinForms Configuration GUI)
+MainForm layout — CheckBox (AllowTSConnections, SingleSessionPerUser, HideUsers, CustomPrg), two GroupBox+RadioButton clusters (NLA ×3, Shadow ×5), NumericUpDown for port, status Label pairs for Service/Listener/Wrapper/TS version/Wrapper version, OK/Cancel/Apply/License Button, System.Windows.Forms.Timer
+ReadSettings() — pull all values from HKLM\...\Terminal Server and RDP-Tcp registry keys into controls
+WriteSettings() — write all controls back to registry; on port change call netsh advfirewall firewall set rule name="Remote Desktop" new localport=…
+TimerTimer() — periodic refresh of all status labels (wrapper installed?, service state, listener active, file versions, support level)
+IsWrapperInstalled() / GetTermSrvState() (via ServiceController) / IsListenerWorking() (via WinStationEnumerateW)
+CheckSupport() — load rdpwrap.ini from install path, search for [major.minor.release.build] section
+LicenseForm — TextBox (multiline, readonly) populated from embedded LICENSE resource + Accept/Decline buttons
+FormCreate — arch detection, Wow64DisableWow64FsRedirection; FormClosed — RevertWow64FsRedirection; unsaved-changes guard on close
+UAC manifest + app.manifest (requireAdministrator)
+
+# Phase 4 — RDPCheck (WinForms RDP Tester)
+Add COM interop reference for mstscax.dll (AxMSTSCLib) — either tlbimp-generated assembly or NuGet Microsoft.Rdp.Client
+MainForm layout — AxMsRdpClient2 ActiveX host filling the form
+FormLoad() — read then zero-out SecurityLayer/UserAuthentication in registry, read PortNumber, Sleep(1000), call .Connect()
+OnDisconnected() — full 50-entry reason-code → English string table (matching the Delphi source exactly), MessageBox for codes >2, restore SecurityLayer/UserAuthentication, Application.Exit()
+UAC manifest (requireAdministrator — needed for HKLM registry writes)
+
+# Phase 5 — Build & CI
+Directory.Build.props — shared net481 (or net8.0-windows), x86;x64, enable, enable
+Update GitHub Actions workflows — replace Delphi compiler steps with dotnet build / dotnet publish -r win-x64 -r win-x86
+Remove Delphi compiler steps, Delphi CI caching, .dproj/.dfm artifact handling from all workflows
+Code-sign configuration — signtool.exe step in release workflow for all four output binaries
+Update README.md with new build prerequisites (.NET SDK), build commands, and note that Delphi is no longer required
\ No newline at end of file
diff --git a/src-csharp/Directory.Build.props b/src-csharp/Directory.Build.props
new file mode 100644
index 0000000..83ab81e
--- /dev/null
+++ b/src-csharp/Directory.Build.props
@@ -0,0 +1,15 @@
+
+
+ net8.0-windows
+ x86;x64
+ latest
+ enable
+ enable
+ true
+ $(Platform)
+
+ false
+
+ true
+
+
diff --git a/src-csharp/RDPCheck/RDPCheck.csproj b/src-csharp/RDPCheck/RDPCheck.csproj
new file mode 100644
index 0000000..2e16a06
--- /dev/null
+++ b/src-csharp/RDPCheck/RDPCheck.csproj
@@ -0,0 +1,26 @@
+
+
+ WinExe
+ true
+ RDPCheck
+ RDPCheck
+ app.manifest
+
+
+
+
+
+
+
+
diff --git a/src-csharp/RDPConf/RDPConf.csproj b/src-csharp/RDPConf/RDPConf.csproj
new file mode 100644
index 0000000..59fb41d
--- /dev/null
+++ b/src-csharp/RDPConf/RDPConf.csproj
@@ -0,0 +1,13 @@
+
+
+ WinExe
+ true
+ RDPConf
+ RDPConf
+ app.manifest
+
+
+
+
+
+
diff --git a/src-csharp/RDPWInst/RDPWInst.csproj b/src-csharp/RDPWInst/RDPWInst.csproj
new file mode 100644
index 0000000..c953051
--- /dev/null
+++ b/src-csharp/RDPWInst/RDPWInst.csproj
@@ -0,0 +1,21 @@
+
+
+ Exe
+ RDPWInst
+ RDPWInst
+
+
+
+
+
+
+
+
+
+
diff --git a/src-csharp/RDPWrap.Common/ArchHelper.cs b/src-csharp/RDPWrap.Common/ArchHelper.cs
new file mode 100644
index 0000000..1a50132
--- /dev/null
+++ b/src-csharp/RDPWrap.Common/ArchHelper.cs
@@ -0,0 +1,80 @@
+// Copyright 2024 sjackson0109 — Apache License 2.0
+using System.Runtime.InteropServices;
+
+namespace RDPWrap.Common;
+
+///
+/// Architecture detection and WOW64 file-system redirection control.
+/// Mirrors the Arch / DisableWowRedirection / RevertWowRedirection logic
+/// from RDPWInst.dpr and RDPConf MainUnit.pas.
+///
+public static class ArchHelper
+{
+ private static readonly Lazy _arch = new(DetectArch);
+
+ /// Raw architecture byte: 32 or 64. 0 = unsupported.
+ public static byte Arch => _arch.Value;
+
+ /// true when running on a 64-bit Windows installation.
+ public static bool Is64Bit => Arch == 64;
+
+ ///
+ /// Returns true when the processor architecture is supported
+ /// (x86 or x64). Itanium and unknown architectures return false.
+ ///
+ public static bool IsSupported => Arch != 0;
+
+ private static byte DetectArch()
+ {
+ NativeMethods.GetNativeSystemInfo(out var si);
+ return si.wProcessorArchitecture switch
+ {
+ NativeMethods.PROCESSOR_ARCHITECTURE_INTEL => 32,
+ NativeMethods.PROCESSOR_ARCHITECTURE_AMD64 => 64,
+ _ => 0 // Itanium or unknown — unsupported
+ };
+ }
+
+ // ── WOW64 filesystem redirection ─────────────────────────────────────────
+
+ private static IntPtr _wow64OldValue = IntPtr.Zero;
+
+ ///
+ /// Disables WOW64 filesystem redirection so that 32-bit processes can
+ /// reach the real %SystemRoot%\System32. Call only on 64-bit hosts.
+ /// Returns true on success.
+ ///
+ public static bool DisableWow64Redirection()
+ {
+ if (!Is64Bit) return false;
+ return NativeMethods.Wow64DisableWow64FsRedirection(out _wow64OldValue);
+ }
+
+ ///
+ /// Reverts the WOW64 filesystem redirection state saved by the last call
+ /// to .
+ ///
+ public static bool RevertWow64Redirection()
+ {
+ if (!Is64Bit) return false;
+ return NativeMethods.Wow64RevertWow64FsRedirection(_wow64OldValue);
+ }
+
+ // ── Environment path expansion ────────────────────────────────────────────
+
+ ///
+ /// Expands environment strings in , replacing
+ /// %ProgramFiles% with %ProgramW6432% on 64-bit hosts
+ /// to avoid redirection to the x86 Program Files folder.
+ ///
+ public static string ExpandPath(string path)
+ {
+ if (Is64Bit)
+ path = path.Replace("%ProgramFiles%", "%ProgramW6432%",
+ StringComparison.OrdinalIgnoreCase);
+
+ var buf = new System.Text.StringBuilder(1024);
+ NativeMethods.ExpandEnvironmentStrings(path, buf, (uint)buf.Capacity);
+ return buf.ToString();
+ }
+}
diff --git a/src-csharp/RDPWrap.Common/FileVersionHelper.cs b/src-csharp/RDPWrap.Common/FileVersionHelper.cs
new file mode 100644
index 0000000..a76d7a6
--- /dev/null
+++ b/src-csharp/RDPWrap.Common/FileVersionHelper.cs
@@ -0,0 +1,64 @@
+// Copyright 2024 sjackson0109 — Apache License 2.0
+using System.Diagnostics;
+
+namespace RDPWrap.Common;
+
+///
+/// File-version reading helper. Mirrors the GetFileVersion function used in
+/// RDPWInst.dpr and RDPConf MainUnit.pas.
+///
+public static class FileVersionHelper
+{
+ ///
+ /// Strongly-typed representation of a Windows file version.
+ ///
+ public record FileVersionInfo(
+ ushort Major,
+ ushort Minor,
+ ushort Release,
+ ushort Build,
+ bool IsDebug,
+ bool IsPrerelease,
+ bool IsPrivate,
+ bool IsSpecial)
+ {
+ /// e.g. "10.0.26100.3476"
+ public override string ToString() => $"{Major}.{Minor}.{Release}.{Build}";
+ }
+
+ ///
+ /// Returns the file version of , or null
+ /// if the file does not exist or has no version resource.
+ /// Uses the BCL which does
+ /// not require loading the DLL as executable — safe for locked DLLs.
+ ///
+ public static FileVersionInfo? GetVersion(string filePath)
+ {
+ if (!File.Exists(filePath)) return null;
+
+ try
+ {
+ var fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(filePath);
+ return new FileVersionInfo(
+ (ushort)(fvi.FileMajorPart),
+ (ushort)(fvi.FileMinorPart),
+ (ushort)(fvi.FileBuildPart),
+ (ushort)(fvi.FilePrivatePart),
+ fvi.IsDebug,
+ fvi.IsPreRelease,
+ fvi.IsPrivateBuild,
+ fvi.IsSpecialBuild);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Convenience overload: resolves the path via
+ /// before reading.
+ ///
+ public static FileVersionInfo? GetVersionExpanded(string pathWithEnvVars)
+ => GetVersion(ArchHelper.ExpandPath(pathWithEnvVars));
+}
diff --git a/src-csharp/RDPWrap.Common/HttpHelper.cs b/src-csharp/RDPWrap.Common/HttpHelper.cs
new file mode 100644
index 0000000..042cfd5
--- /dev/null
+++ b/src-csharp/RDPWrap.Common/HttpHelper.cs
@@ -0,0 +1,80 @@
+// Copyright 2024 sjackson0109 — Apache License 2.0
+
+namespace RDPWrap.Common;
+
+///
+/// HTTP download helpers that replace the WinInet-based GitINIFile /
+/// DownloadFileToDisk procedures from RDPWInst.dpr.
+/// Uses with a shared static instance.
+///
+public static class HttpHelper
+{
+ // Single shared instance — HttpClient is designed to be reused.
+ private static readonly HttpClient _client = new(new HttpClientHandler
+ {
+ AllowAutoRedirect = true,
+ MaxAutomaticRedirections = 5,
+ })
+ {
+ Timeout = TimeSpan.FromSeconds(60),
+ DefaultRequestHeaders = { { "User-Agent", "RDP-Wrapper-Updater/1.0" } }
+ };
+
+ ///
+ /// Downloads the text content at and returns it as
+ /// a string. Returns null on any failure.
+ /// Mirrors the Delphi GitINIFile function.
+ ///
+ public static async Task DownloadStringAsync(string url)
+ {
+ try
+ {
+ return await _client.GetStringAsync(url).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[-] HTTP download failed ({url}): {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// Downloads the binary content at and saves it to
+ /// . Returns true when the file exists
+ /// and is non-empty after download.
+ /// Mirrors the Delphi DownloadFileToDisk function.
+ ///
+ public static async Task DownloadFileAsync(string url, string destPath)
+ {
+ try
+ {
+ using var response = await _client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead)
+ .ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var file = File.Create(destPath);
+ await stream.CopyToAsync(file).ConfigureAwait(false);
+
+ return new FileInfo(destPath).Length > 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[-] HTTP file download failed ({url}): {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Synchronous wrapper for — suitable
+ /// for the installer's purely-sequential flow.
+ ///
+ public static string? DownloadString(string url)
+ => DownloadStringAsync(url).GetAwaiter().GetResult();
+
+ ///
+ /// Synchronous wrapper for .
+ ///
+ public static bool DownloadFile(string url, string destPath)
+ => DownloadFileAsync(url, destPath).GetAwaiter().GetResult();
+}
diff --git a/src-csharp/RDPWrap.Common/IniHelper.cs b/src-csharp/RDPWrap.Common/IniHelper.cs
new file mode 100644
index 0000000..c23f6e2
--- /dev/null
+++ b/src-csharp/RDPWrap.Common/IniHelper.cs
@@ -0,0 +1,61 @@
+// Copyright 2024 sjackson0109 — Apache License 2.0
+
+namespace RDPWrap.Common;
+
+///
+/// Lightweight INI-file helpers used to check whether a specific version
+/// section exists in rdpwrap.ini. Mirrors the INIHasSection function and
+/// CheckSupport version-lookup logic from RDPWInst.dpr and RDPConf MainUnit.pas.
+///
+public static class IniHelper
+{
+ ///
+ /// Returns true when the INI file at
+ /// contains the section header [].
+ /// Mirrors the Delphi INIHasSection function.
+ ///
+ public static bool HasSection(string iniPath, string section)
+ {
+ if (!File.Exists(iniPath)) return false;
+ var needle = $"[{section}]";
+ foreach (var line in File.ReadLines(iniPath))
+ {
+ if (line.Contains(needle, StringComparison.Ordinal))
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Loads the full text of and returns it,
+ /// or an empty string if the file does not exist.
+ ///
+ public static string LoadText(string iniPath)
+ => File.Exists(iniPath) ? File.ReadAllText(iniPath) : string.Empty;
+
+ ///
+ /// Checks the support level of a given termsrv.dll version against the
+ /// INI content string .
+ ///
+ ///
+ /// 0 = not supported, 1 = partially supported (Vista/7 legacy),
+ /// 2 = fully supported (entry found in ini).
+ ///
+ public static int CheckSupportLevel(string iniContent,
+ FileVersionHelper.FileVersionInfo fv)
+ {
+ int level = 0;
+
+ // Vista (6.0) and Windows 7 (6.1) are "partially" supported without
+ // a specific INI entry — mirrors the Delphi CheckSupport logic.
+ if ((fv.Major == 6 && fv.Minor == 0) ||
+ (fv.Major == 6 && fv.Minor == 1))
+ level = 1;
+
+ var verTxt = fv.ToString(); // "major.minor.release.build"
+ if (iniContent.Contains($"[{verTxt}]", StringComparison.Ordinal))
+ level = 2;
+
+ return level;
+ }
+}
diff --git a/src-csharp/RDPWrap.Common/NativeMethods.cs b/src-csharp/RDPWrap.Common/NativeMethods.cs
new file mode 100644
index 0000000..d10505f
--- /dev/null
+++ b/src-csharp/RDPWrap.Common/NativeMethods.cs
@@ -0,0 +1,467 @@
+// Copyright 2024 sjackson0109
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Runtime.InteropServices;
+
+namespace RDPWrap.Common;
+
+///
+/// All P/Invoke declarations used across RDPWInst, RDPConf and RDPCheck.
+/// Mirrors the unhooked Win32 imports from the original Delphi sources.
+///
+internal static class NativeMethods
+{
+ // ─── DLL names ────────────────────────────────────────────────────────────
+ internal const string Kernel32 = "kernel32.dll";
+ internal const string Advapi32 = "advapi32.dll";
+ internal const string WinSta = "winsta.dll";
+
+ // ─── Constants ────────────────────────────────────────────────────────────
+
+ // Architecture
+ internal const ushort PROCESSOR_ARCHITECTURE_INTEL = 0;
+ internal const ushort PROCESSOR_ARCHITECTURE_IA64 = 6;
+ internal const ushort PROCESSOR_ARCHITECTURE_AMD64 = 9;
+
+ // Registry
+ internal const uint KEY_WOW64_64KEY = 0x0100;
+ internal const uint KEY_WOW64_32KEY = 0x0200;
+ internal const uint KEY_READ = 0x20019;
+ internal const uint KEY_WRITE = 0x20006;
+ internal const uint KEY_QUERY_VALUE = 0x0001;
+ internal const uint KEY_SET_VALUE = 0x0002;
+
+ // Service control manager
+ internal const uint SC_MANAGER_CONNECT = 0x0001;
+ internal const uint SC_MANAGER_CREATE_SERVICE = 0x0002;
+ internal const uint SC_MANAGER_ENUMERATE_SERVICE = 0x0004;
+ internal const uint SC_MANAGER_ALL_ACCESS = 0xF003F;
+
+ internal const uint SERVICE_QUERY_CONFIG = 0x0001;
+ internal const uint SERVICE_CHANGE_CONFIG = 0x0002;
+ internal const uint SERVICE_QUERY_STATUS = 0x0004;
+ internal const uint SERVICE_START = 0x0010;
+ internal const uint SERVICE_STOP = 0x0020;
+ internal const uint SERVICE_ALL_ACCESS = 0xF01FF;
+
+ internal const uint SERVICE_WIN32 = 0x30;
+ internal const uint SERVICE_STATE_ALL = 0x03;
+ internal const uint SERVICE_NO_CHANGE = 0xFFFFFFFF;
+ internal const uint SERVICE_AUTO_START = 0x02;
+ internal const uint SERVICE_DEMAND_START = 0x03;
+ internal const uint SERVICE_DISABLED = 0x04;
+
+ internal const uint SERVICE_STOPPED = 0x00000001;
+ internal const uint SERVICE_START_PENDING = 0x00000002;
+ internal const uint SERVICE_STOP_PENDING = 0x00000003;
+ internal const uint SERVICE_RUNNING = 0x00000004;
+ internal const uint SERVICE_CONTINUE_PENDING = 0x00000005;
+ internal const uint SERVICE_PAUSE_PENDING = 0x00000006;
+ internal const uint SERVICE_PAUSED = 0x00000007;
+
+ internal const uint SC_ENUM_PROCESS_INFO = 0;
+ internal const uint SC_STATUS_PROCESS_INFO = 0;
+ internal const uint ERROR_MORE_DATA = 234;
+ internal const uint ERROR_SERVICE_DOES_NOT_EXIST = 1060;
+ internal const uint ERROR_SERVICE_NOT_ACTIVE = 1062;
+
+ // Process/Thread
+ internal const uint PROCESS_TERMINATE = 0x0001;
+ internal const uint THREAD_SUSPEND_RESUME = 0x0002;
+ internal const uint TH32CS_SNAPTHREAD = 0x00000004;
+
+ // Token privileges
+ internal const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
+ internal const uint TOKEN_QUERY = 0x0008;
+ internal const uint SE_PRIVILEGE_ENABLED = 0x00000002;
+
+ // Privilege names
+ internal const string SE_DEBUG_NAME = "SeDebugPrivilege";
+ internal const string SE_RESTORE_NAME = "SeRestorePrivilege";
+ internal const string SE_BACKUP_NAME = "SeBackupPrivilege";
+
+ // Security
+ internal const uint DACL_SECURITY_INFORMATION = 0x00000004;
+ internal const uint SE_FILE_OBJECT = 1;
+ internal const uint GRANT_ACCESS = 1;
+ internal const uint SUB_CONTAINERS_AND_OBJECTS_INHERIT = 0x3;
+ internal const uint NO_MULTIPLE_TRUSTEE = 0;
+ internal const uint TRUSTEE_IS_SID = 0;
+ internal const uint TRUSTEE_IS_WELL_KNOWN_GROUP = 5;
+ internal const uint GENERIC_ALL = 0x10000000;
+
+ // LoadLibraryEx flags
+ internal const uint LOAD_LIBRARY_AS_DATAFILE = 0x00000002;
+
+ // Version resource type
+ internal const uint RT_VERSION = 16;
+
+ // CreateProcess - STARTUPINFO flags
+ internal const uint STARTF_USESHOWWINDOW = 0x00000001;
+ internal const ushort SW_HIDE = 0;
+
+ // GetModuleHandleEx
+ internal const uint GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS = 0x00000004;
+ internal const uint GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 0x00000002;
+
+ // Error codes
+ internal const uint ERROR_SUCCESS = 0;
+ internal const uint ERROR_ACCESS_DENIED = 5;
+ internal const uint ERROR_NOT_SUPPORTED = 50;
+ internal const uint ERROR_SERVICE_ALREADY_RUNNING = 1056;
+
+ // ─── Structures ───────────────────────────────────────────────────────────
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct SYSTEM_INFO
+ {
+ internal ushort wProcessorArchitecture;
+ internal ushort wReserved;
+ internal uint dwPageSize;
+ internal IntPtr lpMinimumApplicationAddress;
+ internal IntPtr lpMaximumApplicationAddress;
+ internal UIntPtr dwActiveProcessorMask;
+ internal uint dwNumberOfProcessors;
+ internal uint dwProcessorType;
+ internal uint dwAllocationGranularity;
+ internal ushort wProcessorLevel;
+ internal ushort wProcessorRevision;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ internal struct STARTUPINFO
+ {
+ internal uint cb;
+ internal string? lpReserved;
+ internal string? lpDesktop;
+ internal string? lpTitle;
+ internal uint dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars;
+ internal uint dwFillAttribute;
+ internal uint dwFlags;
+ internal ushort wShowWindow;
+ internal ushort cbReserved2;
+ internal IntPtr lpReserved2;
+ internal IntPtr hStdInput, hStdOutput, hStdError;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct PROCESS_INFORMATION
+ {
+ internal IntPtr hProcess;
+ internal IntPtr hThread;
+ internal uint dwProcessId;
+ internal uint dwThreadId;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct THREADENTRY32
+ {
+ internal uint dwSize;
+ internal uint cntUsage;
+ internal uint th32ThreadID;
+ internal uint th32OwnerProcessID;
+ internal int tpBasePri;
+ internal int tpDeltaPri;
+ internal uint dwFlags;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct SERVICE_STATUS_PROCESS
+ {
+ internal uint dwServiceType;
+ internal uint dwCurrentState;
+ internal uint dwControlsAccepted;
+ internal uint dwWin32ExitCode;
+ internal uint dwServiceSpecificExitCode;
+ internal uint dwCheckPoint;
+ internal uint dwWaitHint;
+ internal uint dwProcessId;
+ internal uint dwServiceFlags;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ internal struct ENUM_SERVICE_STATUS_PROCESS
+ {
+ internal string lpServiceName;
+ internal string lpDisplayName;
+ internal SERVICE_STATUS_PROCESS ServiceStatusProcess;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ internal struct QUERY_SERVICE_CONFIG
+ {
+ internal uint dwServiceType;
+ internal uint dwStartType;
+ internal uint dwErrorControl;
+ internal string lpBinaryPathName;
+ internal string lpLoadOrderGroup;
+ internal uint dwTagId;
+ internal string lpDependencies;
+ internal string lpServiceStartName;
+ internal string lpDisplayName;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct LUID
+ {
+ internal uint LowPart;
+ internal int HighPart;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct LUID_AND_ATTRIBUTES
+ {
+ internal LUID Luid;
+ internal uint Attributes;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct TOKEN_PRIVILEGES
+ {
+ internal uint PrivilegeCount;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
+ internal LUID_AND_ATTRIBUTES[] Privileges;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ internal struct EXPLICIT_ACCESS
+ {
+ internal uint grfAccessPermissions;
+ internal uint grfAccessMode;
+ internal uint grfInheritance;
+ internal TRUSTEE Trustee;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ internal struct TRUSTEE
+ {
+ internal IntPtr pMultipleTrustee;
+ internal uint MultipleTrusteeOperation;
+ internal uint TrusteeForm;
+ internal uint TrusteeType;
+ internal IntPtr ptstrName; // SID pointer or string pointer
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ internal struct WTS_SESSION_INFO
+ {
+ internal uint SessionId;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 34)]
+ internal string Name;
+ internal uint State;
+ }
+
+ // ─── kernel32.dll ─────────────────────────────────────────────────────────
+
+ [DllImport(Kernel32, SetLastError = true)]
+ internal static extern void GetNativeSystemInfo(out SYSTEM_INFO lpSystemInfo);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool Wow64DisableWow64FsRedirection(out IntPtr oldValue);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool Wow64RevertWow64FsRedirection(IntPtr oldValue);
+
+ [DllImport(Kernel32, CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool FreeLibrary(IntPtr hModule);
+
+ [DllImport(Kernel32, CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern IntPtr FindResource(IntPtr hModule, IntPtr lpName, IntPtr lpType);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ internal static extern IntPtr LoadResource(IntPtr hModule, IntPtr hResInfo);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ internal static extern IntPtr LockResource(IntPtr hResData);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ internal static extern uint SizeofResource(IntPtr hModule, IntPtr hResInfo);
+
+ [DllImport(Kernel32, CharSet = CharSet.Unicode, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool CreateProcess(
+ string? lpApplicationName,
+ string lpCommandLine,
+ IntPtr lpProcessAttributes,
+ IntPtr lpThreadAttributes,
+ [MarshalAs(UnmanagedType.Bool)] bool bInheritHandles,
+ uint dwCreationFlags,
+ IntPtr lpEnvironment,
+ string? lpCurrentDirectory,
+ ref STARTUPINFO lpStartupInfo,
+ out PROCESS_INFORMATION lpProcessInformation);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ internal static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool CloseHandle(IntPtr hObject);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ internal static extern IntPtr OpenProcess(uint dwDesiredAccess,
+ [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwProcessId);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ internal static extern uint GetCurrentProcessId();
+
+ [DllImport(Kernel32, SetLastError = true)]
+ internal static extern uint GetCurrentThreadId();
+
+ [DllImport(Kernel32, SetLastError = true)]
+ internal static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool Thread32First(IntPtr hSnapshot, ref THREADENTRY32 lpte);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool Thread32Next(IntPtr hSnapshot, ref THREADENTRY32 lpte);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ internal static extern IntPtr OpenThread(uint dwDesiredAccess,
+ [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwThreadId);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ internal static extern uint SuspendThread(IntPtr hThread);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ internal static extern uint ResumeThread(IntPtr hThread);
+
+ [DllImport(Kernel32, CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern uint GetModuleFileName(IntPtr hModule,
+ System.Text.StringBuilder lpFilename, uint nSize);
+
+ [DllImport(Kernel32, CharSet = CharSet.Unicode, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool GetModuleHandleEx(uint dwFlags, IntPtr lpModuleName,
+ out IntPtr phModule);
+
+ [DllImport(Kernel32, CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern uint ExpandEnvironmentStrings(string lpSrc,
+ System.Text.StringBuilder lpDst, uint nSize);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool DeleteFile(string lpFileName);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool RemoveDirectory(string lpPathName);
+
+ [DllImport(Kernel32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool OpenProcessToken(IntPtr processHandle,
+ uint desiredAccess, out IntPtr tokenHandle);
+
+ // ─── advapi32.dll ─────────────────────────────────────────────────────────
+
+ [DllImport(Advapi32, SetLastError = true)]
+ internal static extern IntPtr OpenSCManager(string? lpMachineName,
+ string? lpDatabaseName, uint dwDesiredAccess);
+
+ [DllImport(Advapi32, CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern IntPtr OpenService(IntPtr hSCManager,
+ string lpServiceName, uint dwDesiredAccess);
+
+ [DllImport(Advapi32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool CloseServiceHandle(IntPtr hSCObject);
+
+ [DllImport(Advapi32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool QueryServiceConfig(IntPtr hService,
+ IntPtr lpServiceConfig, uint cbBufSize, out uint pcbBytesNeeded);
+
+ [DllImport(Advapi32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool QueryServiceStatusEx(IntPtr hService,
+ uint InfoLevel, IntPtr lpBuffer, uint cbBufSize, out uint pcbBytesNeeded);
+
+ [DllImport(Advapi32, CharSet = CharSet.Unicode, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool ChangeServiceConfig(IntPtr hService,
+ uint dwServiceType, uint dwStartType, uint dwErrorControl,
+ string? lpBinaryPathName, string? lpLoadOrderGroup, IntPtr lpdwTagId,
+ string? lpDependencies, string? lpServiceStartName, string? lpPassword,
+ string? lpDisplayName);
+
+ [DllImport(Advapi32, CharSet = CharSet.Unicode, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool StartService(IntPtr hService,
+ uint dwNumServiceArgs, string[]? lpServiceArgVectors);
+
+ [DllImport(Advapi32, CharSet = CharSet.Unicode, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool EnumServicesStatusEx(
+ IntPtr hSCManager, uint InfoLevel, uint dwServiceType, uint dwServiceState,
+ IntPtr lpServices, uint cbBufSize,
+ out uint pcbBytesNeeded, out uint lpServicesReturned,
+ ref uint lpResumeHandle, string? pszGroupName);
+
+ [DllImport(Advapi32, CharSet = CharSet.Unicode, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool LookupPrivilegeValue(string? lpSystemName,
+ string lpName, out LUID lpLuid);
+
+ [DllImport(Advapi32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool AdjustTokenPrivileges(IntPtr tokenHandle,
+ [MarshalAs(UnmanagedType.Bool)] bool disableAllPrivileges,
+ ref TOKEN_PRIVILEGES newState, uint bufferLength,
+ IntPtr previousState, IntPtr returnLength);
+
+ [DllImport(Advapi32, CharSet = CharSet.Unicode, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool ConvertStringSidToSid(string stringSid, out IntPtr sid);
+
+ [DllImport(Advapi32, SetLastError = true)]
+ internal static extern uint SetEntriesInAcl(uint cCountOfExplicitEntries,
+ ref EXPLICIT_ACCESS pListOfExplicitEntries, IntPtr oldAcl, out IntPtr newAcl);
+
+ [DllImport(Advapi32, CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern uint SetNamedSecurityInfo(string pObjectName,
+ uint ObjectType, uint SecurityInfo,
+ IntPtr psidOwner, IntPtr psidGroup, IntPtr pDacl, IntPtr pSacl);
+
+ [DllImport(Advapi32, SetLastError = true)]
+ internal static extern IntPtr LocalFree(IntPtr hMem);
+
+ // ─── winsta.dll ───────────────────────────────────────────────────────────
+
+ ///
+ /// Enumerates WTS sessions on the local server.
+ /// Pass IntPtr.Zero as hServer for the local machine.
+ ///
+ [DllImport(WinSta, EntryPoint = "WinStationEnumerateW",
+ CharSet = CharSet.Unicode, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool WinStationEnumerate(IntPtr hServer,
+ out IntPtr ppSessionInfo, out uint pCount);
+
+ [DllImport(WinSta, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool WinStationFreeMemory(IntPtr p);
+}
diff --git a/src-csharp/RDPWrap.Common/ProcessHelper.cs b/src-csharp/RDPWrap.Common/ProcessHelper.cs
new file mode 100644
index 0000000..baee9d9
--- /dev/null
+++ b/src-csharp/RDPWrap.Common/ProcessHelper.cs
@@ -0,0 +1,63 @@
+// Copyright 2024 sjackson0109 — Apache License 2.0
+using System.Runtime.InteropServices;
+
+namespace RDPWrap.Common;
+
+///
+/// Process creation and termination helpers. Mirrors ExecWait and KillProcess
+/// from RDPWInst.dpr (console variant) and RDPConf MainUnit.pas (GUI variant).
+///
+public static class ProcessHelper
+{
+ ///
+ /// Creates a process from , waits for it to
+ /// exit, then returns true. The process window is hidden.
+ /// Mirrors the Delphi ExecWait procedure.
+ ///
+ public static bool ExecWait(string commandLine, bool hideWindow = true)
+ {
+ var si = new NativeMethods.STARTUPINFO
+ {
+ cb = (uint)Marshal.SizeOf(),
+ dwFlags = hideWindow ? NativeMethods.STARTF_USESHOWWINDOW : 0u,
+ wShowWindow = hideWindow ? NativeMethods.SW_HIDE : (ushort)1
+ };
+
+ // CommandLine must be mutable — pass a copy
+ string cmdCopy = new(commandLine);
+
+ if (!NativeMethods.CreateProcess(null, cmdCopy, IntPtr.Zero, IntPtr.Zero,
+ true, 0, IntPtr.Zero, null, ref si, out var pi))
+ {
+ Console.Error.WriteLine(
+ $"[-] CreateProcess error (code {Marshal.GetLastWin32Error()}).");
+ return false;
+ }
+
+ NativeMethods.WaitForSingleObject(pi.hProcess, 0xFFFFFFFF);
+ NativeMethods.CloseHandle(pi.hThread);
+ NativeMethods.CloseHandle(pi.hProcess);
+ return true;
+ }
+
+ ///
+ /// Terminates the process with . Mirrors the Delphi
+ /// KillProcess procedure.
+ ///
+ public static void KillProcess(uint pid)
+ {
+ var hProc = NativeMethods.OpenProcess(NativeMethods.PROCESS_TERMINATE, false, pid);
+ if (hProc == IntPtr.Zero)
+ {
+ Console.Error.WriteLine(
+ $"[-] OpenProcess error (code {Marshal.GetLastWin32Error()}).");
+ return;
+ }
+
+ if (!NativeMethods.TerminateProcess(hProc, 0))
+ Console.Error.WriteLine(
+ $"[-] TerminateProcess error (code {Marshal.GetLastWin32Error()}).");
+
+ NativeMethods.CloseHandle(hProc);
+ }
+}
diff --git a/src-csharp/RDPWrap.Common/RDPWrap.Common.csproj b/src-csharp/RDPWrap.Common/RDPWrap.Common.csproj
new file mode 100644
index 0000000..ce081e8
--- /dev/null
+++ b/src-csharp/RDPWrap.Common/RDPWrap.Common.csproj
@@ -0,0 +1,7 @@
+
+
+ Library
+ RDPWrap.Common
+ RDPWrap.Common
+
+
diff --git a/src-csharp/RDPWrap.Common/RegistryHelper.cs b/src-csharp/RDPWrap.Common/RegistryHelper.cs
new file mode 100644
index 0000000..c724367
--- /dev/null
+++ b/src-csharp/RDPWrap.Common/RegistryHelper.cs
@@ -0,0 +1,97 @@
+// Copyright 2024 sjackson0109 — Apache License 2.0
+using Microsoft.Win32;
+
+namespace RDPWrap.Common;
+
+///
+/// Thin wrappers around that mirror the
+/// Delphi TRegistry usage in RDPWInst and RDPConf, with optional WOW64 flag
+/// support (KEY_WOW64_64KEY) for 64-bit registry views from 32-bit processes.
+///
+public static class RegistryHelper
+{
+ // On 64-bit Windows we always open the 64-bit view to match the Delphi code
+ // that passes KEY_WOW64_64KEY when Arch = 64.
+ private static RegistryView ViewForArch() =>
+ ArchHelper.Is64Bit ? RegistryView.Registry64 : RegistryView.Default;
+
+ // ── Convenience open helpers ──────────────────────────────────────────────
+
+ /// Opens a read-only key under HKLM, respecting the host architecture.
+ public static RegistryKey? OpenHklmRead(string subKey)
+ {
+ using var hive = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, ViewForArch());
+ return hive.OpenSubKey(subKey, writable: false);
+ }
+
+ /// Opens a writable key under HKLM (creates if absent).
+ public static RegistryKey OpenHklmWrite(string subKey)
+ {
+ using var hive = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, ViewForArch());
+ return hive.CreateSubKey(subKey, writable: true)
+ ?? throw new InvalidOperationException($"Cannot open/create HKLM\\{subKey}");
+ }
+
+ // ── Typed read helpers ────────────────────────────────────────────────────
+
+ ///
+ /// Reads a string value from HKLM. Returns null if the key or
+ /// value does not exist.
+ ///
+ public static string? ReadString(string subKey, string valueName)
+ {
+ using var key = OpenHklmRead(subKey);
+ return key?.GetValue(valueName) as string;
+ }
+
+ ///
+ /// Reads a DWORD value from HKLM. Returns
+ /// if the key or value does not exist.
+ ///
+ public static int ReadInt(string subKey, string valueName, int defaultValue = 0)
+ {
+ using var key = OpenHklmRead(subKey);
+ if (key is null) return defaultValue;
+ var raw = key.GetValue(valueName);
+ return raw is int i ? i : defaultValue;
+ }
+
+ ///
+ /// Reads a DWORD as a bool (non-zero = true). Returns
+ /// if absent.
+ ///
+ public static bool ReadBool(string subKey, string valueName, bool defaultValue = false)
+ {
+ using var key = OpenHklmRead(subKey);
+ if (key is null) return defaultValue;
+ var raw = key.GetValue(valueName);
+ return raw is int i ? i != 0 : defaultValue;
+ }
+
+ // ── Typed write helpers ───────────────────────────────────────────────────
+
+ /// Writes a REG_SZ string value.
+ public static void WriteString(string subKey, string valueName, string value)
+ {
+ using var key = OpenHklmWrite(subKey);
+ key.SetValue(valueName, value, RegistryValueKind.String);
+ }
+
+ /// Writes a REG_EXPAND_SZ string value (mirrors Delphi WriteExpandString).
+ public static void WriteExpandString(string subKey, string valueName, string value)
+ {
+ using var key = OpenHklmWrite(subKey);
+ key.SetValue(valueName, value, RegistryValueKind.ExpandString);
+ }
+
+ /// Writes a DWORD integer value.
+ public static void WriteInt(string subKey, string valueName, int value)
+ {
+ using var key = OpenHklmWrite(subKey);
+ key.SetValue(valueName, value, RegistryValueKind.DWord);
+ }
+
+ /// Writes a boolean as a DWORD (1/0).
+ public static void WriteBool(string subKey, string valueName, bool value)
+ => WriteInt(subKey, valueName, value ? 1 : 0);
+}
diff --git a/src-csharp/RDPWrap.Common/ResourceHelper.cs b/src-csharp/RDPWrap.Common/ResourceHelper.cs
new file mode 100644
index 0000000..85ebf45
--- /dev/null
+++ b/src-csharp/RDPWrap.Common/ResourceHelper.cs
@@ -0,0 +1,88 @@
+// Copyright 2024 sjackson0109 — Apache License 2.0
+using System.Reflection;
+
+namespace RDPWrap.Common;
+
+///
+/// Helpers for reading and extracting managed embedded resources.
+/// Mirrors the Delphi ExtractRes / ExtractResText procedures from RDPWInst.dpr
+/// and RDPConf MainUnit.pas, translated to the .NET manifest-resource model.
+///
+public static class ResourceHelper
+{
+ ///
+ /// Reads the contents of the embedded resource named
+ /// from
+ /// and returns it as a UTF-8 string. Returns null if not found.
+ ///
+ public static string? ReadText(string resourceName,
+ Assembly? assembly = null)
+ {
+ assembly ??= Assembly.GetCallingAssembly();
+ using var stream = assembly.GetManifestResourceStream(resourceName);
+ if (stream is null) return null;
+ using var reader = new StreamReader(stream, System.Text.Encoding.UTF8);
+ return reader.ReadToEnd();
+ }
+
+ ///
+ /// Reads the contents of the embedded resource named
+ /// and returns the raw bytes.
+ /// Returns null if not found.
+ ///
+ public static byte[]? ReadBytes(string resourceName,
+ Assembly? assembly = null)
+ {
+ assembly ??= Assembly.GetCallingAssembly();
+ using var stream = assembly.GetManifestResourceStream(resourceName);
+ if (stream is null) return null;
+ using var ms = new MemoryStream();
+ stream.CopyTo(ms);
+ return ms.ToArray();
+ }
+
+ ///
+ /// Extracts the embedded resource to disk
+ /// at , creating parent directories as needed.
+ /// Returns true on success. Mirrors the Delphi ExtractRes procedure.
+ ///
+ public static bool ExtractToDisk(string resourceName, string destPath,
+ Assembly? assembly = null)
+ {
+ assembly ??= Assembly.GetCallingAssembly();
+ using var stream = assembly.GetManifestResourceStream(resourceName);
+ if (stream is null)
+ {
+ Console.Error.WriteLine($"[-] Resource not found: {resourceName}");
+ return false;
+ }
+
+ var dir = Path.GetDirectoryName(destPath);
+ if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
+ Directory.CreateDirectory(dir);
+
+ try
+ {
+ using var file = File.Create(destPath);
+ stream.CopyTo(file);
+ Console.WriteLine($"[+] Extracted {resourceName} -> {destPath}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine(
+ $"[-] Failed to extract resource {resourceName} to {destPath}: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Lists all manifest resource names in
+ /// — useful for debugging resource name mismatches.
+ ///
+ public static IEnumerable ListResources(Assembly? assembly = null)
+ {
+ assembly ??= Assembly.GetCallingAssembly();
+ return assembly.GetManifestResourceNames();
+ }
+}
diff --git a/src-csharp/RDPWrap.Common/SecurityHelper.cs b/src-csharp/RDPWrap.Common/SecurityHelper.cs
new file mode 100644
index 0000000..12b7b5b
--- /dev/null
+++ b/src-csharp/RDPWrap.Common/SecurityHelper.cs
@@ -0,0 +1,168 @@
+// Copyright 2024 sjackson0109 — Apache License 2.0
+using System.Runtime.InteropServices;
+
+namespace RDPWrap.Common;
+
+///
+/// Security helpers: granting SID-based DACL entries (GrantSidFullAccess) and
+/// adjusting process token privileges (AddPrivilege). Mirrors the Delphi
+/// implementations in RDPWInst.dpr.
+///
+public static class SecurityHelper
+{
+ // ── DACL: grant a well-known SID full access to a file/folder ─────────────
+
+ ///
+ /// Grants GENERIC_ALL access to the well-known SID string
+ /// on the file/directory at
+ /// . Mirrors the Delphi GrantSidFullAccess
+ /// procedure (used for "S-1-5-18" = Local System, "S-1-5-6" = Service).
+ ///
+ public static void GrantSidFullAccess(string path, string stringSid)
+ {
+ if (!NativeMethods.ConvertStringSidToSid(stringSid, out IntPtr pSid))
+ {
+ Console.Error.WriteLine(
+ $"[-] ConvertStringSidToSid error (code {Marshal.GetLastWin32Error()}) " +
+ $"for SID {stringSid}.");
+ return;
+ }
+
+ try
+ {
+ var ea = new NativeMethods.EXPLICIT_ACCESS
+ {
+ grfAccessPermissions = NativeMethods.GENERIC_ALL,
+ grfAccessMode = NativeMethods.GRANT_ACCESS,
+ grfInheritance = NativeMethods.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
+ Trustee = new NativeMethods.TRUSTEE
+ {
+ pMultipleTrustee = IntPtr.Zero,
+ MultipleTrusteeOperation = NativeMethods.NO_MULTIPLE_TRUSTEE,
+ TrusteeForm = NativeMethods.TRUSTEE_IS_SID,
+ TrusteeType = NativeMethods.TRUSTEE_IS_WELL_KNOWN_GROUP,
+ ptstrName = pSid
+ }
+ };
+
+ uint result = NativeMethods.SetEntriesInAcl(1, ref ea, IntPtr.Zero, out IntPtr pNewAcl);
+ if (result == NativeMethods.ERROR_SUCCESS)
+ {
+ uint secResult = NativeMethods.SetNamedSecurityInfo(
+ path,
+ NativeMethods.SE_FILE_OBJECT,
+ NativeMethods.DACL_SECURITY_INFORMATION,
+ IntPtr.Zero, IntPtr.Zero, pNewAcl, IntPtr.Zero);
+
+ if (secResult != NativeMethods.ERROR_SUCCESS)
+ Console.Error.WriteLine(
+ $"[-] SetNamedSecurityInfo error (code {secResult}).");
+
+ NativeMethods.LocalFree(pNewAcl);
+ }
+ else
+ {
+ Console.Error.WriteLine($"[-] SetEntriesInAcl error (code {result}).");
+ }
+ }
+ finally
+ {
+ NativeMethods.LocalFree(pSid);
+ }
+ }
+
+ // ── Token privileges ──────────────────────────────────────────────────────
+
+ ///
+ /// Enables the named privilege (e.g. SeDebugPrivilege) for the
+ /// current process token. Mirrors the Delphi AddPrivilege function.
+ /// Returns true on success.
+ ///
+ public static bool AddPrivilege(string privilegeName)
+ {
+ if (!NativeMethods.OpenProcessToken(
+ System.Diagnostics.Process.GetCurrentProcess().Handle,
+ NativeMethods.TOKEN_ADJUST_PRIVILEGES | NativeMethods.TOKEN_QUERY,
+ out IntPtr hToken))
+ {
+ Console.Error.WriteLine(
+ $"[-] OpenProcessToken error (code {Marshal.GetLastWin32Error()}).");
+ return false;
+ }
+
+ try
+ {
+ if (!NativeMethods.LookupPrivilegeValue(null, privilegeName, out var luid))
+ {
+ Console.Error.WriteLine(
+ $"[-] LookupPrivilegeValue error (code {Marshal.GetLastWin32Error()}).");
+ return false;
+ }
+
+ var tp = new NativeMethods.TOKEN_PRIVILEGES
+ {
+ PrivilegeCount = 1,
+ Privileges = new[]
+ {
+ new NativeMethods.LUID_AND_ATTRIBUTES
+ {
+ Luid = luid,
+ Attributes = NativeMethods.SE_PRIVILEGE_ENABLED
+ }
+ }
+ };
+
+ if (!NativeMethods.AdjustTokenPrivileges(
+ hToken, false, ref tp,
+ (uint)Marshal.SizeOf(tp),
+ IntPtr.Zero, IntPtr.Zero))
+ {
+ Console.Error.WriteLine(
+ $"[-] AdjustTokenPrivileges error (code {Marshal.GetLastWin32Error()}).");
+ return false;
+ }
+
+ return true;
+ }
+ finally
+ {
+ NativeMethods.CloseHandle(hToken);
+ }
+ }
+
+ // ── RDP-Tcp listener ──────────────────────────────────────────────────────
+
+ ///
+ /// Returns true when there is an active "RDP-Tcp" WTS listener on
+ /// the local machine — i.e. WinStationEnumerateW returns a session
+ /// named "RDP-Tcp". Mirrors the Delphi IsListenerWorking function.
+ ///
+ public static bool IsRdpListenerWorking()
+ {
+ if (!NativeMethods.WinStationEnumerate(IntPtr.Zero,
+ out IntPtr ppInfo, out uint count))
+ return false;
+
+ try
+ {
+ int ptrSize = IntPtr.Size;
+ // WTS_SESSION_INFO is: DWORD SessionId + 34 WCHARs (Name) + DWORD State
+ // = 4 + 68 + 4 = 76 bytes, but aligned to 4-byte boundary → 76 bytes.
+ int entrySize = 4 + 34 * 2 + 4; // = 76
+
+ for (uint i = 0; i < count; i++)
+ {
+ IntPtr entry = ppInfo + (int)(i * (uint)entrySize);
+ // Name starts at offset 4
+ string name = Marshal.PtrToStringUni(entry + 4, 34).TrimEnd('\0');
+ if (name == "RDP-Tcp") return true;
+ }
+ }
+ finally
+ {
+ NativeMethods.WinStationFreeMemory(ppInfo);
+ }
+
+ return false;
+ }
+}
diff --git a/src-csharp/RDPWrap.Common/ServiceHelper.cs b/src-csharp/RDPWrap.Common/ServiceHelper.cs
new file mode 100644
index 0000000..f51aded
--- /dev/null
+++ b/src-csharp/RDPWrap.Common/ServiceHelper.cs
@@ -0,0 +1,219 @@
+// Copyright 2024 sjackson0109 — Apache License 2.0
+using System.Runtime.InteropServices;
+
+namespace RDPWrap.Common;
+
+///
+/// Wrappers around the Windows Service Control Manager API, mirroring the
+/// SvcGetStart / SvcConfigStart / SvcStart / CheckTermsrvProcess helpers
+/// in RDPWInst.dpr and GetTermSrvState in RDPConf MainUnit.pas.
+///
+public static class ServiceHelper
+{
+ // ── Start-type query / change ─────────────────────────────────────────────
+
+ ///
+ /// Returns the configured start type of
+ /// (e.g. SERVICE_AUTO_START) or -1 on failure.
+ ///
+ public static int GetStartType(string serviceName)
+ {
+ var hSC = NativeMethods.OpenSCManager(null, null, NativeMethods.SC_MANAGER_CONNECT);
+ if (hSC == IntPtr.Zero) return -1;
+ try
+ {
+ var hSvc = NativeMethods.OpenService(hSC, serviceName,
+ NativeMethods.SERVICE_QUERY_CONFIG);
+ if (hSvc == IntPtr.Zero) return -1;
+ try
+ {
+ NativeMethods.QueryServiceConfig(hSvc, IntPtr.Zero, 0, out uint needed);
+ var buf = Marshal.AllocHGlobal((int)needed);
+ try
+ {
+ if (!NativeMethods.QueryServiceConfig(hSvc, buf, needed, out _))
+ return -1;
+ // dwStartType is the second DWORD in QUERY_SERVICE_CONFIG
+ return Marshal.ReadInt32(buf, 4);
+ }
+ finally { Marshal.FreeHGlobal(buf); }
+ }
+ finally { NativeMethods.CloseServiceHandle(hSvc); }
+ }
+ finally { NativeMethods.CloseServiceHandle(hSC); }
+ }
+
+ ///
+ /// Changes the start type of to
+ /// (one of the SERVICE_*_START constants).
+ ///
+ public static bool SetStartType(string serviceName, uint startType)
+ {
+ var hSC = NativeMethods.OpenSCManager(null, null, NativeMethods.SC_MANAGER_CONNECT);
+ if (hSC == IntPtr.Zero) return false;
+ try
+ {
+ var hSvc = NativeMethods.OpenService(hSC, serviceName,
+ NativeMethods.SERVICE_CHANGE_CONFIG);
+ if (hSvc == IntPtr.Zero) return false;
+ try
+ {
+ return NativeMethods.ChangeServiceConfig(hSvc,
+ NativeMethods.SERVICE_NO_CHANGE, startType,
+ NativeMethods.SERVICE_NO_CHANGE,
+ null, null, IntPtr.Zero, null, null, null, null);
+ }
+ finally { NativeMethods.CloseServiceHandle(hSvc); }
+ }
+ finally { NativeMethods.CloseServiceHandle(hSC); }
+ }
+
+ // ── Start a service ───────────────────────────────────────────────────────
+
+ ///
+ /// Starts . If the service is already
+ /// running (error 1056) it waits 2 s and retries once, matching the
+ /// original Delphi SvcStart behaviour.
+ /// Returns true on success.
+ ///
+ public static bool StartService(string serviceName)
+ {
+ var hSC = NativeMethods.OpenSCManager(null, null, NativeMethods.SC_MANAGER_CONNECT);
+ if (hSC == IntPtr.Zero) return false;
+ try
+ {
+ var hSvc = NativeMethods.OpenService(hSC, serviceName,
+ NativeMethods.SERVICE_START);
+ if (hSvc == IntPtr.Zero) return false;
+ try
+ {
+ if (NativeMethods.StartService(hSvc, 0, null)) return true;
+
+ var err = Marshal.GetLastWin32Error();
+ if (err == (int)NativeMethods.ERROR_SERVICE_ALREADY_RUNNING)
+ {
+ Thread.Sleep(2000);
+ return NativeMethods.StartService(hSvc, 0, null);
+ }
+ return false;
+ }
+ finally { NativeMethods.CloseServiceHandle(hSvc); }
+ }
+ finally { NativeMethods.CloseServiceHandle(hSC); }
+ }
+
+ // ── Status query ──────────────────────────────────────────────────────────
+
+ ///
+ /// Returns the dwCurrentState for
+ /// (e.g. SERVICE_RUNNING = 4) or -1 on failure.
+ ///
+ public static int GetCurrentState(string serviceName)
+ {
+ var hSC = NativeMethods.OpenSCManager(null, null, NativeMethods.SC_MANAGER_CONNECT);
+ if (hSC == IntPtr.Zero) return -1;
+ try
+ {
+ var hSvc = NativeMethods.OpenService(hSC, serviceName,
+ NativeMethods.SERVICE_QUERY_STATUS);
+ if (hSvc == IntPtr.Zero) return -1;
+ try
+ {
+ NativeMethods.QueryServiceStatusEx(hSvc,
+ NativeMethods.SC_STATUS_PROCESS_INFO,
+ IntPtr.Zero, 0, out uint needed);
+
+ var buf = Marshal.AllocHGlobal((int)needed);
+ try
+ {
+ if (!NativeMethods.QueryServiceStatusEx(hSvc,
+ NativeMethods.SC_STATUS_PROCESS_INFO,
+ buf, needed, out _)) return -1;
+
+ // dwCurrentState is the second DWORD in SERVICE_STATUS_PROCESS
+ return Marshal.ReadInt32(buf, 4);
+ }
+ finally { Marshal.FreeHGlobal(buf); }
+ }
+ finally { NativeMethods.CloseServiceHandle(hSvc); }
+ }
+ finally { NativeMethods.CloseServiceHandle(hSC); }
+ }
+
+ // ── Process-info enumeration ──────────────────────────────────────────────
+
+ ///
+ /// Represents the name and PID of a service process returned by
+ /// .
+ ///
+ public record ServiceProcessInfo(string ServiceName, string DisplayName, uint ProcessId, uint CurrentState);
+
+ ///
+ /// Enumerates all Win32 services (all states) and returns their
+ /// process information. Mirrors the EnumServicesStatusEx loop in
+ /// CheckTermsrvProcess.
+ ///
+ public static IReadOnlyList EnumServiceProcesses()
+ {
+ var result = new List();
+
+ var hSC = NativeMethods.OpenSCManager(null, null,
+ NativeMethods.SC_MANAGER_CONNECT | NativeMethods.SC_MANAGER_ENUMERATE_SERVICE);
+ if (hSC == IntPtr.Zero) return result;
+
+ try
+ {
+ uint resumeHandle = 0;
+ // Ask for the required buffer size
+ NativeMethods.EnumServicesStatusEx(hSC,
+ NativeMethods.SC_ENUM_PROCESS_INFO,
+ NativeMethods.SERVICE_WIN32,
+ NativeMethods.SERVICE_STATE_ALL,
+ IntPtr.Zero, 0,
+ out uint needed, out _, ref resumeHandle, null);
+
+ if (needed == 0) return result;
+
+ var buf = Marshal.AllocHGlobal((int)needed);
+ try
+ {
+ resumeHandle = 0;
+ if (!NativeMethods.EnumServicesStatusEx(hSC,
+ NativeMethods.SC_ENUM_PROCESS_INFO,
+ NativeMethods.SERVICE_WIN32,
+ NativeMethods.SERVICE_STATE_ALL,
+ buf, needed,
+ out _, out uint returned, ref resumeHandle, null))
+ return result;
+
+ // ENUM_SERVICE_STATUS_PROCESS layout (Unicode, packed):
+ // IntPtr lpServiceName (pointer to string)
+ // IntPtr lpDisplayName (pointer to string)
+ // SERVICE_STATUS_PROCESS (9 × DWORD = 36 bytes)
+ int ptrSize = IntPtr.Size;
+ int entrySize = ptrSize * 2 + 36; // 2 string pointers + status struct
+
+ for (int i = 0; i < (int)returned; i++)
+ {
+ var entryPtr = buf + i * entrySize;
+ var namePtr = Marshal.ReadIntPtr(entryPtr);
+ var dispPtr = Marshal.ReadIntPtr(entryPtr + ptrSize);
+
+ var svcName = Marshal.PtrToStringUni(namePtr) ?? string.Empty;
+ var dispName = Marshal.PtrToStringUni(dispPtr) ?? string.Empty;
+
+ // dwCurrentState at offset 4, dwProcessId at offset 28
+ var statusBase = entryPtr + ptrSize * 2;
+ uint currentState = (uint)Marshal.ReadInt32(statusBase + 4);
+ uint pid = (uint)Marshal.ReadInt32(statusBase + 28);
+
+ result.Add(new ServiceProcessInfo(svcName, dispName, pid, currentState));
+ }
+ }
+ finally { Marshal.FreeHGlobal(buf); }
+ }
+ finally { NativeMethods.CloseServiceHandle(hSC); }
+
+ return result;
+ }
+}
diff --git a/src-csharp/RDPWrap.sln b/src-csharp/RDPWrap.sln
new file mode 100644
index 0000000..51b25c1
--- /dev/null
+++ b/src-csharp/RDPWrap.sln
@@ -0,0 +1,58 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.11.35303.130
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RDPWrap.Common", "RDPWrap.Common\RDPWrap.Common.csproj", "{A1B2C3D4-0001-4000-8000-000000000001}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RDPWInst", "RDPWInst\RDPWInst.csproj", "{A1B2C3D4-0001-4000-8000-000000000002}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RDPConf", "RDPConf\RDPConf.csproj", "{A1B2C3D4-0001-4000-8000-000000000003}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RDPCheck", "RDPCheck\RDPCheck.csproj", "{A1B2C3D4-0001-4000-8000-000000000004}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A1B2C3D4-0001-4000-8000-000000000001}.Debug|x64.ActiveCfg = Debug|x64
+ {A1B2C3D4-0001-4000-8000-000000000001}.Debug|x64.Build.0 = Debug|x64
+ {A1B2C3D4-0001-4000-8000-000000000001}.Debug|x86.ActiveCfg = Debug|x86
+ {A1B2C3D4-0001-4000-8000-000000000001}.Debug|x86.Build.0 = Debug|x86
+ {A1B2C3D4-0001-4000-8000-000000000001}.Release|x64.ActiveCfg = Release|x64
+ {A1B2C3D4-0001-4000-8000-000000000001}.Release|x64.Build.0 = Release|x64
+ {A1B2C3D4-0001-4000-8000-000000000001}.Release|x86.ActiveCfg = Release|x86
+ {A1B2C3D4-0001-4000-8000-000000000001}.Release|x86.Build.0 = Release|x86
+ {A1B2C3D4-0001-4000-8000-000000000002}.Debug|x64.ActiveCfg = Debug|x64
+ {A1B2C3D4-0001-4000-8000-000000000002}.Debug|x64.Build.0 = Debug|x64
+ {A1B2C3D4-0001-4000-8000-000000000002}.Debug|x86.ActiveCfg = Debug|x86
+ {A1B2C3D4-0001-4000-8000-000000000002}.Debug|x86.Build.0 = Debug|x86
+ {A1B2C3D4-0001-4000-8000-000000000002}.Release|x64.ActiveCfg = Release|x64
+ {A1B2C3D4-0001-4000-8000-000000000002}.Release|x64.Build.0 = Release|x64
+ {A1B2C3D4-0001-4000-8000-000000000002}.Release|x86.ActiveCfg = Release|x86
+ {A1B2C3D4-0001-4000-8000-000000000002}.Release|x86.Build.0 = Release|x86
+ {A1B2C3D4-0001-4000-8000-000000000003}.Debug|x64.ActiveCfg = Debug|x64
+ {A1B2C3D4-0001-4000-8000-000000000003}.Debug|x64.Build.0 = Debug|x64
+ {A1B2C3D4-0001-4000-8000-000000000003}.Debug|x86.ActiveCfg = Debug|x86
+ {A1B2C3D4-0001-4000-8000-000000000003}.Debug|x86.Build.0 = Debug|x86
+ {A1B2C3D4-0001-4000-8000-000000000003}.Release|x64.ActiveCfg = Release|x64
+ {A1B2C3D4-0001-4000-8000-000000000003}.Release|x64.Build.0 = Release|x64
+ {A1B2C3D4-0001-4000-8000-000000000003}.Release|x86.ActiveCfg = Release|x86
+ {A1B2C3D4-0001-4000-8000-000000000003}.Release|x86.Build.0 = Release|x86
+ {A1B2C3D4-0001-4000-8000-000000000004}.Debug|x64.ActiveCfg = Debug|x64
+ {A1B2C3D4-0001-4000-8000-000000000004}.Debug|x64.Build.0 = Debug|x64
+ {A1B2C3D4-0001-4000-8000-000000000004}.Debug|x86.ActiveCfg = Debug|x86
+ {A1B2C3D4-0001-4000-8000-000000000004}.Debug|x86.Build.0 = Debug|x86
+ {A1B2C3D4-0001-4000-8000-000000000004}.Release|x64.ActiveCfg = Release|x64
+ {A1B2C3D4-0001-4000-8000-000000000004}.Release|x64.Build.0 = Release|x64
+ {A1B2C3D4-0001-4000-8000-000000000004}.Release|x86.ActiveCfg = Release|x86
+ {A1B2C3D4-0001-4000-8000-000000000004}.Release|x86.Build.0 = Release|x86
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal