feat: Phase 1 - C# migration scaffold (RDPWrap.Common shared library)

Add src-csharp/ solution structure for the Delphi-to-C# migration:

Solution & projects:
- RDPWrap.sln (VS 2022, x86/x64 configurations)
- Directory.Build.props (net8.0-windows, shared TFM/platform settings)
- RDPWrap.Common.csproj  - shared class library
- RDPWInst.csproj        - console installer (stub)
- RDPConf.csproj         - WinForms config UI (stub)
- RDPCheck.csproj        - WinForms RDP tester (stub)

RDPWrap.Common helpers (all translated from Delphi source):
- NativeMethods.cs   - all P/Invoke: kernel32, advapi32, winsta.dll
- ArchHelper.cs      - arch detection + WOW64 redirection
- RegistryHelper.cs  - HKLM typed read/write with WOW64 view support
- ServiceHelper.cs   - SCM wrappers (start-type, start, state, enum)
- FileVersionHelper.cs - file version reading via BCL FileVersionInfo
- ProcessHelper.cs   - ExecWait (hidden) + KillProcess
- HttpHelper.cs      - HttpClient replacing WinInet (sync + async)
- ResourceHelper.cs  - embedded manifest resource extract/read
- IniHelper.cs       - INI section presence check + support level
- SecurityHelper.cs  - SID/ACL (GrantSidFullAccess) + token privileges

Also adds TODO.md tracking the full 44-item migration plan.
pull/4062/head
Simon Jackson 1 month ago
parent e57e252d5c
commit ae426da271

@ -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 <TargetFramework>net481</TargetFramework> (or net8.0-windows), <Platforms>x86;x64</Platforms>, <Nullable>enable</Nullable>, <ImplicitUsings>enable</ImplicitUsings>
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

@ -0,0 +1,15 @@
<Project>
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Platforms>x86;x64</Platforms>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PlatformTarget>$(Platform)</PlatformTarget>
<!-- Ensure single-file publish works -->
<SelfContained>false</SelfContained>
<!-- Reproducible builds -->
<Deterministic>true</Deterministic>
</PropertyGroup>
</Project>

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<RootNamespace>RDPCheck</RootNamespace>
<AssemblyName>RDPCheck</AssemblyName>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RDPWrap.Common\RDPWrap.Common.csproj" />
</ItemGroup>
<!-- COM interop for AxMsRdpClient2 — generated via:
tlbimp %SystemRoot%\System32\mstscax.dll /out:MSTSCLib.dll
Then reference it here:
<ItemGroup>
<Reference Include="MSTSCLib">
<HintPath>Interop\MSTSCLib.dll</HintPath>
</Reference>
<Reference Include="AxInterop.MSTSCLib">
<HintPath>Interop\AxInterop.MSTSCLib.dll</HintPath>
</Reference>
</ItemGroup>
-->
</Project>

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<RootNamespace>RDPConf</RootNamespace>
<AssemblyName>RDPConf</AssemblyName>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RDPWrap.Common\RDPWrap.Common.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>RDPWInst</RootNamespace>
<AssemblyName>RDPWInst</AssemblyName>
<!-- Embed binary payloads -->
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RDPWrap.Common\RDPWrap.Common.csproj" />
</ItemGroup>
<!-- Embedded payload resources (DLLs, rdpwrap.ini) go here once binaries are available -->
<!-- Example:
<ItemGroup>
<EmbeddedResource Include="Resources\rdpw32.dll" />
<EmbeddedResource Include="Resources\rdpw64.dll" />
<EmbeddedResource Include="Resources\rdpwrap.ini" />
</ItemGroup>
-->
</Project>

@ -0,0 +1,80 @@
// Copyright 2024 sjackson0109 — Apache License 2.0
using System.Runtime.InteropServices;
namespace RDPWrap.Common;
/// <summary>
/// Architecture detection and WOW64 file-system redirection control.
/// Mirrors the Arch / DisableWowRedirection / RevertWowRedirection logic
/// from RDPWInst.dpr and RDPConf MainUnit.pas.
/// </summary>
public static class ArchHelper
{
private static readonly Lazy<byte> _arch = new(DetectArch);
/// <summary>Raw architecture byte: 32 or 64. 0 = unsupported.</summary>
public static byte Arch => _arch.Value;
/// <summary><c>true</c> when running on a 64-bit Windows installation.</summary>
public static bool Is64Bit => Arch == 64;
/// <summary>
/// Returns <c>true</c> when the processor architecture is supported
/// (x86 or x64). Itanium and unknown architectures return <c>false</c>.
/// </summary>
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;
/// <summary>
/// Disables WOW64 filesystem redirection so that 32-bit processes can
/// reach the real <c>%SystemRoot%\System32</c>. Call only on 64-bit hosts.
/// Returns <c>true</c> on success.
/// </summary>
public static bool DisableWow64Redirection()
{
if (!Is64Bit) return false;
return NativeMethods.Wow64DisableWow64FsRedirection(out _wow64OldValue);
}
/// <summary>
/// Reverts the WOW64 filesystem redirection state saved by the last call
/// to <see cref="DisableWow64Redirection"/>.
/// </summary>
public static bool RevertWow64Redirection()
{
if (!Is64Bit) return false;
return NativeMethods.Wow64RevertWow64FsRedirection(_wow64OldValue);
}
// ── Environment path expansion ────────────────────────────────────────────
/// <summary>
/// Expands environment strings in <paramref name="path"/>, replacing
/// <c>%ProgramFiles%</c> with <c>%ProgramW6432%</c> on 64-bit hosts
/// to avoid redirection to the x86 Program Files folder.
/// </summary>
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();
}
}

@ -0,0 +1,64 @@
// Copyright 2024 sjackson0109 — Apache License 2.0
using System.Diagnostics;
namespace RDPWrap.Common;
/// <summary>
/// File-version reading helper. Mirrors the GetFileVersion function used in
/// RDPWInst.dpr and RDPConf MainUnit.pas.
/// </summary>
public static class FileVersionHelper
{
/// <summary>
/// Strongly-typed representation of a Windows file version.
/// </summary>
public record FileVersionInfo(
ushort Major,
ushort Minor,
ushort Release,
ushort Build,
bool IsDebug,
bool IsPrerelease,
bool IsPrivate,
bool IsSpecial)
{
/// <summary>e.g. "10.0.26100.3476"</summary>
public override string ToString() => $"{Major}.{Minor}.{Release}.{Build}";
}
/// <summary>
/// Returns the file version of <paramref name="filePath"/>, or <c>null</c>
/// if the file does not exist or has no version resource.
/// Uses the BCL <see cref="System.Diagnostics.FileVersionInfo"/> which does
/// not require loading the DLL as executable — safe for locked DLLs.
/// </summary>
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;
}
}
/// <summary>
/// Convenience overload: resolves the path via
/// <see cref="ArchHelper.ExpandPath"/> before reading.
/// </summary>
public static FileVersionInfo? GetVersionExpanded(string pathWithEnvVars)
=> GetVersion(ArchHelper.ExpandPath(pathWithEnvVars));
}

@ -0,0 +1,80 @@
// Copyright 2024 sjackson0109 — Apache License 2.0
namespace RDPWrap.Common;
/// <summary>
/// HTTP download helpers that replace the WinInet-based GitINIFile /
/// DownloadFileToDisk procedures from RDPWInst.dpr.
/// Uses <see cref="HttpClient"/> with a shared static instance.
/// </summary>
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" } }
};
/// <summary>
/// Downloads the text content at <paramref name="url"/> and returns it as
/// a string. Returns <c>null</c> on any failure.
/// Mirrors the Delphi GitINIFile function.
/// </summary>
public static async Task<string?> 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;
}
}
/// <summary>
/// Downloads the binary content at <paramref name="url"/> and saves it to
/// <paramref name="destPath"/>. Returns <c>true</c> when the file exists
/// and is non-empty after download.
/// Mirrors the Delphi DownloadFileToDisk function.
/// </summary>
public static async Task<bool> 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;
}
}
/// <summary>
/// Synchronous wrapper for <see cref="DownloadStringAsync"/> — suitable
/// for the installer's purely-sequential flow.
/// </summary>
public static string? DownloadString(string url)
=> DownloadStringAsync(url).GetAwaiter().GetResult();
/// <summary>
/// Synchronous wrapper for <see cref="DownloadFileAsync"/>.
/// </summary>
public static bool DownloadFile(string url, string destPath)
=> DownloadFileAsync(url, destPath).GetAwaiter().GetResult();
}

@ -0,0 +1,61 @@
// Copyright 2024 sjackson0109 — Apache License 2.0
namespace RDPWrap.Common;
/// <summary>
/// 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.
/// </summary>
public static class IniHelper
{
/// <summary>
/// Returns <c>true</c> when the INI file at <paramref name="iniPath"/>
/// contains the section header <c>[<paramref name="section"/>]</c>.
/// Mirrors the Delphi INIHasSection function.
/// </summary>
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;
}
/// <summary>
/// Loads the full text of <paramref name="iniPath"/> and returns it,
/// or an empty string if the file does not exist.
/// </summary>
public static string LoadText(string iniPath)
=> File.Exists(iniPath) ? File.ReadAllText(iniPath) : string.Empty;
/// <summary>
/// Checks the support level of a given termsrv.dll version against the
/// INI content string <paramref name="iniContent"/>.
/// </summary>
/// <returns>
/// 0 = not supported, 1 = partially supported (Vista/7 legacy),
/// 2 = fully supported (entry found in ini).
/// </returns>
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;
}
}

@ -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;
/// <summary>
/// All P/Invoke declarations used across RDPWInst, RDPConf and RDPCheck.
/// Mirrors the unhooked Win32 imports from the original Delphi sources.
/// </summary>
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 ───────────────────────────────────────────────────────────
/// <summary>
/// Enumerates WTS sessions on the local server.
/// Pass <c>IntPtr.Zero</c> as hServer for the local machine.
/// </summary>
[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);
}

@ -0,0 +1,63 @@
// Copyright 2024 sjackson0109 — Apache License 2.0
using System.Runtime.InteropServices;
namespace RDPWrap.Common;
/// <summary>
/// Process creation and termination helpers. Mirrors ExecWait and KillProcess
/// from RDPWInst.dpr (console variant) and RDPConf MainUnit.pas (GUI variant).
/// </summary>
public static class ProcessHelper
{
/// <summary>
/// Creates a process from <paramref name="commandLine"/>, waits for it to
/// exit, then returns <c>true</c>. The process window is hidden.
/// Mirrors the Delphi ExecWait procedure.
/// </summary>
public static bool ExecWait(string commandLine, bool hideWindow = true)
{
var si = new NativeMethods.STARTUPINFO
{
cb = (uint)Marshal.SizeOf<NativeMethods.STARTUPINFO>(),
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;
}
/// <summary>
/// Terminates the process with <paramref name="pid"/>. Mirrors the Delphi
/// KillProcess procedure.
/// </summary>
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);
}
}

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<RootNamespace>RDPWrap.Common</RootNamespace>
<AssemblyName>RDPWrap.Common</AssemblyName>
</PropertyGroup>
</Project>

@ -0,0 +1,97 @@
// Copyright 2024 sjackson0109 — Apache License 2.0
using Microsoft.Win32;
namespace RDPWrap.Common;
/// <summary>
/// Thin wrappers around <see cref="Microsoft.Win32.Registry"/> 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.
/// </summary>
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 ──────────────────────────────────────────────
/// <summary>Opens a read-only key under HKLM, respecting the host architecture.</summary>
public static RegistryKey? OpenHklmRead(string subKey)
{
using var hive = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, ViewForArch());
return hive.OpenSubKey(subKey, writable: false);
}
/// <summary>Opens a writable key under HKLM (creates if absent).</summary>
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 ────────────────────────────────────────────────────
/// <summary>
/// Reads a string value from HKLM. Returns <c>null</c> if the key or
/// value does not exist.
/// </summary>
public static string? ReadString(string subKey, string valueName)
{
using var key = OpenHklmRead(subKey);
return key?.GetValue(valueName) as string;
}
/// <summary>
/// Reads a DWORD value from HKLM. Returns <paramref name="defaultValue"/>
/// if the key or value does not exist.
/// </summary>
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;
}
/// <summary>
/// Reads a DWORD as a bool (non-zero = true). Returns <paramref name="defaultValue"/>
/// if absent.
/// </summary>
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 ───────────────────────────────────────────────────
/// <summary>Writes a REG_SZ string value.</summary>
public static void WriteString(string subKey, string valueName, string value)
{
using var key = OpenHklmWrite(subKey);
key.SetValue(valueName, value, RegistryValueKind.String);
}
/// <summary>Writes a REG_EXPAND_SZ string value (mirrors Delphi WriteExpandString).</summary>
public static void WriteExpandString(string subKey, string valueName, string value)
{
using var key = OpenHklmWrite(subKey);
key.SetValue(valueName, value, RegistryValueKind.ExpandString);
}
/// <summary>Writes a DWORD integer value.</summary>
public static void WriteInt(string subKey, string valueName, int value)
{
using var key = OpenHklmWrite(subKey);
key.SetValue(valueName, value, RegistryValueKind.DWord);
}
/// <summary>Writes a boolean as a DWORD (1/0).</summary>
public static void WriteBool(string subKey, string valueName, bool value)
=> WriteInt(subKey, valueName, value ? 1 : 0);
}

@ -0,0 +1,88 @@
// Copyright 2024 sjackson0109 — Apache License 2.0
using System.Reflection;
namespace RDPWrap.Common;
/// <summary>
/// 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.
/// </summary>
public static class ResourceHelper
{
/// <summary>
/// Reads the contents of the embedded resource named
/// <paramref name="resourceName"/> from <paramref name="assembly"/>
/// and returns it as a UTF-8 string. Returns <c>null</c> if not found.
/// </summary>
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();
}
/// <summary>
/// Reads the contents of the embedded resource named
/// <paramref name="resourceName"/> and returns the raw bytes.
/// Returns <c>null</c> if not found.
/// </summary>
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();
}
/// <summary>
/// Extracts the embedded resource <paramref name="resourceName"/> to disk
/// at <paramref name="destPath"/>, creating parent directories as needed.
/// Returns <c>true</c> on success. Mirrors the Delphi ExtractRes procedure.
/// </summary>
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;
}
}
/// <summary>
/// Lists all manifest resource names in <paramref name="assembly"/>
/// — useful for debugging resource name mismatches.
/// </summary>
public static IEnumerable<string> ListResources(Assembly? assembly = null)
{
assembly ??= Assembly.GetCallingAssembly();
return assembly.GetManifestResourceNames();
}
}

@ -0,0 +1,168 @@
// Copyright 2024 sjackson0109 — Apache License 2.0
using System.Runtime.InteropServices;
namespace RDPWrap.Common;
/// <summary>
/// Security helpers: granting SID-based DACL entries (GrantSidFullAccess) and
/// adjusting process token privileges (AddPrivilege). Mirrors the Delphi
/// implementations in RDPWInst.dpr.
/// </summary>
public static class SecurityHelper
{
// ── DACL: grant a well-known SID full access to a file/folder ─────────────
/// <summary>
/// Grants GENERIC_ALL access to the well-known SID string
/// <paramref name="stringSid"/> on the file/directory at
/// <paramref name="path"/>. Mirrors the Delphi GrantSidFullAccess
/// procedure (used for "S-1-5-18" = Local System, "S-1-5-6" = Service).
/// </summary>
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 ──────────────────────────────────────────────────────
/// <summary>
/// Enables the named privilege (e.g. <c>SeDebugPrivilege</c>) for the
/// current process token. Mirrors the Delphi AddPrivilege function.
/// Returns <c>true</c> on success.
/// </summary>
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 ──────────────────────────────────────────────────────
/// <summary>
/// Returns <c>true</c> when there is an active "RDP-Tcp" WTS listener on
/// the local machine — i.e. <c>WinStationEnumerateW</c> returns a session
/// named "RDP-Tcp". Mirrors the Delphi IsListenerWorking function.
/// </summary>
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;
}
}

@ -0,0 +1,219 @@
// Copyright 2024 sjackson0109 — Apache License 2.0
using System.Runtime.InteropServices;
namespace RDPWrap.Common;
/// <summary>
/// Wrappers around the Windows Service Control Manager API, mirroring the
/// SvcGetStart / SvcConfigStart / SvcStart / CheckTermsrvProcess helpers
/// in RDPWInst.dpr and GetTermSrvState in RDPConf MainUnit.pas.
/// </summary>
public static class ServiceHelper
{
// ── Start-type query / change ─────────────────────────────────────────────
/// <summary>
/// Returns the configured start type of <paramref name="serviceName"/>
/// (e.g. <c>SERVICE_AUTO_START</c>) or <c>-1</c> on failure.
/// </summary>
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); }
}
/// <summary>
/// Changes the start type of <paramref name="serviceName"/> to
/// <paramref name="startType"/> (one of the <c>SERVICE_*_START</c> constants).
/// </summary>
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 ───────────────────────────────────────────────────────
/// <summary>
/// Starts <paramref name="serviceName"/>. If the service is already
/// running (error 1056) it waits 2 s and retries once, matching the
/// original Delphi SvcStart behaviour.
/// Returns <c>true</c> on success.
/// </summary>
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 ──────────────────────────────────────────────────────────
/// <summary>
/// Returns the <c>dwCurrentState</c> for <paramref name="serviceName"/>
/// (e.g. <c>SERVICE_RUNNING = 4</c>) or <c>-1</c> on failure.
/// </summary>
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 ──────────────────────────────────────────────
/// <summary>
/// Represents the name and PID of a service process returned by
/// <see cref="EnumServiceProcesses"/>.
/// </summary>
public record ServiceProcessInfo(string ServiceName, string DisplayName, uint ProcessId, uint CurrentState);
/// <summary>
/// Enumerates all Win32 services (all states) and returns their
/// process information. Mirrors the EnumServicesStatusEx loop in
/// CheckTermsrvProcess.
/// </summary>
public static IReadOnlyList<ServiceProcessInfo> EnumServiceProcesses()
{
var result = new List<ServiceProcessInfo>();
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;
}
}

@ -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
Loading…
Cancel
Save