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